From 6b69befbbb2f9401ad7babb5187dc93c470897a7 Mon Sep 17 00:00:00 2001
From: Charlie Kroon <ckroon@gitlab.com>
Date: Tue, 10 Dec 2024 10:02:18 +0000
Subject: [PATCH] Add Link to the Commit Sha where vulnerability was resolved
 to Vulnerability Footer

---
 .../vulnerabilities/components/footer.vue     | 36 ++++++--
 .../frontend/vulnerabilities/footer_spec.js   | 91 ++++++++++++++++++-
 2 files changed, 113 insertions(+), 14 deletions(-)

diff --git a/ee/app/assets/javascripts/vulnerabilities/components/footer.vue b/ee/app/assets/javascripts/vulnerabilities/components/footer.vue
index 242ee44d52a96..7a2cead84882d 100644
--- a/ee/app/assets/javascripts/vulnerabilities/components/footer.vue
+++ b/ee/app/assets/javascripts/vulnerabilities/components/footer.vue
@@ -98,18 +98,34 @@ export default {
     issueLinksEndpoint() {
       return Api.buildUrl(Api.vulnerabilityIssueLinksPath).replace(':id', this.vulnerability.id);
     },
+    isRepresentationInfoAvailable() {
+      return (
+        this.glFeatures.vulnerabilityRepresentationInformation &&
+        this.vulnerability.resolvedOnDefaultBranch &&
+        this.vulnerability.representationInformation?.resolvedInCommitShaLink
+      );
+    },
     vulnerabilityDetectionData() {
-      const { pipeline, scanner, detectedAt } = this.vulnerability;
+      const { pipeline, scanner, detectedAt, representationInformation, resolvedOnDefaultBranch } =
+        this.vulnerability;
 
-      // manually submitted vulnerabilities have no associated pipeline, in that case we don't display the detection data
-      return pipeline
-        ? {
-            state: 'detected',
-            pipeline,
-            scanner,
-            detectedAt,
-          }
-        : null;
+      if (!this.isRepresentationInfoAvailable && !pipeline) {
+        return null;
+      }
+
+      return {
+        state: 'detected',
+        pipeline,
+        scanner,
+        ...(this.isRepresentationInfoAvailable
+          ? {
+              representationInformation,
+              resolvedOnDefaultBranch,
+            }
+          : {
+              detectedAt,
+            }),
+      };
     },
     mergeRequest() {
       return this.vulnerability.mergeRequestLinks.at(-1);
diff --git a/ee/spec/frontend/vulnerabilities/footer_spec.js b/ee/spec/frontend/vulnerabilities/footer_spec.js
index 8bcc554ed0689..a8301a6ad59f7 100644
--- a/ee/spec/frontend/vulnerabilities/footer_spec.js
+++ b/ee/spec/frontend/vulnerabilities/footer_spec.js
@@ -38,6 +38,11 @@ describe('Vulnerability Footer', () => {
     relatedIssuesHelpPath: 'help/path',
     pipeline: {},
     mergeRequestLinks: [],
+    representationInformation: {
+      resolvedInCommitShaLink: 'https://gitlab.com/gitlab-org/gitlab/-/commit/0123456789',
+      resolvedInCommitSha: '0123456789',
+    },
+    resolvedOnDefaultBranch: false,
   };
 
   let discussion1;
@@ -61,9 +66,17 @@ describe('Vulnerability Footer', () => {
       },
     });
 
-  const createWrapper = ({ properties, queryHandler, mountOptions } = {}) => {
+  const createWrapper = ({
+    properties,
+    queryHandler,
+    mountOptions,
+    vulnerabilityRepresentationFlag = true,
+  } = {}) => {
     wrapper = shallowMountExtended(VulnerabilityFooter, {
       propsData: { vulnerability: { ...vulnerability, ...properties } },
+      provide: {
+        glFeatures: { vulnerabilityRepresentationInformation: vulnerabilityRepresentationFlag },
+      },
       apolloProvider: createMockApollo([[vulnerabilityDiscussionsQuery, queryHandler]]),
       ...mountOptions,
     });
@@ -348,11 +361,81 @@ describe('Vulnerability Footer', () => {
       },
     );
 
-    it('does not show the detection note when the vulnerability has no pipeline (e.g.: was manually created)', () => {
-      createWrapper({ properties: { pipeline: null } });
+    describe('when the pipeline is null (vulnerability has been created manually)', () => {
+      it('should not show the status description by default', () => {
+        createWrapper({ properties: { pipeline: null } });
+        expect(statusDescription().exists()).toBe(false);
+      });
+
+      it('should not show the status description when the vulnerability is resolved on the default branch and there is no representation information', () => {
+        createWrapper({
+          properties: {
+            pipeline: null,
+            resolvedOnDefaultBranch: true,
+            representationInformation: null,
+          },
+        });
+        expect(statusDescription().exists()).toBe(false);
+      });
 
-      expect(detectionNote().exists()).toBe(false);
+      it('should show the status description when the vulnerability is resolved on the default branch and there is respresentation information', () => {
+        createWrapper({
+          properties: {
+            pipeline: null,
+            resolvedOnDefaultBranch: true,
+            representationInformation: vulnerability.representationInformation,
+          },
+        });
+        expect(statusDescription().exists()).toBe(true);
+      });
     });
+
+    describe('when the vulnerability is resolved on the default branch and there is representation information', () => {
+      it('should pass the correct props to the detection note', () => {
+        createWrapper({
+          properties: {
+            resolvedOnDefaultBranch: true,
+            representationInformation: vulnerability.representationInformation,
+          },
+        });
+
+        expect(statusDescription().props('vulnerability')).toMatchObject({
+          resolvedOnDefaultBranch: true,
+          representationInformation: vulnerability.representationInformation,
+        });
+      });
+    });
+
+    it.each`
+      representationInformation                  | resolvedOnDefaultBranch | vulnerabilityRepresentationFlag | shouldIncludeRepresentationInfo
+      ${vulnerability.representationInformation} | ${true}                 | ${true}                         | ${true}
+      ${vulnerability.representationInformation} | ${false}                | ${true}                         | ${false}
+      ${null}                                    | ${true}                 | ${true}                         | ${false}
+      ${null}                                    | ${false}                | ${true}                         | ${false}
+      ${vulnerability.representationInformation} | ${true}                 | ${false}                        | ${false}
+      ${vulnerability.representationInformation} | ${false}                | ${false}                        | ${false}
+      ${null}                                    | ${true}                 | ${false}                        | ${false}
+    `(
+      'shows representation information: "$shouldIncludeRepresentationInfo" when feature flag is "$vulnerabilityRepresentationFlag", resolvedOnDefaultBranch is "$resolvedOnDefaultBranch" and representationInformation is "$representationInformation"',
+      ({
+        resolvedOnDefaultBranch,
+        vulnerabilityRepresentationFlag,
+        representationInformation,
+        shouldIncludeRepresentationInfo,
+      }) => {
+        createWrapper({
+          properties: {
+            resolvedOnDefaultBranch,
+            representationInformation,
+          },
+          vulnerabilityRepresentationFlag,
+        });
+
+        expect(Boolean(statusDescription().props('vulnerability').representationInformation)).toBe(
+          shouldIncludeRepresentationInfo,
+        );
+      },
+    );
   });
 
   describe('generic report', () => {
-- 
GitLab