Commit 8b37e510 authored by Kushal Pandya's avatar Kushal Pandya

Merge branch '352880_show_scans_status_on_pipeline_security_tab' into 'master'

Show security report status on "pipeline security tab"

See merge request gitlab-org/gitlab!82508
parents c8df6a46 3ebe6780
......@@ -327,6 +327,21 @@ You can find the schemas for these scanners here:
- [Coverage Fuzzing](https://gitlab.com/gitlab-org/security-products/security-report-schemas/-/blob/master/dist/coverage-fuzzing-report-format.json)
- [Secret Detection](https://gitlab.com/gitlab-org/security-products/security-report-schemas/-/blob/master/dist/secret-detection-report-format.json)
### Retention period for vulnerabilities
GitLab has the following retention policies for vulnerabilities on non-default branches. Vulnerabilities are no longer available:
- When the related CI job artifact expires.
- 90 days after the pipeline is created, even if the related CI job artifacts are locked.
To view vulnerabilities, either:
- Re-run the pipeline.
- Download the related CI job artifacts if they are available.
NOTE:
This does not apply for the vulnerabilities existing on the default branch.
### Enable report validation
> [Deprecated](https://gitlab.com/gitlab-org/gitlab/-/issues/354928) in GitLab 14.9, and planned for removal in GitLab 15.0.
......
......@@ -7,6 +7,7 @@ import { fetchPolicies } from '~/lib/graphql';
import { s__ } from '~/locale';
import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import ScanAlerts, { TYPE_ERRORS, TYPE_WARNINGS } from './scan_alerts.vue';
import ReportStatusAlert, { STATUS_PURGED } from './report_status_alert.vue';
import SecurityDashboard from './security_dashboard_vuex.vue';
import SecurityReportsSummary from './security_reports_summary.vue';
import PipelineVulnerabilityReport from './pipeline_vulnerability_report.vue';
......@@ -15,8 +16,10 @@ export default {
name: 'PipelineSecurityDashboard',
errorsAlertType: TYPE_ERRORS,
warningsAlertType: TYPE_WARNINGS,
scanPurgedStatus: STATUS_PURGED,
components: {
GlEmptyState,
ReportStatusAlert,
ScanAlerts,
SecurityReportsSummary,
SecurityDashboard,
......@@ -87,21 +90,27 @@ export default {
.flatMap(getScans)
: [];
},
purgedScans() {
return this.scans.filter((scan) => scan.status === this.$options.scanPurgedStatus);
},
hasPurgedScans() {
return this.purgedScans.length > 0;
},
scansWithErrors() {
const hasErrors = (scan) => Boolean(scan.errors?.length);
return this.scans.filter(hasErrors);
},
hasScansWithErrors() {
return this.scansWithErrors.length > 0;
showScanErrors() {
return this.scansWithErrors.length > 0 && !this.hasPurgedScans;
},
scansWithWarnings() {
const hasWarnings = (scan) => Boolean(scan.warnings?.length);
return this.scans.filter(hasWarnings);
},
hasScansWithWarnings() {
return this.scansWithWarnings.length > 0;
showScanWarnings() {
return this.scansWithWarnings.length > 0 && !this.hasPurgedScans;
},
},
created() {
......@@ -130,7 +139,7 @@ export default {
<div>
<div v-if="reportSummary" class="gl-my-5">
<scan-alerts
v-if="hasScansWithErrors"
v-if="showScanErrors"
:type="$options.errorsAlertType"
:scans="scansWithErrors"
:title="$options.i18n.parsingErrorAlertTitle"
......@@ -138,13 +147,14 @@ export default {
class="gl-mb-5"
/>
<scan-alerts
v-if="hasScansWithWarnings"
v-if="showScanWarnings"
:type="$options.warningsAlertType"
:scans="scansWithWarnings"
:title="$options.i18n.parsingWarningAlertTitle"
:description="$options.i18n.parsingWarningAlertDescription"
class="gl-mb-5"
/>
<report-status-alert v-if="hasPurgedScans" class="gl-mb-5" />
<security-reports-summary :summary="reportSummary" :jobs="jobs" />
</div>
<security-dashboard
......
<script>
import { GlAlert, GlSprintf, GlButton } from '@gitlab/ui';
import { s__ } from '~/locale';
export const STATUS_PURGED = 'PURGED';
export default {
components: {
GlAlert,
GlSprintf,
GlButton,
},
inject: ['vulnerabilityRetentionPeriodHelpPageLink'],
i18n: {
reportStatusTitle: s__('SecurityReports|Report has expired'),
reportExpiredMessage: s__(
`SecurityReports|The security report for this pipeline has %{helpPageLinkStart}expired%{helpPageLinkEnd}. Re-run the pipeline to generate a new security report.`,
),
},
};
</script>
<template>
<gl-alert variant="info" :dismissible="false">
<strong role="heading">{{ $options.i18n.reportStatusTitle }}</strong>
<p class="gl-mt-3">
<gl-sprintf :message="$options.i18n.reportExpiredMessage" data-testid="description">
<template #helpPageLink="{ content }">
<gl-button
variant="link"
icon="external-link"
:href="vulnerabilityRetentionPeriodHelpPageLink"
target="_blank"
>
{{ content }}
</gl-button>
</template>
</gl-sprintf>
</p>
</gl-alert>
</template>
......@@ -2,6 +2,7 @@ fragment SecurityReportSummaryScans on SecurityReportSummarySection {
scans {
nodes {
name
status
errors
warnings
}
......
......@@ -29,6 +29,7 @@ export default () => {
pipelineJobsPath,
canAdminVulnerability,
securityReportHelpPageLink,
vulnerabilityRetentionPeriodHelpPageLink,
falsePositiveDocUrl,
canViewFalsePositive,
} = el.dataset;
......@@ -63,6 +64,7 @@ export default () => {
sourceBranch,
},
securityReportHelpPageLink,
vulnerabilityRetentionPeriodHelpPageLink,
vulnerabilitiesEndpoint,
loadingErrorIllustrations,
falsePositiveDocUrl,
......
......@@ -25,7 +25,8 @@
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('development/integrations/secure', anchor: 'report') } }
security_report_help_page_link: help_page_path('development/integrations/secure', anchor: 'report'),
vulnerability_retention_period_help_page_link: help_page_path('development/integrations/secure', anchor: 'retention-period-for-vulnerabilities') } }
- if pipeline.expose_license_scanning_data?
#js-tab-licenses.tab-pane
......
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`ee/security_dashboard/components/report_status_alert.vue renders the component correctly 1`] = `
VueWrapper {
"_emitted": Object {},
"_emittedByOrder": Array [],
"isFunctionalComponent": undefined,
}
`;
......@@ -220,9 +220,18 @@ export const pipelineSecurityReportSummary = {
},
};
export const scansWithErrors = [{ errors: ['error description'], warnings: [], name: 'scan-name' }];
const purgedScan = {
errors: ['error description'],
warnings: [],
name: 'scan-name',
status: 'PURGED',
};
export const scansWithErrors = [
{ errors: ['error description'], warnings: [], name: 'scan-name', status: 'SUCCEEDED' },
];
export const scansWithWarnings = [
{ errors: [], warnings: ['warning description'], name: 'scan-name' },
{ errors: [], warnings: ['warning description'], name: 'scan-name', status: 'SUCCEEDED' },
];
const getSecurityReportsSummaryMock = (nodes) => ({
......@@ -244,6 +253,18 @@ const getSecurityReportsSummaryMock = (nodes) => ({
},
});
export const purgedPipelineSecurityReportSummaryWithErrors = merge(
{},
pipelineSecurityReportSummary,
getSecurityReportsSummaryMock(scansWithErrors.concat(purgedScan)),
);
export const purgedPipelineSecurityReportSummaryWithWarnings = merge(
{},
pipelineSecurityReportSummary,
getSecurityReportsSummaryMock(scansWithWarnings.concat(purgedScan)),
);
export const pipelineSecurityReportSummaryWithErrors = merge(
{},
pipelineSecurityReportSummary,
......
......@@ -7,6 +7,7 @@ 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 ReportStatusAlert from 'ee/security_dashboard/components/pipeline/report_status_alert.vue';
import ScanAlerts, {
TYPE_ERRORS,
TYPE_WARNINGS,
......@@ -18,6 +19,8 @@ import {
pipelineSecurityReportSummary,
pipelineSecurityReportSummaryWithErrors,
pipelineSecurityReportSummaryWithWarnings,
purgedPipelineSecurityReportSummaryWithErrors,
purgedPipelineSecurityReportSummaryWithWarnings,
scansWithErrors,
scansWithWarnings,
pipelineSecurityReportSummaryEmpty,
......@@ -45,6 +48,7 @@ describe('Pipeline Security Dashboard component', () => {
const findSecurityDashboard = () => wrapper.findComponent(SecurityDashboard);
const findVulnerabilityReport = () => wrapper.findComponent(PipelineVulnerabilityReport);
const findScanAlerts = () => wrapper.findComponent(ScanAlerts);
const findReportStatusAlert = () => wrapper.findComponent(ReportStatusAlert);
const factory = ({ stubs, provide, apolloProvider } = {}) => {
store = new Vuex.Store({
......@@ -163,24 +167,82 @@ describe('Pipeline Security Dashboard component', () => {
});
});
describe('scans error alert', () => {
describe('with errors', () => {
describe('report status alert', () => {
describe('with purged scans', () => {
beforeEach(async () => {
factoryWithApollo({
requestHandlers: [
[
pipelineSecurityReportSummaryQuery,
jest.fn().mockResolvedValueOnce(pipelineSecurityReportSummaryWithErrors),
jest.fn().mockResolvedValueOnce(purgedPipelineSecurityReportSummaryWithErrors),
],
],
});
await waitForPromises();
});
it('shows an alert with information about each scan with errors', () => {
expect(findScanAlerts().props()).toMatchObject({
scans: scansWithErrors,
type: TYPE_ERRORS,
it('shows the alert', () => {
expect(findReportStatusAlert().exists()).toBe(true);
});
});
describe('without purged scans', () => {
beforeEach(async () => {
factoryWithApollo({
requestHandlers: [
[
pipelineSecurityReportSummaryQuery,
jest.fn().mockResolvedValueOnce(pipelineSecurityReportSummary),
],
],
});
await waitForPromises();
});
it('does not show the alert', () => {
expect(findReportStatusAlert().exists()).toBe(false);
});
});
});
describe('scans error alert', () => {
describe('with errors', () => {
describe('with purged scans', () => {
beforeEach(async () => {
factoryWithApollo({
requestHandlers: [
[
pipelineSecurityReportSummaryQuery,
jest.fn().mockResolvedValueOnce(purgedPipelineSecurityReportSummaryWithErrors),
],
],
});
await waitForPromises();
});
it('does not show the alert', () => {
expect(findScanAlerts().exists()).toBe(false);
});
});
describe('without purged scans', () => {
beforeEach(async () => {
factoryWithApollo({
requestHandlers: [
[
pipelineSecurityReportSummaryQuery,
jest.fn().mockResolvedValueOnce(pipelineSecurityReportSummaryWithErrors),
],
],
});
await waitForPromises();
});
it('shows an alert with information about each scan with errors', () => {
expect(findScanAlerts().props()).toMatchObject({
scans: scansWithErrors,
type: TYPE_ERRORS,
});
});
});
});
......@@ -205,22 +267,42 @@ describe('Pipeline Security Dashboard component', () => {
describe('scan warnings', () => {
describe('with warnings', () => {
beforeEach(async () => {
factoryWithApollo({
requestHandlers: [
[
pipelineSecurityReportSummaryQuery,
jest.fn().mockResolvedValueOnce(pipelineSecurityReportSummaryWithWarnings),
describe('with purged scans', () => {
beforeEach(async () => {
factoryWithApollo({
requestHandlers: [
[
pipelineSecurityReportSummaryQuery,
jest.fn().mockResolvedValueOnce(purgedPipelineSecurityReportSummaryWithWarnings),
],
],
],
});
await waitForPromises();
});
it('does not show the alert', () => {
expect(findScanAlerts().exists()).toBe(false);
});
await waitForPromises();
});
it('shows an alert with information about each scan with warnings', () => {
expect(findScanAlerts().props()).toMatchObject({
scans: scansWithWarnings,
type: TYPE_WARNINGS,
describe('without purged scans', () => {
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,
});
});
});
});
......
import { GlAlert, GlSprintf, GlButton } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import ReportStatusAlert from 'ee/security_dashboard/components/pipeline/report_status_alert.vue';
import { extendedWrapper } from 'helpers/vue_test_utils_helper';
import { trimText } from 'helpers/text_helper';
const TEST_HELP_PAGE_LINK = 'https://help.com';
describe('ee/security_dashboard/components/report_status_alert.vue', () => {
let wrapper;
const findAlert = () => wrapper.findComponent(GlAlert);
const findHelpPageLink = () => wrapper.findComponent(GlButton);
const findAlertText = () => trimText(findAlert().text());
const createWrapper = () =>
extendedWrapper(
shallowMount(ReportStatusAlert, {
provide: {
vulnerabilityRetentionPeriodHelpPageLink: TEST_HELP_PAGE_LINK,
},
stubs: {
GlSprintf,
},
}),
);
beforeEach(() => {
wrapper = createWrapper();
});
afterEach(() => {
wrapper.destroy();
});
it('renders the component correctly', () => {
expect(wrapper).toMatchSnapshot();
});
it('shows the correct title for the alert', () => {
expect(findAlertText()).toContain('Report has expired');
});
it('shows the correct description for the alert', () => {
expect(findAlertText()).toContain(
'The security report for this pipeline has expired . Re-run the pipeline to generate a new security report.',
);
});
it('links to the security-report help page', () => {
expect(findHelpPageLink().attributes('href')).toBe(TEST_HELP_PAGE_LINK);
});
});
......@@ -33450,6 +33450,9 @@ msgstr ""
msgid "SecurityReports|Remove project from dashboard"
msgstr ""
msgid "SecurityReports|Report has expired"
msgstr ""
msgid "SecurityReports|Scan details"
msgstr ""
......@@ -33495,6 +33498,9 @@ msgstr ""
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|The security report for this pipeline has %{helpPageLinkStart}expired%{helpPageLinkEnd}. Re-run the pipeline to generate a new security report."
msgstr ""
msgid "SecurityReports|There was an error adding the comment."
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