Commit f209faf2 authored by Nicolò Maria Mezzopera's avatar Nicolò Maria Mezzopera

Merge branch '340873-dast-json-artifacts' into 'master'

Show CSV and json artifact download on security tab

See merge request gitlab-org/gitlab!72060
parents b3073dde 73bce303
......@@ -26,6 +26,11 @@ export default {
type: Number,
required: true,
},
injectedArtifacts: {
type: Array,
required: false,
default: () => [],
},
},
data() {
return {
......@@ -56,6 +61,9 @@ export default {
isLoadingReportArtifacts() {
return this.$apollo.queries.reportArtifacts.loading;
},
mergedReportArtifacts() {
return [...this.reportArtifacts, ...this.injectedArtifacts];
},
},
methods: {
showError(error) {
......@@ -77,7 +85,7 @@ export default {
<template>
<security-report-download-dropdown
:title="s__('SecurityReports|Download results')"
:artifacts="reportArtifacts"
:artifacts="mergedReportArtifacts"
:loading="isLoadingReportArtifacts"
/>
</template>
......@@ -12,9 +12,13 @@ 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 { s__, __, n__ } from '~/locale';
import SecurityReportDownloadDropdown from '~/vue_shared/security_reports/components/security_report_download_dropdown.vue';
import { extractSecurityReportArtifacts } from '~/vue_shared/security_reports/utils';
import {
SECURITY_REPORT_TYPE_ENUM_DAST,
REPORT_TYPE_DAST,
} from 'ee/vue_shared/security_reports/constants';
export default {
name: 'SecurityReportsSummary',
......@@ -41,6 +45,16 @@ export default {
default: () => [],
},
},
i18n: {
scannedResources: s__('SecurityReports|scanned resources'),
scanDetails: s__('SecurityReports|Scan details'),
downloadUrls: s__('SecurityReports|Download scanned URLs'),
downloadResults: s__('SecurityReports|Download results'),
hideDetails: __('Hide details'),
showDetails: __('Show details'),
vulnerabilities: (count) => n__('%d vulnerability', '%d vulnerabilities', count),
scannedUrls: (count) => n__('%d URL scanned', '%d URLs scanned', count),
},
data() {
return {
isVisible: true,
......@@ -48,7 +62,7 @@ export default {
},
computed: {
collapseButtonLabel() {
return this.isVisible ? __('Hide details') : __('Show details');
return this.isVisible ? this.$options.i18n.hideDetails : this.$options.i18n.showDetails;
},
formattedSummary() {
return getFormattedSummary(this.summary);
......@@ -77,6 +91,12 @@ export default {
hasScannedResources(scanSummary) {
return scanSummary.scannedResources?.nodes?.length > 0;
},
hasDastArtifactDownload(scanSummary) {
return (
Boolean(scanSummary.scannedResourcesCsvPath) ||
this.findArtifacts(SECURITY_REPORT_TYPE_ENUM_DAST).length > 0
);
},
downloadLink(scanSummary) {
return scanSummary.scannedResourcesCsvPath || '';
},
......@@ -84,6 +104,15 @@ export default {
const snakeCase = convertToSnakeCase(scanType.toLowerCase());
return extractSecurityReportArtifacts([snakeCase], this.jobs);
},
buildDastArtifacts(scanSummary) {
const csvArtifact = {
name: this.$options.i18n.scannedResources,
path: this.downloadLink(scanSummary),
reportType: REPORT_TYPE_DAST,
};
return [...this.findArtifacts(SECURITY_REPORT_TYPE_ENUM_DAST), csvArtifact];
},
},
};
</script>
......@@ -93,7 +122,7 @@ export default {
<template #header>
<div class="row">
<div class="col-7">
<strong>{{ s__('SecurityReports|Scan details') }}</strong>
<strong>{{ $options.i18n.scanDetails }}</strong>
</div>
<div v-if="localStorageUsable" class="col-5 gl-text-right">
<gl-button
......@@ -111,11 +140,7 @@ export default {
{{ scanType }}
</div>
<div class="col-4">
<gl-sprintf
:message="
n__('%d vulnerability', '%d vulnerabilities', scanSummary.vulnerabilitiesCount)
"
/>
<gl-sprintf :message="$options.i18n.vulnerabilities(scanSummary.vulnerabilitiesCount)" />
</div>
<div class="col-4">
<template v-if="scanSummary.scannedResourcesCount !== undefined">
......@@ -126,14 +151,12 @@ export default {
size="small"
data-testid="modal-button"
>
{{ s__('SecurityReports|Download scanned URLs') }}
{{ $options.i18n.downloadUrls }}
</gl-button>
<template v-else>
(<gl-sprintf
:message="
n__('%d URL scanned', '%d URLs scanned', scanSummary.scannedResourcesCount)
"
:message="$options.i18n.scannedUrls(scanSummary.scannedResourcesCount)"
/>)
</template>
......@@ -145,21 +168,17 @@ export default {
/>
</template>
<template v-else-if="scanSummary.scannedResourcesCsvPath">
<gl-button
icon="download"
size="small"
:href="downloadLink(scanSummary)"
class="gl-ml-1"
<template v-else-if="hasDastArtifactDownload(scanSummary)">
<security-report-download-dropdown
:text="$options.i18n.downloadResults"
:artifacts="buildDastArtifacts(scanSummary)"
data-testid="download-link"
>
{{ s__('SecurityReports|Download scanned URLs') }}
</gl-button>
/>
</template>
<security-report-download-dropdown
v-else
:text="s__('SecurityReports|Download results')"
:text="$options.i18n.downloadResults"
:artifacts="findArtifacts(scanType)"
/>
</div>
......
......@@ -4,6 +4,10 @@ import {
reportTypeToSecurityReportTypeEnum as reportTypeToSecurityReportTypeEnumCE,
REPORT_TYPE_API_FUZZING,
REPORT_TYPE_COVERAGE_FUZZING,
REPORT_TYPE_DAST,
REPORT_TYPE_DEPENDENCY_SCANNING,
REPORT_TYPE_CONTAINER_SCANNING,
REPORT_TYPE_CLUSTER_IMAGE_SCANNING,
} from '~/vue_shared/security_reports/constants';
export * from '~/vue_shared/security_reports/constants';
......@@ -15,6 +19,10 @@ export * from '~/vue_shared/security_reports/constants';
*/
export const SECURITY_REPORT_TYPE_ENUM_API_FUZZING = 'API_FUZZING';
export const SECURITY_REPORT_TYPE_ENUM_COVERAGE_FUZZING = 'COVERAGE_FUZZING';
export const SECURITY_REPORT_TYPE_ENUM_DAST = 'DAST';
export const SECURITY_REPORT_TYPE_ENUM_DEPENDENCY_SCANNING = 'DEPENDENCY_SCANNING';
export const SECURITY_REPORT_TYPE_ENUM_CONTAINER_SCANNING = 'CONTAINER_SCANNING';
export const SECURITY_REPORT_TYPE_ENUM_CLUSTER_IMAGE_SCANNING = 'CLUSTER_IMAGE_SCANNING';
/* Override CE Definitions */
......@@ -25,6 +33,10 @@ export const reportTypeToSecurityReportTypeEnum = {
...reportTypeToSecurityReportTypeEnumCE,
[REPORT_TYPE_API_FUZZING]: SECURITY_REPORT_TYPE_ENUM_API_FUZZING,
[REPORT_TYPE_COVERAGE_FUZZING]: SECURITY_REPORT_TYPE_ENUM_COVERAGE_FUZZING,
[REPORT_TYPE_DAST]: SECURITY_REPORT_TYPE_ENUM_DAST,
[REPORT_TYPE_DEPENDENCY_SCANNING]: SECURITY_REPORT_TYPE_ENUM_DEPENDENCY_SCANNING,
[REPORT_TYPE_CONTAINER_SCANNING]: SECURITY_REPORT_TYPE_ENUM_CONTAINER_SCANNING,
[REPORT_TYPE_CLUSTER_IMAGE_SCANNING]: SECURITY_REPORT_TYPE_ENUM_CLUSTER_IMAGE_SCANNING,
};
/**
......
......@@ -8,6 +8,7 @@ import {
GlModalDirective,
GlTooltipDirective as GlTooltip,
} from '@gitlab/ui';
import { s__, n__, __ } from '~/locale';
import { componentNames } from 'ee/reports/components/issue_body';
import { fetchPolicies } from '~/lib/graphql';
import { mrStates } from '~/mr_popover/constants';
......@@ -18,9 +19,13 @@ import { LOADING } from '~/reports/constants';
import Tracking from '~/tracking';
import MergeRequestArtifactDownload from '~/vue_shared/security_reports/components/artifact_downloads/merge_request_artifact_download.vue';
import SecuritySummary from '~/vue_shared/security_reports/components/security_summary.vue';
import {
REPORT_TYPE_DAST,
securityReportTypeEnumToReportType,
} from 'ee/vue_shared/security_reports/constants';
import DastModal from './components/dast_modal.vue';
import IssueModal from './components/modal.vue';
import { securityReportTypeEnumToReportType } from './constants';
import securityReportSummaryQuery from './graphql/mr_security_report_summary.graphql';
import securityReportsMixin from './mixins/security_report_mixin';
import { vulnerabilityModalMixin } from './mixins/vulnerability_modal_mixin';
......@@ -244,6 +249,18 @@ export default {
required: true,
},
},
i18n: {
scannedResources: s__('SecurityReports|scanned resources'),
viewReport: s__('ciReport|View full report'),
divergedFromTargetBranch: __(
'Security report is out of date. Please update your branch with the latest changes from the target branch (%{targetBranchName})',
),
baseSecurityReportOutOfDate: __(
'Security report is out of date. Run %{newPipelineLinkStart}a new pipeline%{newPipelineLinkEnd} for the target branch (%{targetBranchName})',
),
viewDetails: __('View details'),
scannedUrls: (count) => n__('%d URL scanned', '%d URLs scanned', count),
},
componentNames,
computed: {
...mapState([
......@@ -348,6 +365,15 @@ export default {
shouldShowDownloadGuidance() {
return this.targetProjectFullPath && this.mrIid && this.summaryStatus !== LOADING;
},
dastCsvArtifacts() {
return [
{
name: this.$options.i18n.scannedResources,
path: this.dastDownloadLink,
reportType: REPORT_TYPE_DAST,
},
];
},
},
created() {
......@@ -454,6 +480,7 @@ export default {
reportTypes: {
API_FUZZING: [securityReportTypeEnumToReportType.API_FUZZING],
COVERAGE_FUZZING: [securityReportTypeEnumToReportType.COVERAGE_FUZZING],
DAST: [securityReportTypeEnumToReportType.DAST],
},
};
</script>
......@@ -478,7 +505,7 @@ export default {
icon="external-link"
class="gl-mr-3 report-btn"
>
{{ s__('ciReport|View full report') }}
{{ $options.i18n.viewReport }}
</gl-button>
</template>
......@@ -486,11 +513,7 @@ export default {
<div class="gl-text-gray-700 gl-font-sm">
<gl-sprintf
v-if="hasDivergedFromTargetBranch"
:message="
__(
'Security report is out of date. Please update your branch with the latest changes from the target branch (%{targetBranchName})',
)
"
:message="$options.i18n.divergedFromTargetBranch"
>
<template #targetBranchName>
<gl-link class="gl-font-sm" :href="targetBranchTreePath">{{ targetBranch }}</gl-link>
......@@ -499,11 +522,7 @@ export default {
<gl-sprintf
v-else-if="isBaseSecurityReportOutOfDate"
:message="
__(
'Security report is out of date. Run %{newPipelineLinkStart}a new pipeline%{newPipelineLinkEnd} for the target branch (%{targetBranchName})',
)
"
:message="$options.i18n.baseSecurityReportOutOfDate"
>
<template #newPipelineLink="{ content }">
<gl-link class="gl-font-sm" :href="`${newPipelinePath}?ref=${targetBranch}`">{{
......@@ -602,10 +621,10 @@ export default {
<template v-if="hasDastScannedResources">
<div class="text-nowrap">
{{ n__('%d URL scanned', '%d URLs scanned', dastSummary.scannedResourcesCount) }}
{{ $options.i18n.scannedUrls(dastSummary.scannedResourcesCount) }}
</div>
<gl-link v-gl-modal.dastUrl class="ml-2" data-testid="dast-ci-job-link">
{{ __('View details') }}
{{ $options.i18n.viewDetails }}
</gl-link>
<dast-modal
:scanned-urls="dastSummary.scannedResources.nodes"
......@@ -614,14 +633,12 @@ export default {
/>
</template>
<template v-else-if="dastDownloadLink">
<gl-button
v-gl-tooltip
:title="s__('SecurityReports|Download scanned resources')"
download
size="small"
icon="download"
:href="dastDownloadLink"
class="gl-ml-1"
<merge-request-artifact-download
v-if="shouldShowDownloadGuidance"
:report-types="$options.reportTypes.DAST"
:target-project-full-path="targetProjectFullPath"
:mr-iid="mrIid"
:injected-artifacts="dastCsvArtifacts"
data-testid="download-link"
/>
</template>
......
......@@ -30,7 +30,7 @@ describe('Security reports summary component', () => {
const findToggleButton = () => wrapper.find('[data-testid="collapse-button"]');
const findModalButton = () => wrapper.find('[data-testid="modal-button"]');
const findDownloadLink = () => wrapper.find('[data-testid="download-link"]');
const findDownloadDropdown = () => wrapper.findComponent(SecurityReportDownloadDropdown);
beforeEach(() => {
jest.spyOn(AccessorUtilities, 'canUseLocalStorage').mockReturnValue(true);
......@@ -113,7 +113,7 @@ describe('Security reports summary component', () => {
},
});
expect(wrapper.findComponent(SecurityReportDownloadDropdown).exists()).toBe(hasDropdown);
expect(findDownloadDropdown().exists()).toBe(hasDropdown);
},
);
......@@ -248,8 +248,8 @@ describe('Security reports summary component', () => {
});
});
it('should contain a download link', () => {
expect(findDownloadLink().attributes('href')).toBe('/download/path');
it('should contain a artifact download dropdown', () => {
expect(findDownloadDropdown().exists()).toBe(true);
});
});
});
......@@ -15,6 +15,7 @@ import axios from '~/lib/utils/axios_utils';
import { mrStates } from '~/mr_popover/constants';
import GroupedIssuesList from '~/reports/components/grouped_issues_list.vue';
import ReportSection from '~/reports/components/report_section.vue';
import MergeRequestArtifactDownload from '~/vue_shared/security_reports/components/artifact_downloads/merge_request_artifact_download.vue';
import {
sastDiffSuccessMock,
......@@ -606,11 +607,9 @@ describe('Grouped security reports app', () => {
);
return waitForMutation(wrapper.vm.$store, types.RECEIVE_DAST_DIFF_SUCCESS).then(() => {
const findDownloadLink = wrapper.find('[data-testid="download-link"]');
const findDownloadDropdown = wrapper.findComponent(MergeRequestArtifactDownload);
expect(findDownloadLink.find('[data-testid="download-icon"]').exists()).toBe(true);
expect(findDownloadLink.exists()).toBe(true);
expect(findDownloadLink.attributes('href')).toBe('http://test');
expect(findDownloadDropdown.exists()).toBe(true);
});
});
});
......
......@@ -30418,9 +30418,6 @@ msgstr ""
msgid "SecurityReports|Download scanned URLs"
msgstr ""
msgid "SecurityReports|Download scanned resources"
msgstr ""
msgid "SecurityReports|Either you don't have permission to view this dashboard or the dashboard has not been setup. Please check your permission settings with your administrator or check your dashboard configurations to proceed."
msgstr ""
......@@ -30631,6 +30628,9 @@ msgstr ""
msgid "SecurityReports|Your feedback is important to us! We will ask again in a week."
msgstr ""
msgid "SecurityReports|scanned resources"
msgstr ""
msgid "See example DevOps Score page in our documentation."
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