Commit 3302d1c4 authored by Clement Ho's avatar Clement Ho

Merge branch '11930-dast-multi-sites-fe' into 'master'

Support multiple sites in DAST reports

See merge request gitlab-org/gitlab-ee!14787
parents 4c9586d0 5e9e044a
...@@ -2,7 +2,6 @@ import Vue from 'vue'; ...@@ -2,7 +2,6 @@ import Vue from 'vue';
import * as types from './mutation_types'; import * as types from './mutation_types';
import { import {
parseDependencyScanningIssues, parseDependencyScanningIssues,
getDastSite,
parseDastIssues, parseDastIssues,
getUnapprovedVulnerabilities, getUnapprovedVulnerabilities,
findIssueIndex, findIssueIndex,
...@@ -121,11 +120,9 @@ export default { ...@@ -121,11 +120,9 @@ export default {
}, },
[types.RECEIVE_DAST_REPORTS](state, reports) { [types.RECEIVE_DAST_REPORTS](state, reports) {
const headSite = getDastSite(reports.head.site);
if (reports.head && reports.base) { if (reports.head && reports.base) {
const baseSite = getDastSite(reports.base.site); const headIssues = parseDastIssues(reports.head.site, reports.enrichData);
const headIssues = parseDastIssues(headSite.alerts, reports.enrichData); const baseIssues = parseDastIssues(reports.base.site, reports.enrichData);
const baseIssues = parseDastIssues(baseSite.alerts, reports.enrichData);
const filterKey = 'pluginid'; const filterKey = 'pluginid';
const newIssues = filterByKey(headIssues, baseIssues, filterKey); const newIssues = filterByKey(headIssues, baseIssues, filterKey);
const resolvedIssues = filterByKey(baseIssues, headIssues, filterKey); const resolvedIssues = filterByKey(baseIssues, headIssues, filterKey);
...@@ -133,8 +130,8 @@ export default { ...@@ -133,8 +130,8 @@ export default {
Vue.set(state.dast, 'newIssues', newIssues); Vue.set(state.dast, 'newIssues', newIssues);
Vue.set(state.dast, 'resolvedIssues', resolvedIssues); Vue.set(state.dast, 'resolvedIssues', resolvedIssues);
Vue.set(state.dast, 'isLoading', false); Vue.set(state.dast, 'isLoading', false);
} else if (reports.head && headSite && !reports.base) { } else if (reports.head && reports.head.site && !reports.base) {
const newIssues = parseDastIssues(headSite.alerts, reports.enrichData); const newIssues = parseDastIssues(reports.head.site, reports.enrichData);
Vue.set(state.dast, 'newIssues', newIssues); Vue.set(state.dast, 'newIssues', newIssues);
Vue.set(state.dast, 'isLoading', false); Vue.set(state.dast, 'isLoading', false);
......
...@@ -198,61 +198,67 @@ export const parseDependencyScanningIssues = (report = [], feedback = [], path = ...@@ -198,61 +198,67 @@ export const parseDependencyScanningIssues = (report = [], feedback = [], path =
}; };
/** /**
* Extracts the site property out of a DAST report * Forces the site property to be an Array in DAST reports.
* This should be dropped once we support multi-sites reports * We do this to also support single-site legacy DAST reports.
* *
* @param {Object|Array} site * @param {Object|Array} sites
*/ */
export const getDastSite = site => (Array.isArray(site) && site.length ? site[0] : site); export const getDastSites = sites => (Array.isArray(sites) ? sites : [sites]);
/** /**
* Parses DAST into a common format to allow to use the same Vue component. * Parses DAST into a common format to allow to use the same Vue component.
* DAST report is currently the straigh output from the underlying tool (ZAProxy) * DAST report is currently the straigh output from the underlying tool (ZAProxy)
* hence the formatting happenning here. * hence the formatting happenning here.
* *
* @param {Array} issues * @param {Array} sites
* @param {Array} feedback * @param {Array} feedback
* @returns {Array} * @returns {Array}
*/ */
export const parseDastIssues = (issues = [], feedback = []) => export const parseDastIssues = (sites = [], feedback = []) =>
issues.map(issue => { getDastSites(sites).reduce(
const parsed = { (acc, site) => [
...issue, ...acc,
category: 'dast', ...(site.alerts || []).map(issue => {
project_fingerprint: sha1(issue.pluginid), const parsed = {
title: issue.name, ...issue,
description: stripHtml(issue.desc, ' '), category: 'dast',
solution: stripHtml(issue.solution, ' '), project_fingerprint: sha1(issue.pluginid),
}; title: issue.name,
description: stripHtml(issue.desc, ' '),
if (!_.isEmpty(issue.cweid)) { solution: stripHtml(issue.solution, ' '),
Object.assign(parsed, { };
identifiers: [
{
type: 'CWE',
name: `CWE-${issue.cweid}`,
value: issue.cweid,
url: `https://cwe.mitre.org/data/definitions/${issue.cweid}.html`,
},
],
});
}
if (issue.riskdesc && issue.riskdesc !== '') { if (!_.isEmpty(issue.cweid)) {
// Split riskdesc into severity and confidence. Object.assign(parsed, {
// Riskdesc format is: "severity (confidence)" identifiers: [
const [, severity, confidence] = issue.riskdesc.match(/(.*) \((.*)\)/); {
Object.assign(parsed, { type: 'CWE',
severity, name: `CWE-${issue.cweid}`,
confidence, value: issue.cweid,
}); url: `https://cwe.mitre.org/data/definitions/${issue.cweid}.html`,
} },
],
});
}
if (issue.riskdesc && issue.riskdesc !== '') {
// Split riskdesc into severity and confidence.
// Riskdesc format is: "severity (confidence)"
const [, severity, confidence] = issue.riskdesc.match(/(.*) \((.*)\)/);
Object.assign(parsed, {
severity,
confidence,
});
}
return { return {
...parsed, ...parsed,
...enrichVulnerabilityWithfeedback(parsed, feedback), ...enrichVulnerabilityWithfeedback(parsed, feedback),
}; };
}); }),
],
[],
);
export const getUnapprovedVulnerabilities = (issues = [], unapproved = []) => export const getUnapprovedVulnerabilities = (issues = [], unapproved = []) =>
issues.filter(item => unapproved.find(el => el === item.vulnerability)); issues.filter(item => unapproved.find(el => el === item.vulnerability));
......
---
title: Support multiple sites in DAST reports
merge_request: 14787
author:
type: added
...@@ -930,6 +930,84 @@ export const dockerReportParsed = { ...@@ -930,6 +930,84 @@ export const dockerReportParsed = {
], ],
}; };
export const multiSitesDast = {
site: [
{
'@port': '8080',
'@host': 'goat',
'@name': 'http://goat:8080',
alerts: [
{
name: 'Absence of Anti-CSRF Tokens',
riskcode: '1',
riskdesc: 'Low (Medium)',
cweid: '3',
desc: '<p>No Anti-CSRF tokens were found in a HTML submission form.</p>',
pluginid: '123',
solution: '<p>Update to latest</p>',
instances: [
{
uri: 'http://192.168.32.236:3001/explore?sort=latest_activity_desc',
method: 'GET',
evidence:
"<form class='form-inline' action='/search' accept-charset='UTF-8' method='get'>",
},
{
uri: 'http://192.168.32.236:3001/help/user/group/subgroups/index.md',
method: 'GET',
evidence:
"<form class='form-inline' action='/search' accept-charset='UTF-8' method='get'>",
},
],
},
{
alert: 'X-Content-Type-Options Header Missing',
name: 'X-Content-Type-Options Header Missing',
riskdesc: 'Low (Medium)',
cweid: '4',
desc:
'<p>The Anti-MIME-Sniffing header X-Content-Type-Options was not set to "nosniff".</p>',
pluginid: '3456',
solution: '<p>Update to latest</p>',
instances: [
{
uri: 'http://192.168.32.236:3001/assets/webpack/main.bundle.js',
method: 'GET',
param: 'X-Content-Type-Options',
},
],
},
],
'@ssl': 'false',
},
{
'@port': '8081',
'@host': 'nginx',
'@name': 'http://nginx:8081',
alerts: [
{
alert: 'X-Content-Type-Options Header Missing',
name: 'X-Content-Type-Options Header Missing',
riskdesc: 'Low (Medium)',
cweid: '4',
desc:
'<p>The Anti-MIME-Sniffing header X-Content-Type-Options was not set to "nosniff".</p>',
pluginid: '3456',
solution: '<p>Update to latest</p>',
instances: [
{
uri: 'http://192.168.32.236:3001/assets/webpack/main.bundle.js',
method: 'GET',
param: 'X-Content-Type-Options',
},
],
},
],
'@ssl': 'false',
},
],
};
export const dast = { export const dast = {
site: { site: {
alerts: [ alerts: [
...@@ -1007,6 +1085,104 @@ export const dastBase = { ...@@ -1007,6 +1085,104 @@ export const dastBase = {
}, },
}; };
export const parsedMultiSitesDast = [
{
category: 'dast',
project_fingerprint: '40bd001563085fc35165329ea1ff5c5ecbdbbeef',
name: 'Absence of Anti-CSRF Tokens',
title: 'Absence of Anti-CSRF Tokens',
riskcode: '1',
riskdesc: 'Low (Medium)',
severity: 'Low',
confidence: 'Medium',
cweid: '3',
desc: '<p>No Anti-CSRF tokens were found in a HTML submission form.</p>',
pluginid: '123',
identifiers: [
{
type: 'CWE',
name: 'CWE-3',
value: '3',
url: 'https://cwe.mitre.org/data/definitions/3.html',
},
],
instances: [
{
uri: 'http://192.168.32.236:3001/explore?sort=latest_activity_desc',
method: 'GET',
evidence: "<form class='form-inline' action='/search' accept-charset='UTF-8' method='get'>",
},
{
uri: 'http://192.168.32.236:3001/help/user/group/subgroups/index.md',
method: 'GET',
evidence: "<form class='form-inline' action='/search' accept-charset='UTF-8' method='get'>",
},
],
solution: ' Update to latest ',
description: ' No Anti-CSRF tokens were found in a HTML submission form. ',
},
{
category: 'dast',
project_fingerprint: 'ae8fe380dd9aa5a7a956d9085fe7cf6b87d0d028',
alert: 'X-Content-Type-Options Header Missing',
name: 'X-Content-Type-Options Header Missing',
title: 'X-Content-Type-Options Header Missing',
riskdesc: 'Low (Medium)',
identifiers: [
{
type: 'CWE',
name: 'CWE-4',
value: '4',
url: 'https://cwe.mitre.org/data/definitions/4.html',
},
],
severity: 'Low',
confidence: 'Medium',
cweid: '4',
desc: '<p>The Anti-MIME-Sniffing header X-Content-Type-Options was not set to "nosniff".</p>',
pluginid: '3456',
instances: [
{
uri: 'http://192.168.32.236:3001/assets/webpack/main.bundle.js',
method: 'GET',
param: 'X-Content-Type-Options',
},
],
solution: ' Update to latest ',
description: ' The Anti-MIME-Sniffing header X-Content-Type-Options was not set to "nosniff". ',
},
{
category: 'dast',
project_fingerprint: 'ae8fe380dd9aa5a7a956d9085fe7cf6b87d0d028',
alert: 'X-Content-Type-Options Header Missing',
name: 'X-Content-Type-Options Header Missing',
title: 'X-Content-Type-Options Header Missing',
riskdesc: 'Low (Medium)',
identifiers: [
{
type: 'CWE',
name: 'CWE-4',
value: '4',
url: 'https://cwe.mitre.org/data/definitions/4.html',
},
],
severity: 'Low',
confidence: 'Medium',
cweid: '4',
desc: '<p>The Anti-MIME-Sniffing header X-Content-Type-Options was not set to "nosniff".</p>',
pluginid: '3456',
instances: [
{
uri: 'http://192.168.32.236:3001/assets/webpack/main.bundle.js',
method: 'GET',
param: 'X-Content-Type-Options',
},
],
solution: ' Update to latest ',
description: ' The Anti-MIME-Sniffing header X-Content-Type-Options was not set to "nosniff". ',
},
];
export const parsedDast = [ export const parsedDast = [
{ {
category: 'dast', category: 'dast',
......
...@@ -4,7 +4,7 @@ import { ...@@ -4,7 +4,7 @@ import {
findMatchingRemediations, findMatchingRemediations,
parseSastIssues, parseSastIssues,
parseDependencyScanningIssues, parseDependencyScanningIssues,
getDastSite, getDastSites,
parseDastIssues, parseDastIssues,
getUnapprovedVulnerabilities, getUnapprovedVulnerabilities,
groupedTextBuilder, groupedTextBuilder,
...@@ -33,7 +33,9 @@ import { ...@@ -33,7 +33,9 @@ import {
dockerReport, dockerReport,
containerScanningFeedbacks, containerScanningFeedbacks,
dast, dast,
multiSitesDast,
dastFeedbacks, dastFeedbacks,
parsedMultiSitesDast,
parsedDast, parsedDast,
} from '../mock_data'; } from '../mock_data';
...@@ -351,29 +353,38 @@ describe('security reports utils', () => { ...@@ -351,29 +353,38 @@ describe('security reports utils', () => {
}); });
}); });
describe('getDastSite', () => { describe('getDastSites', () => {
it.each([{}, 'site', 1, [], undefined])('returns argument as is if arg is %p', arg => { it.each([{}, 'site', 1, undefined])('wraps non-array argument %p into an array', arg => {
expect(getDastSite(arg)).toEqual(arg); expect(getDastSites(arg)).toEqual([arg]);
}); });
it('returns first item if arg is a non-empty array', () => { it("returns argument if it's an array", () => {
expect(getDastSite([{}])).toEqual({}); const sites = [];
expect(getDastSites(sites)).toEqual(sites);
}); });
}); });
describe('parseDastIssues', () => { describe('parseDastIssues', () => {
it('parses dast report', () => { it.each`
expect(parseDastIssues(dast.site.alerts)).toEqual(parsedDast); description | report
}); ${'multi-sites dast report'} | ${multiSitesDast}
${'legacy dast report'} | ${dast}
it('includes vulnerability feedbacks', () => { `('includes vulnerability feedbacks in $description', ({ report }) => {
const parsed = parseDastIssues(dast.site.alerts, dastFeedbacks)[0]; const parsed = parseDastIssues(report.site, dastFeedbacks)[0];
expect(parsed.hasIssue).toEqual(true); expect(parsed.hasIssue).toEqual(true);
expect(parsed.isDismissed).toEqual(true); expect(parsed.isDismissed).toEqual(true);
expect(parsed.dismissalFeedback).toEqual(dastFeedbacks[0]); expect(parsed.dismissalFeedback).toEqual(dastFeedbacks[0]);
expect(parsed.issue_feedback).toEqual(dastFeedbacks[1]); expect(parsed.issue_feedback).toEqual(dastFeedbacks[1]);
}); });
it('parses dast report', () => {
expect(parseDastIssues(multiSitesDast.site)).toEqual(parsedMultiSitesDast);
});
it('parses legacy dast report', () => {
expect(parseDastIssues(dast.site)).toEqual(parsedDast);
});
}); });
describe('filterByKey', () => { describe('filterByKey', () => {
......
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