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