Commit 66e2a2c1 authored by Fernando's avatar Fernando

Show artifact downloads for security reports

We now show artifact download dropdowns on the security tab
in the single pipeline view.

Changelog: added
parent 034d776d
......@@ -32,6 +32,11 @@ export default {
default: '',
},
},
computed: {
showDropdown() {
return this.loading || this.artifacts.length > 0;
},
},
methods: {
artifactText({ name }) {
return sprintf(s__('SecurityReports|Download %{artifactName}'), {
......@@ -44,6 +49,7 @@ export default {
<template>
<gl-dropdown
v-if="showDropdown"
v-gl-tooltip
:text="text"
:title="title"
......
......@@ -14,7 +14,7 @@ const addReportTypeIfExists = (acc, reportTypes, reportType, getName, downloadPa
}
};
const extractSecurityReportArtifacts = (reportTypes, jobs) => {
export const extractSecurityReportArtifacts = (reportTypes, jobs) => {
return jobs.reduce((acc, job) => {
const artifacts = job.artifacts?.nodes ?? [];
......
......@@ -2,6 +2,7 @@
import { GlEmptyState } from '@gitlab/ui';
import { mapActions } from 'vuex';
import pipelineSecurityReportSummaryQuery from 'ee/security_dashboard/graphql/queries/pipeline_security_report_summary.query.graphql';
import { reportTypeToSecurityReportTypeEnum } from 'ee/vue_shared/security_reports/constants';
import { fetchPolicies } from '~/lib/graphql';
import { s__ } from '~/locale';
import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
......@@ -42,15 +43,25 @@ export default {
return {
fullPath: this.projectFullPath,
pipelineIid: this.pipeline.iid,
reportTypes: Object.values(reportTypeToSecurityReportTypeEnum),
};
},
update(data) {
const summary = data?.project?.pipeline?.securityReportSummary;
return summary && Object.keys(summary).length ? summary : null;
const summary = {
reports: data?.project?.pipeline?.securityReportSummary,
jobs: data?.project?.pipeline?.jobs.nodes,
};
return summary?.reports && Object.keys(summary.reports).length ? summary : null;
},
},
},
computed: {
reportSummary() {
return this.securityReportSummary?.reports;
},
jobs() {
return this.securityReportSummary?.jobs;
},
shouldShowGraphqlVulnerabilityReport() {
return this.glFeatures.pipelineSecurityDashboardGraphql;
},
......@@ -69,8 +80,8 @@ export default {
const getScans = (reportSummary) => reportSummary?.scans?.nodes || [];
const hasErrors = (scan) => Boolean(scan.errors?.length);
return this.securityReportSummary
? Object.values(this.securityReportSummary)
return this.reportSummary
? Object.values(this.reportSummary)
// generate flat array of all scans
.flatMap(getScans)
.filter(hasErrors)
......@@ -94,19 +105,17 @@ export default {
<template>
<div>
<div v-if="securityReportSummary" class="gl-my-5">
<div v-if="reportSummary" class="gl-my-5">
<scan-errors-alert v-if="hasScansWithErrors" :scans="scansWithErrors" class="gl-mb-5" />
<security-reports-summary :summary="securityReportSummary" />
<security-reports-summary :summary="reportSummary" :jobs="jobs" />
</div>
<security-dashboard
v-if="!shouldShowGraphqlVulnerabilityReport"
:vulnerabilities-endpoint="vulnerabilitiesEndpoint"
:lock-to-project="{ id: projectId }"
:pipeline-id="pipeline.id"
:pipeline-iid="pipeline.iid"
:project-full-path="projectFullPath"
:loading-error-illustrations="loadingErrorIllustrations"
:security-report-summary="securityReportSummary"
:security-report-summary="reportSummary"
>
<template #empty-state>
<gl-empty-state v-bind="emptyStateProps" />
......
<script>
import { mapActions, mapState, mapGetters } from 'vuex';
import PipelineArtifactDownload from 'ee/vue_shared/security_reports/components/artifact_downloads/pipeline_artifact_download.vue';
import IssueModal from 'ee/vue_shared/security_reports/components/modal.vue';
import { securityReportTypeEnumToReportType } from 'ee/vue_shared/security_reports/constants';
import { vulnerabilityModalMixin } from 'ee/vue_shared/security_reports/mixins/vulnerability_modal_mixin';
import VulnerabilityReportLayout from '../shared/vulnerability_report_layout.vue';
import Filters from './filters.vue';
......@@ -16,7 +14,6 @@ export default {
VulnerabilityReportLayout,
SecurityDashboardTable,
LoadingError,
PipelineArtifactDownload,
},
mixins: [vulnerabilityModalMixin('vulnerabilities')],
props: {
......@@ -24,20 +21,11 @@ export default {
type: String,
required: true,
},
projectFullPath: {
type: String,
required: true,
},
pipelineId: {
type: Number,
required: false,
default: null,
},
pipelineIid: {
type: Number,
required: false,
default: null,
},
securityReportSummary: {
type: Object,
required: false,
......@@ -61,9 +49,6 @@ export default {
...mapState('pipelineJobs', ['projectId']),
...mapState('filters', ['filters']),
...mapGetters('vulnerabilities', ['loadingVulnerabilitiesFailedWithRecognizedErrorCode']),
shouldShowDownloadGuidance() {
return this.projectFullPath && this.pipelineIid && this.securityReportSummary.coverageFuzzing;
},
canCreateIssue() {
const gitLabIssuePath = this.vulnerability.create_vulnerability_feedback_issue_path;
const jiraIssueUrl = this.vulnerability.create_jira_issue_url;
......@@ -102,9 +87,6 @@ export default {
...mapActions('pipelineJobs', ['fetchPipelineJobs']),
...mapActions('filters', ['lockFilter', 'setHideDismissedToggleInitialState']),
},
reportTypes: {
COVERAGE_FUZZING: [securityReportTypeEnumToReportType.COVERAGE_FUZZING],
},
};
</script>
......@@ -118,20 +100,7 @@ export default {
<template v-else>
<vulnerability-report-layout>
<template #header>
<filters>
<template v-if="shouldShowDownloadGuidance" #buttons>
<pipeline-artifact-download
class="gl-display-flex gl-flex-direction-column gl-align-self-center"
:report-types="$options.reportTypes.COVERAGE_FUZZING"
:target-project-full-path="projectFullPath"
:pipeline-iid="pipelineIid"
>
<template #label>
<strong class="gl-mb-2">{{ s__('SecurityReports|Coverage fuzzing') }}</strong>
</template>
</pipeline-artifact-download>
</template>
</filters>
<filters />
</template>
<security-dashboard-table>
......
......@@ -12,7 +12,10 @@ import { COLLAPSE_SECURITY_REPORTS_SUMMARY_LOCAL_STORAGE_KEY as LOCAL_STORAGE_KE
import { getFormattedSummary } from 'ee/security_dashboard/helpers';
import Modal from 'ee/vue_shared/security_reports/components/dast_modal.vue';
import AccessorUtilities from '~/lib/utils/accessor';
import { convertToSnakeCase } from '~/lib/utils/text_utility';
import { __ } from '~/locale';
import SecurityReportDownloadDropdown from '~/vue_shared/security_reports/components/security_report_download_dropdown.vue';
import { extractSecurityReportArtifacts } from '~/vue_shared/security_reports/utils';
export default {
name: 'SecurityReportsSummary',
......@@ -23,6 +26,7 @@ export default {
GlSprintf,
Modal,
GlLink,
SecurityReportDownloadDropdown,
},
directives: {
collapseToggle: GlCollapseToggleDirective,
......@@ -33,6 +37,11 @@ export default {
type: Object,
required: true,
},
jobs: {
type: Array,
required: false,
default: () => [],
},
},
data() {
return {
......@@ -73,6 +82,10 @@ export default {
downloadLink(scanSummary) {
return scanSummary.scannedResourcesCsvPath || '';
},
findArtifacts(scanType) {
const snakeCase = convertToSnakeCase(scanType.toLowerCase());
return extractSecurityReportArtifacts([snakeCase], this.jobs);
},
},
};
</script>
......@@ -96,10 +109,10 @@ export default {
</template>
<gl-collapse id="security-reports-summary-details" v-model="isVisible" class="gl-pb-3">
<div v-for="[scanType, scanSummary] in formattedSummary" :key="scanType" class="row gl-my-3">
<div class="col-6 col-md-4 col-lg-2">
<div class="col-4">
{{ scanType }}
</div>
<div class="col-6 col-md-8 col-lg-10">
<div class="col-4">
<gl-sprintf
:message="
n__('%d vulnerability', '%d vulnerabilities', scanSummary.vulnerabilitiesCount)
......@@ -145,6 +158,12 @@ export default {
</gl-link>
</template>
</div>
<div class="col-4">
<security-report-download-dropdown
:text="s__('SecurityReports|Download results')"
:artifacts="findArtifacts(scanType)"
/>
</div>
</div>
</gl-collapse>
</gl-card>
......
#import "../fragments/security_report_scans.fragment.graphql"
#import "~/vue_shared/security_reports/graphql/fragments/job_artifacts.fragment.graphql"
query pipelineSecuritySummary($fullPath: ID!, $pipelineIid: ID!) {
query pipelineSecuritySummary(
$fullPath: ID!
$pipelineIid: ID!
$reportTypes: [SecurityReportTypeEnum!]
) {
project(fullPath: $fullPath) {
pipeline(iid: $pipelineIid) {
id
...JobArtifacts
securityReportSummary {
dast {
vulnerabilitiesCount
......
......@@ -150,7 +150,7 @@ describe('Pipeline Security Dashboard component', () => {
describe('scans error alert', () => {
describe('with errors', () => {
const securityReportSummary = {
const reportSummary = {
scanner_1: {
// this scan contains errors
scans: {
......@@ -174,10 +174,14 @@ describe('Pipeline Security Dashboard component', () => {
},
};
const scansWithErrors = [
...securityReportSummary.scanner_1.scans.nodes,
...securityReportSummary.scanner_3.scans.nodes,
...reportSummary.scanner_1.scans.nodes,
...reportSummary.scanner_3.scans.nodes,
];
const securityReportSummary = {
reports: reportSummary,
};
beforeEach(() => {
factory({
data: {
......@@ -192,7 +196,7 @@ describe('Pipeline Security Dashboard component', () => {
});
describe('without errors', () => {
const securityReportSummary = {
const reportSummary = {
dast: {
scans: [
{
......@@ -203,6 +207,10 @@ describe('Pipeline Security Dashboard component', () => {
},
};
const securityReportSummary = {
reports: reportSummary,
};
beforeEach(() => {
factory({
data: {
......@@ -218,12 +226,16 @@ describe('Pipeline Security Dashboard component', () => {
});
describe('security reports summary', () => {
const securityReportSummary = {
const reportSummary = {
dast: {
vulnerabilitiesCount: 123,
},
};
const securityReportSummary = {
reports: reportSummary,
};
it('shows the summary if it is non-empty', () => {
factory({
data: {
......
......@@ -8,7 +8,6 @@ import LoadingError from 'ee/security_dashboard/components/pipeline/loading_erro
import SecurityDashboardTable from 'ee/security_dashboard/components/pipeline/security_dashboard_table.vue';
import SecurityDashboard from 'ee/security_dashboard/components/pipeline/security_dashboard_vuex.vue';
import { getStoreConfig } from 'ee/security_dashboard/store';
import PipelineArtifactDownload from 'ee/vue_shared/security_reports/components/artifact_downloads/pipeline_artifact_download.vue';
import { VULNERABILITY_MODAL_ID } from 'ee/vue_shared/security_reports/components/constants';
import IssueModal from 'ee/vue_shared/security_reports/components/modal.vue';
import { TEST_HOST } from 'helpers/test_constants';
......@@ -48,9 +47,6 @@ describe('Security Dashboard component', () => {
wrapper = mount(SecurityDashboard, {
store,
stubs: {
PipelineArtifactDownload: true,
},
propsData: {
dashboardDocumentation: '',
projectFullPath: '/path',
......@@ -97,10 +93,6 @@ describe('Security Dashboard component', () => {
expect(wrapper.find(IssueModal).exists()).toBe(true);
});
it('does not render coverage fuzzing artifact download', () => {
expect(wrapper.find(PipelineArtifactDownload).exists()).toBe(false);
});
it.each`
emittedModalEvent | eventPayload | expectedDispatchedAction | expectedActionPayload
${'addDismissalComment'} | ${'foo'} | ${'vulnerabilities/addDismissalComment'} | ${{ comment: 'foo', vulnerability: 'bar' }}
......@@ -137,18 +129,6 @@ describe('Security Dashboard component', () => {
});
});
describe('with coverage fuzzing', () => {
beforeEach(() => {
createComponent({
props: { securityReportSummary: { coverageFuzzing: { scannedResourcesCount: 1 } } },
});
});
it('renders coverage fuzzing artifact download', () => {
expect(wrapper.find(PipelineArtifactDownload).exists()).toBe(true);
});
});
describe('issue modal', () => {
it.each`
givenState | expectedProps
......
......@@ -2,9 +2,11 @@ import { GlSprintf } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import SecurityReportsSummary from 'ee/security_dashboard/components/pipeline/security_reports_summary.vue';
import Modal from 'ee/vue_shared/security_reports/components/dast_modal.vue';
import { mockPipelineJobs } from 'ee_jest/security_dashboard/mock_data/jobs';
import { useLocalStorageSpy } from 'helpers/local_storage_helper';
import { trimText } from 'helpers/text_helper';
import AccessorUtilities from '~/lib/utils/accessor';
import SecurityReportDownloadDropdown from '~/vue_shared/security_reports/components/security_report_download_dropdown.vue';
describe('Security reports summary component', () => {
useLocalStorageSpy();
......@@ -97,6 +99,24 @@ describe('Security reports summary component', () => {
expect(trimText(wrapper.text())).toContain(string);
});
it.each`
summaryProp | jobsProp | hasDropdown
${{ coverageFuzzing: { vulnerabilitiesCount: 123 } }} | ${mockPipelineJobs} | ${true}
${{ coverageFuzzing: null }} | ${[]} | ${false}
`(
'artifact download dropdown is visible $hasDropdown',
({ summaryProp, jobsProp, hasDropdown }) => {
createWrapper({
propsData: {
summary: summaryProp,
jobs: jobsProp,
},
});
expect(wrapper.findComponent(SecurityReportDownloadDropdown).exists()).toBe(hasDropdown);
},
);
it.each`
summaryProp | report
${{ dast: null }} | ${'DAST'}
......
export const mockPipelineJobs = [
{
name: 'my_fuzz_target',
artifacts: {
nodes: [
{
downloadPath: '/debug-cov-fuzz-project/-/jobs/1133/artifacts/download?file_type=trace',
fileType: 'TRACE',
__typename: 'CiJobArtifact',
},
{
downloadPath:
'debug-cov-fuzz-project/-/jobs/1133/artifacts/download?file_type=coverage_fuzzing',
fileType: 'COVERAGE_FUZZING',
__typename: 'CiJobArtifact',
},
{
downloadPath: '/debug-cov-fuzz-project/-/jobs/1133/artifacts/download?file_type=metadata',
fileType: 'METADATA',
__typename: 'CiJobArtifact',
},
{
downloadPath: '/debug-cov-fuzz-project/-/jobs/1133/artifacts/download?file_type=archive',
fileType: 'ARCHIVE',
__typename: 'CiJobArtifact',
},
],
__typename: 'CiJobArtifactConnection',
},
__typename: 'CiJob',
},
{
name: 'gosec-sast',
artifacts: {
nodes: [
{
downloadPath: '/debug-cov-fuzz-project/-/jobs/1131/artifacts/download?file_type=trace',
fileType: 'TRACE',
__typename: 'CiJobArtifact',
},
{
downloadPath: '/debug-cov-fuzz-project/-/jobs/1131/artifacts/download?file_type=sast',
fileType: 'SAST',
__typename: 'CiJobArtifact',
},
],
__typename: 'CiJobArtifactConnection',
},
__typename: 'CiJob',
},
];
......@@ -29576,9 +29576,6 @@ msgstr ""
msgid "SecurityReports|Configure security testing"
msgstr ""
msgid "SecurityReports|Coverage fuzzing"
msgstr ""
msgid "SecurityReports|Create Jira issue"
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