From 74dbc9f002c41f543ae42f55a4e049a72c3f1cf8 Mon Sep 17 00:00:00 2001
From: mykter <git@mykter.com>
Date: Tue, 26 Dec 2023 18:35:02 +0000
Subject: [PATCH 1/4] Refactor finding audit detail to SFC

Signed-off-by: mykter <git@mykter.com>
---
 src/views/portfolio/projects/FindingAudit.vue | 246 ++++++++++++++++++
 .../portfolio/projects/ProjectFindings.vue    | 224 +---------------
 vue.config.js                                 |   1 +
 3 files changed, 260 insertions(+), 211 deletions(-)
 create mode 100644 src/views/portfolio/projects/FindingAudit.vue

diff --git a/src/views/portfolio/projects/FindingAudit.vue b/src/views/portfolio/projects/FindingAudit.vue
new file mode 100644
index 000000000..b3038f944
--- /dev/null
+++ b/src/views/portfolio/projects/FindingAudit.vue
@@ -0,0 +1,246 @@
+<template>
+    <b-row class="expanded-row">
+        <b-col sm="6">
+            <div v-if="finding.vulnerability.aliases && finding.vulnerability.aliases.length > 0">
+                <label>Aliases</label>
+                <b-card class="font-weight-bold">
+                    <b-card-text>
+                        <span
+                            v-for="alias in resolveVulnAliases(finding.vulnerability.aliases, finding.vulnerability.source)">
+                            <b-link style="margin-right:1.0rem"
+                                :href="'/vulnerabilities/' + alias.source + '/' + alias.vulnId">{{ alias.vulnId }}</b-link>
+                        </span>
+                    </b-card-text>
+                </b-card>
+            </div>
+            <b-form-group v-if="finding.vulnerability.title" id="fieldset-1" :label="this.$t('message.title')"
+                label-for="input-1">
+                <b-form-input id="input-1" v-model="finding.vulnerability.title" class="form-control disabled" readonly
+                    trim />
+            </b-form-group>
+            <b-form-group v-if="finding.vulnerability.subtitle" id="fieldset-2" :label="this.$t('message.subtitle')"
+                label-for="input-2">
+                <b-form-input id="input-2" v-model="finding.vulnerability.subtitle" class="form-control disabled" readonly
+                    trim />
+            </b-form-group>
+            <b-form-group v-if="finding.vulnerability.description" id="fieldset-3" :label="this.$t('message.description')"
+                label-for="input-3">
+                <b-form-textarea id="input-3" v-model="finding.vulnerability.description" rows="7"
+                    class="form-control disabled" readonly trim />
+            </b-form-group>
+            <b-form-group v-if="finding.vulnerability.recommendation" id="fieldset-4"
+                :label="this.$t('message.recommendation')" label-for="input-4">
+                <b-form-textarea id="input-4" v-model="finding.vulnerability.recommendation" rows="7"
+                    class="form-control disabled" readonly trim />
+            </b-form-group>
+            <b-form-group v-if="finding.vulnerability.cvssV2Vector" id="fieldset-5"
+                :label="this.$t('message.cvss_v2_vector')" label-for="input-5">
+                <b-form-input id="input-5" v-model="finding.vulnerability.cvssV2Vector" class="form-control disabled"
+                    readonly trim />
+            </b-form-group>
+            <b-form-group v-if="finding.vulnerability.cvssV3Vector" id="fieldset-6"
+                :label="this.$t('message.cvss_v3_vector')" label-for="input-6">
+                <b-form-input id="input-6" v-model="finding.vulnerability.cvssV3Vector" class="form-control disabled"
+                    readonly trim />
+            </b-form-group>
+        </b-col>
+        <b-col sm="6">
+            <b-form-group id="fieldset-7" :label="this.$t('message.audit_trail')" label-for="auditTrailField">
+                <b-form-textarea id="auditTrailField" v-model="auditTrail" rows="7" class="form-control disabled" readonly
+                    trim />
+            </b-form-group>
+            <b-form-group id="fieldset-8" v-if="this.isPermitted(this.PERMISSIONS.VULNERABILITY_ANALYSIS)"
+                :label="this.$t('message.comment')" label-for="input-8">
+                <b-form-textarea id="input-8" v-model="comment" rows="4" class="form-control" trim />
+                <div class="pull-right">
+                    <b-button size="sm" variant="outline-primary" @click="addComment"><span class="fa fa-comment-o"></span>
+                        {{ this.$t('message.add_comment') }}</b-button>
+                </div>
+            </b-form-group>
+            <b-form-group id="fieldset-9" v-if="this.isPermitted(this.PERMISSIONS.VULNERABILITY_ANALYSIS)"
+                :label="this.$t('message.analysis')" label-for="input-9">
+                <b-input-group id="input-9">
+                    <b-form-select v-model="analysisState" :options="analysisChoices" @change="makeAnalysis"
+                        style="flex:0 1 auto; width:auto; margin-right:2rem;" v-b-tooltip.hover
+                        :title="this.$t('message.analysis_tooltip')" />
+                    <bootstrap-toggle v-model="isSuppressed"
+                        :options="{ on: this.$t('message.suppressed'), off: this.$t('message.suppress'), onstyle: 'warning', offstyle: 'outline-disabled' }"
+                        :disabled="analysisState === null" />
+                </b-input-group>
+            </b-form-group>
+            <b-row v-if="this.isPermitted(this.PERMISSIONS.VULNERABILITY_ANALYSIS)">
+                <b-col sm="6">
+                    <b-form-group id="fieldset-10" :label="this.$t('message.justification')" label-for="input-10">
+                        <b-input-group id="input-10">
+                            <b-form-select v-model="analysisJustification" :options="justificationChoices"
+                                @change="makeAnalysis"
+                                :disabled="analysisState === null || analysisState !== 'NOT_AFFECTED'" v-b-tooltip.hover
+                                :title="$t('message.justification_tooltip')" />
+                        </b-input-group>
+                    </b-form-group>
+                </b-col>
+                <b-col sm="6">
+                    <b-form-group id="fieldset-11" :label="this.$t('message.response')" label-for="input-11">
+                        <b-input-group id="input-11">
+                            <b-form-select v-model="analysisResponse" :options="responseChoices"
+                                :disabled="analysisState === null" @change="makeAnalysis" v-b-tooltip.hover
+                                :title="this.$t('message.response_tooltip')" />
+                        </b-input-group>
+                    </b-form-group>
+                </b-col>
+            </b-row>
+            <b-form-group id="fieldset-12" v-if="this.isPermitted(this.PERMISSIONS.VIEW_VULNERABILITY)"
+                :label="this.$t('message.details')" label-for="analysisDetailsField">
+                <b-form-textarea id="analysisDetailsField" v-model="analysisDetails" rows="7" class="form-control"
+                    :disabled="analysisState === null || !this.isPermitted(this.PERMISSIONS.VULNERABILITY_ANALYSIS)"
+                    v-b-tooltip.hover :title="this.$t('message.analysis_details_tooltip')" />
+                <div class="pull-right">
+                    <b-button v-if="this.isPermitted(this.PERMISSIONS.VULNERABILITY_ANALYSIS)"
+                        :disabled="analysisState === null" size="sm" variant="outline-primary" @click="makeAnalysis"><span
+                            class="fa fa-comment-o"></span> {{ this.$t('message.update_details') }}</b-button>
+                </div>
+            </b-form-group>
+        </b-col>
+    </b-row>
+</template>
+
+<script>
+import common from "@/shared/common";
+import BootstrapToggle from 'vue-bootstrap-toggle';
+import permissionsMixin from "@/mixins/permissionsMixin";
+
+export default {
+    props: {
+        finding: Object,
+        projectUuid: String
+    },
+    data() {
+        return {
+            auditTrail: null,
+            comment: null,
+            isSuppressed: !!(this.finding?.analysis?.isSuppressed),
+            analysisChoices: [
+                { value: 'NOT_SET', text: this.$t('message.not_set') },
+                { value: 'EXPLOITABLE', text: this.$t('message.exploitable') },
+                { value: 'IN_TRIAGE', text: this.$t('message.in_triage') },
+                { value: 'RESOLVED', text: this.$t('message.resolved') },
+                { value: 'FALSE_POSITIVE', text: this.$t('message.false_positive') },
+                { value: 'NOT_AFFECTED', text: this.$t('message.not_affected') },
+            ],
+            justificationChoices: [
+                { value: 'NOT_SET', text: this.$t('message.not_set') },
+                { value: 'CODE_NOT_PRESENT', text: this.$t('message.code_not_present') },
+                { value: 'CODE_NOT_REACHABLE', text: this.$t('message.code_not_reachable') },
+                { value: 'REQUIRES_CONFIGURATION', text: this.$t('message.requires_configuration') },
+                { value: 'REQUIRES_DEPENDENCY', text: this.$t('message.requires_dependency') },
+                { value: 'REQUIRES_ENVIRONMENT', text: this.$t('message.requires_environment') },
+                { value: 'PROTECTED_BY_COMPILER', text: this.$t('message.protected_by_compiler') },
+                { value: 'PROTECTED_AT_RUNTIME', text: this.$t('message.protected_at_runtime') },
+                { value: 'PROTECTED_AT_PERIMETER', text: this.$t('message.protected_at_perimeter') },
+                { value: 'PROTECTED_BY_MITIGATING_CONTROL', text: this.$t('message.protected_by_mitigating_control') }
+            ],
+            responseChoices: [
+                { value: 'NOT_SET', text: this.$t('message.not_set') },
+                { value: 'CAN_NOT_FIX', text: this.$t('message.can_not_fix') },
+                { value: 'WILL_NOT_FIX', text: this.$t('message.will_not_fix') },
+                { value: 'UPDATE', text: this.$t('message.update') },
+                { value: 'ROLLBACK', text: this.$t('message.rollback') },
+                { value: 'WORKAROUND_AVAILABLE', text: this.$t('message.workaround_available') }
+            ],
+            analysisState: null,
+            analysisJustification: null,
+            analysisResponse: null,
+            analysisDetails: null,
+        }
+    },
+    watch: {
+        isSuppressed: function (currentValue, oldValue) {
+            if (oldValue != null) {
+                this.callRestEndpoint(this.analysisState, this.analysisJustification, this.analysisResponse, null, null, currentValue);
+            }
+        }
+    },
+    mixins: [permissionsMixin],
+    methods: {
+        resolveVulnAliases: function (aliases, vulnSource) {
+            return common.resolveVulnAliases(vulnSource ? vulnSource : this.source, aliases);
+        },
+        getAnalysis: function () {
+            let queryString = "?project=" + this.projectUuid + "&component=" + this.finding.component.uuid + "&vulnerability=" + this.finding.vulnerability.uuid;
+            let url = `${this.$api.BASE_URL}/${this.$api.URL_ANALYSIS}` + queryString;
+            this.axios.get(url, {
+                validateStatus: (status) => status === 200 || status === 404
+            }).then((response) => {
+                this.updateAnalysisData(response.data);
+            });
+        },
+        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;
+            }
+        },
+        makeAnalysis: function () {
+            this.callRestEndpoint(this.analysisState, this.analysisJustification, this.analysisResponse, this.analysisDetails, null, null);
+        },
+        addComment: function () {
+            if (this.comment != null) {
+                this.callRestEndpoint(this.analysisState, this.analysisJustification, this.analysisResponse, this.analysisDetails, this.comment, null);
+            }
+            this.comment = null;
+        },
+        callRestEndpoint: function (analysisState, analysisJustification, analysisResponse, analysisDetails, comment, isSuppressed) {
+            let url = `${this.$api.BASE_URL}/${this.$api.URL_ANALYSIS}`;
+            this.axios.put(url, {
+                project: this.projectUuid,
+                component: this.finding.component.uuid,
+                vulnerability: this.finding.vulnerability.uuid,
+                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'));
+            });
+        }
+    },
+    beforeMount() {
+        this.finding && this.getAnalysis();
+    },
+    components: {
+        BootstrapToggle
+    }
+
+}
+</script>
\ No newline at end of file
diff --git a/src/views/portfolio/projects/ProjectFindings.vue b/src/views/portfolio/projects/ProjectFindings.vue
index 6f342ea17..de0cbe9fe 100644
--- a/src/views/portfolio/projects/ProjectFindings.vue
+++ b/src/views/portfolio/projects/ProjectFindings.vue
@@ -59,16 +59,17 @@
 </template>
 
 <script>
