Commit 3d355055 authored by Mark Florian's avatar Mark Florian Committed by Jose Ivan Vargas

Colorize security summaries in merge requests

This adds bold and severity-specific colour styling to the numbers of
critical and high severity vulnerabilities in the security-related merge
request widgets.

Addresses https://gitlab.com/gitlab-org/gitlab/-/issues/221084.
parent f35d6e43
...@@ -21,7 +21,8 @@ export default { ...@@ -21,7 +21,8 @@ export default {
props: { props: {
summary: { summary: {
type: String, type: String,
required: true, required: false,
default: '',
}, },
statusIcon: { statusIcon: {
type: String, type: String,
...@@ -58,8 +59,8 @@ export default { ...@@ -58,8 +59,8 @@ export default {
class="report-block-list-issue-description-text" class="report-block-list-issue-description-text"
data-testid="test-summary-row-description" data-testid="test-summary-row-description"
> >
{{ summary <slot name="summary">{{ summary }}</slot
}}<span v-if="popoverOptions" class="text-nowrap" ><span v-if="popoverOptions" class="text-nowrap"
>&nbsp;<popover v-if="popoverOptions" :options="popoverOptions" class="align-top" /> >&nbsp;<popover v-if="popoverOptions" :options="popoverOptions" class="align-top" />
</span> </span>
</div> </div>
......
...@@ -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_1.png) ![Container Scanning Widget](img/container_scanning_v13_2.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_1.png) ![DAST Widget](img/dast_v13_2.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_1.png) ![Dependency Scanning Widget](img/dependency_scanning_v13_2.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_1.png) ![SAST Widget](img/sast_v13_2.png)
The results are sorted by the priority of the vulnerability: The results are sorted by the priority of the vulnerability:
......
...@@ -25,7 +25,7 @@ GitLab displays identified secrets as part of the SAST reports visibly in a few ...@@ -25,7 +25,7 @@ GitLab displays identified secrets as part of the SAST reports visibly in a few
- Pipelines' **Security** tab - Pipelines' **Security** tab
- Report in the merge request widget - Report in the merge request widget
![Secret Detection in merge request widget](img/secret-detection-merge-request-ui.png) ![Secret Detection in merge request widget](img/secret_detection_v13_2.png)
## Use cases ## Use cases
......
import { s__ } from '~/locale';
export const SEVERITY_CLASS_NAME_MAP = {
critical: 'text-danger-800',
high: 'text-danger-600',
medium: 'text-warning-400',
low: 'text-warning-300',
info: 'text-primary-400',
unknown: 'text-secondary-400',
};
export const SEVERITY_TOOLTIP_TITLE_MAP = {
unknown: s__(
`SecurityReports|The rating "unknown" indicates that the underlying scanner doesn’t contain or provide a severity rating.`,
),
};
<script>
import { GlSprintf } from '@gitlab/ui';
import { SEVERITY_CLASS_NAME_MAP } from './constants';
const makeSeveritySlot = (createElement, severity) => ({ content }) =>
createElement('strong', { class: SEVERITY_CLASS_NAME_MAP[severity] }, content);
export default {
functional: true,
props: {
message: {
type: String,
required: true,
},
},
render(createElement, context) {
const { message } = context.props;
return createElement(GlSprintf, {
props: { message },
scopedSlots: {
critical: makeSeveritySlot(createElement, 'critical'),
high: makeSeveritySlot(createElement, 'high'),
},
});
},
};
</script>
<script> <script>
import { s__ } from '~/locale';
import { SEVERITY_LEVELS } from 'ee/security_dashboard/store/constants'; import { SEVERITY_LEVELS } from 'ee/security_dashboard/store/constants';
import { GlIcon, GlTooltipDirective } from '@gitlab/ui'; import { GlIcon, GlTooltipDirective } from '@gitlab/ui';
import { SEVERITY_CLASS_NAME_MAP, SEVERITY_TOOLTIP_TITLE_MAP } from './constants';
export const CLASS_NAME_MAP = {
critical: 'text-danger-800',
high: 'text-danger-600',
medium: 'text-warning-400',
low: 'text-warning-300',
info: 'text-primary-400',
unknown: 'text-secondary-400',
};
export const TOOLTIP_TITLE_MAP = {
unknown: s__(
`SecurityReports|The rating "unknown" indicates that the underlying scanner doesn’t contain or provide a severity rating.`,
),
};
export default { export default {
name: 'SeverityBadge', name: 'SeverityBadge',
...@@ -34,13 +19,13 @@ export default { ...@@ -34,13 +19,13 @@ export default {
}, },
computed: { computed: {
hasSeverityBadge() { hasSeverityBadge() {
return Object.keys(CLASS_NAME_MAP).includes(this.severityKey); return Object.keys(SEVERITY_CLASS_NAME_MAP).includes(this.severityKey);
}, },
severityKey() { severityKey() {
return this.severity.toLowerCase(); return this.severity.toLowerCase();
}, },
className() { className() {
return CLASS_NAME_MAP[this.severityKey]; return SEVERITY_CLASS_NAME_MAP[this.severityKey];
}, },
iconName() { iconName() {
return `severity-${this.severityKey}`; return `severity-${this.severityKey}`;
...@@ -49,7 +34,7 @@ export default { ...@@ -49,7 +34,7 @@ export default {
return SEVERITY_LEVELS[this.severityKey] || this.severity; return SEVERITY_LEVELS[this.severityKey] || this.severity;
}, },
tooltipTitle() { tooltipTitle() {
return TOOLTIP_TITLE_MAP[this.severityKey]; return SEVERITY_TOOLTIP_TITLE_MAP[this.severityKey];
}, },
}, },
}; };
......
...@@ -17,6 +17,7 @@ import { mrStates } from '~/mr_popover/constants'; ...@@ -17,6 +17,7 @@ import { mrStates } from '~/mr_popover/constants';
import { trackMrSecurityReportDetails } from 'ee/vue_shared/security_reports/store/constants'; import { trackMrSecurityReportDetails } from 'ee/vue_shared/security_reports/store/constants';
import { fetchPolicies } from '~/lib/graphql'; import { fetchPolicies } from '~/lib/graphql';
import securityReportSummaryQuery from './graphql/mr_security_report_summary.graphql'; import securityReportSummaryQuery from './graphql/mr_security_report_summary.graphql';
import SecuritySummary from './components/security_summary.vue';
export default { export default {
store: createStore(), store: createStore(),
...@@ -24,6 +25,7 @@ export default { ...@@ -24,6 +25,7 @@ export default {
GroupedIssuesList, GroupedIssuesList,
ReportSection, ReportSection,
SummaryRow, SummaryRow,
SecuritySummary,
IssueModal, IssueModal,
Icon, Icon,
GlSprintf, GlSprintf,
...@@ -326,20 +328,22 @@ export default { ...@@ -326,20 +328,22 @@ export default {
fetchSastDiff: 'fetchDiff', fetchSastDiff: 'fetchDiff',
}), }),
}, },
summarySlots: ['success', 'error', 'loading'],
}; };
</script> </script>
<template> <template>
<report-section <report-section
:status="summaryStatus" :status="summaryStatus"
:success-text="groupedSummaryText"
:loading-text="groupedSummaryText"
:error-text="groupedSummaryText"
:has-issues="true" :has-issues="true"
:should-emit-toggle-event="true" :should-emit-toggle-event="true"
class="mr-widget-border-top grouped-security-reports mr-report" class="mr-widget-border-top grouped-security-reports mr-report"
data-qa-selector="vulnerability_report_grouped" data-qa-selector="vulnerability_report_grouped"
@toggleEvent="handleToggleEvent" @toggleEvent="handleToggleEvent"
> >
<template v-for="slot in $options.summarySlots" #[slot]>
<security-summary :key="slot" :message="groupedSummaryText" />
</template>
<template v-if="pipelinePath" #actionButtons> <template v-if="pipelinePath" #actionButtons>
<div> <div>
<a :href="securityTab" target="_blank" class="btn btn-default btn-sm float-right gl-mr-3"> <a :href="securityTab" target="_blank" class="btn btn-default btn-sm float-right gl-mr-3">
...@@ -388,12 +392,15 @@ export default { ...@@ -388,12 +392,15 @@ export default {
<div class="mr-widget-grouped-section report-block"> <div class="mr-widget-grouped-section report-block">
<template v-if="hasSastReports"> <template v-if="hasSastReports">
<summary-row <summary-row
:summary="groupedSastText"
:status-icon="sastStatusIcon" :status-icon="sastStatusIcon"
:popover-options="sastPopover" :popover-options="sastPopover"
class="js-sast-widget" class="js-sast-widget"
data-qa-selector="sast_scan_report" data-qa-selector="sast_scan_report"
/> >
<template #summary>
<security-summary :message="groupedSastText" />
</template>
</summary-row>
<grouped-issues-list <grouped-issues-list
v-if="sast.newIssues.length || sast.resolvedIssues.length" v-if="sast.newIssues.length || sast.resolvedIssues.length"
...@@ -407,12 +414,15 @@ export default { ...@@ -407,12 +414,15 @@ export default {
<template v-if="hasDependencyScanningReports"> <template v-if="hasDependencyScanningReports">
<summary-row <summary-row
:summary="groupedDependencyText"
:status-icon="dependencyScanningStatusIcon" :status-icon="dependencyScanningStatusIcon"
:popover-options="dependencyScanningPopover" :popover-options="dependencyScanningPopover"
class="js-dependency-scanning-widget" class="js-dependency-scanning-widget"
data-qa-selector="dependency_scan_report" data-qa-selector="dependency_scan_report"
/> >
<template #summary>
<security-summary :message="groupedDependencyText" />
</template>
</summary-row>
<grouped-issues-list <grouped-issues-list
v-if="dependencyScanning.newIssues.length || dependencyScanning.resolvedIssues.length" v-if="dependencyScanning.newIssues.length || dependencyScanning.resolvedIssues.length"
...@@ -426,12 +436,15 @@ export default { ...@@ -426,12 +436,15 @@ export default {
<template v-if="hasContainerScanningReports"> <template v-if="hasContainerScanningReports">
<summary-row <summary-row
:summary="groupedContainerScanningText"
:status-icon="containerScanningStatusIcon" :status-icon="containerScanningStatusIcon"
:popover-options="containerScanningPopover" :popover-options="containerScanningPopover"
class="js-container-scanning" class="js-container-scanning"
data-qa-selector="container_scan_report" data-qa-selector="container_scan_report"
/> >
<template #summary>
<security-summary :message="groupedContainerScanningText" />
</template>
</summary-row>
<grouped-issues-list <grouped-issues-list
v-if="containerScanning.newIssues.length || containerScanning.resolvedIssues.length" v-if="containerScanning.newIssues.length || containerScanning.resolvedIssues.length"
...@@ -445,12 +458,15 @@ export default { ...@@ -445,12 +458,15 @@ export default {
<template v-if="hasDastReports"> <template v-if="hasDastReports">
<summary-row <summary-row
:summary="groupedDastText"
:status-icon="dastStatusIcon" :status-icon="dastStatusIcon"
:popover-options="dastPopover" :popover-options="dastPopover"
class="js-dast-widget" class="js-dast-widget"
data-qa-selector="dast_scan_report" data-qa-selector="dast_scan_report"
> >
<template #summary>
<security-summary :message="groupedDastText" />
</template>
<template v-if="dastScans.length"> <template v-if="dastScans.length">
<div class="text-nowrap"> <div class="text-nowrap">
{{ n__('%d URL scanned', '%d URLs scanned', dastScans[0].scanned_resources_count) }} {{ n__('%d URL scanned', '%d URLs scanned', dastScans[0].scanned_resources_count) }}
...@@ -478,12 +494,15 @@ export default { ...@@ -478,12 +494,15 @@ export default {
<template v-if="hasSecretScanningReports"> <template v-if="hasSecretScanningReports">
<summary-row <summary-row
:summary="groupedSecretScanningText"
:status-icon="secretScanningStatusIcon" :status-icon="secretScanningStatusIcon"
:popover-options="secretScanningPopover" :popover-options="secretScanningPopover"
class="js-secret-scanning" class="js-secret-scanning"
data-qa-selector="secret_scan_report" data-qa-selector="secret_scan_report"
/> >
<template #summary>
<security-summary :message="groupedSecretScanningText" />
</template>
</summary-row>
<grouped-issues-list <grouped-issues-list
v-if="secretScanning.newIssues.length || secretScanning.resolvedIssues.length" v-if="secretScanning.newIssues.length || secretScanning.resolvedIssues.length"
......
...@@ -46,6 +46,10 @@ export const enrichVulnerabilityWithFeedback = (vulnerability, feedback = []) => ...@@ -46,6 +46,10 @@ export const enrichVulnerabilityWithFeedback = (vulnerability, feedback = []) =>
/** /**
* Takes an object of options and returns an externalized string representing * Takes an object of options and returns an externalized string representing
* the critical, high, and other severity vulnerabilities for a given report. * the critical, high, and other severity vulnerabilities for a given report.
*
* The resulting string _may_ still contain sprintf-style placeholders. These
* are left in place so they can be replaced with markup, via the
* SecuritySummary component.
* @param {{reportType: string, status: string, critical: number, high: number, other: number}} options * @param {{reportType: string, status: string, critical: number, high: number, other: number}} options
* @returns {string} * @returns {string}
*/ */
...@@ -84,16 +88,16 @@ export const groupedTextBuilder = ({ ...@@ -84,16 +88,16 @@ export const groupedTextBuilder = ({
switch (options) { switch (options) {
case HAS_CRITICAL: case HAS_CRITICAL:
message = n__( message = n__(
'%{reportType} %{status} detected %{critical} critical severity vulnerability.', '%{reportType} %{status} detected %{criticalStart}%{critical} new critical%{criticalEnd} severity vulnerability.',
'%{reportType} %{status} detected %{critical} critical severity vulnerabilities.', '%{reportType} %{status} detected %{criticalStart}%{critical} new critical%{criticalEnd} severity vulnerabilities.',
critical, critical,
); );
break; break;
case HAS_HIGH: case HAS_HIGH:
message = n__( message = n__(
'%{reportType} %{status} detected %{high} high severity vulnerability.', '%{reportType} %{status} detected %{highStart}%{high} new high%{highEnd} severity vulnerability.',
'%{reportType} %{status} detected %{high} high severity vulnerabilities.', '%{reportType} %{status} detected %{highStart}%{high} new high%{highEnd} severity vulnerabilities.',
high, high,
); );
break; break;
...@@ -108,25 +112,25 @@ export const groupedTextBuilder = ({ ...@@ -108,25 +112,25 @@ export const groupedTextBuilder = ({
case HAS_CRITICAL + HAS_HIGH: case HAS_CRITICAL + HAS_HIGH:
message = __( message = __(
'%{reportType} %{status} detected %{critical} critical and %{high} high severity vulnerabilities.', '%{reportType} %{status} detected %{criticalStart}%{critical} new critical%{criticalEnd} and %{highStart}%{high} new high%{highEnd} severity vulnerabilities.',
); );
break; break;
case HAS_CRITICAL + HAS_OTHER: case HAS_CRITICAL + HAS_OTHER:
message = __( message = __(
'%{reportType} %{status} detected %{critical} critical severity vulnerabilities out of %{total}.', '%{reportType} %{status} detected %{criticalStart}%{critical} new critical%{criticalEnd} severity vulnerabilities out of %{total}.',
); );
break; break;
case HAS_HIGH + HAS_OTHER: case HAS_HIGH + HAS_OTHER:
message = __( message = __(
'%{reportType} %{status} detected %{high} high severity vulnerabilities out of %{total}.', '%{reportType} %{status} detected %{highStart}%{high} new high%{highEnd} severity vulnerabilities out of %{total}.',
); );
break; break;
case HAS_CRITICAL + HAS_HIGH + HAS_OTHER: case HAS_CRITICAL + HAS_HIGH + HAS_OTHER:
message = __( message = __(
'%{reportType} %{status} detected %{critical} critical and %{high} high severity vulnerabilities out of %{total}.', '%{reportType} %{status} detected %{criticalStart}%{critical} new critical%{criticalEnd} and %{highStart}%{high} new high%{highEnd} severity vulnerabilities out of %{total}.',
); );
break; break;
......
---
title: Colorize security summaries in merge requests
merge_request: 36035
author:
type: changed
...@@ -139,7 +139,7 @@ describe('ee merge request widget options', () => { ...@@ -139,7 +139,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 vulnerability.'); ).toEqual('SAST detected 1 new critical severity vulnerability.');
done(); done();
}); });
}); });
...@@ -229,7 +229,9 @@ describe('ee merge request widget options', () => { ...@@ -229,7 +229,9 @@ 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 vulnerabilities.'); ).toEqual(
'Dependency scanning detected 1 new critical and 1 new high severity vulnerabilities.',
);
done(); done();
}); });
}); });
...@@ -845,7 +847,9 @@ describe('ee merge request widget options', () => { ...@@ -845,7 +847,9 @@ 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 vulnerabilities.'); ).toEqual(
'Container scanning detected 1 new critical and 1 new high severity vulnerabilities.',
);
done(); done();
}); });
}); });
...@@ -915,7 +919,7 @@ describe('ee merge request widget options', () => { ...@@ -915,7 +919,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 vulnerability.'); ).toEqual('DAST detected 1 new critical severity vulnerability.');
done(); done();
}); });
}); });
...@@ -989,7 +993,9 @@ describe('ee merge request widget options', () => { ...@@ -989,7 +993,9 @@ 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 vulnerabilities.'); ).toEqual(
'Secret scanning detected 1 new critical and 1 new high severity vulnerabilities.',
);
done(); done();
}); });
}); });
......
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Severity Summary given the message "" interpolates correctly 1`] = `<div />`;
exports[`Severity Summary given the message "%{criticalStart}1 critical%{criticalEnd} and %{highStart}2 high%{highEnd}" interpolates correctly 1`] = `
<div>
<strong
class="text-danger-800"
>
1 critical
</strong>
and
<strong
class="text-danger-600"
>
2 high
</strong>
</div>
`;
exports[`Severity Summary given the message "%{criticalStart}1 critical%{criticalEnd}" interpolates correctly 1`] = `
<div>
<strong
class="text-danger-800"
>
1 critical
</strong>
</div>
`;
exports[`Severity Summary given the message "%{highStart}1 high%{highEnd}" interpolates correctly 1`] = `
<div>
<strong
class="text-danger-600"
>
1 high
</strong>
</div>
`;
exports[`Severity Summary given the message "foo" interpolates correctly 1`] = `
<div>
foo
</div>
`;
import { mount } from '@vue/test-utils';
import SecuritySummary from 'ee/vue_shared/security_reports/components/security_summary.vue';
describe('Severity Summary', () => {
let wrapper;
const createWrapper = message => {
wrapper = mount({
components: {
SecuritySummary,
},
data() {
return {
message,
};
},
template: `<div><security-summary :message="message" /></div>`,
});
};
afterEach(() => {
wrapper.destroy();
wrapper = null;
});
describe.each([
'',
'foo',
'%{criticalStart}1 critical%{criticalEnd}',
'%{highStart}1 high%{highEnd}',
'%{criticalStart}1 critical%{criticalEnd} and %{highStart}2 high%{highEnd}',
])('given the message %p', message => {
beforeEach(() => {
createWrapper(message);
});
it('interpolates correctly', () => {
expect(wrapper.element).toMatchSnapshot();
});
});
});
import { shallowMount } from '@vue/test-utils'; import { shallowMount } from '@vue/test-utils';
import SeverityBadge, { import SeverityBadge from 'ee/vue_shared/security_reports/components/severity_badge.vue';
CLASS_NAME_MAP, import {
TOOLTIP_TITLE_MAP, SEVERITY_CLASS_NAME_MAP,
} from 'ee/vue_shared/security_reports/components/severity_badge.vue'; SEVERITY_TOOLTIP_TITLE_MAP,
} from 'ee/vue_shared/security_reports/components/constants';
import { GlIcon } from '@gitlab/ui'; import { GlIcon } from '@gitlab/ui';
import { createMockDirective, getBinding } from 'helpers/vue_mock_directive'; import { createMockDirective, getBinding } from 'helpers/vue_mock_directive';
...@@ -33,7 +34,7 @@ describe('Severity Badge', () => { ...@@ -33,7 +34,7 @@ describe('Severity Badge', () => {
createWrapper({ severity }); createWrapper({ severity });
}); });
const className = CLASS_NAME_MAP[severity]; const className = SEVERITY_CLASS_NAME_MAP[severity];
it(`renders the component with ${severity} badge`, () => { it(`renders the component with ${severity} badge`, () => {
expect(wrapper.find(`.${className}`).exists()).toBe(true); expect(wrapper.find(`.${className}`).exists()).toBe(true);
...@@ -49,7 +50,7 @@ describe('Severity Badge', () => { ...@@ -49,7 +50,7 @@ describe('Severity Badge', () => {
}); });
it('renders tooltip', () => { it('renders tooltip', () => {
expect(findTooltip()).toBe(TOOLTIP_TITLE_MAP[severity]); expect(findTooltip()).toBe(SEVERITY_TOOLTIP_TITLE_MAP[severity]);
}); });
}); });
......
...@@ -139,7 +139,7 @@ key2: value2" ...@@ -139,7 +139,7 @@ key2: value2"
> >
<severity-badge-stub <severity-badge-stub
class="d-inline" class="d-inline"
severity="unknown" severity="critical"
/> />
</vulnerability-detail-stub> </vulnerability-detail-stub>
......
...@@ -183,6 +183,55 @@ describe('Grouped security reports app', () => { ...@@ -183,6 +183,55 @@ describe('Grouped security reports app', () => {
}); });
}); });
describe('with empty reports', () => {
beforeEach(() => {
const emptyResponse = { ...dastDiffSuccessMock, fixed: [], added: [] };
mock.onGet(CONTAINER_SCANNING_DIFF_ENDPOINT).reply(200, emptyResponse);
mock.onGet(DEPENDENCY_SCANNING_DIFF_ENDPOINT).reply(200, emptyResponse);
mock.onGet(DAST_DIFF_ENDPOINT).reply(200, emptyResponse);
mock.onGet(SAST_DIFF_ENDPOINT).reply(200, emptyResponse);
mock.onGet(SECRET_SCANNING_DIFF_ENDPOINT).reply(200, emptyResponse);
createWrapper(allReportProps);
return Promise.all([
waitForMutation(wrapper.vm.$store, `sast/${sastTypes.RECEIVE_DIFF_SUCCESS}`),
waitForMutation(wrapper.vm.$store, types.RECEIVE_DAST_DIFF_SUCCESS),
waitForMutation(wrapper.vm.$store, types.RECEIVE_CONTAINER_SCANNING_DIFF_SUCCESS),
waitForMutation(wrapper.vm.$store, types.RECEIVE_DEPENDENCY_SCANNING_DIFF_SUCCESS),
waitForMutation(wrapper.vm.$store, types.RECEIVE_SECRET_SCANNING_DIFF_SUCCESS),
]);
});
it('renders reports', () => {
// It's not loading
expect(wrapper.vm.$el.querySelector('.gl-spinner')).toBeNull();
// Renders the summary text
expect(wrapper.vm.$el.querySelector('.js-code-text').textContent.trim()).toEqual(
'Security scanning detected no new vulnerabilities.',
);
// Renders Sast result
expect(trimText(wrapper.vm.$el.textContent)).toContain(
'SAST detected no new vulnerabilities.',
);
// Renders DSS result
expect(trimText(wrapper.vm.$el.textContent)).toContain(
'Dependency scanning detected no new vulnerabilities.',
);
// Renders container scanning result
expect(wrapper.vm.$el.textContent).toContain(
'Container scanning detected no new vulnerabilities.',
);
// Renders DAST result
expect(wrapper.vm.$el.textContent).toContain('DAST detected no new vulnerabilities.');
});
});
describe('with successful responses', () => { describe('with successful responses', () => {
beforeEach(() => { beforeEach(() => {
mock.onGet(CONTAINER_SCANNING_DIFF_ENDPOINT).reply(200, containerScanningDiffSuccessMock); mock.onGet(CONTAINER_SCANNING_DIFF_ENDPOINT).reply(200, containerScanningDiffSuccessMock);
...@@ -208,7 +257,7 @@ describe('Grouped security reports app', () => { ...@@ -208,7 +257,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 vulnerabilities.', 'Security scanning detected 5 new critical and 3 new high severity vulnerabilities.',
); );
// Renders the expand button // Renders the expand button
...@@ -217,20 +266,24 @@ describe('Grouped security reports app', () => { ...@@ -217,20 +266,24 @@ describe('Grouped security reports app', () => {
); );
// Renders Sast result // Renders Sast result
expect(trimText(wrapper.vm.$el.textContent)).toContain('SAST detected 1 vulnerability'); expect(trimText(wrapper.vm.$el.textContent)).toContain(
'SAST detected 1 new critical severity vulnerability',
);
// Renders DSS result // Renders DSS result
expect(trimText(wrapper.vm.$el.textContent)).toContain( expect(trimText(wrapper.vm.$el.textContent)).toContain(
'Dependency scanning detected 2 vulnerabilities.', 'Dependency scanning detected 1 new critical and 1 new high severity 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 vulnerabilities.', 'Container scanning detected 1 new critical and 1 new high severity vulnerabilities.',
); );
// Renders DAST result // Renders DAST result
expect(wrapper.vm.$el.textContent).toContain('DAST detected 1 vulnerability.'); expect(wrapper.vm.$el.textContent).toContain(
'DAST detected 1 new critical severity vulnerability.',
);
}); });
it('opens modal with more information', () => { it('opens modal with more information', () => {
...@@ -311,7 +364,9 @@ describe('Grouped security reports app', () => { ...@@ -311,7 +364,9 @@ 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('Container scanning detected 2 vulnerabilities.'); expect(wrapper.text()).toContain(
'Container scanning detected 1 new critical and 1 new high severity vulnerabilities.',
);
}); });
}); });
...@@ -340,7 +395,7 @@ describe('Grouped security reports app', () => { ...@@ -340,7 +395,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 vulnerabilities.', 'Dependency scanning detected 1 new critical and 1 new high severity vulnerabilities.',
); );
}); });
}); });
...@@ -378,7 +433,9 @@ describe('Grouped security reports app', () => { ...@@ -378,7 +433,9 @@ 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('DAST detected 1 vulnerability'); expect(wrapper.vm.$el.textContent).toContain(
'DAST detected 1 new critical severity vulnerability',
);
}); });
it('shows the scanned URLs count and opens a modal', async () => { it('shows the scanned URLs count and opens a modal', async () => {
...@@ -449,7 +506,9 @@ describe('Grouped security reports app', () => { ...@@ -449,7 +506,9 @@ 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('Secret scanning detected 2 vulnerabilities.'); expect(wrapper.text()).toContain(
'Secret scanning detected 1 new critical and 1 new high severity vulnerabilities.',
);
}); });
}); });
...@@ -486,7 +545,9 @@ describe('Grouped security reports app', () => { ...@@ -486,7 +545,9 @@ 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('SAST detected 1 vulnerability.'); expect(wrapper.vm.$el.textContent).toContain(
'SAST detected 1 new critical severity vulnerability.',
);
}); });
}); });
......
...@@ -307,7 +307,7 @@ export const mockFindings = [ ...@@ -307,7 +307,7 @@ export const mockFindings = [
id: null, id: null,
report_type: 'dependency_scanning', report_type: 'dependency_scanning',
name: 'Cross-site Scripting in serialize-javascript', name: 'Cross-site Scripting in serialize-javascript',
severity: 'unknown', severity: 'critical',
scanner: { scanner: {
external_id: 'gemnasium', external_id: 'gemnasium',
name: 'Gemnasium', name: 'Gemnasium',
...@@ -360,7 +360,7 @@ export const mockFindings = [ ...@@ -360,7 +360,7 @@ export const mockFindings = [
id: null, id: null,
report_type: 'dependency_scanning', report_type: 'dependency_scanning',
name: '3rd party CORS request may execute in jquery', name: '3rd party CORS request may execute in jquery',
severity: 'medium', severity: 'high',
scanner: { external_id: 'retire.js', name: 'Retire.js' }, scanner: { external_id: 'retire.js', name: 'Retire.js' },
identifiers: [ identifiers: [
{ {
......
...@@ -54,11 +54,11 @@ describe('Security reports getters', () => { ...@@ -54,11 +54,11 @@ describe('Security reports getters', () => {
it.each` it.each`
vulnerabilities | message vulnerabilities | message
${[]} | ${`${name} detected no new vulnerabilities.`} ${[]} | ${`${name} detected no new vulnerabilities.`}
${[generateVuln(CRITICAL), generateVuln(CRITICAL)]} | ${`${name} detected 2 critical severity vulnerabilities.`} ${[generateVuln(CRITICAL), generateVuln(CRITICAL)]} | ${`${name} detected %{criticalStart}2 new critical%{criticalEnd} severity vulnerabilities.`}
${[generateVuln(HIGH), generateVuln(HIGH)]} | ${`${name} detected 2 high severity vulnerabilities.`} ${[generateVuln(HIGH), generateVuln(HIGH)]} | ${`${name} detected %{highStart}2 new high%{highEnd} severity vulnerabilities.`}
${[generateVuln(LOW), generateVuln(MEDIUM)]} | ${`${name} detected 2 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(HIGH)]} | ${`${name} detected %{criticalStart}1 new critical%{criticalEnd} and %{highStart}1 new high%{highEnd} severity vulnerabilities.`}
${[generateVuln(CRITICAL), generateVuln(LOW)]} | ${`${name} detected 1 critical severity vulnerabilities out of 2.`} ${[generateVuln(CRITICAL), generateVuln(LOW)]} | ${`${name} detected %{criticalStart}1 new critical%{criticalEnd} severity vulnerabilities out of 2.`}
`('should build the message as "$message"', ({ vulnerabilities, message }) => { `('should build the message as "$message"', ({ vulnerabilities, message }) => {
state[scanner].newIssues = vulnerabilities; state[scanner].newIssues = vulnerabilities;
expect(getter(state)).toEqual(message); expect(getter(state)).toEqual(message);
...@@ -133,7 +133,7 @@ describe('Security reports getters', () => { ...@@ -133,7 +133,7 @@ describe('Security reports getters', () => {
}, },
}), }),
).toEqual( ).toEqual(
'Security scanning (is loading) detected 2 critical and 4 high severity vulnerabilities.', 'Security scanning (is loading) detected %{criticalStart}2 new critical%{criticalEnd} and %{highStart}4 new high%{highEnd} severity vulnerabilities.',
); );
}); });
......
...@@ -128,21 +128,21 @@ describe('security reports utils', () => { ...@@ -128,21 +128,21 @@ describe('security reports utils', () => {
it.each` it.each`
vulnerabilities | message vulnerabilities | message
${undefined} | ${' detected no new vulnerabilities.'} ${undefined} | ${' detected no new vulnerabilities.'}
${{ critical }} | ${' detected 2 critical severity vulnerabilities.'} ${{ critical }} | ${' detected %{criticalStart}2 new critical%{criticalEnd} severity vulnerabilities.'}
${{ high }} | ${' detected 4 high severity vulnerabilities.'} ${{ high }} | ${' detected %{highStart}4 new high%{highEnd} severity vulnerabilities.'}
${{ other }} | ${' detected 7 vulnerabilities.'} ${{ other }} | ${' detected 7 vulnerabilities.'}
${{ critical, high }} | ${' detected 2 critical and 4 high severity vulnerabilities.'} ${{ critical, high }} | ${' detected %{criticalStart}2 new critical%{criticalEnd} and %{highStart}4 new high%{highEnd} severity vulnerabilities.'}
${{ critical, other }} | ${' detected 2 critical severity vulnerabilities out of 9.'} ${{ critical, other }} | ${' detected %{criticalStart}2 new critical%{criticalEnd} severity vulnerabilities out of 9.'}
${{ high, other }} | ${' detected 4 high severity vulnerabilities out of 11.'} ${{ high, other }} | ${' detected %{highStart}4 new high%{highEnd} severity vulnerabilities out of 11.'}
${{ critical, high, other }} | ${' detected 2 critical and 4 high severity vulnerabilities out of 13.'} ${{ critical, high, other }} | ${' detected %{criticalStart}2 new critical%{criticalEnd} and %{highStart}4 new high%{highEnd} severity vulnerabilities out of 13.'}
`('should build the message as "$message"', ({ vulnerabilities, message }) => { `('should build the message as "$message"', ({ vulnerabilities, message }) => {
expect(groupedTextBuilder(vulnerabilities)).toEqual(message); expect(groupedTextBuilder(vulnerabilities)).toEqual(message);
}); });
it.each` it.each`
vulnerabilities | message vulnerabilities | message
${{ critical: 1 }} | ${' detected 1 critical severity vulnerability.'} ${{ critical: 1 }} | ${' detected %{criticalStart}1 new critical%{criticalEnd} severity vulnerability.'}
${{ high: 1 }} | ${' detected 1 high severity vulnerability.'} ${{ high: 1 }} | ${' detected %{highStart}1 new high%{highEnd} severity vulnerability.'}
${{ other: 1 }} | ${' detected 1 vulnerability.'} ${{ other: 1 }} | ${' detected 1 vulnerability.'}
`('should handle single vulnerabilities for "$message"', ({ vulnerabilities, message }) => { `('should handle single vulnerabilities for "$message"', ({ vulnerabilities, message }) => {
expect(groupedTextBuilder(vulnerabilities)).toEqual(message); expect(groupedTextBuilder(vulnerabilities)).toEqual(message);
......
...@@ -571,25 +571,25 @@ msgstr[1] "" ...@@ -571,25 +571,25 @@ 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}." msgid "%{reportType} %{status} detected %{criticalStart}%{critical} new critical%{criticalEnd} and %{highStart}%{high} new high%{highEnd} severity vulnerabilities out of %{total}."
msgstr "" msgstr ""
msgid "%{reportType} %{status} detected %{critical} critical and %{high} high severity vulnerabilities." msgid "%{reportType} %{status} detected %{criticalStart}%{critical} new critical%{criticalEnd} and %{highStart}%{high} new high%{highEnd} severity vulnerabilities."
msgstr "" msgstr ""
msgid "%{reportType} %{status} detected %{critical} critical severity vulnerabilities out of %{total}." msgid "%{reportType} %{status} detected %{criticalStart}%{critical} new critical%{criticalEnd} severity vulnerabilities out of %{total}."
msgstr "" msgstr ""
msgid "%{reportType} %{status} detected %{critical} critical severity vulnerability." msgid "%{reportType} %{status} detected %{criticalStart}%{critical} new critical%{criticalEnd} severity vulnerability."
msgid_plural "%{reportType} %{status} detected %{critical} critical severity vulnerabilities." msgid_plural "%{reportType} %{status} detected %{criticalStart}%{critical} new critical%{criticalEnd} severity vulnerabilities."
msgstr[0] "" msgstr[0] ""
msgstr[1] "" msgstr[1] ""
msgid "%{reportType} %{status} detected %{high} high severity vulnerabilities out of %{total}." msgid "%{reportType} %{status} detected %{highStart}%{high} new high%{highEnd} severity vulnerabilities out of %{total}."
msgstr "" msgstr ""
msgid "%{reportType} %{status} detected %{high} high severity vulnerability." msgid "%{reportType} %{status} detected %{highStart}%{high} new high%{highEnd} severity vulnerability."
msgid_plural "%{reportType} %{status} detected %{high} high severity vulnerabilities." msgid_plural "%{reportType} %{status} detected %{highStart}%{high} new high%{highEnd} severity vulnerabilities."
msgstr[0] "" msgstr[0] ""
msgstr[1] "" msgstr[1] ""
......
import Vue from 'vue'; import { mount } from '@vue/test-utils';
import mountComponent from 'helpers/vue_mount_component_helper'; import SummaryRow from '~/reports/components/summary_row.vue';
import component from '~/reports/components/summary_row.vue';
describe('Summary row', () => { describe('Summary row', () => {
const Component = Vue.extend(component); let wrapper;
let vm;
const props = { const props = {
summary: 'SAST detected 1 new vulnerability and 1 fixed vulnerability', summary: 'SAST detected 1 new vulnerability and 1 fixed vulnerability',
...@@ -15,23 +13,42 @@ describe('Summary row', () => { ...@@ -15,23 +13,42 @@ describe('Summary row', () => {
statusIcon: 'warning', statusIcon: 'warning',
}; };
beforeEach(() => { const createComponent = ({ propsData = {}, slots = {} } = {}) => {
vm = mountComponent(Component, props); wrapper = mount(SummaryRow, {
}); propsData: {
...props,
...propsData,
},
slots,
});
};
const findSummary = () => wrapper.find('.report-block-list-issue-description-text');
afterEach(() => { afterEach(() => {
vm.$destroy(); wrapper.destroy();
wrapper = null;
}); });
it('renders provided summary', () => { it('renders provided summary', () => {
expect( createComponent();
vm.$el.querySelector('.report-block-list-issue-description-text').textContent.trim(), expect(findSummary().text()).toEqual(props.summary);
).toEqual(props.summary);
}); });
it('renders provided icon', () => { it('renders provided icon', () => {
expect(vm.$el.querySelector('.report-block-list-icon span').classList).toContain( createComponent();
expect(wrapper.find('.report-block-list-icon span').classes()).toContain(
'js-ci-status-icon-warning', 'js-ci-status-icon-warning',
); );
}); });
describe('summary slot', () => {
it('replaces the summary prop', () => {
const summarySlotContent = 'Summary slot content';
createComponent({ slots: { summary: summarySlotContent } });
expect(wrapper.text()).not.toContain(props.summary);
expect(findSummary().text()).toEqual(summarySlotContent);
});
});
}); });
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