From d1a0e431c1b7140b5ccec83c39072fb116fa6a10 Mon Sep 17 00:00:00 2001 From: Axel Date: Tue, 29 Apr 2025 09:45:07 +0200 Subject: [PATCH] Add bulk update triage, suppress and unsuppress. Signed-off-by: Axel --- .github/ISSUE_TEMPLATE/defect-report.yml | 1 + docker/Dockerfile.alpine | 2 +- src/i18n/locales/de.json | 7 +- src/i18n/locales/en.json | 7 +- src/i18n/locales/es.json | 7 +- src/i18n/locales/fr.json | 7 +- src/i18n/locales/hi.json | 7 +- src/i18n/locales/it.json | 7 +- src/i18n/locales/ja.json | 7 +- src/i18n/locales/pl.json | 7 +- src/i18n/locales/pt-BR.json | 7 +- src/i18n/locales/pt.json | 7 +- src/i18n/locales/ru.json | 7 +- src/i18n/locales/uk-UA.json | 7 +- src/i18n/locales/zh.json | 7 +- src/views/portfolio/projects/FindingAudit.vue | 6 +- .../vulnerabilities/AffectedProjects.vue | 174 +++++++++++++++ .../vulnerabilities/BulkUpdateModal.vue | 211 ++++++++++++++++++ 18 files changed, 468 insertions(+), 17 deletions(-) create mode 100644 src/views/portfolio/vulnerabilities/BulkUpdateModal.vue diff --git a/.github/ISSUE_TEMPLATE/defect-report.yml b/.github/ISSUE_TEMPLATE/defect-report.yml index cb971b148..0f062bb4c 100644 --- a/.github/ISSUE_TEMPLATE/defect-report.yml +++ b/.github/ISSUE_TEMPLATE/defect-report.yml @@ -67,6 +67,7 @@ body: - 4.12.x - 4.13.0 - 4.13.1 + - 4.13.2 - 4.14.0-SNAPSHOT validations: required: true diff --git a/docker/Dockerfile.alpine b/docker/Dockerfile.alpine index 085195e81..6a5d84d87 100644 --- a/docker/Dockerfile.alpine +++ b/docker/Dockerfile.alpine @@ -1,4 +1,4 @@ -FROM nginxinc/nginx-unprivileged:1.27.5-alpine@sha256:b16a09b2066fdc77e883e9977390ba2dcb9f1850df0cafacb5167f7b4bc35df1 +FROM nginxinc/nginx-unprivileged:1.27.5-alpine@sha256:82a240b6d2f12d0154090b4a43425ea49c29f25e77683f36e52bca74da79bf0e # Arguments that can be passed at build time ARG COMMIT_SHA=unknown diff --git a/src/i18n/locales/de.json b/src/i18n/locales/de.json index 311c17a8c..6a52ae7e9 100644 --- a/src/i18n/locales/de.json +++ b/src/i18n/locales/de.json @@ -913,7 +913,12 @@ "weakness": "Schwäche", "will_not_fix": "Wird nicht repariert", "workaround_available": "Problemumgehung verfügbar", - "x_trust_boundary": "Vertrauensgrenze überschreiten" + "x_trust_boundary": "Vertrauensgrenze überschreiten", + "apply": "Anwenden", + "bulk_update": "Bulk-Update", + "no_projects_selected": "Keine Projekte ausgewählt", + "projects_selected": "Projekte ausgewählt", + "unsuppress": "Unsuppression" }, "operator": { "contains_all": "enthält alle", diff --git a/src/i18n/locales/en.json b/src/i18n/locales/en.json index 527fb23f7..0f3a8a478 100644 --- a/src/i18n/locales/en.json +++ b/src/i18n/locales/en.json @@ -850,6 +850,7 @@ "supplier_name": "Supplier name", "suppress": "Suppress", "suppressed": "Suppressed", + "unsuppress": "Unsuppress", "swid": "swid", "swid_tagid": "SWID Tag ID", "switch_view": "Cannot switch view while searching", @@ -913,7 +914,11 @@ "weakness": "Weakness", "will_not_fix": "Will not fix", "workaround_available": "Workaround available", - "x_trust_boundary": "Cross Trust Boundary" + "x_trust_boundary": "Cross Trust Boundary", + "apply": "Apply", + "bulk_update": "Bulk-Update", + "projects_selected": "projects selected", + "no_projects_selected": "No projects selected" }, "operator": { "contains_all": "contains all", diff --git a/src/i18n/locales/es.json b/src/i18n/locales/es.json index 072b460b7..f3fef23df 100644 --- a/src/i18n/locales/es.json +++ b/src/i18n/locales/es.json @@ -913,7 +913,12 @@ "weakness": "Debilidad", "will_not_fix": "No se reparara", "workaround_available": "Solución alternativa disponible", - "x_trust_boundary": "Cruzar el límite de confianza" + "x_trust_boundary": "Cruzar el límite de confianza", + "apply": "Aplicar", + "bulk_update": "A granel-actualización", + "no_projects_selected": "No hay proyectos seleccionados", + "projects_selected": "Proyectos seleccionados", + "unsuppress": "No compresión" }, "operator": { "contains_all": "contiene todo", diff --git a/src/i18n/locales/fr.json b/src/i18n/locales/fr.json index 933d617b3..abe988508 100644 --- a/src/i18n/locales/fr.json +++ b/src/i18n/locales/fr.json @@ -913,7 +913,12 @@ "weakness": "Faiblesse", "will_not_fix": "Ne sera pas corrigée", "workaround_available": "Solution de contournement disponible", - "x_trust_boundary": "Limte de confiance mutuelle" + "x_trust_boundary": "Limte de confiance mutuelle", + "apply": "Appliquer", + "bulk_update": "Mettre à jour", + "no_projects_selected": "Aucun projet sélectionné", + "projects_selected": "Projets sélectionnés", + "unsuppress": "Se défoncer" }, "operator": { "contains_all": "contient tous", diff --git a/src/i18n/locales/hi.json b/src/i18n/locales/hi.json index 5c0ef58c8..522b762ab 100644 --- a/src/i18n/locales/hi.json +++ b/src/i18n/locales/hi.json @@ -913,7 +913,12 @@ "weakness": "कमजोरी", "will_not_fix": "ठीक नहीं होगा", "workaround_available": "वैकल्पिक उपाय उपलब्ध है", - "x_trust_boundary": "क्रॉस ट्रस्ट सीमा" + "x_trust_boundary": "क्रॉस ट्रस्ट सीमा", + "apply": "आवेदन करना", + "bulk_update": "उकसाने वाला", + "no_projects_selected": "कोई परियोजना नहीं चुनी", + "projects_selected": "चयनित परियोजनाएं", + "unsuppress": "बेकार" }, "operator": { "contains_all": "इसमें सभी शामिल हैं", diff --git a/src/i18n/locales/it.json b/src/i18n/locales/it.json index f1af39dc8..e96de4374 100644 --- a/src/i18n/locales/it.json +++ b/src/i18n/locales/it.json @@ -913,7 +913,12 @@ "weakness": "Debolezza", "will_not_fix": "Non risolverà", "workaround_available": "Soluzione disponibile", - "x_trust_boundary": "Confine di fiducia incrociata" + "x_trust_boundary": "Confine di fiducia incrociata", + "apply": "Fare domanda a", + "bulk_update": "Aggiornamento in blocco", + "no_projects_selected": "Nessun progetto selezionato", + "projects_selected": "progetti selezionati", + "unsuppress": "Non sopprimere" }, "operator": { "contains_all": "contiene tutto", diff --git a/src/i18n/locales/ja.json b/src/i18n/locales/ja.json index 6dbe38318..2fe4ed4f4 100644 --- a/src/i18n/locales/ja.json +++ b/src/i18n/locales/ja.json @@ -913,7 +913,12 @@ "weakness": "弱点", "will_not_fix": "修正しない", "workaround_available": "回避策あり", - "x_trust_boundary": "信頼境界を越える" + "x_trust_boundary": "信頼境界を越える", + "apply": "適用する", + "bulk_update": "バルクアップデート", + "no_projects_selected": "選択されたプロジェクトはありません", + "projects_selected": "選択されたプロジェクト", + "unsuppress": "サプレスを抑制します" }, "operator": { "contains_all": "すべてを含む", diff --git a/src/i18n/locales/pl.json b/src/i18n/locales/pl.json index 2fcb25a1e..8f48061d7 100644 --- a/src/i18n/locales/pl.json +++ b/src/i18n/locales/pl.json @@ -913,7 +913,12 @@ "weakness": "Słabość", "will_not_fix": "Nie naprawi", "workaround_available": "Dostępne obejście", - "x_trust_boundary": "Granica zaufania krzyżowego" + "x_trust_boundary": "Granica zaufania krzyżowego", + "apply": "Stosować", + "bulk_update": "Zakład", + "no_projects_selected": "Nie wybrano projektów", + "projects_selected": "Wybrane projekty", + "unsuppress": "Unsuppress" }, "operator": { "contains_all": "zawiera wszystko", diff --git a/src/i18n/locales/pt-BR.json b/src/i18n/locales/pt-BR.json index 69c9f87f0..9f80ff88f 100644 --- a/src/i18n/locales/pt-BR.json +++ b/src/i18n/locales/pt-BR.json @@ -913,7 +913,12 @@ "weakness": "Fraqueza", "will_not_fix": "Não irá corrigir", "workaround_available": "Solução alternativa disponível", - "x_trust_boundary": "Limite de confiança cruzada" + "x_trust_boundary": "Limite de confiança cruzada", + "apply": "Aplicar", + "bulk_update": "Update em massa", + "no_projects_selected": "Nenhum projeto selecionado", + "projects_selected": "projetos selecionados", + "unsuppress": "Não suprimido" }, "operator": { "contains_all": "contém tudo", diff --git a/src/i18n/locales/pt.json b/src/i18n/locales/pt.json index b2c4ce1fa..f856216f0 100644 --- a/src/i18n/locales/pt.json +++ b/src/i18n/locales/pt.json @@ -913,7 +913,12 @@ "weakness": "Fraqueza", "will_not_fix": "Não irá corrigir", "workaround_available": "Solução alternativa disponível", - "x_trust_boundary": "Limite de confiança cruzada" + "x_trust_boundary": "Limite de confiança cruzada", + "apply": "Aplicar", + "bulk_update": "Update em massa", + "no_projects_selected": "Nenhum projeto selecionado", + "projects_selected": "projetos selecionados", + "unsuppress": "Não suprimido" }, "operator": { "contains_all": "contém tudo", diff --git a/src/i18n/locales/ru.json b/src/i18n/locales/ru.json index da0fc2705..7ce3e4b5b 100644 --- a/src/i18n/locales/ru.json +++ b/src/i18n/locales/ru.json @@ -913,7 +913,12 @@ "weakness": "Слабость", "will_not_fix": "Не будет исправлено", "workaround_available": "Доступно обходное решение", - "x_trust_boundary": "Пересечение границы доверия" + "x_trust_boundary": "Пересечение границы доверия", + "apply": "Применять", + "bulk_update": "Насыпный обрыв", + "no_projects_selected": "Никаких проектов не выбрано", + "projects_selected": "Проекты выбраны", + "unsuppress": "Неспособность" }, "operator": { "contains_all": "содержит все", diff --git a/src/i18n/locales/uk-UA.json b/src/i18n/locales/uk-UA.json index be1993309..7e96f0ced 100644 --- a/src/i18n/locales/uk-UA.json +++ b/src/i18n/locales/uk-UA.json @@ -913,7 +913,12 @@ "weakness": "Слабкість", "will_not_fix": "Не виправить", "workaround_available": "Доступний обхідний шлях", - "x_trust_boundary": "Перетнути кордон довіри" + "x_trust_boundary": "Перетнути кордон довіри", + "apply": "Застосовувати", + "bulk_update": "Об'ємний", + "no_projects_selected": "Жодних проектів не вибрано", + "projects_selected": "Вибрані проекти", + "unsuppress": "Невимушений" }, "operator": { "contains_all": "містить усе", diff --git a/src/i18n/locales/zh.json b/src/i18n/locales/zh.json index 959658517..cff38b6e0 100644 --- a/src/i18n/locales/zh.json +++ b/src/i18n/locales/zh.json @@ -913,7 +913,12 @@ "weakness": "弱点", "will_not_fix": "不会修复", "workaround_available": "有解决方法", - "x_trust_boundary": "跨越信任边界" + "x_trust_boundary": "跨越信任边界", + "apply": "申请", + "bulk_update": "散装", + "no_projects_selected": "没有选择项目", + "projects_selected": "选择的项目", + "unsuppress": "取消抑制" }, "operator": { "contains_all": "包含全部", diff --git a/src/views/portfolio/projects/FindingAudit.vue b/src/views/portfolio/projects/FindingAudit.vue index 2668c0780..ad495cd17 100644 --- a/src/views/portfolio/projects/FindingAudit.vue +++ b/src/views/portfolio/projects/FindingAudit.vue @@ -20,7 +20,7 @@ {{ alias.vulnId }}{{ alias.vulnId }} @@ -143,7 +143,7 @@ />
+ > {{ this.$t('message.add_comment') }}
@@ -240,7 +240,7 @@ size="sm" variant="outline-primary" @click="makeAnalysis" - > + > {{ this.$t('message.update_details') }} diff --git a/src/views/portfolio/vulnerabilities/AffectedProjects.vue b/src/views/portfolio/vulnerabilities/AffectedProjects.vue index 050ffe2c9..87f693109 100644 --- a/src/views/portfolio/vulnerabilities/AffectedProjects.vue +++ b/src/views/portfolio/vulnerabilities/AffectedProjects.vue @@ -11,6 +11,32 @@ />{{ $t('message.show_inactive_projects') }} + + {{ $t('message.bulk_update') }} + + + + {{ $t('message.suppress') }} + + + + {{ $t('message.unsuppress') }} + + + @@ -28,11 +60,14 @@ import xssFilters from 'xss-filters'; import permissionsMixin from '../../../mixins/permissionsMixin'; import { Switch as cSwitch } from '@coreui/vue'; +import BulkUpdateModal from '@/views/portfolio/vulnerabilities/BulkUpdateModal.vue'; +import common from '@/shared/common'; export default { mixins: [permissionsMixin], components: { cSwitch, + BulkUpdateModal, }, beforeCreate() { this.showInactiveProjects = @@ -50,11 +85,17 @@ export default { data() { return { showInactiveProjects: this.showInactiveProjects, + selectedProjects: [], labelIcon: { dataOn: '\u2713', dataOff: '\u2715', }, columns: [ + { + field: 'state', + checkbox: true, + align: 'center', + }, { title: this.$t('message.name'), field: 'name', @@ -139,7 +180,140 @@ export default { onPostBody: function () { this.$refs.table.hideLoading(); }, + + openModal() { + this.selectedProjects = this.$refs.table.getSelections(); + const selected = this.$refs.table.getSelections(); + if (!selected || selected.length === 0) { + this.$toastr.w(this.$t('message.no_projects_selected')); + return; + } + + this.$bvModal.show('bulkUpdateModal'); + }, + + // Method for receiving update from modal and calling API. + handleBulkApply(output) { + // Iterate over each selected project + for (const project of output.selectedProjects) { + for (const component of project.affectedComponentUuids) { + this.callRestEndpoint( + project.uuid, + component, + this.vulnerability, + output.analysisState, + output.analysisJustification, + output.analysisResponse, + output.analysisDetails, + output.comment, + output.isSuppressed, + ); + } + } + this.refreshTable(); + }, + + suppressSelected(suppress) { + const selected = this.$refs.table.getSelections(); + if (!selected || selected.length === 0) { + this.$toastr.w(this.$t('message.no_projects_selected')); + return; + } + for (const project of selected) { + for (const component of project.affectedComponentUuids) { + this.callRestEndpoint( + project.uuid, + component, + this.vulnerability, + null, // analysisState + null, // justification + null, // response + null, // details + null, // comment + suppress + ); + } + } + this.refreshTable(); + }, + + updateAnalysisData: function (analysis) { + if (Object.prototype.hasOwnProperty.call(analysis, 'analysisComments')) { + let trail = ''; + for (let i = 0; i < analysis.analysisComments.length; i++) { + if ( + Object.prototype.hasOwnProperty.call( + analysis.analysisComments[i], + 'commenter', + ) + ) { + trail += analysis.analysisComments[i].commenter + ' - '; + } + trail += common.formatTimestamp( + analysis.analysisComments[i].timestamp, + true, + ); + trail += '\n'; + trail += analysis.analysisComments[i].comment; + trail += '\n\n'; + } + this.auditTrail = trail; + } + if (Object.prototype.hasOwnProperty.call(analysis, 'analysisState')) { + this.analysisState = analysis.analysisState; + } + if ( + Object.prototype.hasOwnProperty.call(analysis, 'analysisJustification') + ) { + this.analysisJustification = analysis.analysisJustification; + } + if (Object.prototype.hasOwnProperty.call(analysis, 'analysisResponse')) { + this.analysisResponse = analysis.analysisResponse; + } + if (Object.prototype.hasOwnProperty.call(analysis, 'analysisDetails')) { + this.analysisDetails = analysis.analysisDetails; + } + if (Object.prototype.hasOwnProperty.call(analysis, 'isSuppressed')) { + this.isSuppressed = analysis.isSuppressed; + } else { + this.isSuppressed = false; + } + }, + + callRestEndpoint: function ( + projectUuid, + componentUuid, + vulnerabilityUuid, + analysisState, + analysisJustification, + analysisResponse, + analysisDetails, + comment, + isSuppressed, + ) { + let url = `${this.$api.BASE_URL}/${this.$api.URL_ANALYSIS}`; + this.axios + .put(url, { + project: projectUuid, + component: componentUuid, + vulnerability: vulnerabilityUuid, + analysisState: analysisState, + analysisJustification: analysisJustification, + analysisResponse: analysisResponse, + analysisDetails: analysisDetails, + comment: comment, + isSuppressed: isSuppressed, + }) + .then((response) => { + this.$toastr.s(this.$t('message.updated')); + this.updateAnalysisData(response.data); + }) + .catch((error) => { + this.$toastr.w(this.$t('condition.unsuccessful_action')); + }); + }, }, + watch: { showInactiveProjects() { if (localStorage) { diff --git a/src/views/portfolio/vulnerabilities/BulkUpdateModal.vue b/src/views/portfolio/vulnerabilities/BulkUpdateModal.vue new file mode 100644 index 000000000..15fe78a59 --- /dev/null +++ b/src/views/portfolio/vulnerabilities/BulkUpdateModal.vue @@ -0,0 +1,211 @@ + + + + +