-import { compareVersions, loadUserPreferencesForBootstrapTable } from "@/shared/utils";
-import ProjectUploadVexModal from "@/views/portfolio/projects/ProjectUploadVexModal";
 import { Switch as cSwitch } from '@coreui/vue';
 import $ from "jquery";
-import BootstrapToggle from 'vue-bootstrap-toggle';
 import xssFilters from "xss-filters";
-import i18n from "../../../i18n";
-import bootstrapTableMixin from "../../../mixins/bootstrapTableMixin";
-import permissionsMixin from "../../../mixins/permissionsMixin";
-import common from "../../../shared/common";
+
+import common from "@/shared/common";
+import i18n from "@/i18n";
+import { compareVersions, loadUserPreferencesForBootstrapTable } from "@/shared/utils";
+import bootstrapTableMixin from "@/mixins/bootstrapTableMixin";
+import permissionsMixin from "@/mixins/permissionsMixin";
+import FindingAudit from "./FindingAudit";
+import ProjectUploadVexModal from "./ProjectUploadVexModal";
 
   export default {
     props: {
@@ -80,7 +81,6 @@ import common from "../../../shared/common";
     ],
     components: {
       cSwitch,
-      BootstrapToggle,
       ProjectUploadVexModal
     },
     beforeCreate() {
@@ -249,211 +249,13 @@ import common from "../../../shared/common";
           detailViewIcon: true,
           detailViewByClick: false,
           detailFormatter: (index, row) => {
-            let projectUuid = this.uuid;
-            return this.vueFormatter({
+            return row && this.vueFormatter({
               i18n,
-              template: `
-                <b-row class="expanded-row">
-                  <b-col sm="6">
-                    <div v-if="finding.vulnerability.aliases && finding.vulnerability.aliases.length > 0">
-                    <label>Aliases</label>
-                      <b-card class="font-weight-bold">
-                        <b-card-text>
-                          <span v-for="alias in resolveVulnAliases(finding.vulnerability.aliases, finding.vulnerability.source)">
-                          <b-link style="margin-right:1.0rem" :href="'/vulnerabilities/' + alias.source + '/' + alias.vulnId">{{ alias.vulnId }}</b-link>
-                          </span>
-                        </b-card-text>
-                     </b-card>
-                    </div>
-                    <b-form-group v-if="finding.vulnerability.title" id="fieldset-1" :label="this.$t('message.title')" label-for="input-1">
-                      <b-form-input id="input-1" v-model="finding.vulnerability.title" class="form-control disabled" readonly trim />
-                    </b-form-group>
-                    <b-form-group v-if="finding.vulnerability.subtitle" id="fieldset-2" :label="this.$t('message.subtitle')" label-for="input-2">
-                      <b-form-input id="input-2" v-model="finding.vulnerability.subtitle" class="form-control disabled" readonly trim />
-                    </b-form-group>
-                    <b-form-group v-if="finding.vulnerability.description" id="fieldset-3" :label="this.$t('message.description')" label-for="input-3">
-                      <b-form-textarea id="input-3" v-model="finding.vulnerability.description" rows="7" class="form-control disabled" readonly trim />
-                    </b-form-group>
-                    <b-form-group v-if="finding.vulnerability.recommendation" id="fieldset-4" :label="this.$t('message.recommendation')" label-for="input-4">
-                      <b-form-textarea id="input-4" v-model="finding.vulnerability.recommendation" rows="7" class="form-control disabled" readonly trim />
-                    </b-form-group>
-                    <b-form-group v-if="finding.vulnerability.cvssV2Vector" id="fieldset-5" :label="this.$t('message.cvss_v2_vector')" label-for="input-5">
-                      <b-form-input id="input-5" v-model="finding.vulnerability.cvssV2Vector" class="form-control disabled" readonly trim />
-                    </b-form-group>
-                    <b-form-group v-if="finding.vulnerability.cvssV3Vector" id="fieldset-6" :label="this.$t('message.cvss_v3_vector')" label-for="input-6">
-                      <b-form-input id="input-6" v-model="finding.vulnerability.cvssV3Vector" class="form-control disabled" readonly trim />
-                    </b-form-group>
-                  </b-col>
-                  <b-col sm="6">
-                    <b-form-group id="fieldset-7" :label="this.$t('message.audit_trail')" label-for="auditTrailField">
-                      <b-form-textarea id="auditTrailField" v-model="auditTrail" rows="7" class="form-control disabled" readonly trim />
-                    </b-form-group>
-                    <b-form-group id="fieldset-8" v-if="this.isPermitted(this.PERMISSIONS.VULNERABILITY_ANALYSIS)" :label="this.$t('message.comment')" label-for="input-8">
-                      <b-form-textarea id="input-8" v-model="comment" rows="4" class="form-control" trim />
-                      <div class="pull-right">
-                        <b-button size="sm" variant="outline-primary" @click="addComment"><span class="fa fa-comment-o"></span> {{ this.$t('message.add_comment') }}</b-button>
-                      </div>
-                    </b-form-group>
-                    <b-form-group id="fieldset-9" v-if="this.isPermitted(this.PERMISSIONS.VULNERABILITY_ANALYSIS)" :label="this.$t('message.analysis')" label-for="input-9">
-                      <b-input-group id="input-9">
-                        <b-form-select v-model="analysisState" :options="analysisChoices" @change="makeAnalysis" style="flex:0 1 auto; width:auto; margin-right:2rem;" v-b-tooltip.hover :title="this.$t('message.analysis_tooltip')"/>
-                        <bootstrap-toggle v-model="isSuppressed" :options="{ on: this.$t('message.suppressed'), off: this.$t('message.suppress'), onstyle: 'warning', offstyle: 'outline-disabled'}" :disabled="analysisState === null" />
-                      </b-input-group>
-                    </b-form-group>
-                    <b-row v-if="this.isPermitted(this.PERMISSIONS.VULNERABILITY_ANALYSIS)">
-                      <b-col sm="6">
-                        <b-form-group id="fieldset-10" :label="this.$t('message.justification')" label-for="input-10">
-                          <b-input-group id="input-10">
-                            <b-form-select v-model="analysisJustification" :options="justificationChoices" @change="makeAnalysis" :disabled="analysisState === null || analysisState !== 'NOT_AFFECTED'" v-b-tooltip.hover :title="$t('message.justification_tooltip')" />
-                          </b-input-group>
-                        </b-form-group>
-                      </b-col>
-                      <b-col sm="6">
-                        <b-form-group id="fieldset-11" :label="this.$t('message.response')" label-for="input-11">
-                          <b-input-group id="input-11">
-                            <b-form-select v-model="analysisResponse" :options="responseChoices" :disabled="analysisState === null" @change="makeAnalysis" v-b-tooltip.hover :title="this.$t('message.response_tooltip')" />
-                          </b-input-group>
-                        </b-form-group>
-                      </b-col>
-                    </b-row>
-                    <b-form-group id="fieldset-12" v-if="this.isPermitted(this.PERMISSIONS.VIEW_VULNERABILITY)" :label="this.$t('message.details')" label-for="analysisDetailsField">
-                      <b-form-textarea id="analysisDetailsField" v-model="analysisDetails" rows="7" class="form-control" :disabled="analysisState === null || !this.isPermitted(this.PERMISSIONS.VULNERABILITY_ANALYSIS)" v-b-tooltip.hover :title="this.$t('message.analysis_details_tooltip')" />
-                      <div class="pull-right">
-                        <b-button v-if="this.isPermitted(this.PERMISSIONS.VULNERABILITY_ANALYSIS)" :disabled="analysisState === null" size="sm" variant="outline-primary" @click="makeAnalysis"><span class="fa fa-comment-o"></span> {{ this.$t('message.update_details') }}</b-button>
-                      </div>
-                    </b-form-group>
-                  </b-col>
-                </b-row>
-              `,
-              data() {
-                return {
-                  auditTrail: null,
-                  comment: null,
-                  isSuppressed: !!(row && row.analysis && row.analysis.isSuppressed),
-                  finding: row,
-                  analysisChoices: [
-                    { value: 'NOT_SET', text: this.$t('message.not_set') },
-                    { value: 'EXPLOITABLE', text: this.$t('message.exploitable') },
-                    { value: 'IN_TRIAGE', text: this.$t('message.in_triage') },
-                    { value: 'RESOLVED', text: this.$t('message.resolved') },
-                    { value: 'FALSE_POSITIVE', text: this.$t('message.false_positive') },
-                    { value: 'NOT_AFFECTED', text: this.$t('message.not_affected') },
-                  ],
-                  justificationChoices: [
-                    { value: 'NOT_SET', text: this.$t('message.not_set') },
-                    { value: 'CODE_NOT_PRESENT', text: this.$t('message.code_not_present') },
-                    { value: 'CODE_NOT_REACHABLE', text: this.$t('message.code_not_reachable') },
-                    { value: 'REQUIRES_CONFIGURATION', text: this.$t('message.requires_configuration') },
-                    { value: 'REQUIRES_DEPENDENCY', text: this.$t('message.requires_dependency') },
-                    { value: 'REQUIRES_ENVIRONMENT', text: this.$t('message.requires_environment') },
-                    { value: 'PROTECTED_BY_COMPILER', text: this.$t('message.protected_by_compiler') },
-                    { value: 'PROTECTED_AT_RUNTIME', text: this.$t('message.protected_at_runtime') },
-                    { value: 'PROTECTED_AT_PERIMETER', text: this.$t('message.protected_at_perimeter') },
-                    { value: 'PROTECTED_BY_MITIGATING_CONTROL', text: this.$t('message.protected_by_mitigating_control') }
-                  ],
-                  responseChoices: [
-                    { value: 'NOT_SET', text: this.$t('message.not_set') },
-                    { value: 'CAN_NOT_FIX', text: this.$t('message.can_not_fix') },
-                    { value: 'WILL_NOT_FIX', text: this.$t('message.will_not_fix') },
-                    { value: 'UPDATE', text: this.$t('message.update') },
-                    { value: 'ROLLBACK', text: this.$t('message.rollback') },
-                    { value: 'WORKAROUND_AVAILABLE', text: this.$t('message.workaround_available') }
-                  ],
-                  analysisState: null,
-                  analysisJustification: null,
-                  analysisResponse: null,
-                  analysisDetails: null,
-                  projectUuid: projectUuid
-                }
-              },
-              watch: {
-                isSuppressed: function (currentValue, oldValue) {
-                  if (oldValue != null) {
-                    this.callRestEndpoint(this.analysisState, this.analysisJustification, this.analysisResponse, null, null, currentValue);
-                  }
-                }
-              },
-              mixins: [permissionsMixin],
-              methods: {
-                resolveVulnAliases: function(aliases, vulnSource) {
-                  return common.resolveVulnAliases(vulnSource ? vulnSource : this.source, aliases);
-                },
-                getAnalysis: function() {
-                  let queryString = "?project=" + projectUuid + "&component=" + this.finding.component.uuid + "&vulnerability=" + this.finding.vulnerability.uuid;
-                  let url = `${this.$api.BASE_URL}/${this.$api.URL_ANALYSIS}` + queryString;
-                  this.axios.get(url, {
-                    validateStatus: (status) => status === 200 || status === 404
-                  }).then((response) => {
-                    this.updateAnalysisData(response.data);
-                  });
-                },
-                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;
-                  }
-                },
-                makeAnalysis: function() {
-                  this.callRestEndpoint(this.analysisState,  this.analysisJustification, this.analysisResponse, this.analysisDetails, null, null);
-                },
-                addComment: function() {
-                  if (this.comment != null) {
-                    this.callRestEndpoint(this.analysisState, this.analysisJustification, this.analysisResponse, this.analysisDetails, this.comment, null);
-                  }
-                  this.comment = null;
-                },
-                callRestEndpoint: function(analysisState, analysisJustification, analysisResponse, analysisDetails, comment, isSuppressed) {
-                  let url = `${this.$api.BASE_URL}/${this.$api.URL_ANALYSIS}`;
-                  this.axios.put(url, {
-                    project: projectUuid,
-                    component: this.finding.component.uuid,
-                    vulnerability: this.finding.vulnerability.uuid,
-                    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'));
-                  });
-                }
+              propsData: {
+                finding: row,
+                projectUuid: this.uuid
               },
-              beforeMount() {
-                this.getAnalysis();
-              },
-              components: {
-                BootstrapToggle
-              }
+              ...FindingAudit
             })
           },
           onExpandRow: this.vueFormatterInit,
diff --git a/vue.config.js b/vue.config.js
index 9cf4b618f..d95b9a695 100644
--- a/vue.config.js
+++ b/vue.config.js
@@ -11,6 +11,7 @@ module.exports = {
     proxy: { "/api": { target: process.env.VUE_APP_SERVER_URL} }
   },
   configureWebpack: {
+    devtool: 'source-map',
     plugins: [
       new CopyPlugin([
         { from: "node_modules/axios/dist/axios.min.js", to: "static/js", force: true },

From b10be4c10449d9c0b8b309032ee7699ff2bccd7c Mon Sep 17 00:00:00 2001
From: mykter <git@mykter.com>
Date: Thu, 28 Dec 2023 19:39:57 +0000
Subject: [PATCH 2/4] Fix static analysis issues in formerly inlined code

Signed-off-by: mykter <git@mykter.com>
---
 src/views/portfolio/projects/FindingAudit.vue | 17 +++++++++--------
 1 file changed, 9 insertions(+), 8 deletions(-)

diff --git a/src/views/portfolio/projects/FindingAudit.vue b/src/views/portfolio/projects/FindingAudit.vue
index b3038f944..b271d8d4d 100644
--- a/src/views/portfolio/projects/FindingAudit.vue
+++ b/src/views/portfolio/projects/FindingAudit.vue
@@ -6,7 +6,8 @@
                 <b-card class="font-weight-bold">
                     <b-card-text>
                         <span
-                            v-for="alias in resolveVulnAliases(finding.vulnerability.aliases, finding.vulnerability.source)">
+                            v-for="alias in resolveVulnAliases(finding.vulnerability.aliases, finding.vulnerability.source)"
+                            :key="alias.vulnId">
                             <b-link style="margin-right:1.0rem"
                                 :href="'/vulnerabilities/' + alias.source + '/' + alias.vulnId">{{ alias.vulnId }}</b-link>
                         </span>
@@ -15,32 +16,32 @@
             </div>
             <b-form-group v-if="finding.vulnerability.title" id="fieldset-1" :label="this.$t('message.title')"
                 label-for="input-1">
-                <b-form-input id="input-1" v-model="finding.vulnerability.title" class="form-control disabled" readonly
+                <b-form-input id="input-1" :value="finding.vulnerability.title" class="form-control disabled" readonly
                     trim />
             </b-form-group>
             <b-form-group v-if="finding.vulnerability.subtitle" id="fieldset-2" :label="this.$t('message.subtitle')"
                 label-for="input-2">
-                <b-form-input id="input-2" v-model="finding.vulnerability.subtitle" class="form-control disabled" readonly
+                <b-form-input id="input-2" :value="finding.vulnerability.subtitle" class="form-control disabled" readonly
                     trim />
             </b-form-group>
             <b-form-group v-if="finding.vulnerability.description" id="fieldset-3" :label="this.$t('message.description')"
                 label-for="input-3">
-                <b-form-textarea id="input-3" v-model="finding.vulnerability.description" rows="7"
+                <b-form-textarea id="input-3" :value="finding.vulnerability.description" rows="7"
                     class="form-control disabled" readonly trim />
             </b-form-group>
             <b-form-group v-if="finding.vulnerability.recommendation" id="fieldset-4"
                 :label="this.$t('message.recommendation')" label-for="input-4">
-                <b-form-textarea id="input-4" v-model="finding.vulnerability.recommendation" rows="7"
+                <b-form-textarea id="input-4" :value="finding.vulnerability.recommendation" rows="7"
                     class="form-control disabled" readonly trim />
             </b-form-group>
             <b-form-group v-if="finding.vulnerability.cvssV2Vector" id="fieldset-5"
                 :label="this.$t('message.cvss_v2_vector')" label-for="input-5">
-                <b-form-input id="input-5" v-model="finding.vulnerability.cvssV2Vector" class="form-control disabled"
+                <b-form-input id="input-5" :value="finding.vulnerability.cvssV2Vector" class="form-control disabled"
                     readonly trim />
             </b-form-group>
             <b-form-group v-if="finding.vulnerability.cvssV3Vector" id="fieldset-6"
                 :label="this.$t('message.cvss_v3_vector')" label-for="input-6">
-                <b-form-input id="input-6" v-model="finding.vulnerability.cvssV3Vector" class="form-control disabled"
+                <b-form-input id="input-6" :value="finding.vulnerability.cvssV3Vector" class="form-control disabled"
                     readonly trim />
             </b-form-group>
         </b-col>
@@ -230,7 +231,7 @@ export default {
             }).then((response) => {
                 this.$toastr.s(this.$t('message.updated'));
                 this.updateAnalysisData(response.data);
-            }).catch((error) => {
+            }).catch(() => {
                 this.$toastr.w(this.$t('condition.unsuccessful_action'));
             });
         }

From 3895307e761d56e259b340edfc6e9a320db98e41 Mon Sep 17 00:00:00 2001
From: mykter <git@mykter.com>
Date: Tue, 26 Dec 2023 22:15:43 +0000
Subject: [PATCH 3/4] Fix project finding route with components and/or
 vulnerabilities

Signed-off-by: mykter <git@mykter.com>
---
 .../portfolio/projects/ProjectFindings.vue      | 17 ++++++++++++++---
 1 file changed, 14 insertions(+), 3 deletions(-)

diff --git a/src/views/portfolio/projects/ProjectFindings.vue b/src/views/portfolio/projects/ProjectFindings.vue
index de0cbe9fe..3b0165d99 100644
--- a/src/views/portfolio/projects/ProjectFindings.vue
+++ b/src/views/portfolio/projects/ProjectFindings.vue
@@ -85,6 +85,15 @@ import ProjectUploadVexModal from "./ProjectUploadVexModal";
     },
     beforeCreate() {
       this.showSuppressedFindings = (localStorage && localStorage.getItem("ProjectFindingsShowSuppressedFindings") !== null) ? (localStorage.getItem("ProjectFindingsShowSuppressedFindings") === "true") : false;
+
+      if (this.$route.params.vulnerability) {
+        if (this.$route.params.affectedComponent) {
+          // search for the last portion of the finding's matrix ID
+          this.initialSearchText =  this.$route.params.affectedComponent + ":" + this.$route.params.vulnerability
+        } else {
+          this.initialSearchText = this.$route.params.vulnerability
+        } // the route doesn't allow a component to be specified without a vulnerability
+      }
     },
     data() {
       return {
@@ -239,7 +248,7 @@ import ProjectUploadVexModal from "./ProjectUploadVexModal";
           pageSize: (localStorage && localStorage.getItem("ProjectFindingsPageSize") !== null) ? Number(localStorage.getItem("ProjectFindingsPageSize")) : 10,
           sortName: (localStorage && localStorage.getItem("ProjectFindingsSortName") !== null) ? localStorage.getItem("ProjectFindingsSortName") : undefined,
           sortOrder: (localStorage && localStorage.getItem("ProjectFindingsSortOrder") !== null) ? localStorage.getItem("ProjectFindingsSortOrder") : undefined,
-          searchText: this.$route.params.vulnerability ? ":" + this.$route.params.vulnerability : undefined,
+          searchText: this.initialSearchText,
           icons: {
             detailOpen: 'fa-fw fa-angle-right',
             detailClose: 'fa-fw fa-angle-down',
@@ -366,8 +375,10 @@ import ProjectUploadVexModal from "./ProjectUploadVexModal";
       },
       tableLoaded: function(data) {
         loadUserPreferencesForBootstrapTable(this, "ProjectFindings", this.$refs.table.columns);
-        this.$emit('total', data.total);
-        if (this.$route.params.vulnerability) {
+        this.$emit('total', data.total); // the unfiltered length
+        if (this.$route.params.vulnerability && this.$refs.table.getData().length === 1) {
+          // If there's only one visible row due to a URL that selects a single finding, show it in full
+          // Don't expand if there are multiple findings, as it makes it harder to notice that there is more than one result
           this.$refs.table.expandRow(0);
         }
       },

From cec631bb099cfb885e2c5c03e8b7c2863eab36e7 Mon Sep 17 00:00:00 2001
From: mykter <git@mykter.com>
Date: Thu, 28 Dec 2023 19:54:46 +0000
Subject: [PATCH 4/4] De-duplicate route names

Signed-off-by: mykter <git@mykter.com>
---
 src/router/index.js                                      | 2 +-
 src/views/portfolio/vulnerabilities/AffectedProjects.vue | 2 +-
 2 files changed, 2 insertions(+), 2 deletions(-)

diff --git a/src/router/index.js b/src/router/index.js
index 8f8c76c15..dfae068f9 100644
--- a/src/router/index.js
+++ b/src/router/index.js
@@ -132,7 +132,7 @@ function configRoutes() {
         },
         {
           path: 'projects/:uuid/findings/:vulnerability',
-          name: 'Project Finding Lookup',
+          name: 'Project Vulnerability Lookup',
           props: (route) => ( {
             uuid: route.params.uuid,
             vulnerability: route.params.vulnerability
diff --git a/src/views/portfolio/vulnerabilities/AffectedProjects.vue b/src/views/portfolio/vulnerabilities/AffectedProjects.vue
index 9e36900ce..f3f3e9a9e 100644
--- a/src/views/portfolio/vulnerabilities/AffectedProjects.vue
+++ b/src/views/portfolio/vulnerabilities/AffectedProjects.vue
@@ -27,7 +27,7 @@
             field: "name",
             sortable: true,
             formatter: (value, row) => {
-              const url = this.$router.resolve({name: 'Project Finding Lookup',
+              const url = this.$router.resolve({name: 'Project Vulnerability Lookup',
                   params: {'uuid': row.uuid, vulnerability:this.vulnerability}}).href;
 
               let html = `<a href="${url}">${xssFilters.inHTMLData(value)}</a>`;