Commit 33a594e8 authored by Mark Florian's avatar Mark Florian

Merge branch '216140-grouped-report-text' into 'master'

Updates the groupedReportText builder

See merge request gitlab-org/gitlab!33857
parents 5b839c58 6faaf6a9
......@@ -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
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
make a change to this heading, make sure to update the documentation URLs used in the
......
......@@ -36,7 +36,7 @@ NOTE: **Note:**
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.
![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
see the details and the URL(s) affected.
......
......@@ -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
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:
......
......@@ -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
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:
......
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 messages from './messages';
......@@ -30,27 +30,27 @@ export const groupedDependencyText = ({ dependencyScanning }) =>
messages.DEPENDENCY_SCANNING_IS_LOADING,
);
export const summaryCounts = state =>
[
state.sast,
state.containerScanning,
state.dast,
state.dependencyScanning,
state.secretScanning,
].reduce(
(acc, report) => {
const curr = countIssues(report);
acc.added += curr.added;
acc.dismissed += curr.dismissed;
acc.fixed += curr.fixed;
acc.existing += curr.existing;
return acc;
},
{ added: 0, dismissed: 0, fixed: 0, existing: 0 },
);
export const summaryCounts = ({
containerScanning,
dast,
dependencyScanning,
sast,
secretScanning,
} = {}) => {
const allNewVulns = [
...containerScanning.newIssues,
...dast.newIssues,
...dependencyScanning.newIssues,
...sast.newIssues,
...secretScanning.newIssues,
];
return countVulnerabilities(allNewVulns);
};
export const groupedSummaryText = (state, getters) => {
const reportType = s__('ciReport|Security scanning');
let status = '';
// All reports are loading
if (getters.areAllReportsLoading) {
......@@ -62,10 +62,6 @@ export const groupedSummaryText = (state, getters) => {
return s__('ciReport|Security scanning failed loading any results');
}
const { added, fixed, existing, dismissed } = getters.summaryCounts;
let status = '';
if (getters.areReportsLoading && getters.anyReportHasError) {
status = s__('ciReport|(is loading, errors when loading results)');
} else if (getters.areReportsLoading && !getters.anyReportHasError) {
......@@ -74,13 +70,9 @@ export const groupedSummaryText = (state, getters) => {
status = s__('ciReport|(errors when loading results)');
}
/*
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 };
const { critical, high, other } = getters.summaryCounts;
return groupedTextBuilder({ reportType, paths, added, fixed, existing, dismissed, status });
return groupedTextBuilder({ reportType, status, critical, high, other });
};
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
......@@ -42,103 +43,105 @@ export const enrichVulnerabilityWithFeedback = (vulnerability, feedback = []) =>
return vuln;
}, 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 = ({
reportType = '',
paths = {},
added = 0,
fixed = 0,
existing = 0,
dismissed = 0,
status = '',
}) => {
let baseString = '';
if (!paths.base && !paths.diffEndpoint) {
if (added && !dismissed) {
// added
baseString = n__(
'ciReport|%{reportType} %{status} detected %{newCount} vulnerability for the source branch only',
'ciReport|%{reportType} %{status} detected %{newCount} vulnerabilities for the source branch only',
added,
);
} else if (!added && dismissed) {
// dismissed
baseString = n__(
'ciReport|%{reportType} %{status} detected %{dismissedCount} dismissed vulnerability for the source branch only',
'ciReport|%{reportType} %{status} detected %{dismissedCount} dismissed vulnerabilities for the source branch only',
dismissed,
);
} else if (added && dismissed) {
// added & dismissed
baseString = s__(
'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 (added && !fixed && !dismissed) {
// added
baseString = n__(
'ciReport|%{reportType} %{status} detected %{newCount} new vulnerability',
'ciReport|%{reportType} %{status} detected %{newCount} new vulnerabilities',
added,
critical = 0,
high = 0,
other = 0,
} = {}) => {
// This approach uses bitwise (ish) flags to determine which vulnerabilities
// we have, without the need for too many nested levels of if/else statements.
//
// Here's a video explaining how it works
// https://youtu.be/qZzKNC7TPbA
//
// Here's a link to a similar approach on MDN:
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Bitwise_Operators#Examples
let options = 0;
const HAS_CRITICAL = 1;
const HAS_HIGH = 2;
const HAS_OTHER = 4;
let message;
if (critical) {
options += HAS_CRITICAL;
}
if (high) {
options += HAS_HIGH;
}
if (other) {
options += HAS_OTHER;
}
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) {
// fixed
baseString = n__(
'ciReport|%{reportType} %{status} detected %{fixedCount} fixed vulnerability',
'ciReport|%{reportType} %{status} detected %{fixedCount} fixed vulnerabilities',
fixed,
break;
case HAS_HIGH:
message = n__(
'%{reportType} %{status} detected %{high} high severity vulnerability.',
'%{reportType} %{status} detected %{high} high severity vulnerabilities.',
high,
);
} else if (!added && !fixed && dismissed) {
// dismissed
baseString = n__(
'ciReport|%{reportType} %{status} detected %{dismissedCount} dismissed vulnerability',
'ciReport|%{reportType} %{status} detected %{dismissedCount} dismissed vulnerabilities',
dismissed,
break;
case HAS_OTHER:
message = n__(
'%{reportType} %{status} detected %{other} vulnerability.',
'%{reportType} %{status} detected %{other} vulnerabilities.',
other,
);
} else if (added && fixed && !dismissed) {
// added & fixed
baseString = s__(
'ciReport|%{reportType} %{status} detected %{newCount} new, and %{fixedCount} fixed vulnerabilities',
break;
case HAS_CRITICAL + HAS_HIGH:
message = __(
'%{reportType} %{status} detected %{critical} critical and %{high} high severity vulnerabilities.',
);
} else if (added && !fixed && dismissed) {
// added & dismissed
baseString = s__(
'ciReport|%{reportType} %{status} detected %{newCount} new, and %{dismissedCount} dismissed vulnerabilities',
break;
case HAS_CRITICAL + HAS_OTHER:
message = __(
'%{reportType} %{status} detected %{critical} critical severity vulnerabilities out of %{total}.',
);
} else if (!added && fixed && dismissed) {
// fixed & dismissed
baseString = s__(
'ciReport|%{reportType} %{status} detected %{fixedCount} fixed, and %{dismissedCount} dismissed vulnerabilities',
break;
case HAS_HIGH + HAS_OTHER:
message = __(
'%{reportType} %{status} detected %{high} high severity vulnerabilities out of %{total}.',
);
} else if (added && fixed && dismissed) {
// added & fixed & dismissed
baseString = s__(
'ciReport|%{reportType} %{status} detected %{newCount} new, %{fixedCount} fixed, and %{dismissedCount} dismissed vulnerabilities',
break;
case HAS_CRITICAL + HAS_HIGH + HAS_OTHER:
message = __(
'%{reportType} %{status} detected %{critical} critical and %{high} high severity vulnerabilities out of %{total}.',
);
} else if (existing) {
baseString = s__('ciReport|%{reportType} %{status} detected no new vulnerabilities');
} else {
baseString = s__('ciReport|%{reportType} %{status} detected no vulnerabilities');
}
}
break;
if (!status) {
baseString = baseString.replace('%{status}', '').replace(' ', ' ');
default:
message = __('%{reportType} %{status} detected no new vulnerabilities.');
}
return sprintf(baseString, {
status,
return sprintf(message, {
reportType,
newCount: added,
fixedCount: fixed,
dismissedCount: dismissed,
});
status,
critical,
high,
other,
total: critical + high + other,
}).replace(/\s\s+/g, ' ');
};
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
};
/**
* Counts issues. Simply returns the amount of existing and fixed Issues.
* New Issues are divided into dismissed and added.
* Counts vulnerabilities.
* Returns the amount of critical, high, and other vulnerabilities.
*
* @param newIssues
* @param resolvedIssues
* @param allIssues
* @returns {{existing: number, added: number, dismissed: number, fixed: number}}
* @param {Array} vulnerabilities The raw vulnerabilities to parse
* @returns {{critical: number, high: number, other: number}}
*/
export const countIssues = ({ newIssues = [], resolvedIssues = [], allIssues = [] } = {}) => {
const dismissed = newIssues.reduce((sum, issue) => (issue.isDismissed ? sum + 1 : sum), 0);
export const countVulnerabilities = (vulnerabilities = []) => {
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 {
added: newIssues.length - dismissed,
dismissed,
existing: allIssues.length,
fixed: resolvedIssues.length,
critical,
high,
other,
};
};
......@@ -183,8 +185,6 @@ export const countIssues = ({ newIssues = [], resolvedIssues = [], allIssues = [
* @returns {String}
*/
export const groupedReportText = (report, reportType, errorMessage, loadingMessage) => {
const { paths } = report;
if (report.hasError) {
return errorMessage;
}
......@@ -194,9 +194,8 @@ export const groupedReportText = (report, reportType, errorMessage, loadingMessa
}
return groupedTextBuilder({
...countIssues(report),
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', () => {
`${SAST_SELECTOR} .report-block-list-issue-description`,
).textContent,
),
).toEqual('SAST detected 1 new, and 2 fixed vulnerabilities');
).toEqual('SAST detected 1 vulnerability.');
done();
});
});
......@@ -147,7 +147,7 @@ describe('ee merge request widget options', () => {
`${SAST_SELECTOR} .report-block-list-issue-description`,
).textContent,
).trim(),
).toEqual('SAST detected no vulnerabilities');
).toEqual('SAST detected no new vulnerabilities.');
done();
});
});
......@@ -215,7 +215,7 @@ describe('ee merge request widget options', () => {
`${DEPENDENCY_SCANNING_SELECTOR} .report-block-list-issue-description`,
).textContent,
),
).toEqual('Dependency scanning detected 2 new, and 1 fixed vulnerabilities');
).toEqual('Dependency scanning detected 2 vulnerabilities.');
done();
});
});
......@@ -241,7 +241,7 @@ describe('ee merge request widget options', () => {
`${DEPENDENCY_SCANNING_SELECTOR} .report-block-list-issue-description`,
).textContent,
),
).toEqual('Dependency scanning detected no new vulnerabilities');
).toEqual('Dependency scanning detected no new vulnerabilities.');
done();
});
});
......@@ -263,7 +263,7 @@ describe('ee merge request widget options', () => {
`${DEPENDENCY_SCANNING_SELECTOR} .report-block-list-issue-description`,
).textContent,
),
).toEqual('Dependency scanning detected no vulnerabilities');
).toEqual('Dependency scanning detected no new vulnerabilities.');
done();
});
});
......@@ -687,7 +687,7 @@ describe('ee merge request widget options', () => {
`${CONTAINER_SCANNING_SELECTOR} .report-block-list-issue-description`,
).textContent,
),
).toEqual('Container scanning detected 2 new, and 1 fixed vulnerabilities');
).toEqual('Container scanning detected 2 vulnerabilities.');
done();
});
});
......@@ -757,7 +757,7 @@ describe('ee merge request widget options', () => {
findSecurityWidget()
.querySelector(`${DAST_SELECTOR} .report-block-list-issue-description`)
.textContent.trim(),
).toEqual('DAST detected 1 new, and 2 fixed vulnerabilities');
).toEqual('DAST detected 1 vulnerability.');
done();
});
});
......@@ -831,7 +831,7 @@ describe('ee merge request widget options', () => {
`${SECRET_SCANNING_SELECTOR} .report-block-list-issue-description`,
).textContent,
),
).toEqual('Secret scanning detected 2 new, and 1 fixed vulnerabilities');
).toEqual('Secret scanning detected 2 vulnerabilities.');
done();
});
});
......
......@@ -97,7 +97,9 @@ describe('Report issues', () => {
});
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', () => {
......@@ -121,7 +123,7 @@ describe('Report issues', () => {
it('renders severity and 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', () => {
});
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', () => {
......
......@@ -97,7 +97,9 @@ describe('Report issue', () => {
});
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', () => {
......@@ -121,7 +123,7 @@ describe('Report issue', () => {
it('renders severity and 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', () => {
});
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', () => {
......
......@@ -10,6 +10,12 @@ import {
dependencyScanningIssues,
secretScanningParsedIssues,
} from '../mock_data';
import {
CRITICAL,
HIGH,
MEDIUM,
LOW,
} from 'ee/security_dashboard/store/modules/vulnerabilities/constants';
describe('Security Issue Body', () => {
let wrapper;
......@@ -31,11 +37,11 @@ describe('Security Issue Body', () => {
});
describe.each([
['SAST', sastParsedIssues[0], true, 'High'],
['DAST', parsedDast[0], false, 'Low'],
['Container Scanning', dockerReportParsed.vulnerabilities[0], false, 'Medium'],
['SAST', sastParsedIssues[0], true, HIGH],
['DAST', parsedDast[0], false, LOW],
['Container Scanning', dockerReportParsed.vulnerabilities[0], false, MEDIUM],
['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) => {
beforeEach(() => {
createComponent(vuln);
......
......@@ -187,7 +187,7 @@ describe('Grouped security reports app', () => {
// Renders the summary text
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
......@@ -196,24 +196,20 @@ describe('Grouped security reports app', () => {
);
// Renders Sast result
expect(trimText(wrapper.vm.$el.textContent)).toContain(
'SAST detected 1 new, and 2 fixed vulnerabilities',
);
expect(trimText(wrapper.vm.$el.textContent)).toContain('SAST detected 1 vulnerability');
// Renders DSS result
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
expect(wrapper.vm.$el.textContent).toContain(
'Container scanning detected 2 new, and 1 fixed vulnerabilities',
'Container scanning detected 2 vulnerabilities.',
);
// Renders DAST result
expect(wrapper.vm.$el.textContent).toContain(
'DAST detected 1 new, and 2 fixed vulnerabilities',
);
expect(wrapper.vm.$el.textContent).toContain('DAST detected 1 vulnerability.');
});
it('opens modal with more information', () => {
......@@ -293,9 +289,7 @@ describe('Grouped security reports app', () => {
});
it('should display the correct numbers of vulnerabilities', () => {
expect(wrapper.text()).toContain(
'Container scanning detected 2 new, and 1 fixed vulnerabilities',
);
expect(wrapper.text()).toContain('Container scanning detected 2 vulnerabilities.');
});
});
......@@ -324,7 +318,7 @@ describe('Grouped security reports app', () => {
it('should display the correct numbers of vulnerabilities', () => {
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', () => {
});
it('should display the correct numbers of vulnerabilities', () => {
expect(wrapper.vm.$el.textContent).toContain(
'DAST detected 1 new, and 2 fixed vulnerabilities',
);
expect(wrapper.vm.$el.textContent).toContain('DAST detected 1 vulnerability');
});
it('shows the scanned URLs count and a link to the CI job if available', () => {
......@@ -431,9 +423,7 @@ describe('Grouped security reports app', () => {
});
it('should display the correct numbers of vulnerabilities', () => {
expect(wrapper.text()).toContain(
'Secret scanning detected 2 new, and 1 fixed vulnerabilities',
);
expect(wrapper.text()).toContain('Secret scanning detected 2 vulnerabilities.');
});
});
......@@ -470,9 +460,7 @@ describe('Grouped security reports app', () => {
});
it('should display the correct numbers of vulnerabilities', () => {
expect(wrapper.vm.$el.textContent).toContain(
'SAST detected 1 new, and 2 fixed vulnerabilities',
);
expect(wrapper.vm.$el.textContent).toContain('SAST detected 1 vulnerability.');
});
});
......
......@@ -5,7 +5,7 @@ export const sastParsedIssues = [
title: 'Arbitrary file existence disclosure in Action Pack',
path: 'Gemfile.lock',
line: 12,
severity: 'High',
severity: 'high',
urlPath: 'foo/Gemfile.lock',
report_type: 'sast',
},
......@@ -32,7 +32,7 @@ export const dockerReportParsed = {
{
vulnerability: 'CVE-2017-12944',
namespace: 'debian:8',
severity: 'Medium',
severity: 'medium',
title: 'CVE-2017-12944',
path: 'debian:8',
identifiers: [
......@@ -47,7 +47,7 @@ export const dockerReportParsed = {
{
vulnerability: 'CVE-2017-16232',
namespace: 'debian:8',
severity: 'Negligible',
severity: 'low',
title: 'CVE-2017-16232',
path: 'debian:8',
identifiers: [
......@@ -64,7 +64,7 @@ export const dockerReportParsed = {
{
vulnerability: 'CVE-2014-8130',
namespace: 'debian:8',
severity: 'Negligible',
severity: 'low',
title: 'CVE-2014-8130',
path: 'debian:8',
identifiers: [
......@@ -81,7 +81,7 @@ export const dockerReportParsed = {
{
vulnerability: 'CVE-2017-12944',
namespace: 'debian:8',
severity: 'Medium',
severity: 'medium',
title: 'CVE-2017-12944',
path: 'debian:8',
identifiers: [
......@@ -96,7 +96,7 @@ export const dockerReportParsed = {
{
vulnerability: 'CVE-2017-16232',
namespace: 'debian:8',
severity: 'Negligible',
severity: 'low',
title: 'CVE-2017-16232',
path: 'debian:8',
identifiers: [
......@@ -111,7 +111,7 @@ export const dockerReportParsed = {
{
vulnerability: 'CVE-2014-8130',
namespace: 'debian:8',
severity: 'Negligible',
severity: 'low',
title: 'CVE-2014-8130',
path: 'debian:8',
identifiers: [
......@@ -134,7 +134,7 @@ export const parsedDast = [
title: 'Absence of Anti-CSRF Tokens',
riskcode: '1',
riskdesc: 'Low (Medium)',
severity: 'Low',
severity: 'low',
cweid: '3',
desc: '<p>No Anti-CSRF tokens were found in a HTML submission form.</p>',
pluginid: '123',
......@@ -176,7 +176,7 @@ export const parsedDast = [
url: 'https://cwe.mitre.org/data/definitions/4.html',
},
],
severity: 'Low',
severity: 'low',
cweid: '4',
desc: '<p>The Anti-MIME-Sniffing header X-Content-Type-Options was not set to "nosniff".</p>',
pluginid: '3456',
......@@ -197,7 +197,7 @@ export const secretScanningParsedIssues = [
title: 'AWS SecretKey detected',
path: 'Gemfile.lock',
line: 12,
severity: 'Critical',
severity: 'critical',
urlPath: 'foo/Gemfile.lock',
},
];
......
......@@ -19,351 +19,83 @@ import {
canCreateMergeRequest,
canDismissVulnerability,
} from 'ee/vue_shared/security_reports/store/getters';
import {
CRITICAL,
HIGH,
MEDIUM,
LOW,
} from 'ee/security_dashboard/store/modules/vulnerabilities/constants';
const BASE_PATH = 'fake/base/path.json';
const HEAD_PATH = 'fake/head/path.json';
const MOCK_PATH = 'fake/path.json';
describe('Security reports getters', () => {
function removeBreakLine(data) {
return data.replace(/\r?\n|\r/g, '').replace(/\s\s+/g, ' ');
}
const generateVuln = severity => ({ severity });
let state;
describe('Security reports getters', () => {
let state = {};
beforeEach(() => {
state = createState();
state.sast = createSastState();
});
describe('groupedContainerScanningText', () => {
describe.each`
name | scanner | getter
${'Secret scanning'} | ${'secretScanning'} | ${groupedSecretScanningText}
${'Dependency scanning'} | ${'dependencyScanning'} | ${groupedDependencyText}
${'Container scanning'} | ${'containerScanning'} | ${groupedContainerScanningText}
${'DAST'} | ${'dast'} | ${groupedDastText}
`('grouped text for $name', ({ name, scanner, getter }) => {
describe('with no issues', () => {
it('returns no issues text', () => {
state.containerScanning.paths.head = HEAD_PATH;
state.containerScanning.paths.base = BASE_PATH;
expect(groupedContainerScanningText(state)).toEqual(
'Container scanning detected no vulnerabilities',
);
expect(getter(state)).toEqual(`${name} detected no new vulnerabilities.`);
});
});
describe('with new issues and without base', () => {
it('returns unable to compare text', () => {
state.containerScanning.paths.head = HEAD_PATH;
state.containerScanning.newIssues = [{}];
expect(groupedContainerScanningText(state)).toEqual(
'Container scanning detected 1 vulnerability for the source branch only',
);
});
});
describe('with base and head', () => {
describe('with only new issues', () => {
it('returns new issues text', () => {
state.containerScanning.paths.head = HEAD_PATH;
state.containerScanning.paths.base = BASE_PATH;
state.containerScanning.newIssues = [{}];
expect(groupedContainerScanningText(state)).toEqual(
'Container scanning detected 1 new vulnerability',
);
});
});
describe('with only dismissed issues', () => {
it('returns dismissed issues text', () => {
state.containerScanning.paths.head = HEAD_PATH;
state.containerScanning.paths.base = BASE_PATH;
state.containerScanning.newIssues = [{ isDismissed: true }];
expect(groupedContainerScanningText(state)).toEqual(
'Container scanning detected 1 dismissed vulnerability',
);
});
});
describe('with new and resolved issues', () => {
it('returns new and fixed issues text', () => {
state.containerScanning.paths.head = HEAD_PATH;
state.containerScanning.paths.base = BASE_PATH;
state.containerScanning.newIssues = [{}];
state.containerScanning.resolvedIssues = [{}];
expect(removeBreakLine(groupedContainerScanningText(state))).toEqual(
'Container scanning detected 1 new, and 1 fixed vulnerabilities',
);
});
});
describe('with only resolved issues', () => {
it('returns fixed issues text', () => {
state.containerScanning.paths.head = HEAD_PATH;
state.containerScanning.paths.base = BASE_PATH;
state.containerScanning.resolvedIssues = [{}];
expect(groupedContainerScanningText(state)).toEqual(
'Container scanning detected 1 fixed vulnerability',
);
});
});
});
});
describe('groupedDastText', () => {
describe('with no issues', () => {
it('returns no issues text', () => {
state.dast.paths.head = HEAD_PATH;
state.dast.paths.base = BASE_PATH;
expect(groupedDastText(state)).toEqual('DAST detected no vulnerabilities');
});
});
describe('with new issues and without base', () => {
it('returns unable to compare text', () => {
state.dast.paths.head = HEAD_PATH;
state.dast.newIssues = [{}];
expect(groupedDastText(state)).toEqual(
'DAST detected 1 vulnerability for the source branch only',
);
});
});
describe('with base and head', () => {
describe('with only new issues', () => {
it('returns new issues text', () => {
state.dast.paths.head = HEAD_PATH;
state.dast.paths.base = BASE_PATH;
state.dast.newIssues = [{}];
expect(groupedDastText(state)).toEqual('DAST detected 1 new vulnerability');
});
});
describe('with only dismissed issues', () => {
it('returns dismissed issues text', () => {
state.dast.paths.head = HEAD_PATH;
state.dast.paths.base = BASE_PATH;
state.dast.newIssues = [{ isDismissed: true }];
expect(groupedDastText(state)).toEqual('DAST detected 1 dismissed vulnerability');
});
});
describe('with new and resolved issues', () => {
it('returns new and fixed issues text', () => {
state.dast.paths.head = HEAD_PATH;
state.dast.paths.base = BASE_PATH;
state.dast.newIssues = [{}];
state.dast.resolvedIssues = [{}];
expect(removeBreakLine(groupedDastText(state))).toEqual(
'DAST detected 1 new, and 1 fixed vulnerabilities',
);
});
});
describe('with only resolved issues', () => {
it('returns fixed issues text', () => {
state.dast.paths.head = HEAD_PATH;
state.dast.paths.base = BASE_PATH;
state.dast.resolvedIssues = [{}];
expect(groupedDastText(state)).toEqual('DAST detected 1 fixed vulnerability');
});
});
});
});
describe('groupedDependencyText', () => {
describe('with no issues', () => {
it('returns no issues text', () => {
state.dependencyScanning.paths.head = HEAD_PATH;
state.dependencyScanning.paths.base = BASE_PATH;
expect(groupedDependencyText(state)).toEqual(
'Dependency scanning detected no vulnerabilities',
);
});
});
describe('with new issues and without base', () => {
it('returns unable to compare text', () => {
state.dependencyScanning.paths.head = HEAD_PATH;
state.dependencyScanning.newIssues = [{}];
expect(groupedDependencyText(state)).toEqual(
'Dependency scanning detected 1 vulnerability for the source branch only',
);
});
});
describe('with base and head', () => {
describe('with only new issues', () => {
it('returns new issues text', () => {
state.dependencyScanning.paths.head = HEAD_PATH;
state.dependencyScanning.paths.base = BASE_PATH;
state.dependencyScanning.newIssues = [{}];
expect(groupedDependencyText(state)).toEqual(
'Dependency scanning detected 1 new vulnerability',
);
});
});
describe('with only dismissed issues', () => {
it('returns dismissed issues text', () => {
state.dependencyScanning.paths.head = HEAD_PATH;
state.dependencyScanning.paths.base = BASE_PATH;
state.dependencyScanning.newIssues = [{ isDismissed: true }];
expect(groupedDependencyText(state)).toEqual(
'Dependency scanning detected 1 dismissed vulnerability',
);
});
});
describe('with new and resolved issues', () => {
it('returns new and fixed issues text', () => {
state.dependencyScanning.paths.head = HEAD_PATH;
state.dependencyScanning.paths.base = BASE_PATH;
state.dependencyScanning.newIssues = [{}];
state.dependencyScanning.resolvedIssues = [{}];
expect(removeBreakLine(groupedDependencyText(state))).toEqual(
'Dependency scanning detected 1 new, and 1 fixed vulnerabilities',
);
});
});
describe('with only resolved issues', () => {
it('returns fixed issues text', () => {
state.dependencyScanning.paths.head = HEAD_PATH;
state.dependencyScanning.paths.base = BASE_PATH;
state.dependencyScanning.resolvedIssues = [{}];
expect(groupedDependencyText(state)).toEqual(
'Dependency scanning detected 1 fixed vulnerability',
);
});
});
});
});
describe('groupedSecretScanningText', () => {
describe('with no issues', () => {
it('returns no issues text', () => {
state.secretScanning.paths.head = HEAD_PATH;
state.secretScanning.paths.base = BASE_PATH;
expect(groupedSecretScanningText(state)).toEqual(
'Secret scanning detected no vulnerabilities',
);
});
});
describe('with new issues and without base', () => {
it('returns unable to compare text', () => {
state.secretScanning.paths.head = HEAD_PATH;
state.secretScanning.newIssues = [{}];
expect(groupedSecretScanningText(state)).toEqual(
'Secret scanning detected 1 vulnerability for the source branch only',
);
});
});
describe('with base and head', () => {
describe('with only new issues', () => {
it('returns new issues text', () => {
state.secretScanning.paths.head = HEAD_PATH;
state.secretScanning.paths.base = BASE_PATH;
state.secretScanning.newIssues = [{}];
expect(groupedSecretScanningText(state)).toEqual(
'Secret scanning detected 1 new vulnerability',
);
});
});
describe('with only dismissed issues', () => {
it('returns dismissed issues text', () => {
state.secretScanning.paths.head = HEAD_PATH;
state.secretScanning.paths.base = BASE_PATH;
state.secretScanning.newIssues = [{ isDismissed: true }];
expect(groupedSecretScanningText(state)).toEqual(
'Secret scanning detected 1 dismissed vulnerability',
);
});
});
describe('with new and resolved issues', () => {
it('returns new and fixed issues text', () => {
state.secretScanning.paths.head = HEAD_PATH;
state.secretScanning.paths.base = BASE_PATH;
state.secretScanning.newIssues = [{}];
state.secretScanning.resolvedIssues = [{}];
expect(removeBreakLine(groupedSecretScanningText(state))).toEqual(
'Secret scanning detected 1 new, and 1 fixed vulnerabilities',
);
});
});
describe('with only resolved issues', () => {
it('returns fixed issues text', () => {
state.secretScanning.paths.head = HEAD_PATH;
state.secretScanning.paths.base = BASE_PATH;
state.secretScanning.resolvedIssues = [{}];
expect(groupedSecretScanningText(state)).toEqual(
'Secret scanning detected 1 fixed vulnerability',
);
});
});
it.each`
vulnerabilities | message
${[]} | ${`${name} detected no new vulnerabilities.`}
${[generateVuln(CRITICAL), generateVuln(CRITICAL)]} | ${`${name} detected 2 critical severity vulnerabilities.`}
${[generateVuln(HIGH), generateVuln(HIGH)]} | ${`${name} detected 2 high severity vulnerabilities.`}
${[generateVuln(LOW), generateVuln(MEDIUM)]} | ${`${name} detected 2 vulnerabilities.`}
${[generateVuln(CRITICAL), generateVuln(HIGH)]} | ${`${name} detected 1 critical and 1 high severity vulnerabilities.`}
${[generateVuln(CRITICAL), generateVuln(LOW)]} | ${`${name} detected 1 critical severity vulnerabilities out of 2.`}
`('should build the message as "$message"', ({ vulnerabilities, message }) => {
state[scanner].newIssues = vulnerabilities;
expect(getter(state)).toEqual(message);
});
});
describe('summaryCounts', () => {
it('returns 0 count for empty state', () => {
expect(summaryCounts(state)).toEqual({
added: 0,
dismissed: 0,
existing: 0,
fixed: 0,
critical: 0,
high: 0,
other: 0,
});
});
describe('combines all reports', () => {
it('of the same type', () => {
state.containerScanning.resolvedIssues = [{}];
state.dast.resolvedIssues = [{}];
state.dependencyScanning.resolvedIssues = [{}];
it('of the same severity', () => {
state.containerScanning.newIssues = [generateVuln(CRITICAL)];
state.dast.newIssues = [generateVuln(CRITICAL)];
state.dependencyScanning.newIssues = [generateVuln(CRITICAL)];
expect(summaryCounts(state)).toEqual({
added: 0,
dismissed: 0,
existing: 0,
fixed: 3,
critical: 3,
high: 0,
other: 0,
});
});
it('of the different types', () => {
state.containerScanning.resolvedIssues = [{}];
state.dast.allIssues = [{}];
state.dast.newIssues = [{ isDismissed: true }];
state.dependencyScanning.newIssues = [{ isDismissed: false }];
it('of different severities', () => {
state.containerScanning.newIssues = [generateVuln(CRITICAL)];
state.dast.newIssues = [generateVuln(CRITICAL), generateVuln(HIGH)];
state.dependencyScanning.newIssues = [generateVuln(LOW)];
expect(summaryCounts(state)).toEqual({
added: 1,
dismissed: 1,
existing: 1,
fixed: 1,
critical: 2,
high: 1,
other: 1,
});
});
});
......@@ -374,128 +106,45 @@ describe('Security reports getters', () => {
expect(
groupedSummaryText(state, {
allReportsHaveError: true,
noBaseInAllReports: false,
areReportsLoading: false,
summaryCounts: {},
}),
).toEqual('Security scanning failed loading any results');
});
it('returns no compare text', () => {
expect(
groupedSummaryText(state, {
allReportsHaveError: false,
noBaseInAllReports: true,
areReportsLoading: false,
summaryCounts: {},
}),
).toEqual('Security scanning detected no vulnerabilities for the source branch only');
});
it('returns is loading text', () => {
expect(
groupedSummaryText(state, {
allReportsHaveError: false,
noBaseInAllReports: false,
areReportsLoading: true,
summaryCounts: {},
}),
).toContain('(is loading)');
});
it('returns added and fixed text', () => {
expect(
groupedSummaryText(state, {
allReportsHaveError: false,
noBaseInAllReports: false,
areReportsLoading: false,
summaryCounts: {
added: 2,
fixed: 4,
existing: 5,
},
}),
).toEqual('Security scanning detected 2 new, and 4 fixed vulnerabilities');
});
it('returns added text', () => {
expect(
groupedSummaryText(state, {
allReportsHaveError: false,
noBaseInAllReports: false,
areReportsLoading: false,
summaryCounts: {
added: 2,
existing: 5,
},
}),
).toEqual('Security scanning detected 2 new vulnerabilities');
});
it('returns fixed text', () => {
expect(
groupedSummaryText(state, {
allReportsHaveError: false,
noBaseInAllReports: false,
areReportsLoading: false,
summaryCounts: {
fixed: 4,
existing: 5,
},
}),
).toEqual('Security scanning detected 4 fixed vulnerabilities');
});
it('returns dismissed text', () => {
expect(
groupedSummaryText(state, {
allReportsHaveError: false,
noBaseInAllReports: false,
areReportsLoading: false,
summaryCounts: {
dismissed: 4,
},
}),
).toEqual('Security scanning detected 4 dismissed vulnerabilities');
});
it('returns added and fixed while loading text', () => {
it('returns vulnerabilities while loading text', () => {
expect(
groupedSummaryText(state, {
allReportsHaveError: false,
noBaseInAllReports: false,
areReportsLoading: true,
summaryCounts: {
added: 2,
fixed: 4,
existing: 5,
critical: 2,
high: 4,
},
}),
).toEqual('Security scanning (is loading) detected 2 new, and 4 fixed vulnerabilities');
).toEqual(
'Security scanning (is loading) detected 2 critical and 4 high severity vulnerabilities.',
);
});
it('returns no new text if there are existing ones', () => {
expect(
groupedSummaryText(state, {
allReportsHaveError: false,
noBaseInAllReports: false,
areReportsLoading: false,
summaryCounts: {
existing: 5,
},
}),
).toEqual('Security scanning detected no new vulnerabilities');
});
it('returns no text if there are existing ones', () => {
expect(
groupedSummaryText(state, {
allReportsHaveError: false,
noBaseInAllReports: false,
areReportsLoading: false,
summaryCounts: {},
}),
).toEqual('Security scanning detected no vulnerabilities');
).toEqual('Security scanning detected no new vulnerabilities.');
});
});
......
......@@ -29,7 +29,7 @@ describe('groupedSastText', () => {
const sast = createReport();
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 {
findIssueIndex,
groupedTextBuilder,
statusIcon,
countIssues,
countVulnerabilities,
groupedReportText,
} from 'ee/vue_shared/security_reports/store/utils';
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 {
CRITICAL,
HIGH,
MEDIUM,
LOW,
} from 'ee/security_dashboard/store/modules/vulnerabilities/constants';
describe('security reports utils', () => {
describe('findIssueIndex', () => {
......@@ -75,111 +81,45 @@ describe('security reports utils', () => {
});
});
describe('textBuilder', () => {
describe('with only the head', () => {
const paths = { head: 'foo' };
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(
' detected 2 fixed vulnerabilities',
);
});
});
describe('with dismissed issues', () => {
it('should return dismissed text', () => {
expect(groupedTextBuilder({ paths, dismissed: 2 })).toEqual(
' detected 2 dismissed vulnerabilities',
);
});
it('should return new and dismissed text', () => {
expect(groupedTextBuilder({ paths, added: 1, dismissed: 2 })).toEqual(
' detected 1 new, and 2 dismissed 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', () => {
expect(groupedTextBuilder({ paths, fixed: 1, added: 1, dismissed: 2 })).toEqual(
' detected 1 new, 1 fixed, and 2 dismissed vulnerabilities',
);
});
});
describe('groupedTextBuilder', () => {
const critical = 2;
const high = 4;
const other = 7;
it.each`
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);
});
it.each`
vulnerabilities | message
${{ critical: 1 }} | ${' detected 1 critical severity vulnerability.'}
${{ 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 pass through the report type', () => {
const reportType = 'HAL';
expect(groupedTextBuilder({ reportType })).toEqual('HAL detected no new vulnerabilities.');
});
it('should pass through the status', () => {
const reportType = 'HAL';
const status = '(is loading)';
expect(groupedTextBuilder({ reportType, status })).toEqual(
'HAL (is loading) detected no new vulnerabilities.',
);
});
});
......@@ -209,66 +149,17 @@ describe('security reports utils', () => {
});
});
describe('countIssues', () => {
const allIssues = [{}];
const resolvedIssues = [{}];
const dismissedIssues = [{ isDismissed: true }];
const addedIssues = [{ isDismissed: false }];
it('returns 0 for all counts if everything is empty', () => {
expect(countIssues()).toEqual({
added: 0,
dismissed: 0,
existing: 0,
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,
});
describe('countVulnerabilities', () => {
it.each`
vulnerabilities | response
${[]} | ${{ critical: 0, high: 0, other: 0 }}
${[{ severity: CRITICAL }, { severity: CRITICAL }]} | ${{ critical: 2, high: 0, other: 0 }}
${[{ severity: HIGH }, { severity: HIGH }]} | ${{ critical: 0, high: 2, other: 0 }}
${[{ severity: LOW }, { severity: MEDIUM }]} | ${{ critical: 0, high: 0, other: 2 }}
${[{ severity: CRITICAL }, { severity: HIGH }]} | ${{ critical: 1, high: 1, other: 0 }}
${[{ severity: CRITICAL }, { severity: LOW }]} | ${{ critical: 1, high: 0, other: 1 }}
`('should count the vulnerabilities correctly', ({ vulnerabilities, response }) => {
expect(countVulnerabilities(vulnerabilities)).toEqual(response);
});
});
......@@ -296,7 +187,7 @@ describe('security reports utils', () => {
const report = { ...baseReport };
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] ""
msgid "%{remaining_approvals} left"
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}"
msgstr ""
......@@ -26788,55 +26818,6 @@ msgstr ""
msgid "ciReport|%{remainingPackagesCount} more"
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"
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