Commit 4a5b54e4 authored by Paul Gascou-Vaillancourt's avatar Paul Gascou-Vaillancourt Committed by Natalia Tepluhina

Add link to scanned resources in DAST reports

- Retrieve scanned URLs count and CI job URL from the API
- Show the information in the MR widget
- Force the help icon to stay on the same line as the preceding word for
  a better UX on mobile layouts
- Updated tests
parent 3824edb1
......@@ -53,11 +53,19 @@ export default {
/>
<ci-icon v-else :status="iconStatus" :size="24" />
</div>
<div class="report-block-list-issue-description">
<div class="report-block-list-issue-description-text">{{ summary }}</div>
<popover v-if="popoverOptions" :options="popoverOptions" />
<div class="report-block-list-issue-description-text">
{{ summary
}}<span v-if="popoverOptions" class="text-nowrap"
>&nbsp;<popover v-if="popoverOptions" :options="popoverOptions" class="align-top" />
</span>
</div>
</div>
<div
v-if="$slots.default"
class="text-right flex-fill d-flex justify-content-end flex-column flex-sm-row"
>
<slot></slot>
</div>
</div>
</template>
......@@ -174,6 +174,9 @@ export default {
isMRBranchOutdated() {
return this.divergedCommitsCount > 0;
},
dastScans() {
return this.dast.scans.filter(scan => scan.scanned_resources_count > 0);
},
},
created() {
......@@ -377,7 +380,20 @@ export default {
:popover-options="dastPopover"
class="js-dast-widget"
data-qa-selector="dast_scan_report"
/>
>
<template v-if="dastScans.length">
<div class="text-nowrap">
{{ n__('%d URL scanned', '%d URLs scanned', dastScans[0].scanned_resources_count) }}
</div>
<gl-link
class="ml-2"
data-qa-selector="dast-ci-job-link"
:href="dastScans[0].job_path"
>
{{ __('View details') }}
</gl-link>
</template>
</summary-row>
<issues-list
v-if="dast.newIssues.length || dast.resolvedIssues.length"
......
......@@ -80,6 +80,7 @@ export default {
[types.RECEIVE_DAST_DIFF_SUCCESS](state, { diff, enrichData }) {
const { added, fixed, existing } = parseDiff(diff, enrichData);
const baseReportOutofDate = diff.base_report_out_of_date || false;
const scans = diff.scans || [];
const hasBaseReport = Boolean(diff.base_report_created_at);
Vue.set(state.dast, 'isLoading', false);
......@@ -88,6 +89,7 @@ export default {
Vue.set(state.dast, 'allIssues', existing);
Vue.set(state.dast, 'baseReportOutofDate', baseReportOutofDate);
Vue.set(state.dast, 'hasBaseReport', hasBaseReport);
Vue.set(state.dast, 'scans', scans);
},
[types.RECEIVE_DAST_DIFF_ERROR](state) {
......
......@@ -41,6 +41,7 @@ export default () => ({
resolvedIssues: [],
baseReportOutofDate: false,
hasBaseReport: false,
scans: [],
},
dependencyScanning: {
......
---
title: Add scanned URL count and link to scanned resources in DAST reports
merge_request: 26825
author:
type: added
......@@ -10,6 +10,7 @@ import { waitForMutation } from 'helpers/vue_test_utils_helper';
import { trimText } from 'helpers/text_helper';
import axios from '~/lib/utils/axios_utils';
import { mrStates } from '~/mr_popover/constants';
import { TEST_HOST } from 'helpers/test_constants';
import {
sastDiffSuccessMock,
......@@ -302,13 +303,22 @@ describe('Grouped security reports app', () => {
});
describe('dast reports', () => {
const scanUrl = `${TEST_HOST}/group/project/-/jobs/123546789`;
beforeEach(() => {
gl.mrWidgetData = gl.mrWidgetData || {};
gl.mrWidgetData.dast_comparison_path = DAST_DIFF_ENDPOINT;
mock
.onGet(DAST_DIFF_ENDPOINT)
.reply(200, { ...dastDiffSuccessMock, base_report_out_of_date: true });
mock.onGet(DAST_DIFF_ENDPOINT).reply(200, {
...dastDiffSuccessMock,
base_report_out_of_date: true,
scans: [
{
scanned_resources_count: 211,
job_path: scanUrl,
},
],
});
createWrapper({
...props,
......@@ -329,6 +339,38 @@ describe('Grouped security reports app', () => {
'DAST detected 1 new, and 2 fixed vulnerabilities',
);
});
it('shows the scanned URLs count and a link to the CI job if available', () => {
const jobLink = wrapper.find('[data-qa-selector="dast-ci-job-link"]');
expect(wrapper.text()).toContain('211 URLs scanned');
expect(jobLink.exists()).toBe(true);
expect(jobLink.text()).toBe('View details');
expect(jobLink.attributes('href')).toBe(scanUrl);
});
it('does not show scanned resources info if there is 0 scanned URL', () => {
mock.onGet(DAST_DIFF_ENDPOINT).reply(200, {
...dastDiffSuccessMock,
base_report_out_of_date: true,
scans: [
{
scanned_resources_count: 0,
job_path: scanUrl,
},
],
});
createWrapper({
...props,
enabledReports: {
dast: true,
},
});
return waitForMutation(wrapper.vm.$store, types.RECEIVE_DAST_DIFF_SUCCESS).then(() => {
expect(wrapper.text()).not.toContain('0 URLs scanned');
expect(wrapper.contains('[data-qa-selector="dast-ci-job-link"]')).toBe(false);
});
});
});
describe('sast reports', () => {
......
......@@ -624,6 +624,16 @@ describe('security reports mutations', () => {
describe('RECEIVE_DAST_DIFF_SUCCESS', () => {
let reports = {};
const scans = [
{
scanned_resources_count: 123,
job_path: '/group/project/-/jobs/123546789',
},
{
scanned_resources_count: 321,
job_path: '/group/project/-/jobs/987654321',
},
];
beforeEach(() => {
reports = {
......@@ -635,6 +645,7 @@ describe('security reports mutations', () => {
fixed: [{ name: 'fixed vuln 1', report_type: 'dast' }],
existing: [{ name: 'existing vuln 1', report_type: 'dast' }],
base_report_out_of_date: true,
scans,
},
};
mutations[types.RECEIVE_DAST_DIFF_SUCCESS](stateCopy, reports);
......@@ -644,6 +655,10 @@ describe('security reports mutations', () => {
expect(stateCopy.dast.isLoading).toBe(false);
});
it('should set scans', () => {
expect(stateCopy.dast.scans).toEqual(scans);
});
it('should set baseReportOutofDate to true', () => {
expect(stateCopy.dast.baseReportOutofDate).toBe(true);
});
......
......@@ -71,6 +71,11 @@ msgstr ""
msgid "\"%{path}\" did not exist on \"%{ref}\""
msgstr ""
msgid "%d URL scanned"
msgid_plural "%d URLs scanned"
msgstr[0] ""
msgstr[1] ""
msgid "%d changed file"
msgid_plural "%d changed files"
msgstr[0] ""
......@@ -22621,6 +22626,9 @@ msgstr ""
msgid "View deployment"
msgstr ""
msgid "View details"
msgstr ""
msgid "View details: %{details_url}"
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