Commit 3340ab89 authored by Savas Vedova's avatar Savas Vedova

Merge branch 'add_warnings_widget_to_pipeline_security_tab' into 'master'

Add warnings to "pipeline security tab"

See merge request gitlab-org/gitlab!80934
parents 0554c4df 22e8a70f
......@@ -7,15 +7,17 @@ import { fetchPolicies } from '~/lib/graphql';
import { s__ } from '~/locale';
import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import VulnerabilityReport from '../shared/vulnerability_report.vue';
import ScanErrorsAlert from './scan_errors_alert.vue';
import ScanAlerts, { TYPE_ERRORS, TYPE_WARNINGS } from './scan_alerts.vue';
import SecurityDashboard from './security_dashboard_vuex.vue';
import SecurityReportsSummary from './security_reports_summary.vue';
export default {
name: 'PipelineSecurityDashboard',
errorsAlertType: TYPE_ERRORS,
warningsAlertType: TYPE_WARNINGS,
components: {
GlEmptyState,
ScanErrorsAlert,
ScanAlerts,
SecurityReportsSummary,
SecurityDashboard,
VulnerabilityReport,
......@@ -76,20 +78,31 @@ export default {
primaryButtonText: s__('SecurityReports|Learn more about setting up your dashboard'),
};
},
scansWithErrors() {
scans() {
const getScans = (reportSummary) => reportSummary?.scans?.nodes || [];
const hasErrors = (scan) => Boolean(scan.errors?.length);
return this.reportSummary
? Object.values(this.reportSummary)
// generate flat array of all scans
.flatMap(getScans)
.filter(hasErrors)
: [];
},
scansWithErrors() {
const hasErrors = (scan) => Boolean(scan.errors?.length);
return this.scans.filter(hasErrors);
},
hasScansWithErrors() {
return this.scansWithErrors.length > 0;
},
scansWithWarnings() {
const hasWarnings = (scan) => Boolean(scan.warnings?.length);
return this.scans.filter(hasWarnings);
},
hasScansWithWarnings() {
return this.scansWithWarnings.length > 0;
},
},
created() {
this.setSourceBranch(this.pipeline.sourceBranch);
......@@ -100,13 +113,38 @@ export default {
...mapActions('vulnerabilities', ['setSourceBranch']),
...mapActions('pipelineJobs', ['setPipelineJobsPath', 'setProjectId']),
},
i18n: {
parsingErrorAlertTitle: s__('SecurityReports|Error parsing security reports'),
parsingErrorAlertDescription: s__(
'SecurityReports|The following security reports contain one or more vulnerability findings that could not be parsed and were not recorded. To investigate a report, download the artifacts in the job output. Ensure the security report conforms to the relevant %{helpPageLinkStart}JSON schema%{helpPageLinkEnd}.',
),
parsingWarningAlertTitle: s__('SecurityReports|Warning parsing security reports'),
parsingWarningAlertDescription: s__(
'SecurityReports|Check the messages generated while parsing the following security reports, as they may prevent the results from being ingested by GitLab. Ensure the security report conforms to a supported %{helpPageLinkStart}JSON schema%{helpPageLinkEnd}.',
),
},
};
</script>
<template>
<div>
<div v-if="reportSummary" class="gl-my-5">
<scan-errors-alert v-if="hasScansWithErrors" :scans="scansWithErrors" class="gl-mb-5" />
<scan-alerts
v-if="hasScansWithErrors"
:type="$options.errorsAlertType"
:scans="scansWithErrors"
:title="$options.i18n.parsingErrorAlertTitle"
:description="$options.i18n.parsingErrorAlertDescription"
class="gl-mb-5"
/>
<scan-alerts
v-if="hasScansWithWarnings"
:type="$options.warningsAlertType"
:scans="scansWithWarnings"
:title="$options.i18n.parsingWarningAlertTitle"
:description="$options.i18n.parsingWarningAlertDescription"
class="gl-mb-5"
/>
<security-reports-summary :summary="reportSummary" :jobs="jobs" />
</div>
<security-dashboard
......
<script>
import { GlAccordion, GlAccordionItem, GlAlert, GlButton, GlSprintf } from '@gitlab/ui';
import { s__ } from '~/locale';
export const TYPE_ERRORS = 'errors';
export const TYPE_WARNINGS = 'warnings';
export default {
components: {
......@@ -16,31 +18,45 @@ export default {
type: Array,
required: true,
},
type: {
type: String,
required: true,
validator: (value) => [TYPE_ERRORS, TYPE_WARNINGS].includes(value),
},
title: {
type: String,
required: true,
},
description: {
type: String,
required: true,
},
},
computed: {
alertVariant() {
return {
[TYPE_ERRORS]: 'danger',
[TYPE_WARNINGS]: 'warning',
}[this.type];
},
scansWithTitles() {
return this.scans.map((scan) => ({
...scan,
title: `${scan.name} (${scan.errors.length})`,
issues: scan[this.type],
accordionTitle: `${scan.name} (${scan[this.type].length})`,
}));
},
},
i18n: {
title: s__('SecurityReports|Error parsing security reports'),
description: s__(
'SecurityReports|The security reports below contain one or more vulnerability findings that could not be parsed and were not recorded. Download the artifacts in the job output to investigate. Ensure any security report created conforms to the relevant %{helpPageLinkStart}JSON schema%{helpPageLinkEnd}.',
),
},
};
</script>
<template>
<gl-alert variant="danger" :dismissible="false">
<gl-alert :variant="alertVariant" :dismissible="false">
<strong role="heading">
{{ $options.i18n.title }}
{{ title }}
</strong>
<p class="gl-mt-3">
<gl-sprintf :message="$options.i18n.description" data-testid="description">
<gl-sprintf :message="description" data-testid="description">
<template #helpPageLink="{ content }">
<gl-button
variant="link"
......@@ -55,12 +71,12 @@ export default {
</p>
<gl-accordion :header-level="3">
<gl-accordion-item
v-for="{ name, errors, title } in scansWithTitles"
v-for="{ name, issues, accordionTitle } in scansWithTitles"
:key="name"
:title="title"
:title="accordionTitle"
>
<ul class="gl-pl-4">
<li v-for="error in errors" :key="error">{{ error }}</li>
<li v-for="issue in issues" :key="issue">{{ issue }}</li>
</ul>
</gl-accordion-item>
</gl-accordion>
......
......@@ -3,6 +3,7 @@ fragment SecurityReportSummaryScans on SecurityReportSummarySection {
nodes {
name
errors
warnings
}
}
}
......@@ -25,7 +25,7 @@
can_admin_vulnerability: can?(current_user, :admin_vulnerability, project).to_s,
false_positive_doc_url: help_page_path('user/application_security/vulnerabilities/index'),
can_view_false_positive: project.licensed_feature_available?(:sast_fp_reduction).to_s,
security_report_help_page_link: help_page_path('user/application_security/index', anchor: 'security-report-validation') } }
security_report_help_page_link: help_page_path('development/integrations/secure', anchor: 'report') } }
- if pipeline.expose_license_scanning_data?
#js-tab-licenses.tab-pane
......
......@@ -220,9 +220,12 @@ export const pipelineSecurityReportSummary = {
},
};
export const scansWithErrors = [{ errors: ['error description'], name: 'scan-name' }];
export const scansWithErrors = [{ errors: ['error description'], warnings: [], name: 'scan-name' }];
export const scansWithWarnings = [
{ errors: [], warnings: ['warning description'], name: 'scan-name' },
];
export const pipelineSecurityReportSummaryWithErrors = merge({}, pipelineSecurityReportSummary, {
const getSecurityReportsSummaryMock = (nodes) => ({
data: {
project: {
id: 'project-1',
......@@ -232,7 +235,7 @@ export const pipelineSecurityReportSummaryWithErrors = merge({}, pipelineSecurit
dast: {
__typename: 'SecurityReportSummarySection',
scans: {
nodes: scansWithErrors,
nodes,
},
},
},
......@@ -241,6 +244,18 @@ export const pipelineSecurityReportSummaryWithErrors = merge({}, pipelineSecurit
},
});
export const pipelineSecurityReportSummaryWithErrors = merge(
{},
pipelineSecurityReportSummary,
getSecurityReportsSummaryMock(scansWithErrors),
);
export const pipelineSecurityReportSummaryWithWarnings = merge(
{},
pipelineSecurityReportSummary,
getSecurityReportsSummaryMock(scansWithWarnings),
);
export const pipelineSecurityReportSummaryEmpty = merge({}, pipelineSecurityReportSummary, {
data: {
project: {
......
......@@ -7,14 +7,19 @@ import createMockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises';
import pipelineSecurityReportSummaryQuery from 'ee/security_dashboard/graphql/queries/pipeline_security_report_summary.query.graphql';
import PipelineSecurityDashboard from 'ee/security_dashboard/components/pipeline/pipeline_security_dashboard.vue';
import ScanErrorsAlert from 'ee/security_dashboard/components/pipeline/scan_errors_alert.vue';
import ScanAlerts, {
TYPE_ERRORS,
TYPE_WARNINGS,
} from 'ee/security_dashboard/components/pipeline/scan_alerts.vue';
import SecurityDashboard from 'ee/security_dashboard/components/pipeline/security_dashboard_vuex.vue';
import SecurityReportsSummary from 'ee/security_dashboard/components/pipeline/security_reports_summary.vue';
import VulnerabilityReport from 'ee/security_dashboard/components/shared/vulnerability_report.vue';
import {
pipelineSecurityReportSummary,
pipelineSecurityReportSummaryWithErrors,
pipelineSecurityReportSummaryWithWarnings,
scansWithErrors,
scansWithWarnings,
pipelineSecurityReportSummaryEmpty,
} from './mock_data';
......@@ -39,7 +44,7 @@ describe('Pipeline Security Dashboard component', () => {
const findSecurityDashboard = () => wrapper.findComponent(SecurityDashboard);
const findVulnerabilityReport = () => wrapper.findComponent(VulnerabilityReport);
const findScanErrorsAlert = () => wrapper.findComponent(ScanErrorsAlert);
const findScanAlerts = () => wrapper.findComponent(ScanAlerts);
const factory = ({ stubs, provide, apolloProvider } = {}) => {
store = new Vuex.Store({
......@@ -173,7 +178,10 @@ describe('Pipeline Security Dashboard component', () => {
});
it('shows an alert with information about each scan with errors', () => {
expect(findScanErrorsAlert().props('scans')).toEqual(scansWithErrors);
expect(findScanAlerts().props()).toMatchObject({
scans: scansWithErrors,
type: TYPE_ERRORS,
});
});
});
......@@ -190,7 +198,47 @@ describe('Pipeline Security Dashboard component', () => {
});
it('does not show the alert', () => {
expect(findScanErrorsAlert().exists()).toBe(false);
expect(findScanAlerts().exists()).toBe(false);
});
});
});
describe('scan warnings', () => {
describe('with warnings', () => {
beforeEach(async () => {
factoryWithApollo({
requestHandlers: [
[
pipelineSecurityReportSummaryQuery,
jest.fn().mockResolvedValueOnce(pipelineSecurityReportSummaryWithWarnings),
],
],
});
await waitForPromises();
});
it('shows an alert with information about each scan with warnings', () => {
expect(findScanAlerts().props()).toMatchObject({
scans: scansWithWarnings,
type: TYPE_WARNINGS,
});
});
});
describe('without warnings', () => {
beforeEach(() => {
factoryWithApollo({
requestHandlers: [
[
pipelineSecurityReportSummaryQuery,
jest.fn().mockResolvedValueOnce(pipelineSecurityReportSummary),
],
],
});
});
it('does not show the alert', () => {
expect(findScanAlerts().exists()).toBe(false);
});
});
});
......
import { GlAccordion, GlAccordionItem, GlAlert, GlButton, GlSprintf } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import PipelineScanErrorsAlert from 'ee/security_dashboard/components/pipeline/scan_errors_alert.vue';
import PipelineScanAlerts from 'ee/security_dashboard/components/pipeline/scan_alerts.vue';
import { trimText } from 'helpers/text_helper';
import { extendedWrapper } from 'helpers/vue_test_utils_helper';
const TEST_HELP_PAGE_LINK = 'http://help.com';
const TEST_SCANS_WITH_ERRORS = [
{ errors: ['scanner 1 - error 1', 'scanner 1 - error 2'], name: 'foo' },
{ errors: ['scanner 1 - error 3', 'scanner 1 - error 4'], name: 'bar' },
{ errors: ['scanner 3 - error 1', 'scanner 3 - error 2'], name: 'baz' },
{
errors: ['scanner 1 - error 1', 'scanner 1 - error 2'],
warnings: ['scanner 1 - warning 1', 'scanner 1 - warning 2'],
name: 'foo',
},
{
errors: ['scanner 2 - error 1', 'scanner 2 - error 2'],
warnings: ['scanner 2 - warning 1', 'scanner 2 - warning 2'],
name: 'bar',
},
{
errors: ['scanner 3 - error 1', 'scanner 3 - error 2'],
warnings: ['scanner 3 - warning 1', 'scanner 3 - warning 2'],
name: 'baz',
},
];
describe('ee/security_dashboard/components/pipeline_scan_errors_alert.vue', () => {
describe('ee/security_dashboard/components/pipeline_scan_alerts.vue', () => {
let wrapper;
let type = 'errors';
const createWrapper = () =>
extendedWrapper(
shallowMount(PipelineScanErrorsAlert, {
shallowMount(PipelineScanAlerts, {
propsData: {
scans: TEST_SCANS_WITH_ERRORS,
type,
title: 'Test title',
description: 'Test description %{helpPageLinkStart}link text%{helpPageLinkEnd}.',
},
provide: {
securityReportHelpPageLink: TEST_HELP_PAGE_LINK,
......@@ -45,39 +61,32 @@ describe('ee/security_dashboard/components/pipeline_scan_errors_alert.vue', () =
wrapper = createWrapper();
});
it('shows a non-dismissible error alert', () => {
expect(findAlert().props()).toMatchObject({
variant: 'danger',
dismissible: false,
});
});
it('shows the correct title for the error alert', () => {
expect(findAlert().text()).toContain('Error parsing security reports');
it('shows the correct title for the alert', () => {
expect(findAlert().text()).toContain('Test title');
});
it('shows the correct description for the error-alert', () => {
expect(trimText(findAlert().text())).toContain(
'The security reports below contain one or more vulnerability findings that could not be parsed and were not recorded. Download the artifacts in the job output to investigate. Ensure any security report created conforms to the relevant JSON schema',
);
it('shows the correct description for the alert', () => {
expect(trimText(findAlert().text())).toContain('Test description');
});
it('links to the security-report help page', () => {
expect(findHelpPageLink().attributes('href')).toBe(TEST_HELP_PAGE_LINK);
});
describe('errors details', () => {
it('shows an accordion containing a list of scans with errors', () => {
describe('alert details', () => {
it('shows an accordion containing a list of scans with messages', () => {
expect(findAccordion().exists()).toBe(true);
expect(findAllAccordionItems()).toHaveLength(TEST_SCANS_WITH_ERRORS.length);
});
it('shows a list containing details about each error', () => {
it('shows a list containing details about each message', () => {
expect(findErrorList().exists()).toBe(true);
});
});
const sharedAlertMessagesTest = () => {
describe.each(TEST_SCANS_WITH_ERRORS)('scan errors', (scan) => {
const currentScanTitle = `${scan.name} (${scan.errors.length})`;
const currentScanTitle = `${scan.name} (${scan[type].length})`;
const findAllAccordionItemsForCurrentScan = () =>
findAccordionItemsWithTitle(currentScanTitle);
const findAccordionItemForCurrentScan = () => findAllAccordionItemsForCurrentScan().at(0);
......@@ -86,10 +95,36 @@ describe('ee/security_dashboard/components/pipeline_scan_errors_alert.vue', () =
expect(findAllAccordionItemsForCurrentScan()).toHaveLength(1);
});
it(`contains a detailed list of errors for scan "${scan.name}}"`, () => {
it(`contains a detailed list of messages for scan "${scan.name}}"`, () => {
expect(findAccordionItemForCurrentScan().find('ul').exists()).toBe(true);
expect(findAccordionItemForCurrentScan().findAll('li')).toHaveLength(scan.errors.length);
expect(findAccordionItemForCurrentScan().findAll('li')).toHaveLength(scan[type].length);
});
});
};
describe('when the type is errors', () => {
it('shows a non-dismissible error alert', () => {
expect(findAlert().props()).toMatchObject({
variant: 'danger',
dismissible: false,
});
});
sharedAlertMessagesTest();
});
describe('when the type is warnings', () => {
beforeAll(() => {
type = 'warnings';
});
it('shows a non-dismissible warning alert', () => {
expect(findAlert().props()).toMatchObject({
variant: 'warning',
dismissible: false,
});
});
sharedAlertMessagesTest();
});
});
......@@ -32827,6 +32827,9 @@ msgstr ""
msgid "SecurityReports|Change status"
msgstr ""
msgid "SecurityReports|Check the messages generated while parsing the following security reports, as they may prevent the results from being ingested by GitLab. Ensure the security report conforms to a supported %{helpPageLinkStart}JSON schema%{helpPageLinkEnd}."
msgstr ""
msgid "SecurityReports|Comment added to '%{vulnerabilityName}'"
msgstr ""
......@@ -33013,7 +33016,7 @@ msgstr ""
msgid "SecurityReports|The Vulnerability Report shows the results of the latest successful pipeline on your project's default branch, as well as vulnerabilities from your latest container scan. %{linkStart}Learn more.%{linkEnd}"
msgstr ""
msgid "SecurityReports|The security reports below contain one or more vulnerability findings that could not be parsed and were not recorded. Download the artifacts in the job output to investigate. Ensure any security report created conforms to the relevant %{helpPageLinkStart}JSON schema%{helpPageLinkEnd}."
msgid "SecurityReports|The following security reports contain one or more vulnerability findings that could not be parsed and were not recorded. To investigate a report, download the artifacts in the job output. Ensure the security report conforms to the relevant %{helpPageLinkStart}JSON schema%{helpPageLinkEnd}."
msgstr ""
msgid "SecurityReports|There was an error adding the comment."
......@@ -33073,6 +33076,9 @@ msgstr ""
msgid "SecurityReports|Vulnerability Report"
msgstr ""
msgid "SecurityReports|Warning parsing security reports"
msgstr ""
msgid "SecurityReports|While it's rare to have no vulnerabilities for your pipeline, it can happen. In any event, we ask that you double check your settings to make sure all security scanning jobs have passed successfully."
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