Commit 6faaf6a9 authored by Sam Beckham's avatar Sam Beckham Committed by Mark Florian

Updates the groupedReportText builder

- Takes into account severities
- a11y compatible
- Updates all the helpers that go along with it
- Updates the documentation
- Updates all the tests that rely on this function.
parent 2154869a
...@@ -32,7 +32,7 @@ You can enable container scanning by doing one of the following: ...@@ -32,7 +32,7 @@ You can enable container scanning by doing one of the following:
GitLab compares the found vulnerabilities between the source and target branches, and shows the GitLab compares the found vulnerabilities between the source and target branches, and shows the
information directly in the merge request. information directly in the merge request.
![Container Scanning Widget](img/container_scanning_v13_0.png) ![Container Scanning Widget](img/container_scanning_v13_1.png)
<!-- NOTE: The container scanning tool references the following heading in the code, so if you <!-- NOTE: The container scanning tool references the following heading in the code, so if you
make a change to this heading, make sure to update the documentation URLs used in the make a change to this heading, make sure to update the documentation URLs used in the
......
...@@ -36,7 +36,7 @@ NOTE: **Note:** ...@@ -36,7 +36,7 @@ NOTE: **Note:**
This comparison logic uses only the latest pipeline executed for the target branch's base commit. This comparison logic uses only the latest pipeline executed for the target branch's base commit.
Running the pipeline on any other commit has no effect on the merge request. Running the pipeline on any other commit has no effect on the merge request.
![DAST Widget](img/dast_all_v13_0.png) ![DAST Widget](img/dast_all_v13_1.png)
By clicking on one of the detected linked vulnerabilities, you can By clicking on one of the detected linked vulnerabilities, you can
see the details and the URL(s) affected. see the details and the URL(s) affected.
......
...@@ -27,7 +27,7 @@ GitLab checks the Dependency Scanning report, compares the found vulnerabilities ...@@ -27,7 +27,7 @@ GitLab checks the Dependency Scanning report, compares the found vulnerabilities
between the source and target branches, and shows the information on the between the source and target branches, and shows the information on the
merge request. merge request.
![Dependency Scanning Widget](img/dependency_scanning_v13_0.png) ![Dependency Scanning Widget](img/dependency_scanning_v13_1.png)
The results are sorted by the severity of the vulnerability: The results are sorted by the severity of the vulnerability:
......
...@@ -28,7 +28,7 @@ You can take advantage of SAST by doing one of the following: ...@@ -28,7 +28,7 @@ You can take advantage of SAST by doing one of the following:
GitLab checks the SAST report, compares the found vulnerabilities between the GitLab checks the SAST report, compares the found vulnerabilities between the
source and target branches, and shows the information right on the merge request. source and target branches, and shows the information right on the merge request.
![SAST Widget](img/sast_v13_0.png) ![SAST Widget](img/sast_v13_1.png)
The results are sorted by the priority of the vulnerability: The results are sorted by the priority of the vulnerability:
......
import { s__, sprintf } from '~/locale'; import { s__, sprintf } from '~/locale';
import { countIssues, groupedTextBuilder, statusIcon, groupedReportText } from './utils'; import { countVulnerabilities, groupedTextBuilder, statusIcon, groupedReportText } from './utils';
import { LOADING, ERROR, SUCCESS } from './constants'; import { LOADING, ERROR, SUCCESS } from './constants';
import messages from './messages'; import messages from './messages';
...@@ -30,27 +30,27 @@ export const groupedDependencyText = ({ dependencyScanning }) => ...@@ -30,27 +30,27 @@ export const groupedDependencyText = ({ dependencyScanning }) =>
messages.DEPENDENCY_SCANNING_IS_LOADING, messages.DEPENDENCY_SCANNING_IS_LOADING,
); );
export const summaryCounts = state => export const summaryCounts = ({
[ containerScanning,
state.sast, dast,
state.containerScanning, dependencyScanning,
state.dast, sast,
state.dependencyScanning, secretScanning,
state.secretScanning, } = {}) => {
].reduce( const allNewVulns = [
(acc, report) => { ...containerScanning.newIssues,
const curr = countIssues(report); ...dast.newIssues,
acc.added += curr.added; ...dependencyScanning.newIssues,
acc.dismissed += curr.dismissed; ...sast.newIssues,
acc.fixed += curr.fixed; ...secretScanning.newIssues,
acc.existing += curr.existing; ];
return acc;
}, return countVulnerabilities(allNewVulns);
{ added: 0, dismissed: 0, fixed: 0, existing: 0 }, };
);
export const groupedSummaryText = (state, getters) => { export const groupedSummaryText = (state, getters) => {
const reportType = s__('ciReport|Security scanning'); const reportType = s__('ciReport|Security scanning');
let status = '';
// All reports are loading // All reports are loading
if (getters.areAllReportsLoading) { if (getters.areAllReportsLoading) {
...@@ -62,10 +62,6 @@ export const groupedSummaryText = (state, getters) => { ...@@ -62,10 +62,6 @@ export const groupedSummaryText = (state, getters) => {
return s__('ciReport|Security scanning failed loading any results'); return s__('ciReport|Security scanning failed loading any results');
} }
const { added, fixed, existing, dismissed } = getters.summaryCounts;
let status = '';
if (getters.areReportsLoading && getters.anyReportHasError) { if (getters.areReportsLoading && getters.anyReportHasError) {
status = s__('ciReport|(is loading, errors when loading results)'); status = s__('ciReport|(is loading, errors when loading results)');
} else if (getters.areReportsLoading && !getters.anyReportHasError) { } else if (getters.areReportsLoading && !getters.anyReportHasError) {
...@@ -74,13 +70,9 @@ export const groupedSummaryText = (state, getters) => { ...@@ -74,13 +70,9 @@ export const groupedSummaryText = (state, getters) => {
status = s__('ciReport|(errors when loading results)'); status = s__('ciReport|(errors when loading results)');
} }
/* const { critical, high, other } = getters.summaryCounts;
In order to correct wording, we ne to set the base property to true,
if at least one report has a base.
*/
const paths = { head: true, base: !getters.noBaseInAllReports };
return groupedTextBuilder({ reportType, paths, added, fixed, existing, dismissed, status }); return groupedTextBuilder({ reportType, status, critical, high, other });
}; };
export const summaryStatus = (state, getters) => { export const summaryStatus = (state, getters) => {
......
import { n__, s__, sprintf } from '~/locale'; import { __, n__, sprintf } from '~/locale';
import { CRITICAL, HIGH } from 'ee/security_dashboard/store/modules/vulnerabilities/constants';
/** /**
* Returns the index of an issue in given list * Returns the index of an issue in given list
...@@ -42,103 +43,105 @@ export const enrichVulnerabilityWithFeedback = (vulnerability, feedback = []) => ...@@ -42,103 +43,105 @@ export const enrichVulnerabilityWithFeedback = (vulnerability, feedback = []) =>
return vuln; return vuln;
}, vulnerability); }, vulnerability);
/**
* Takes an object of options and returns an externalized string representing
* the critical, high, and other severity vulnerabilities for a given report.
* @param {{reportType: string, status: string, critical: number, high: number, other: number}} options
* @returns {string}
*/
export const groupedTextBuilder = ({ export const groupedTextBuilder = ({
reportType = '', reportType = '',
paths = {},
added = 0,
fixed = 0,
existing = 0,
dismissed = 0,
status = '', status = '',
}) => { critical = 0,
let baseString = ''; high = 0,
other = 0,
if (!paths.base && !paths.diffEndpoint) { } = {}) => {
if (added && !dismissed) { // This approach uses bitwise (ish) flags to determine which vulnerabilities
// added // we have, without the need for too many nested levels of if/else statements.
baseString = n__( //
'ciReport|%{reportType} %{status} detected %{newCount} vulnerability for the source branch only', // Here's a video explaining how it works
'ciReport|%{reportType} %{status} detected %{newCount} vulnerabilities for the source branch only', // https://youtu.be/qZzKNC7TPbA
added, //
); // Here's a link to a similar approach on MDN:
} else if (!added && dismissed) { // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Bitwise_Operators#Examples
// dismissed
baseString = n__( let options = 0;
'ciReport|%{reportType} %{status} detected %{dismissedCount} dismissed vulnerability for the source branch only', const HAS_CRITICAL = 1;
'ciReport|%{reportType} %{status} detected %{dismissedCount} dismissed vulnerabilities for the source branch only', const HAS_HIGH = 2;
dismissed, const HAS_OTHER = 4;
); let message;
} else if (added && dismissed) {
// added & dismissed if (critical) {
baseString = s__( options += HAS_CRITICAL;
'ciReport|%{reportType} %{status} detected %{newCount} new, and %{dismissedCount} dismissed vulnerabilities for the source branch only',
);
} else {
// no vulnerabilities
baseString = s__(
'ciReport|%{reportType} %{status} detected no vulnerabilities for the source branch only',
);
} }
} else if (paths.head || paths.diffEndpoint) { if (high) {
if (added && !fixed && !dismissed) { options += HAS_HIGH;
// added }
baseString = n__( if (other) {
'ciReport|%{reportType} %{status} detected %{newCount} new vulnerability', options += HAS_OTHER;
'ciReport|%{reportType} %{status} detected %{newCount} new vulnerabilities', }
added,
switch (options) {
case HAS_CRITICAL:
message = n__(
'%{reportType} %{status} detected %{critical} critical severity vulnerability.',
'%{reportType} %{status} detected %{critical} critical severity vulnerabilities.',
critical,
); );
} else if (!added && fixed && !dismissed) { break;
// fixed
baseString = n__( case HAS_HIGH:
'ciReport|%{reportType} %{status} detected %{fixedCount} fixed vulnerability', message = n__(
'ciReport|%{reportType} %{status} detected %{fixedCount} fixed vulnerabilities', '%{reportType} %{status} detected %{high} high severity vulnerability.',
fixed, '%{reportType} %{status} detected %{high} high severity vulnerabilities.',
high,
); );
} else if (!added && !fixed && dismissed) { break;
// dismissed
baseString = n__( case HAS_OTHER:
'ciReport|%{reportType} %{status} detected %{dismissedCount} dismissed vulnerability', message = n__(
'ciReport|%{reportType} %{status} detected %{dismissedCount} dismissed vulnerabilities', '%{reportType} %{status} detected %{other} vulnerability.',
dismissed, '%{reportType} %{status} detected %{other} vulnerabilities.',
other,
); );
} else if (added && fixed && !dismissed) { break;
// added & fixed
baseString = s__( case HAS_CRITICAL + HAS_HIGH:
'ciReport|%{reportType} %{status} detected %{newCount} new, and %{fixedCount} fixed vulnerabilities', message = __(
'%{reportType} %{status} detected %{critical} critical and %{high} high severity vulnerabilities.',
); );
} else if (added && !fixed && dismissed) { break;
// added & dismissed
baseString = s__( case HAS_CRITICAL + HAS_OTHER:
'ciReport|%{reportType} %{status} detected %{newCount} new, and %{dismissedCount} dismissed vulnerabilities', message = __(
'%{reportType} %{status} detected %{critical} critical severity vulnerabilities out of %{total}.',
); );
} else if (!added && fixed && dismissed) { break;
// fixed & dismissed
baseString = s__( case HAS_HIGH + HAS_OTHER:
'ciReport|%{reportType} %{status} detected %{fixedCount} fixed, and %{dismissedCount} dismissed vulnerabilities', message = __(
'%{reportType} %{status} detected %{high} high severity vulnerabilities out of %{total}.',
); );
} else if (added && fixed && dismissed) { break;
// added & fixed & dismissed
baseString = s__( case HAS_CRITICAL + HAS_HIGH + HAS_OTHER:
'ciReport|%{reportType} %{status} detected %{newCount} new, %{fixedCount} fixed, and %{dismissedCount} dismissed vulnerabilities', message = __(
'%{reportType} %{status} detected %{critical} critical and %{high} high severity vulnerabilities out of %{total}.',
); );
} else if (existing) { break;
baseString = s__('ciReport|%{reportType} %{status} detected no new vulnerabilities');
} else {
baseString = s__('ciReport|%{reportType} %{status} detected no vulnerabilities');
}
}
if (!status) { default:
baseString = baseString.replace('%{status}', '').replace(' ', ' '); message = __('%{reportType} %{status} detected no new vulnerabilities.');
} }
return sprintf(baseString, { return sprintf(message, {
status,
reportType, reportType,
newCount: added, status,
fixedCount: fixed, critical,
dismissedCount: dismissed, high,
}); other,
total: critical + high + other,
}).replace(/\s\s+/g, ' ');
}; };
export const statusIcon = (loading = false, failed = false, newIssues = 0, neutralIssues = 0) => { export const statusIcon = (loading = false, failed = false, newIssues = 0, neutralIssues = 0) => {
...@@ -154,22 +157,21 @@ export const statusIcon = (loading = false, failed = false, newIssues = 0, neutr ...@@ -154,22 +157,21 @@ export const statusIcon = (loading = false, failed = false, newIssues = 0, neutr
}; };
/** /**
* Counts issues. Simply returns the amount of existing and fixed Issues. * Counts vulnerabilities.
* New Issues are divided into dismissed and added. * Returns the amount of critical, high, and other vulnerabilities.
* *
* @param newIssues * @param {Array} vulnerabilities The raw vulnerabilities to parse
* @param resolvedIssues * @returns {{critical: number, high: number, other: number}}
* @param allIssues
* @returns {{existing: number, added: number, dismissed: number, fixed: number}}
*/ */
export const countIssues = ({ newIssues = [], resolvedIssues = [], allIssues = [] } = {}) => { export const countVulnerabilities = (vulnerabilities = []) => {
const dismissed = newIssues.reduce((sum, issue) => (issue.isDismissed ? sum + 1 : sum), 0); const critical = vulnerabilities.filter(vuln => vuln.severity === CRITICAL).length;
const high = vulnerabilities.filter(vuln => vuln.severity === HIGH).length;
const other = vulnerabilities.length - critical - high;
return { return {
added: newIssues.length - dismissed, critical,
dismissed, high,
existing: allIssues.length, other,
fixed: resolvedIssues.length,
}; };
}; };
...@@ -183,8 +185,6 @@ export const countIssues = ({ newIssues = [], resolvedIssues = [], allIssues = [ ...@@ -183,8 +185,6 @@ export const countIssues = ({ newIssues = [], resolvedIssues = [], allIssues = [
* @returns {String} * @returns {String}
*/ */
export const groupedReportText = (report, reportType, errorMessage, loadingMessage) => { export const groupedReportText = (report, reportType, errorMessage, loadingMessage) => {
const { paths } = report;
if (report.hasError) { if (report.hasError) {
return errorMessage; return errorMessage;
} }
...@@ -194,9 +194,8 @@ export const groupedReportText = (report, reportType, errorMessage, loadingMessa ...@@ -194,9 +194,8 @@ export const groupedReportText = (report, reportType, errorMessage, loadingMessa
} }
return groupedTextBuilder({ return groupedTextBuilder({
...countIssues(report),
reportType, reportType,
paths, ...countVulnerabilities(report.newIssues),
}); });
}; };
......
---
title: Changes the displayed summary text in security reports in merge requests
merge_request: 33857
author:
type: changed
...@@ -125,7 +125,7 @@ describe('ee merge request widget options', () => { ...@@ -125,7 +125,7 @@ describe('ee merge request widget options', () => {
`${SAST_SELECTOR} .report-block-list-issue-description`, `${SAST_SELECTOR} .report-block-list-issue-description`,
).textContent, ).textContent,
), ),
).toEqual('SAST detected 1 new, and 2 fixed vulnerabilities'); ).toEqual('SAST detected 1 vulnerability.');
done(); done();
}); });
}); });
...@@ -147,7 +147,7 @@ describe('ee merge request widget options', () => { ...@@ -147,7 +147,7 @@ describe('ee merge request widget options', () => {
`${SAST_SELECTOR} .report-block-list-issue-description`, `${SAST_SELECTOR} .report-block-list-issue-description`,
).textContent, ).textContent,
).trim(), ).trim(),
).toEqual('SAST detected no vulnerabilities'); ).toEqual('SAST detected no new vulnerabilities.');
done(); done();
}); });
}); });
...@@ -215,7 +215,7 @@ describe('ee merge request widget options', () => { ...@@ -215,7 +215,7 @@ describe('ee merge request widget options', () => {
`${DEPENDENCY_SCANNING_SELECTOR} .report-block-list-issue-description`, `${DEPENDENCY_SCANNING_SELECTOR} .report-block-list-issue-description`,
).textContent, ).textContent,
), ),
).toEqual('Dependency scanning detected 2 new, and 1 fixed vulnerabilities'); ).toEqual('Dependency scanning detected 2 vulnerabilities.');
done(); done();
}); });
}); });
...@@ -241,7 +241,7 @@ describe('ee merge request widget options', () => { ...@@ -241,7 +241,7 @@ describe('ee merge request widget options', () => {
`${DEPENDENCY_SCANNING_SELECTOR} .report-block-list-issue-description`, `${DEPENDENCY_SCANNING_SELECTOR} .report-block-list-issue-description`,
).textContent, ).textContent,
), ),
).toEqual('Dependency scanning detected no new vulnerabilities'); ).toEqual('Dependency scanning detected no new vulnerabilities.');
done(); done();
}); });
}); });
...@@ -263,7 +263,7 @@ describe('ee merge request widget options', () => { ...@@ -263,7 +263,7 @@ describe('ee merge request widget options', () => {
`${DEPENDENCY_SCANNING_SELECTOR} .report-block-list-issue-description`, `${DEPENDENCY_SCANNING_SELECTOR} .report-block-list-issue-description`,
).textContent, ).textContent,
), ),
).toEqual('Dependency scanning detected no vulnerabilities'); ).toEqual('Dependency scanning detected no new vulnerabilities.');
done(); done();
}); });
}); });
...@@ -687,7 +687,7 @@ describe('ee merge request widget options', () => { ...@@ -687,7 +687,7 @@ describe('ee merge request widget options', () => {
`${CONTAINER_SCANNING_SELECTOR} .report-block-list-issue-description`, `${CONTAINER_SCANNING_SELECTOR} .report-block-list-issue-description`,
).textContent, ).textContent,
), ),
).toEqual('Container scanning detected 2 new, and 1 fixed vulnerabilities'); ).toEqual('Container scanning detected 2 vulnerabilities.');
done(); done();
}); });
}); });
...@@ -757,7 +757,7 @@ describe('ee merge request widget options', () => { ...@@ -757,7 +757,7 @@ describe('ee merge request widget options', () => {
findSecurityWidget() findSecurityWidget()
.querySelector(`${DAST_SELECTOR} .report-block-list-issue-description`) .querySelector(`${DAST_SELECTOR} .report-block-list-issue-description`)
.textContent.trim(), .textContent.trim(),
).toEqual('DAST detected 1 new, and 2 fixed vulnerabilities'); ).toEqual('DAST detected 1 vulnerability.');
done(); done();
}); });
}); });
...@@ -831,7 +831,7 @@ describe('ee merge request widget options', () => { ...@@ -831,7 +831,7 @@ describe('ee merge request widget options', () => {
`${SECRET_SCANNING_SELECTOR} .report-block-list-issue-description`, `${SECRET_SCANNING_SELECTOR} .report-block-list-issue-description`,
).textContent, ).textContent,
), ),
).toEqual('Secret scanning detected 2 new, and 1 fixed vulnerabilities'); ).toEqual('Secret scanning detected 2 vulnerabilities.');
done(); done();
}); });
}); });
......
...@@ -97,7 +97,9 @@ describe('Report issues', () => { ...@@ -97,7 +97,9 @@ describe('Report issues', () => {
}); });
it('renders severity', () => { it('renders severity', () => {
expect(vm.$el.textContent.trim()).toContain(dockerReportParsed.unapproved[0].severity); expect(vm.$el.textContent.trim().toLowerCase()).toContain(
dockerReportParsed.unapproved[0].severity,
);
}); });
it('renders CVE name', () => { it('renders CVE name', () => {
...@@ -121,7 +123,7 @@ describe('Report issues', () => { ...@@ -121,7 +123,7 @@ describe('Report issues', () => {
it('renders severity and title', () => { it('renders severity and title', () => {
expect(vm.$el.textContent).toContain(parsedDast[0].title); expect(vm.$el.textContent).toContain(parsedDast[0].title);
expect(vm.$el.textContent).toContain(`${parsedDast[0].severity}`); expect(vm.$el.textContent.toLowerCase()).toContain(`${parsedDast[0].severity}`);
}); });
}); });
...@@ -135,7 +137,9 @@ describe('Report issues', () => { ...@@ -135,7 +137,9 @@ describe('Report issues', () => {
}); });
it('renders severity', () => { it('renders severity', () => {
expect(vm.$el.textContent.trim()).toContain(secretScanningParsedIssues[0].severity); expect(vm.$el.textContent.trim().toLowerCase()).toContain(
secretScanningParsedIssues[0].severity,
);
}); });
it('renders CVE name', () => { it('renders CVE name', () => {
......
...@@ -97,7 +97,9 @@ describe('Report issue', () => { ...@@ -97,7 +97,9 @@ describe('Report issue', () => {
}); });
it('renders severity', () => { it('renders severity', () => {
expect(vm.$el.textContent.trim()).toContain(dockerReportParsed.unapproved[0].severity); expect(vm.$el.textContent.trim().toLowerCase()).toContain(
dockerReportParsed.unapproved[0].severity,
);
}); });
it('renders CVE name', () => { it('renders CVE name', () => {
...@@ -121,7 +123,7 @@ describe('Report issue', () => { ...@@ -121,7 +123,7 @@ describe('Report issue', () => {
it('renders severity and title', () => { it('renders severity and title', () => {
expect(vm.$el.textContent).toContain(parsedDast[0].title); expect(vm.$el.textContent).toContain(parsedDast[0].title);
expect(vm.$el.textContent).toContain(`${parsedDast[0].severity}`); expect(vm.$el.textContent.toLowerCase()).toContain(`${parsedDast[0].severity}`);
}); });
}); });
...@@ -135,7 +137,9 @@ describe('Report issue', () => { ...@@ -135,7 +137,9 @@ describe('Report issue', () => {
}); });
it('renders severity', () => { it('renders severity', () => {
expect(vm.$el.textContent.trim()).toContain(secretScanningParsedIssues[0].severity); expect(vm.$el.textContent.trim().toLowerCase()).toContain(
secretScanningParsedIssues[0].severity,
);
}); });
it('renders CVE name', () => { it('renders CVE name', () => {
......
...@@ -10,6 +10,12 @@ import { ...@@ -10,6 +10,12 @@ import {
dependencyScanningIssues, dependencyScanningIssues,
secretScanningParsedIssues, secretScanningParsedIssues,
} from '../mock_data'; } from '../mock_data';
import {
CRITICAL,
HIGH,
MEDIUM,
LOW,
} from 'ee/security_dashboard/store/modules/vulnerabilities/constants';
describe('Security Issue Body', () => { describe('Security Issue Body', () => {
let wrapper; let wrapper;
...@@ -31,11 +37,11 @@ describe('Security Issue Body', () => { ...@@ -31,11 +37,11 @@ describe('Security Issue Body', () => {
}); });
describe.each([ describe.each([
['SAST', sastParsedIssues[0], true, 'High'], ['SAST', sastParsedIssues[0], true, HIGH],
['DAST', parsedDast[0], false, 'Low'], ['DAST', parsedDast[0], false, LOW],
['Container Scanning', dockerReportParsed.vulnerabilities[0], false, 'Medium'], ['Container Scanning', dockerReportParsed.vulnerabilities[0], false, MEDIUM],
['Dependency Scanning', dependencyScanningIssues[0], true], ['Dependency Scanning', dependencyScanningIssues[0], true],
['Secret Scanning', secretScanningParsedIssues[0], false, 'Critical'], ['Secret Scanning', secretScanningParsedIssues[0], false, CRITICAL],
])('for a %s vulnerability', (name, vuln, hasReportLink, severity) => { ])('for a %s vulnerability', (name, vuln, hasReportLink, severity) => {
beforeEach(() => { beforeEach(() => {
createComponent(vuln); createComponent(vuln);
......
...@@ -187,7 +187,7 @@ describe('Grouped security reports app', () => { ...@@ -187,7 +187,7 @@ describe('Grouped security reports app', () => {
// Renders the summary text // Renders the summary text
expect(wrapper.vm.$el.querySelector('.js-code-text').textContent.trim()).toEqual( expect(wrapper.vm.$el.querySelector('.js-code-text').textContent.trim()).toEqual(
'Security scanning detected 8 new, and 7 fixed vulnerabilities', 'Security scanning detected 8 vulnerabilities.',
); );
// Renders the expand button // Renders the expand button
...@@ -196,24 +196,20 @@ describe('Grouped security reports app', () => { ...@@ -196,24 +196,20 @@ describe('Grouped security reports app', () => {
); );
// Renders Sast result // Renders Sast result
expect(trimText(wrapper.vm.$el.textContent)).toContain( expect(trimText(wrapper.vm.$el.textContent)).toContain('SAST detected 1 vulnerability');
'SAST detected 1 new, and 2 fixed vulnerabilities',
);
// Renders DSS result // Renders DSS result
expect(trimText(wrapper.vm.$el.textContent)).toContain( expect(trimText(wrapper.vm.$el.textContent)).toContain(
'Dependency scanning detected 2 new, and 1 fixed vulnerabilities', 'Dependency scanning detected 2 vulnerabilities.',
); );
// Renders container scanning result // Renders container scanning result
expect(wrapper.vm.$el.textContent).toContain( expect(wrapper.vm.$el.textContent).toContain(
'Container scanning detected 2 new, and 1 fixed vulnerabilities', 'Container scanning detected 2 vulnerabilities.',
); );
// Renders DAST result // Renders DAST result
expect(wrapper.vm.$el.textContent).toContain( expect(wrapper.vm.$el.textContent).toContain('DAST detected 1 vulnerability.');
'DAST detected 1 new, and 2 fixed vulnerabilities',
);
}); });
it('opens modal with more information', () => { it('opens modal with more information', () => {
...@@ -293,9 +289,7 @@ describe('Grouped security reports app', () => { ...@@ -293,9 +289,7 @@ describe('Grouped security reports app', () => {
}); });
it('should display the correct numbers of vulnerabilities', () => { it('should display the correct numbers of vulnerabilities', () => {
expect(wrapper.text()).toContain( expect(wrapper.text()).toContain('Container scanning detected 2 vulnerabilities.');
'Container scanning detected 2 new, and 1 fixed vulnerabilities',
);
}); });
}); });
...@@ -324,7 +318,7 @@ describe('Grouped security reports app', () => { ...@@ -324,7 +318,7 @@ describe('Grouped security reports app', () => {
it('should display the correct numbers of vulnerabilities', () => { it('should display the correct numbers of vulnerabilities', () => {
expect(wrapper.vm.$el.textContent).toContain( expect(wrapper.vm.$el.textContent).toContain(
'Dependency scanning detected 2 new, and 1 fixed vulnerabilities', 'Dependency scanning detected 2 vulnerabilities.',
); );
}); });
}); });
...@@ -362,9 +356,7 @@ describe('Grouped security reports app', () => { ...@@ -362,9 +356,7 @@ describe('Grouped security reports app', () => {
}); });
it('should display the correct numbers of vulnerabilities', () => { it('should display the correct numbers of vulnerabilities', () => {
expect(wrapper.vm.$el.textContent).toContain( expect(wrapper.vm.$el.textContent).toContain('DAST detected 1 vulnerability');
'DAST detected 1 new, and 2 fixed vulnerabilities',
);
}); });
it('shows the scanned URLs count and a link to the CI job if available', () => { it('shows the scanned URLs count and a link to the CI job if available', () => {
...@@ -431,9 +423,7 @@ describe('Grouped security reports app', () => { ...@@ -431,9 +423,7 @@ describe('Grouped security reports app', () => {
}); });
it('should display the correct numbers of vulnerabilities', () => { it('should display the correct numbers of vulnerabilities', () => {
expect(wrapper.text()).toContain( expect(wrapper.text()).toContain('Secret scanning detected 2 vulnerabilities.');
'Secret scanning detected 2 new, and 1 fixed vulnerabilities',
);
}); });
}); });
...@@ -470,9 +460,7 @@ describe('Grouped security reports app', () => { ...@@ -470,9 +460,7 @@ describe('Grouped security reports app', () => {
}); });
it('should display the correct numbers of vulnerabilities', () => { it('should display the correct numbers of vulnerabilities', () => {
expect(wrapper.vm.$el.textContent).toContain( expect(wrapper.vm.$el.textContent).toContain('SAST detected 1 vulnerability.');
'SAST detected 1 new, and 2 fixed vulnerabilities',
);
}); });
}); });
......
...@@ -5,7 +5,7 @@ export const sastParsedIssues = [ ...@@ -5,7 +5,7 @@ export const sastParsedIssues = [
title: 'Arbitrary file existence disclosure in Action Pack', title: 'Arbitrary file existence disclosure in Action Pack',
path: 'Gemfile.lock', path: 'Gemfile.lock',
line: 12, line: 12,
severity: 'High', severity: 'high',
urlPath: 'foo/Gemfile.lock', urlPath: 'foo/Gemfile.lock',
report_type: 'sast', report_type: 'sast',
}, },
...@@ -32,7 +32,7 @@ export const dockerReportParsed = { ...@@ -32,7 +32,7 @@ export const dockerReportParsed = {
{ {
vulnerability: 'CVE-2017-12944', vulnerability: 'CVE-2017-12944',
namespace: 'debian:8', namespace: 'debian:8',
severity: 'Medium', severity: 'medium',
title: 'CVE-2017-12944', title: 'CVE-2017-12944',
path: 'debian:8', path: 'debian:8',
identifiers: [ identifiers: [
...@@ -47,7 +47,7 @@ export const dockerReportParsed = { ...@@ -47,7 +47,7 @@ export const dockerReportParsed = {
{ {
vulnerability: 'CVE-2017-16232', vulnerability: 'CVE-2017-16232',
namespace: 'debian:8', namespace: 'debian:8',
severity: 'Negligible', severity: 'low',
title: 'CVE-2017-16232', title: 'CVE-2017-16232',
path: 'debian:8', path: 'debian:8',
identifiers: [ identifiers: [
...@@ -64,7 +64,7 @@ export const dockerReportParsed = { ...@@ -64,7 +64,7 @@ export const dockerReportParsed = {
{ {
vulnerability: 'CVE-2014-8130', vulnerability: 'CVE-2014-8130',
namespace: 'debian:8', namespace: 'debian:8',
severity: 'Negligible', severity: 'low',
title: 'CVE-2014-8130', title: 'CVE-2014-8130',
path: 'debian:8', path: 'debian:8',
identifiers: [ identifiers: [
...@@ -81,7 +81,7 @@ export const dockerReportParsed = { ...@@ -81,7 +81,7 @@ export const dockerReportParsed = {
{ {
vulnerability: 'CVE-2017-12944', vulnerability: 'CVE-2017-12944',
namespace: 'debian:8', namespace: 'debian:8',
severity: 'Medium', severity: 'medium',
title: 'CVE-2017-12944', title: 'CVE-2017-12944',
path: 'debian:8', path: 'debian:8',
identifiers: [ identifiers: [
...@@ -96,7 +96,7 @@ export const dockerReportParsed = { ...@@ -96,7 +96,7 @@ export const dockerReportParsed = {
{ {
vulnerability: 'CVE-2017-16232', vulnerability: 'CVE-2017-16232',
namespace: 'debian:8', namespace: 'debian:8',
severity: 'Negligible', severity: 'low',
title: 'CVE-2017-16232', title: 'CVE-2017-16232',
path: 'debian:8', path: 'debian:8',
identifiers: [ identifiers: [
...@@ -111,7 +111,7 @@ export const dockerReportParsed = { ...@@ -111,7 +111,7 @@ export const dockerReportParsed = {
{ {
vulnerability: 'CVE-2014-8130', vulnerability: 'CVE-2014-8130',
namespace: 'debian:8', namespace: 'debian:8',
severity: 'Negligible', severity: 'low',
title: 'CVE-2014-8130', title: 'CVE-2014-8130',
path: 'debian:8', path: 'debian:8',
identifiers: [ identifiers: [
...@@ -134,7 +134,7 @@ export const parsedDast = [ ...@@ -134,7 +134,7 @@ export const parsedDast = [
title: 'Absence of Anti-CSRF Tokens', title: 'Absence of Anti-CSRF Tokens',
riskcode: '1', riskcode: '1',
riskdesc: 'Low (Medium)', riskdesc: 'Low (Medium)',
severity: 'Low', severity: 'low',
cweid: '3', cweid: '3',
desc: '<p>No Anti-CSRF tokens were found in a HTML submission form.</p>', desc: '<p>No Anti-CSRF tokens were found in a HTML submission form.</p>',
pluginid: '123', pluginid: '123',
...@@ -176,7 +176,7 @@ export const parsedDast = [ ...@@ -176,7 +176,7 @@ export const parsedDast = [
url: 'https://cwe.mitre.org/data/definitions/4.html', url: 'https://cwe.mitre.org/data/definitions/4.html',
}, },
], ],
severity: 'Low', severity: 'low',
cweid: '4', cweid: '4',
desc: '<p>The Anti-MIME-Sniffing header X-Content-Type-Options was not set to "nosniff".</p>', desc: '<p>The Anti-MIME-Sniffing header X-Content-Type-Options was not set to "nosniff".</p>',
pluginid: '3456', pluginid: '3456',
...@@ -197,7 +197,7 @@ export const secretScanningParsedIssues = [ ...@@ -197,7 +197,7 @@ export const secretScanningParsedIssues = [
title: 'AWS SecretKey detected', title: 'AWS SecretKey detected',
path: 'Gemfile.lock', path: 'Gemfile.lock',
line: 12, line: 12,
severity: 'Critical', severity: 'critical',
urlPath: 'foo/Gemfile.lock', urlPath: 'foo/Gemfile.lock',
}, },
]; ];
......
...@@ -29,7 +29,7 @@ describe('groupedSastText', () => { ...@@ -29,7 +29,7 @@ describe('groupedSastText', () => {
const sast = createReport(); const sast = createReport();
const result = getters.groupedSastText(sast); const result = getters.groupedSastText(sast);
expect(result).toBe('SAST detected no vulnerabilities for the source branch only'); expect(result).toBe('SAST detected no new vulnerabilities.');
}); });
}); });
......
...@@ -2,11 +2,17 @@ import { ...@@ -2,11 +2,17 @@ import {
findIssueIndex, findIssueIndex,
groupedTextBuilder, groupedTextBuilder,
statusIcon, statusIcon,
countIssues, countVulnerabilities,
groupedReportText, groupedReportText,
} from 'ee/vue_shared/security_reports/store/utils'; } from 'ee/vue_shared/security_reports/store/utils';
import filterByKey from 'ee/vue_shared/security_reports/store/utils/filter_by_key'; import filterByKey from 'ee/vue_shared/security_reports/store/utils/filter_by_key';
import getFileLocation from 'ee/vue_shared/security_reports/store/utils/get_file_location'; import getFileLocation from 'ee/vue_shared/security_reports/store/utils/get_file_location';
import {
CRITICAL,
HIGH,
MEDIUM,
LOW,
} from 'ee/security_dashboard/store/modules/vulnerabilities/constants';
describe('security reports utils', () => { describe('security reports utils', () => {
describe('findIssueIndex', () => { describe('findIssueIndex', () => {
...@@ -75,113 +81,47 @@ describe('security reports utils', () => { ...@@ -75,113 +81,47 @@ describe('security reports utils', () => {
}); });
}); });
describe('textBuilder', () => { describe('groupedTextBuilder', () => {
describe('with only the head', () => { const critical = 2;
const paths = { head: 'foo' }; const high = 4;
const other = 7;
it('should return unable to compare text', () => {
expect(groupedTextBuilder({ paths, added: 1 })).toEqual(
' detected 1 vulnerability for the source branch only',
);
});
it('should return unable to compare text with no vulnerability', () => {
expect(groupedTextBuilder({ paths })).toEqual(
' detected no vulnerabilities for the source branch only',
);
});
it('should return dismissed text', () => {
expect(groupedTextBuilder({ paths, dismissed: 2 })).toEqual(
' detected 2 dismissed vulnerabilities for the source branch only',
);
});
it('should return new and dismissed text', () => {
expect(groupedTextBuilder({ paths, added: 1, dismissed: 2 })).toEqual(
' detected 1 new, and 2 dismissed vulnerabilities for the source branch only',
);
});
});
describe('with base and head', () => {
const paths = { head: 'foo', base: 'foo' };
describe('with no issues', () => {
it('should return no vulnerabiltities text', () => {
expect(groupedTextBuilder({ paths })).toEqual(' detected no vulnerabilities');
});
});
describe('with only `all` issues', () => {
it('should return no new vulnerabiltities text', () => {
expect(groupedTextBuilder({ paths, existing: 1 })).toEqual(
' detected no new vulnerabilities',
);
});
});
describe('with only new issues', () => {
it('should return new issues text', () => {
expect(groupedTextBuilder({ paths, added: 1 })).toEqual(' detected 1 new vulnerability');
expect(groupedTextBuilder({ paths, added: 2 })).toEqual(
' detected 2 new vulnerabilities',
);
});
});
describe('with new and resolved issues', () => {
it('should return new and fixed issues text', () => {
expect(groupedTextBuilder({ paths, added: 1, fixed: 1 }).replace(/\n+\s+/m, ' ')).toEqual(
' detected 1 new, and 1 fixed vulnerabilities',
);
expect(groupedTextBuilder({ paths, added: 2, fixed: 2 }).replace(/\n+\s+/m, ' ')).toEqual(
' detected 2 new, and 2 fixed vulnerabilities',
);
});
});
describe('with only resolved issues', () => {
it('should return fixed issues text', () => {
expect(groupedTextBuilder({ paths, fixed: 1 })).toEqual(
' detected 1 fixed vulnerability',
);
expect(groupedTextBuilder({ paths, fixed: 2 })).toEqual( it.each`
' detected 2 fixed vulnerabilities', vulnerabilities | message
); ${undefined} | ${' detected no new vulnerabilities.'}
}); ${{ critical }} | ${' detected 2 critical severity vulnerabilities.'}
${{ high }} | ${' detected 4 high severity vulnerabilities.'}
${{ other }} | ${' detected 7 vulnerabilities.'}
${{ critical, high }} | ${' detected 2 critical and 4 high severity vulnerabilities.'}
${{ critical, other }} | ${' detected 2 critical severity vulnerabilities out of 9.'}
${{ high, other }} | ${' detected 4 high severity vulnerabilities out of 11.'}
${{ critical, high, other }} | ${' detected 2 critical and 4 high severity vulnerabilities out of 13.'}
`('should build the message as "$message"', ({ vulnerabilities, message }) => {
expect(groupedTextBuilder(vulnerabilities)).toEqual(message);
}); });
describe('with dismissed issues', () => { it.each`
it('should return dismissed text', () => { vulnerabilities | message
expect(groupedTextBuilder({ paths, dismissed: 2 })).toEqual( ${{ critical: 1 }} | ${' detected 1 critical severity vulnerability.'}
' detected 2 dismissed vulnerabilities', ${{ high: 1 }} | ${' detected 1 high severity vulnerability.'}
); ${{ other: 1 }} | ${' detected 1 vulnerability.'}
`('should handle single vulnerabilities for "$message"', ({ vulnerabilities, message }) => {
expect(groupedTextBuilder(vulnerabilities)).toEqual(message);
}); });
it('should return new and dismissed text', () => { it('should pass through the report type', () => {
expect(groupedTextBuilder({ paths, added: 1, dismissed: 2 })).toEqual( const reportType = 'HAL';
' detected 1 new, and 2 dismissed vulnerabilities', expect(groupedTextBuilder({ reportType })).toEqual('HAL detected no new vulnerabilities.');
);
});
it('should return fixed and dismissed text', () => {
expect(groupedTextBuilder({ paths, fixed: 1, dismissed: 2 })).toEqual(
' detected 1 fixed, and 2 dismissed vulnerabilities',
);
}); });
it('should return new, fixed and dismissed text', () => { it('should pass through the status', () => {
expect(groupedTextBuilder({ paths, fixed: 1, added: 1, dismissed: 2 })).toEqual( const reportType = 'HAL';
' detected 1 new, 1 fixed, and 2 dismissed vulnerabilities', const status = '(is loading)';
expect(groupedTextBuilder({ reportType, status })).toEqual(
'HAL (is loading) detected no new vulnerabilities.',
); );
}); });
}); });
});
});
describe('statusIcon', () => { describe('statusIcon', () => {
describe('with failed report', () => { describe('with failed report', () => {
...@@ -209,66 +149,17 @@ describe('security reports utils', () => { ...@@ -209,66 +149,17 @@ describe('security reports utils', () => {
}); });
}); });
describe('countIssues', () => { describe('countVulnerabilities', () => {
const allIssues = [{}]; it.each`
const resolvedIssues = [{}]; vulnerabilities | response
const dismissedIssues = [{ isDismissed: true }]; ${[]} | ${{ critical: 0, high: 0, other: 0 }}
const addedIssues = [{ isDismissed: false }]; ${[{ severity: CRITICAL }, { severity: CRITICAL }]} | ${{ critical: 2, high: 0, other: 0 }}
${[{ severity: HIGH }, { severity: HIGH }]} | ${{ critical: 0, high: 2, other: 0 }}
it('returns 0 for all counts if everything is empty', () => { ${[{ severity: LOW }, { severity: MEDIUM }]} | ${{ critical: 0, high: 0, other: 2 }}
expect(countIssues()).toEqual({ ${[{ severity: CRITICAL }, { severity: HIGH }]} | ${{ critical: 1, high: 1, other: 0 }}
added: 0, ${[{ severity: CRITICAL }, { severity: LOW }]} | ${{ critical: 1, high: 0, other: 1 }}
dismissed: 0, `('should count the vulnerabilities correctly', ({ vulnerabilities, response }) => {
existing: 0, expect(countVulnerabilities(vulnerabilities)).toEqual(response);
fixed: 0,
});
});
it('counts `allIssues` as existing', () => {
expect(countIssues({ allIssues })).toEqual({
added: 0,
dismissed: 0,
existing: 1,
fixed: 0,
});
});
it('counts `resolvedIssues` as fixed', () => {
expect(countIssues({ resolvedIssues })).toEqual({
added: 0,
dismissed: 0,
existing: 0,
fixed: 1,
});
});
it('counts `newIssues` which are dismissed as dismissed', () => {
expect(countIssues({ newIssues: dismissedIssues })).toEqual({
added: 0,
dismissed: 1,
existing: 0,
fixed: 0,
});
});
it('counts `newIssues` which are not dismissed as added', () => {
expect(countIssues({ newIssues: addedIssues })).toEqual({
added: 1,
dismissed: 0,
existing: 0,
fixed: 0,
});
});
it('counts everything', () => {
expect(
countIssues({ newIssues: [...addedIssues, ...dismissedIssues], resolvedIssues, allIssues }),
).toEqual({
added: 1,
dismissed: 1,
existing: 1,
fixed: 1,
});
}); });
}); });
...@@ -296,7 +187,7 @@ describe('security reports utils', () => { ...@@ -296,7 +187,7 @@ describe('security reports utils', () => {
const report = { ...baseReport }; const report = { ...baseReport };
const result = groupedReportText(report, reportType, errorMessage, loadingMessage); const result = groupedReportText(report, reportType, errorMessage, loadingMessage);
expect(result).toBe(`${reportType} detected no vulnerabilities for the source branch only`); expect(result).toBe(`${reportType} detected no new vulnerabilities.`);
}); });
}); });
}); });
...@@ -534,6 +534,36 @@ msgstr[1] "" ...@@ -534,6 +534,36 @@ msgstr[1] ""
msgid "%{remaining_approvals} left" msgid "%{remaining_approvals} left"
msgstr "" msgstr ""
msgid "%{reportType} %{status} detected %{critical} critical and %{high} high severity vulnerabilities out of %{total}."
msgstr ""
msgid "%{reportType} %{status} detected %{critical} critical and %{high} high severity vulnerabilities."
msgstr ""
msgid "%{reportType} %{status} detected %{critical} critical severity vulnerabilities out of %{total}."
msgstr ""
msgid "%{reportType} %{status} detected %{critical} critical severity vulnerability."
msgid_plural "%{reportType} %{status} detected %{critical} critical severity vulnerabilities."
msgstr[0] ""
msgstr[1] ""
msgid "%{reportType} %{status} detected %{high} high severity vulnerabilities out of %{total}."
msgstr ""
msgid "%{reportType} %{status} detected %{high} high severity vulnerability."
msgid_plural "%{reportType} %{status} detected %{high} high severity vulnerabilities."
msgstr[0] ""
msgstr[1] ""
msgid "%{reportType} %{status} detected %{other} vulnerability."
msgid_plural "%{reportType} %{status} detected %{other} vulnerabilities."
msgstr[0] ""
msgstr[1] ""
msgid "%{reportType} %{status} detected no new vulnerabilities."
msgstr ""
msgid "%{retryButtonStart}Try again%{retryButtonEnd} or %{newFileButtonStart}attach a new file%{newFileButtonEnd}" msgid "%{retryButtonStart}Try again%{retryButtonEnd} or %{newFileButtonStart}attach a new file%{newFileButtonEnd}"
msgstr "" msgstr ""
...@@ -26788,55 +26818,6 @@ msgstr "" ...@@ -26788,55 +26818,6 @@ msgstr ""
msgid "ciReport|%{remainingPackagesCount} more" msgid "ciReport|%{remainingPackagesCount} more"
msgstr "" msgstr ""
msgid "ciReport|%{reportType} %{status} detected %{dismissedCount} dismissed vulnerability"
msgid_plural "ciReport|%{reportType} %{status} detected %{dismissedCount} dismissed vulnerabilities"
msgstr[0] ""
msgstr[1] ""
msgid "ciReport|%{reportType} %{status} detected %{dismissedCount} dismissed vulnerability for the source branch only"
msgid_plural "ciReport|%{reportType} %{status} detected %{dismissedCount} dismissed vulnerabilities for the source branch only"
msgstr[0] ""
msgstr[1] ""
msgid "ciReport|%{reportType} %{status} detected %{fixedCount} fixed vulnerability"
msgid_plural "ciReport|%{reportType} %{status} detected %{fixedCount} fixed vulnerabilities"
msgstr[0] ""
msgstr[1] ""
msgid "ciReport|%{reportType} %{status} detected %{fixedCount} fixed, and %{dismissedCount} dismissed vulnerabilities"
msgstr ""
msgid "ciReport|%{reportType} %{status} detected %{newCount} new vulnerability"
msgid_plural "ciReport|%{reportType} %{status} detected %{newCount} new vulnerabilities"
msgstr[0] ""
msgstr[1] ""
msgid "ciReport|%{reportType} %{status} detected %{newCount} new, %{fixedCount} fixed, and %{dismissedCount} dismissed vulnerabilities"
msgstr ""
msgid "ciReport|%{reportType} %{status} detected %{newCount} new, and %{dismissedCount} dismissed vulnerabilities"
msgstr ""
msgid "ciReport|%{reportType} %{status} detected %{newCount} new, and %{dismissedCount} dismissed vulnerabilities for the source branch only"
msgstr ""
msgid "ciReport|%{reportType} %{status} detected %{newCount} new, and %{fixedCount} fixed vulnerabilities"
msgstr ""
msgid "ciReport|%{reportType} %{status} detected %{newCount} vulnerability for the source branch only"
msgid_plural "ciReport|%{reportType} %{status} detected %{newCount} vulnerabilities for the source branch only"
msgstr[0] ""
msgstr[1] ""
msgid "ciReport|%{reportType} %{status} detected no new vulnerabilities"
msgstr ""
msgid "ciReport|%{reportType} %{status} detected no vulnerabilities"
msgstr ""
msgid "ciReport|%{reportType} %{status} detected no vulnerabilities for the source branch only"
msgstr ""
msgid "ciReport|%{reportType} is loading" msgid "ciReport|%{reportType} is loading"
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