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: ...@@ -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) - [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) - [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 ### Enable report validation
> [Deprecated](https://gitlab.com/gitlab-org/gitlab/-/issues/354928) in GitLab 14.9, and planned for removal in GitLab 15.0. > [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'; ...@@ -7,6 +7,7 @@ import { fetchPolicies } from '~/lib/graphql';
import { s__ } from '~/locale'; import { s__ } from '~/locale';
import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin'; import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import ScanAlerts, { TYPE_ERRORS, TYPE_WARNINGS } from './scan_alerts.vue'; 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 SecurityDashboard from './security_dashboard_vuex.vue';
import SecurityReportsSummary from './security_reports_summary.vue'; import SecurityReportsSummary from './security_reports_summary.vue';
import PipelineVulnerabilityReport from './pipeline_vulnerability_report.vue'; import PipelineVulnerabilityReport from './pipeline_vulnerability_report.vue';
...@@ -15,8 +16,10 @@ export default { ...@@ -15,8 +16,10 @@ export default {
name: 'PipelineSecurityDashboard', name: 'PipelineSecurityDashboard',
errorsAlertType: TYPE_ERRORS, errorsAlertType: TYPE_ERRORS,
warningsAlertType: TYPE_WARNINGS, warningsAlertType: TYPE_WARNINGS,
scanPurgedStatus: STATUS_PURGED,
components: { components: {
GlEmptyState, GlEmptyState,
ReportStatusAlert,
ScanAlerts, ScanAlerts,
SecurityReportsSummary, SecurityReportsSummary,
SecurityDashboard, SecurityDashboard,
...@@ -87,21 +90,27 @@ export default { ...@@ -87,21 +90,27 @@ export default {
.flatMap(getScans) .flatMap(getScans)
: []; : [];
}, },
purgedScans() {
return this.scans.filter((scan) => scan.status === this.$options.scanPurgedStatus);
},
hasPurgedScans() {
return this.purgedScans.length > 0;
},
scansWithErrors() { scansWithErrors() {
const hasErrors = (scan) => Boolean(scan.errors?.length); const hasErrors = (scan) => Boolean(scan.errors?.length);
return this.scans.filter(hasErrors); return this.scans.filter(hasErrors);
}, },
hasScansWithErrors() { showScanErrors() {
return this.scansWithErrors.length > 0; return this.scansWithErrors.length > 0 && !this.hasPurgedScans;
}, },
scansWithWarnings() { scansWithWarnings() {
const hasWarnings = (scan) => Boolean(scan.warnings?.length); const hasWarnings = (scan) => Boolean(scan.warnings?.length);
return this.scans.filter(hasWarnings); return this.scans.filter(hasWarnings);
}, },
hasScansWithWarnings() { showScanWarnings() {
return this.scansWithWarnings.length > 0; return this.scansWithWarnings.length > 0 && !this.hasPurgedScans;
}, },
}, },
created() { created() {
...@@ -130,7 +139,7 @@ export default { ...@@ -130,7 +139,7 @@ export default {
<div> <div>
<div v-if="reportSummary" class="gl-my-5"> <div v-if="reportSummary" class="gl-my-5">
<scan-alerts <scan-alerts
v-if="hasScansWithErrors" v-if="showScanErrors"
:type="$options.errorsAlertType" :type="$options.errorsAlertType"
:scans="scansWithErrors" :scans="scansWithErrors"
:title="$options.i18n.parsingErrorAlertTitle" :title="$options.i18n.parsingErrorAlertTitle"
...@@ -138,13 +147,14 @@ export default { ...@@ -138,13 +147,14 @@ export default {
class="gl-mb-5" class="gl-mb-5"
/> />
<scan-alerts <scan-alerts
v-if="hasScansWithWarnings" v-if="showScanWarnings"
:type="$options.warningsAlertType" :type="$options.warningsAlertType"
:scans="scansWithWarnings" :scans="scansWithWarnings"
:title="$options.i18n.parsingWarningAlertTitle" :title="$options.i18n.parsingWarningAlertTitle"
:description="$options.i18n.parsingWarningAlertDescription" :description="$options.i18n.parsingWarningAlertDescription"
class="gl-mb-5" class="gl-mb-5"
/> />
<report-status-alert v-if="hasPurgedScans" class="gl-mb-5" />
<security-reports-summary :summary="reportSummary" :jobs="jobs" /> <security-reports-summary :summary="reportSummary" :jobs="jobs" />
</div> </div>
<security-dashboard <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 { ...@@ -2,6 +2,7 @@ fragment SecurityReportSummaryScans on SecurityReportSummarySection {
scans { scans {
nodes { nodes {
name name
status
errors errors
warnings warnings
} }
......
...@@ -29,6 +29,7 @@ export default () => { ...@@ -29,6 +29,7 @@ export default () => {
pipelineJobsPath, pipelineJobsPath,
canAdminVulnerability, canAdminVulnerability,
securityReportHelpPageLink, securityReportHelpPageLink,
vulnerabilityRetentionPeriodHelpPageLink,
falsePositiveDocUrl, falsePositiveDocUrl,
canViewFalsePositive, canViewFalsePositive,
} = el.dataset; } = el.dataset;
...@@ -63,6 +64,7 @@ export default () => { ...@@ -63,6 +64,7 @@ export default () => {
sourceBranch, sourceBranch,
}, },
securityReportHelpPageLink, securityReportHelpPageLink,
vulnerabilityRetentionPeriodHelpPageLink,
vulnerabilitiesEndpoint, vulnerabilitiesEndpoint,
loadingErrorIllustrations, loadingErrorIllustrations,
falsePositiveDocUrl, falsePositiveDocUrl,
......
...@@ -25,7 +25,8 @@ ...@@ -25,7 +25,8 @@
can_admin_vulnerability: can?(current_user, :admin_vulnerability, project).to_s, can_admin_vulnerability: can?(current_user, :admin_vulnerability, project).to_s,
false_positive_doc_url: help_page_path('user/application_security/vulnerabilities/index'), 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, 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? - if pipeline.expose_license_scanning_data?
#js-tab-licenses.tab-pane #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 = { ...@@ -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 = [ export const scansWithWarnings = [
{ errors: [], warnings: ['warning description'], name: 'scan-name' }, { errors: [], warnings: ['warning description'], name: 'scan-name', status: 'SUCCEEDED' },
]; ];
const getSecurityReportsSummaryMock = (nodes) => ({ const getSecurityReportsSummaryMock = (nodes) => ({
...@@ -244,6 +253,18 @@ 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( export const pipelineSecurityReportSummaryWithErrors = merge(
{}, {},
pipelineSecurityReportSummary, pipelineSecurityReportSummary,
......
...@@ -7,6 +7,7 @@ import createMockApollo from 'helpers/mock_apollo_helper'; ...@@ -7,6 +7,7 @@ import createMockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises'; import waitForPromises from 'helpers/wait_for_promises';
import pipelineSecurityReportSummaryQuery from 'ee/security_dashboard/graphql/queries/pipeline_security_report_summary.query.graphql'; 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 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, { import ScanAlerts, {
TYPE_ERRORS, TYPE_ERRORS,
TYPE_WARNINGS, TYPE_WARNINGS,
...@@ -18,6 +19,8 @@ import { ...@@ -18,6 +19,8 @@ import {
pipelineSecurityReportSummary, pipelineSecurityReportSummary,
pipelineSecurityReportSummaryWithErrors, pipelineSecurityReportSummaryWithErrors,
pipelineSecurityReportSummaryWithWarnings, pipelineSecurityReportSummaryWithWarnings,
purgedPipelineSecurityReportSummaryWithErrors,
purgedPipelineSecurityReportSummaryWithWarnings,
scansWithErrors, scansWithErrors,
scansWithWarnings, scansWithWarnings,
pipelineSecurityReportSummaryEmpty, pipelineSecurityReportSummaryEmpty,
...@@ -45,6 +48,7 @@ describe('Pipeline Security Dashboard component', () => { ...@@ -45,6 +48,7 @@ describe('Pipeline Security Dashboard component', () => {
const findSecurityDashboard = () => wrapper.findComponent(SecurityDashboard); const findSecurityDashboard = () => wrapper.findComponent(SecurityDashboard);
const findVulnerabilityReport = () => wrapper.findComponent(PipelineVulnerabilityReport); const findVulnerabilityReport = () => wrapper.findComponent(PipelineVulnerabilityReport);
const findScanAlerts = () => wrapper.findComponent(ScanAlerts); const findScanAlerts = () => wrapper.findComponent(ScanAlerts);
const findReportStatusAlert = () => wrapper.findComponent(ReportStatusAlert);
const factory = ({ stubs, provide, apolloProvider } = {}) => { const factory = ({ stubs, provide, apolloProvider } = {}) => {
store = new Vuex.Store({ store = new Vuex.Store({
...@@ -163,24 +167,82 @@ describe('Pipeline Security Dashboard component', () => { ...@@ -163,24 +167,82 @@ describe('Pipeline Security Dashboard component', () => {
}); });
}); });
describe('scans error alert', () => { describe('report status alert', () => {
describe('with errors', () => { describe('with purged scans', () => {
beforeEach(async () => { beforeEach(async () => {
factoryWithApollo({ factoryWithApollo({
requestHandlers: [ requestHandlers: [
[ [
pipelineSecurityReportSummaryQuery, pipelineSecurityReportSummaryQuery,
jest.fn().mockResolvedValueOnce(pipelineSecurityReportSummaryWithErrors), jest.fn().mockResolvedValueOnce(purgedPipelineSecurityReportSummaryWithErrors),
], ],
], ],
}); });
await waitForPromises(); await waitForPromises();
}); });
it('shows an alert with information about each scan with errors', () => { it('shows the alert', () => {
expect(findScanAlerts().props()).toMatchObject({ expect(findReportStatusAlert().exists()).toBe(true);
scans: scansWithErrors, });
type: TYPE_ERRORS, });
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', () => { ...@@ -205,22 +267,42 @@ describe('Pipeline Security Dashboard component', () => {
describe('scan warnings', () => { describe('scan warnings', () => {
describe('with warnings', () => { describe('with warnings', () => {
beforeEach(async () => { describe('with purged scans', () => {
factoryWithApollo({ beforeEach(async () => {
requestHandlers: [ factoryWithApollo({
[ requestHandlers: [
pipelineSecurityReportSummaryQuery, [
jest.fn().mockResolvedValueOnce(pipelineSecurityReportSummaryWithWarnings), 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', () => { describe('without purged scans', () => {
expect(findScanAlerts().props()).toMatchObject({ beforeEach(async () => {
scans: scansWithWarnings, factoryWithApollo({
type: TYPE_WARNINGS, 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 "" ...@@ -33450,6 +33450,9 @@ msgstr ""
msgid "SecurityReports|Remove project from dashboard" msgid "SecurityReports|Remove project from dashboard"
msgstr "" msgstr ""
msgid "SecurityReports|Report has expired"
msgstr ""
msgid "SecurityReports|Scan details" msgid "SecurityReports|Scan details"
msgstr "" msgstr ""
...@@ -33495,6 +33498,9 @@ 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}." 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 "" 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." msgid "SecurityReports|There was an error adding the comment."
msgstr "" msgstr ""
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment