Skip to content

Commit

Permalink
Support new collection projects. Display collection logic used in pro…
Browse files Browse the repository at this point in the history
…ject list and mark collection projects visually.

Signed-off-by: Ralf King <[email protected]>
  • Loading branch information
rkg-mm committed Dec 2, 2023
1 parent 5bdcc9b commit ac7c288
Show file tree
Hide file tree
Showing 6 changed files with 99 additions and 6 deletions.
7 changes: 6 additions & 1 deletion src/assets/scss/_custom.scss
Original file line number Diff line number Diff line change
Expand Up @@ -469,4 +469,9 @@ td a.detail-icon {
.keep-together {
display: inline;
white-space: nowrap;
}
}

.icon-cellend {
float: right;
padding-top: 0.3rem;
}
8 changes: 8 additions & 0 deletions src/i18n/locales/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,7 @@
"description": "Description",
"tags": "Tags",
"add_tag": "Add Tag",
"project_add_collection_tag": "Aggregate data of children with this tag",
"update": "Update",
"cancel": "Cancel",
"add_version": "Add Version",
Expand Down Expand Up @@ -285,6 +286,8 @@
"cpe": "CPE",
"cpe_full": "Common Platform Enumeration (CPE)",
"classifier": "Classifier",
"collectionLogic": "Project Collection Logic",
"collection_indicator_tooltip": "Collection project with values calculated based on children.",
"search_parent": "Type to search parent",
"inactive_active_children" : "The project cannot be set to inactive if it has active children",
"filename": "Filename",
Expand All @@ -310,6 +313,7 @@
"component_cpe_desc": "The CPE v2.2 or v2.3 URI as provided by MITRE or NIST. All assets (applications, operating systems, and hardware) should have a CPE specified",
"component_swid_tagid_desc": "The ISO/IEC 19770-2:2015 (SWID) tag ID provided by the software vendor",
"component_classifier_desc": "Specifies the type of component: Assets (applications, operating systems, and hardware) and non-assets (libraries, frameworks, and files)",
"project_collection_logic_desc": "Specifies if this project is a collection project and which metrics calculation logic to apply for a collection project. Collection projects do not have own components but display data of their children using one of the available logics.",
"component_spdx_license_desc": "Specifies the SPDX license ID of the component",
"component_license_expression_desc": "Specifies license information for the component in the form of an SPDX expression",
"component_license_url_desc": "Specifies the URL to the license of the component",
Expand Down Expand Up @@ -415,6 +419,10 @@
"component_device": "Device",
"component_firmware": "Firmware",
"component_file": "File",
"project_collection_logic_none": "None",
"project_collection_logic_aggregate_direct_children": "Aggregate direct children",
"project_collection_logic_aggregate_direct_children_with_tag": "Aggregate direct children with tag",
"project_collection_logic_highest_semver_child": "Show highest SemVer child",
"dates": "Dates",
"owasp_rr": "OWASP Risk Rating",
"owasp_rr_likelihood_score": "OWASP RR Likelihood score",
Expand Down
4 changes: 4 additions & 0 deletions src/views/portfolio/projects/Project.vue
Original file line number Diff line number Diff line change
Expand Up @@ -247,6 +247,10 @@
}
}).then((response) => {
this.project = response.data;
// metrics are not always returned by API, fix error sometimes raised in following lines
if(!Object.hasOwn(this.project, 'metrics')) {
this.project.metrics = {}
}
this.currentCritical = common.valueWithDefault(this.project.metrics.critical, 0);
this.currentHigh = common.valueWithDefault(this.project.metrics.high, 0);
this.currentMedium = common.valueWithDefault(this.project.metrics.medium, 0);
Expand Down
39 changes: 35 additions & 4 deletions src/views/portfolio/projects/ProjectCreateProjectModal.vue
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,15 @@
<b-input-group-form-select id="v-classifier-input" required="true"
v-model="project.classifier" :options="sortAvailableClassifiers"
:label="$t('message.classifier')" :tooltip="$t('message.component_classifier_desc')" />
<b-input-group-form-select id="v-collection-logic-input" required="true"
v-model="project.collectionLogic" :options="availableCollectionLogics"
:label="$t('message.collectionLogic')" :tooltip="$t('message.project_collection_logic_desc')"
:readonly="this.isNotPermitted(PERMISSIONS.PORTFOLIO_MANAGEMENT)"
v-on:change="syncCollectionTagsVisibility" />
<vue-tags-input id="input-collectionTags" v-model="collectionTagTyping" :tags="collectionTags" :add-on-key="addOnKeys"
:placeholder="$t('message.project_add_collection_tag')" @tags-changed="newCollectionTags => this.collectionTags = newCollectionTags"
class="mw-100 bg-transparent text-lowercase" :max-tags="1" v-show="showCollectionTags"
:readonly="this.isNotPermitted(PERMISSIONS.PORTFOLIO_MANAGEMENT)"/>
<div style="margin-bottom: 1rem">
<label>Parent</label>
<multiselect v-model="selectedParent" id="multiselect" :custom-label="createProjectLabel" :placeholder="this.$t('message.search_parent')" open-direction="bottom" :options="availableParents"
Expand Down Expand Up @@ -117,13 +126,24 @@
{ value: 'FIRMWARE', text: this.$i18n.t('message.component_firmware') },
{ value: 'FILE', text: this.$i18n.t('message.component_file') }
],
availableCollectionLogics: [
{ value: 'NONE', text: this.$i18n.t('message.project_collection_logic_none') },
{ value: 'AGGREGATE_DIRECT_CHILDREN', text: this.$i18n.t('message.project_collection_logic_aggregate_direct_children') },
{ value: 'AGGREGATE_DIRECT_CHILDREN_WITH_TAG', text: this.$i18n.t('message.project_collection_logic_aggregate_direct_children_with_tag') },
{ value: 'HIGHEST_SEMVER_CHILD', text: this.$i18n.t('message.project_collection_logic_highest_semver_child') }
],
selectableLicenses: [],
selectedLicense: '',
selectedParent: null,
availableParents: [],
project: {},
project: {
collectionLogic: 'NONE' // set default to regular project
},
tag: '', // The contents of a tag as its being typed into the vue-tag-input
tags: [], // An array of tags bound to the vue-tag-input
collectionTagTyping: '', // The contents of a collection tag as its being typed into the vue-tag-input
collectionTags: [], // An array of tags bound to the vue-tag-input for collection tag
showCollectionTags: false,
addOnKeys: [9, 13, 32, ':', ';', ','], // Separators used when typing tags into the vue-tag-input
labelIcon: {
dataOn: '\u2713',
Expand Down Expand Up @@ -160,6 +180,9 @@
syncReadOnlyVersionField: function(value) {
this.readOnlyProjectVersion = value;
},
syncCollectionTagsVisibility: function(value) {
this.showCollectionTags = value === 'AGGREGATE_DIRECT_CHILDREN_WITH_TAG';
},
createProject: function() {
let url = `${this.$api.BASE_URL}/${this.$api.URL_PROJECT}`;
let tagsNode = [];
Expand All @@ -176,6 +199,10 @@
//license: this.selectedLicense,
parent: parent,
classifier: this.project.classifier,
collectionLogic: this.project.collectionLogic,
collectionTag: ( this.project.collectionLogic === 'AGGREGATE_DIRECT_CHILDREN_WITH_TAG' &&
this.collectionTags &&
this.collectionTags.length > 0 ) ? {name: this.collectionTags[0].text} : null,
purl: this.project.purl,
cpe: this.project.cpe,
swidTagId: this.project.swidTagId,
Expand Down Expand Up @@ -212,11 +239,15 @@
})
},
resetValues: function () {
this.project = {};
this.project = {
collectionLogic: 'NONE' // set default to regular project
};
this.tag = "";
this.tags = [];
this.selectedParent = null
this.availableParents = []
this.selectedParent = null;
this.availableParents = [];
this.collectionTags = [];
this.showCollectionTags = false;
},
createProjectLabel: function (project) {
if (project.version){
Expand Down
28 changes: 28 additions & 0 deletions src/views/portfolio/projects/ProjectDetailsModal.vue
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,16 @@
v-model="project.classifier" :options="availableClassifiers"
:label="$t('message.classifier')" :tooltip="$t('message.component_classifier_desc')"
:readonly="this.isNotPermitted(PERMISSIONS.PORTFOLIO_MANAGEMENT)" />
<b-input-group-form-select id="v-collection-logic-input" required="true"
v-model="project.collectionLogic" :options="availableCollectionLogics"
:label="$t('message.collectionLogic')" :tooltip="$t('message.project_collection_logic_desc')"
:readonly="this.isNotPermitted(PERMISSIONS.PORTFOLIO_MANAGEMENT)"
v-on:change="syncCollectionTagsVisibility" />
<vue-tags-input id="input-collectionTags" v-model="collectionTagTyping" :tags="collectionTags" :add-on-key="addOnKeys"
:placeholder="$t('message.project_add_collection_tag')" @tags-changed="newCollectionTags => this.collectionTags = newCollectionTags"
class="mw-100 bg-transparent text-lowercase" :max-tags="1" v-show="showCollectionTags"
:readonly="this.isNotPermitted(PERMISSIONS.PORTFOLIO_MANAGEMENT)"/>

<div style="margin-bottom: 1rem">
<label>Parent</label>
<multiselect v-model="selectedParent" id="multiselect" :custom-label="createProjectLabel" :placeholder="this.$t('message.search_parent')" open-direction="bottom" :options="availableParents"
Expand Down Expand Up @@ -138,11 +148,20 @@
{ value: 'FIRMWARE', text: this.$i18n.t('message.component_firmware') },
{ value: 'FILE', text: this.$i18n.t('message.component_file') }
],
availableCollectionLogics: [
{ value: 'NONE', text: this.$i18n.t('message.project_collection_logic_none') },
{ value: 'AGGREGATE_DIRECT_CHILDREN', text: this.$i18n.t('message.project_collection_logic_aggregate_direct_children') },
{ value: 'AGGREGATE_DIRECT_CHILDREN_WITH_TAG', text: this.$i18n.t('message.project_collection_logic_aggregate_direct_children_with_tag') },
{ value: 'HIGHEST_SEMVER_CHILD', text: this.$i18n.t('message.project_collection_logic_highest_semver_child') }
],
parent: null,
selectedParent: null,
availableParents: [],
tag: '', // The contents of a tag as its being typed into the vue-tag-input
tags: [], // An array of tags bound to the vue-tag-input
collectionTagTyping: '', // The contents of a collection tag as its being typed into the vue-tag-input
collectionTags: [], // An array of tags bound to the vue-tag-input for collection tag
showCollectionTags: false,
addOnKeys: [9, 13, 32, ':', ';', ','], // Separators used when typing tags into the vue-tag-input
labelIcon: {
dataOn: '\u2713',
Expand Down Expand Up @@ -206,13 +225,18 @@
methods: {
initializeTags: function() {
this.tags = (this.project.tags || []).map(tag => ({ text: tag.name }));
this.collectionTags = this.project.collectionTag ? [{ text: this.project.collectionTag.name }] : [];
this.syncCollectionTagsVisibility(this.project.collectionLogic);
},
syncReadOnlyNameField: function(value) {
this.readOnlyProjectName = value;
},
syncReadOnlyVersionField: function(value) {
this.readOnlyProjectVersion = value;
},
syncCollectionTagsVisibility: function(value) {
this.showCollectionTags = value === 'AGGREGATE_DIRECT_CHILDREN_WITH_TAG';
},
updateProject: function() {
let url = `${this.$api.BASE_URL}/${this.$api.URL_PROJECT}`;
let tagsNode = [];
Expand All @@ -230,6 +254,10 @@
version: this.project.version,
description: this.project.description,
classifier: this.project.classifier,
collectionLogic: this.project.collectionLogic,
collectionTag: ( this.project.collectionLogic === 'AGGREGATE_DIRECT_CHILDREN_WITH_TAG' &&
this.collectionTags &&
this.collectionTags.length > 0 ) ? {name: this.collectionTags[0].text} : null,
parent: parent,
cpe: this.project.cpe,
purl: this.project.purl,
Expand Down
19 changes: 18 additions & 1 deletion src/views/portfolio/projects/ProjectList.vue
Original file line number Diff line number Diff line change
Expand Up @@ -177,7 +177,24 @@ import ProjectCreateProjectModal from "./ProjectCreateProjectModal";
sortable: true,
formatter(value, row, index) {
let url = xssFilters.uriInUnQuotedAttr("../projects/" + row.uuid);
return `<a href="${url}">${xssFilters.inHTMLData(value)}</a>`;
let collectionIcon = '';
if(row.collectionLogic !== 'NONE') {
let title = 'Metrics of collection project are calculated '
switch (row.collectionLogic) {
case 'AGGREGATE_DIRECT_CHILDREN':
title += 'by aggregating numbers of all direct children.';
break;
case 'AGGREGATE_DIRECT_CHILDREN_WITH_TAG':
const tag = !row.collectionTag ? '' : xssFilters.inDoubleQuotedAttr(row.collectionTag.name);
title += `by aggregating numbers of direct children with tag '${tag}'.`;
break;
case 'HIGHEST_SEMVER_CHILD':
title += 'by using the child with highest SemVer version.'
break;
}
collectionIcon = ` <i class="fa fa-calculator fa-fw icon-cellend" title="${title}"></i>`;
}
return `<a href="${url}">${xssFilters.inHTMLData(value)}</a>${collectionIcon}`;
}
},
{
Expand Down

0 comments on commit ac7c288

Please sign in to comment.