Commit 5e9e044a authored by Paul Gascou-Vaillancourt's avatar Paul Gascou-Vaillancourt Committed by Clement Ho

Support multiple sites in DAST reports

- getDastSite behaviour has been changed to force convert reports site
  property into an Array if needed
- parseDastIssues now concatenates issues found in all sites
- legacy DAST reports are still supported
- tests have been updated accordinly
parent 4c9586d0
......@@ -2,7 +2,6 @@ import Vue from 'vue';
import * as types from './mutation_types';
import {
parseDependencyScanningIssues,
getDastSite,
parseDastIssues,
getUnapprovedVulnerabilities,
findIssueIndex,
......@@ -121,11 +120,9 @@ export default {
},
[types.RECEIVE_DAST_REPORTS](state, reports) {
const headSite = getDastSite(reports.head.site);
if (reports.head && reports.base) {
const baseSite = getDastSite(reports.base.site);
const headIssues = parseDastIssues(headSite.alerts, reports.enrichData);
const baseIssues = parseDastIssues(baseSite.alerts, reports.enrichData);
const headIssues = parseDastIssues(reports.head.site, reports.enrichData);
const baseIssues = parseDastIssues(reports.base.site, reports.enrichData);
const filterKey = 'pluginid';
const newIssues = filterByKey(headIssues, baseIssues, filterKey);
const resolvedIssues = filterByKey(baseIssues, headIssues, filterKey);
......@@ -133,8 +130,8 @@ export default {
Vue.set(state.dast, 'newIssues', newIssues);
Vue.set(state.dast, 'resolvedIssues', resolvedIssues);
Vue.set(state.dast, 'isLoading', false);
} else if (reports.head && headSite && !reports.base) {
const newIssues = parseDastIssues(headSite.alerts, reports.enrichData);
} else if (reports.head && reports.head.site && !reports.base) {
const newIssues = parseDastIssues(reports.head.site, reports.enrichData);
Vue.set(state.dast, 'newIssues', newIssues);
Vue.set(state.dast, 'isLoading', false);
......
......@@ -198,61 +198,67 @@ export const parseDependencyScanningIssues = (report = [], feedback = [], path =
};
/**
* Extracts the site property out of a DAST report
* This should be dropped once we support multi-sites reports
* Forces the site property to be an Array in DAST 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.
* DAST report is currently the straigh output from the underlying tool (ZAProxy)
* hence the formatting happenning here.
*
* @param {Array} issues
* @param {Array} sites
* @param {Array} feedback
* @returns {Array}
*/
export const parseDastIssues = (issues = [], feedback = []) =>
issues.map(issue => {
const parsed = {
...issue,
category: 'dast',
project_fingerprint: sha1(issue.pluginid),
title: issue.name,
description: stripHtml(issue.desc, ' '),
solution: stripHtml(issue.solution, ' '),
};
if (!_.isEmpty(issue.cweid)) {
Object.assign(parsed, {
identifiers: [
{
type: 'CWE',
name: `CWE-${issue.cweid}`,
value: issue.cweid,
url: `https://cwe.mitre.org/data/definitions/${issue.cweid}.html`,
},
],
});
}
export const parseDastIssues = (sites = [], feedback = []) =>
getDastSites(sites).reduce(
(acc, site) => [
...acc,
...(site.alerts || []).map(issue => {
const parsed = {
...issue,
category: 'dast',
project_fingerprint: sha1(issue.pluginid),
title: issue.name,
description: stripHtml(issue.desc, ' '),
solution: stripHtml(issue.solution, ' '),
};
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,
});
}
if (!_.isEmpty(issue.cweid)) {
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 !== '') {
// Split riskdesc into severity and confidence.
// Riskdesc format is: "severity (confidence)"
const [, severity, confidence] = issue.riskdesc.match(/(.*) \((.*)\)/);
Object.assign(parsed, {
severity,
confidence,
});
}
return {
...parsed,
...enrichVulnerabilityWithfeedback(parsed, feedback),
};
});
return {
...parsed,
...enrichVulnerabilityWithfeedback(parsed, feedback),
};
}),
],
[],
);
export const getUnapprovedVulnerabilities = (issues = [], unapproved = []) =>
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 = {
],
};
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 = {
site: {
alerts: [
......@@ -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 = [
{
category: 'dast',
......
......@@ -4,7 +4,7 @@ import {
findMatchingRemediations,
parseSastIssues,
parseDependencyScanningIssues,
getDastSite,
getDastSites,
parseDastIssues,
getUnapprovedVulnerabilities,
groupedTextBuilder,
......@@ -33,7 +33,9 @@ import {
dockerReport,
containerScanningFeedbacks,
dast,
multiSitesDast,
dastFeedbacks,
parsedMultiSitesDast,
parsedDast,
} from '../mock_data';
......@@ -351,29 +353,38 @@ describe('security reports utils', () => {
});
});
describe('getDastSite', () => {
it.each([{}, 'site', 1, [], undefined])('returns argument as is if arg is %p', arg => {
expect(getDastSite(arg)).toEqual(arg);
describe('getDastSites', () => {
it.each([{}, 'site', 1, undefined])('wraps non-array argument %p into an array', arg => {
expect(getDastSites(arg)).toEqual([arg]);
});
it('returns first item if arg is a non-empty array', () => {
expect(getDastSite([{}])).toEqual({});
it("returns argument if it's an array", () => {
const sites = [];
expect(getDastSites(sites)).toEqual(sites);
});
});
describe('parseDastIssues', () => {
it('parses dast report', () => {
expect(parseDastIssues(dast.site.alerts)).toEqual(parsedDast);
});
it('includes vulnerability feedbacks', () => {
const parsed = parseDastIssues(dast.site.alerts, dastFeedbacks)[0];
it.each`
description | report
${'multi-sites dast report'} | ${multiSitesDast}
${'legacy dast report'} | ${dast}
`('includes vulnerability feedbacks in $description', ({ report }) => {
const parsed = parseDastIssues(report.site, dastFeedbacks)[0];
expect(parsed.hasIssue).toEqual(true);
expect(parsed.isDismissed).toEqual(true);
expect(parsed.dismissalFeedback).toEqual(dastFeedbacks[0]);
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', () => {
......
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