Commit db70a774 authored by Alexander Turinske's avatar Alexander Turinske Committed by Andrew Fontaine

Add identifier column to the project security dash

- take vulnerability identifiers and get primary identifer
  and display it
- conditionally render the column on the project-level
  security dashboard and not on the group/instance-level
  security dashboards
- add tests
- update docs
parent d20e41ca
...@@ -74,7 +74,7 @@ You can also dismiss vulnerabilities in the table: ...@@ -74,7 +74,7 @@ You can also dismiss vulnerabilities in the table:
1. Select the checkbox for each vulnerability you want to dismiss. 1. Select the checkbox for each vulnerability you want to dismiss.
1. In the menu that appears, select the reason for dismissal and click **Dismiss Selected**. 1. In the menu that appears, select the reason for dismissal and click **Dismiss Selected**.
![Project Security Dashboard](img/project_security_dashboard_v13_2_noNav.png) ![Project Security Dashboard](img/project_security_dashboard_v13_2.png)
## Group Security Dashboard ## Group Security Dashboard
......
...@@ -39,7 +39,7 @@ export default { ...@@ -39,7 +39,7 @@ export default {
return this.vulnerability.severity || ' '; return this.vulnerability.severity || ' ';
}, },
vulnerabilityIdentifier() { vulnerabilityIdentifier() {
return getPrimaryIdentifier(this.vulnerability.identifiers); return getPrimaryIdentifier(this.vulnerability.identifiers, 'external_type');
}, },
vulnerabilityNamespace() { vulnerabilityNamespace() {
const { project, location } = this.vulnerability; const { project, location } = this.vulnerability;
......
...@@ -5,8 +5,8 @@ import { PRIMARY_IDENTIFIER_TYPE } from 'ee/security_dashboard/store/constants'; ...@@ -5,8 +5,8 @@ import { PRIMARY_IDENTIFIER_TYPE } from 'ee/security_dashboard/store/constants';
* @param {Array} identifiers all available identifiers * @param {Array} identifiers all available identifiers
* @returns {String} the primary identifier's name * @returns {String} the primary identifier's name
*/ */
const getPrimaryIdentifier = (identifiers = []) => { const getPrimaryIdentifier = (identifiers = [], property) => {
const identifier = identifiers.find(value => value.external_type === PRIMARY_IDENTIFIER_TYPE); const identifier = identifiers.find(value => value[property] === PRIMARY_IDENTIFIER_TYPE);
return identifier?.name || identifiers[0]?.name || ''; return identifier?.name || identifiers[0]?.name || '';
}; };
......
...@@ -109,7 +109,8 @@ export default { ...@@ -109,7 +109,8 @@ export default {
:empty-state-svg-path="emptyStateSvgPath" :empty-state-svg-path="emptyStateSvgPath"
:filters="filters" :filters="filters"
:vulnerabilities="vulnerabilities" :vulnerabilities="vulnerabilities"
should-show-report-type :should-show-identifier="true"
:should-show-report-type="true"
@refetch-vulnerabilities="refetchVulnerabilities" @refetch-vulnerabilities="refetchVulnerabilities"
> >
<template #emptyState> <template #emptyState>
......
...@@ -2,6 +2,7 @@ ...@@ -2,6 +2,7 @@
import { s__, __ } from '~/locale'; import { s__, __ } from '~/locale';
import { GlEmptyState, GlFormCheckbox, GlLink, GlSkeletonLoading, GlTable } from '@gitlab/ui'; import { GlEmptyState, GlFormCheckbox, GlLink, GlSkeletonLoading, GlTable } from '@gitlab/ui';
import RemediatedBadge from './remediated_badge.vue'; import RemediatedBadge from './remediated_badge.vue';
import getPrimaryIdentifier from 'ee/vue_shared/security_reports/store/utils/get_primary_identifier';
import SeverityBadge from 'ee/vue_shared/security_reports/components/severity_badge.vue'; import SeverityBadge from 'ee/vue_shared/security_reports/components/severity_badge.vue';
import SelectionSummary from 'ee/security_dashboard/components/selection_summary.vue'; import SelectionSummary from 'ee/security_dashboard/components/selection_summary.vue';
import IssueLink from './issue_link.vue'; import IssueLink from './issue_link.vue';
...@@ -37,6 +38,11 @@ export default { ...@@ -37,6 +38,11 @@ export default {
required: false, required: false,
default: null, default: null,
}, },
shouldShowIdentifier: {
type: Boolean,
required: false,
default: false,
},
shouldShowReportType: { shouldShowReportType: {
type: Boolean, type: Boolean,
required: false, required: false,
...@@ -108,6 +114,15 @@ export default { ...@@ -108,6 +114,15 @@ export default {
}); });
} }
if (this.shouldShowIdentifier) {
baseFields.push({
key: 'identifier',
label: s__('Vulnerability|Identifier'),
thClass: commonThClass,
tdClass: 'gl-word-break-all',
});
}
if (this.shouldShowReportType) { if (this.shouldShowReportType) {
baseFields.push({ baseFields.push({
key: 'reportType', key: 'reportType',
...@@ -136,6 +151,9 @@ export default { ...@@ -136,6 +151,9 @@ export default {
deselectAllVulnerabilities() { deselectAllVulnerabilities() {
this.selectedVulnerabilities = {}; this.selectedVulnerabilities = {};
}, },
primaryIdentifier(identifiers) {
return getPrimaryIdentifier(identifiers, 'externalType');
},
isSelected(vulnerability = {}) { isSelected(vulnerability = {}) {
return Boolean(this.selectedVulnerabilities[vulnerability.id]); return Boolean(this.selectedVulnerabilities[vulnerability.id]);
}, },
...@@ -251,6 +269,12 @@ export default { ...@@ -251,6 +269,12 @@ export default {
<remediated-badge v-if="item.resolved_on_default_branch" class="ml-2" /> <remediated-badge v-if="item.resolved_on_default_branch" class="ml-2" />
</template> </template>
<template #cell(identifier)="{ item }">
<span data-testid="vulnerability-identifier">
{{ primaryIdentifier(item.identifiers) }}
</span>
</template>
<template #cell(reportType)="{ item }"> <template #cell(reportType)="{ item }">
<span data-testid="vulnerability-report-type" class="text-capitalize">{{ <span data-testid="vulnerability-report-type" class="text-capitalize">{{
useConvertReportType(item.reportType) useConvertReportType(item.reportType)
......
...@@ -15,6 +15,10 @@ fragment Vulnerability on Vulnerability { ...@@ -15,6 +15,10 @@ fragment Vulnerability on Vulnerability {
} }
} }
} }
identifiers {
externalType
name
}
location { location {
... on VulnerabilityLocationContainerScanning { ... on VulnerabilityLocationContainerScanning {
image image
......
---
title: Add identifier column to the project security dashoard
merge_request: 35760
author:
type: changed
...@@ -52,6 +52,7 @@ describe('First Class Group Dashboard Vulnerabilities Component', () => { ...@@ -52,6 +52,7 @@ describe('First Class Group Dashboard Vulnerabilities Component', () => {
emptyStateSvgPath, emptyStateSvgPath,
filters: null, filters: null,
isLoading: true, isLoading: true,
shouldShowIdentifier: false,
shouldShowReportType: false, shouldShowReportType: false,
shouldShowSelection: true, shouldShowSelection: true,
shouldShowProjectNamespace: true, shouldShowProjectNamespace: true,
...@@ -144,6 +145,7 @@ describe('First Class Group Dashboard Vulnerabilities Component', () => { ...@@ -144,6 +145,7 @@ describe('First Class Group Dashboard Vulnerabilities Component', () => {
emptyStateSvgPath, emptyStateSvgPath,
filters: null, filters: null,
isLoading: false, isLoading: false,
shouldShowIdentifier: false,
shouldShowReportType: false, shouldShowReportType: false,
shouldShowSelection: true, shouldShowSelection: true,
shouldShowProjectNamespace: true, shouldShowProjectNamespace: true,
......
...@@ -77,6 +77,7 @@ describe('First Class Instance Dashboard Vulnerabilities Component', () => { ...@@ -77,6 +77,7 @@ describe('First Class Instance Dashboard Vulnerabilities Component', () => {
emptyStateSvgPath, emptyStateSvgPath,
filters: null, filters: null,
isLoading: true, isLoading: true,
shouldShowIdentifier: false,
shouldShowReportType: false, shouldShowReportType: false,
shouldShowSelection: true, shouldShowSelection: true,
shouldShowProjectNamespace: true, shouldShowProjectNamespace: true,
...@@ -160,6 +161,7 @@ describe('First Class Instance Dashboard Vulnerabilities Component', () => { ...@@ -160,6 +161,7 @@ describe('First Class Instance Dashboard Vulnerabilities Component', () => {
emptyStateSvgPath, emptyStateSvgPath,
filters: null, filters: null,
isLoading: false, isLoading: false,
shouldShowIdentifier: false,
shouldShowReportType: false, shouldShowReportType: false,
shouldShowSelection: true, shouldShowSelection: true,
shouldShowProjectNamespace: true, shouldShowProjectNamespace: true,
......
...@@ -107,10 +107,13 @@ describe('security reports utils', () => { ...@@ -107,10 +107,13 @@ describe('security reports utils', () => {
{ external_type: 'gemnaisum', name: 'GEMNASIUM-1337' }, { external_type: 'gemnaisum', name: 'GEMNASIUM-1337' },
]; ];
it('should return the `cve` identifier if a `cve` identifier does exist', () => { it('should return the `cve` identifier if a `cve` identifier does exist', () => {
expect(getPrimaryIdentifiers(identifiers)).toBe(identifiers[0].name); expect(getPrimaryIdentifiers(identifiers, 'external_type')).toBe(identifiers[0].name);
});
it('should return the first identifier if the property for type does not exist', () => {
expect(getPrimaryIdentifiers(identifiers, 'externalType')).toBe(identifiers[0].name);
}); });
it('should return the first identifier if a `cve` identifier does not exist', () => { it('should return the first identifier if a `cve` identifier does not exist', () => {
expect(getPrimaryIdentifiers([identifiers[1]])).toBe(identifiers[1].name); expect(getPrimaryIdentifiers([identifiers[1]], 'external_type')).toBe(identifiers[1].name);
}); });
it('should return an empty string if identifiers is empty', () => { it('should return an empty string if identifiers is empty', () => {
expect(getPrimaryIdentifiers()).toBe(''); expect(getPrimaryIdentifiers()).toBe('');
......
export const generateVulnerabilities = () => [ export const generateVulnerabilities = () => [
{ {
id: 'id_0', id: 'id_0',
identifiers: [
{
externalType: 'cve',
name: 'CVE-2018-1234',
},
{
externalType: 'gemnasium',
name: 'Gemnasium-2018-1234',
},
],
title: 'Vulnerability 0', title: 'Vulnerability 0',
severity: 'critical', severity: 'critical',
state: 'dismissed', state: 'dismissed',
...@@ -15,6 +25,12 @@ export const generateVulnerabilities = () => [ ...@@ -15,6 +25,12 @@ export const generateVulnerabilities = () => [
}, },
{ {
id: 'id_1', id: 'id_1',
identifiers: [
{
externalType: 'gemnasium',
name: 'Gemnasium-2018-1234',
},
],
title: 'Vulnerability 1', title: 'Vulnerability 1',
severity: 'high', severity: 'high',
state: 'opened', state: 'opened',
......
...@@ -131,9 +131,14 @@ describe('Vulnerability list component', () => { ...@@ -131,9 +131,14 @@ describe('Vulnerability list component', () => {
); );
}); });
it('should not display the vulnerability identifier', () => {
const cell = findDataCell('vulnerability-identifier');
expect(cell.exists()).toBe(false);
});
it('should not display the vulnerability report type', () => { it('should not display the vulnerability report type', () => {
const scannerCell = findRow().find('[data-testid="vulnerability-report-type"'); const cell = findDataCell('vulnerability-report-type');
expect(scannerCell.exists()).toBe(false); expect(cell.exists()).toBe(false);
}); });
it('should not display the vulnerability locations', () => { it('should not display the vulnerability locations', () => {
...@@ -166,7 +171,11 @@ describe('Vulnerability list component', () => { ...@@ -166,7 +171,11 @@ describe('Vulnerability list component', () => {
beforeEach(() => { beforeEach(() => {
newVulnerabilities = generateVulnerabilities(); newVulnerabilities = generateVulnerabilities();
wrapper = createWrapper({ wrapper = createWrapper({
props: { vulnerabilities: newVulnerabilities, shouldShowReportType: true }, props: {
vulnerabilities: newVulnerabilities,
shouldShowIdentifier: true,
shouldShowReportType: true,
},
}); });
}); });
...@@ -185,6 +194,13 @@ describe('Vulnerability list component', () => { ...@@ -185,6 +194,13 @@ describe('Vulnerability list component', () => {
); );
}); });
it('should correctly render the identifier', () => {
const cells = findDataCells('vulnerability-identifier');
expect(cells.at(0).text()).toBe(newVulnerabilities[0].identifiers[0].name);
expect(cells.at(1).text()).toBe(newVulnerabilities[1].identifiers[0].name);
});
it('should display the vulnerability report type', () => { it('should display the vulnerability report type', () => {
const cells = findDataCells('vulnerability-report-type'); const cells = findDataCells('vulnerability-report-type');
expect(cells.at(0).text()).toBe('SAST'); expect(cells.at(0).text()).toBe('SAST');
......
...@@ -25677,6 +25677,9 @@ msgstr "" ...@@ -25677,6 +25677,9 @@ msgstr ""
msgid "Vulnerability|File" msgid "Vulnerability|File"
msgstr "" msgstr ""
msgid "Vulnerability|Identifier"
msgstr ""
msgid "Vulnerability|Identifiers" msgid "Vulnerability|Identifiers"
msgstr "" msgstr ""
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment