Commit 8acfd615 authored by Lukas Eipert's avatar Lukas Eipert

Support for new dependency scanning report format

This adds support for the new report syntax to DS. We add support by
wrapping the old simple array in a hash within a `vulnerabilities` key.
parent 14ee3a2c
export const LOADING = 'LOADING'; export const LOADING = 'LOADING';
export const ERROR = 'ERROR'; export const ERROR = 'ERROR';
export const SUCCESS = 'SUCCESS'; export const SUCCESS = 'SUCCESS';
export const DEPRECATED_SAST_REPORT_VERSION = '1.2';
...@@ -2,9 +2,6 @@ import sha1 from 'sha1'; ...@@ -2,9 +2,6 @@ import sha1 from 'sha1';
import _ from 'underscore'; import _ from 'underscore';
import { stripHtml } from '~/lib/utils/text_utility'; import { stripHtml } from '~/lib/utils/text_utility';
import { n__, s__, sprintf } from '~/locale'; import { n__, s__, sprintf } from '~/locale';
import {
DEPRECATED_SAST_REPORT_VERSION,
} from 'ee/vue_shared/security_reports/store/constants';
/** /**
* Returns the index of an issue in given list * Returns the index of an issue in given list
...@@ -98,13 +95,11 @@ function adaptDeprecatedIssueFormat(issue) { ...@@ -98,13 +95,11 @@ function adaptDeprecatedIssueFormat(issue) {
* Wraps old report formats (plain array of vulnerabilities). * Wraps old report formats (plain array of vulnerabilities).
* *
* @param {Array|Object} report * @param {Array|Object} report
* @param {String} deprecatedReportVersion
* @returns {Object} * @returns {Object}
*/ */
function adaptDeprecatedReportFormat(report, deprecatedReportVersion) { function adaptDeprecatedReportFormat(report) {
if (Array.isArray(report)) { if (Array.isArray(report)) {
return { return {
version: deprecatedReportVersion,
vulnerabilities: report, vulnerabilities: report,
}; };
} }
...@@ -121,7 +116,7 @@ function adaptDeprecatedReportFormat(report, deprecatedReportVersion) { ...@@ -121,7 +116,7 @@ function adaptDeprecatedReportFormat(report, deprecatedReportVersion) {
* @returns {Array} * @returns {Array}
*/ */
export const parseSastIssues = (report = [], feedback = [], path = '') => export const parseSastIssues = (report = [], feedback = [], path = '') =>
adaptDeprecatedReportFormat(report, DEPRECATED_SAST_REPORT_VERSION).vulnerabilities.map(issue => { adaptDeprecatedReportFormat(report).vulnerabilities.map(issue => {
const parsed = { const parsed = {
...adaptDeprecatedIssueFormat(issue), ...adaptDeprecatedIssueFormat(issue),
category: 'sast', category: 'sast',
...@@ -140,15 +135,15 @@ export const parseSastIssues = (report = [], feedback = [], path = '') => ...@@ -140,15 +135,15 @@ export const parseSastIssues = (report = [], feedback = [], path = '') =>
/** /**
* Parses Dependency Scanning results into a common format to allow to use the same Vue component. * Parses Dependency Scanning results into a common format to allow to use the same Vue component.
* *
* @param {Array} issues * @param {Array|Object} report
* @param {Array} feedback * @param {Array} feedback
* @param {String} path * @param {String} path
* @returns {Array} * @returns {Array}
*/ */
export const parseDependencyScanningIssues = (issues = [], feedback = [], path = '') => export const parseDependencyScanningIssues = (report = [], feedback = [], path = '') =>
issues.map(issue => { adaptDeprecatedReportFormat(report).vulnerabilities.map(issue => {
const parsed = { const parsed = {
...adaptDeprecatedFormat(issue), ...adaptDeprecatedIssueFormat(issue),
category: 'dependency_scanning', category: 'dependency_scanning',
project_fingerprint: sha1(issue.cve || issue.message), project_fingerprint: sha1(issue.cve || issue.message),
title: issue.message, title: issue.message,
......
---
title: Support for new SAST and dependency scanning report format
merge_request: 8869
author:
type: other
...@@ -364,7 +364,7 @@ export const parsedSastBaseStore = [ ...@@ -364,7 +364,7 @@ export const parsedSastBaseStore = [
}, },
]; ];
export const dependencyScanningIssues = [ export const dependencyScanningIssuesOld = [
{ {
tool: 'bundler_audit', tool: 'bundler_audit',
message: 'Arbitrary file existence disclosure in Action Pack', message: 'Arbitrary file existence disclosure in Action Pack',
...@@ -394,6 +394,111 @@ export const dependencyScanningIssues = [ ...@@ -394,6 +394,111 @@ export const dependencyScanningIssues = [
}, },
]; ];
export const dependencyScanningIssues = [
{
category: 'dependency_scanning',
message: 'ruby-ffi DDL loading issue on Windows OS',
cve: 'sast-sample-rails/Gemfile.lock:ffi:cve:CVE-2018-1000201',
severity: 'High',
solution: 'upgrade to \u003e= 1.9.24',
scanner: {
id: 'bundler_audit',
name: 'bundler-audit',
},
location: {
file: 'sast-sample-rails/Gemfile.lock',
dependency: {
package: {
name: 'ffi',
},
version: '1.9.18',
},
},
identifiers: [
{
type: 'cve',
name: 'CVE-2018-1000201',
value: 'CVE-2018-1000201',
url: 'https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2018-1000201',
},
],
links: [
{
url: 'https://github.com/ffi/ffi/releases/tag/1.9.24',
},
],
},
{
category: 'dependency_scanning',
message: 'XSS vulnerability in rails-html-sanitizer',
cve: 'sast-sample-rails/Gemfile.lock:rails-html-sanitizer:cve:CVE-2018-3741',
severity: 'Unknown',
solution: 'upgrade to \u003e= 1.0.4',
scanner: {
id: 'bundler_audit',
name: 'bundler-audit',
},
location: {
file: 'sast-sample-rails/Gemfile.lock',
dependency: {
package: {
name: 'rails-html-sanitizer',
},
version: '1.0.3',
},
},
identifiers: [
{
type: 'cve',
name: 'CVE-2018-3741',
value: 'CVE-2018-3741',
url: 'https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2018-3741',
},
],
links: [
{
url: 'https://groups.google.com/d/msg/rubyonrails-security/tP7W3kLc5u4/uDy2Br7xBgAJ',
},
],
},
{
category: 'dependency_scanning',
message: 'Vulnerability in ansi2html',
cve: ':ansi2html:npm:51',
severity: 'High',
scanner: {
id: 'retire.js',
name: 'Retire.js',
},
location: {
dependency: {
package: {
name: 'ansi2html',
},
version: '0.0.1',
},
},
identifiers: [
{
type: 'npm',
name: 'NPM-51',
value: '51',
url: 'https://www.npmjs.com/advisories/51',
},
],
links: [
{
url: 'https://nodesecurity.io/advisories/51',
},
],
},
];
export const dependencyScanningIssuesMajor2 = {
version: '2.0',
vulnerabilities: dependencyScanningIssues,
};
export const dependencyScanningIssuesBase = [ export const dependencyScanningIssuesBase = [
{ {
tool: 'bundler_audit', tool: 'bundler_audit',
......
...@@ -7,7 +7,7 @@ import { ...@@ -7,7 +7,7 @@ import {
parsedSastIssuesHead, parsedSastIssuesHead,
parsedSastBaseStore, parsedSastBaseStore,
parsedSastIssuesStore, parsedSastIssuesStore,
dependencyScanningIssues, dependencyScanningIssuesOld,
dependencyScanningIssuesBase, dependencyScanningIssuesBase,
parsedDependencyScanningIssuesHead, parsedDependencyScanningIssuesHead,
parsedDependencyScanningBaseStore, parsedDependencyScanningBaseStore,
...@@ -303,7 +303,7 @@ describe('security reports mutations', () => { ...@@ -303,7 +303,7 @@ describe('security reports mutations', () => {
mutations[types.SET_BASE_BLOB_PATH](stateCopy, 'path'); mutations[types.SET_BASE_BLOB_PATH](stateCopy, 'path');
mutations[types.SET_HEAD_BLOB_PATH](stateCopy, 'path'); mutations[types.SET_HEAD_BLOB_PATH](stateCopy, 'path');
mutations[types.RECEIVE_DEPENDENCY_SCANNING_REPORTS](stateCopy, { mutations[types.RECEIVE_DEPENDENCY_SCANNING_REPORTS](stateCopy, {
head: dependencyScanningIssues, head: dependencyScanningIssuesOld,
base: dependencyScanningIssuesBase, base: dependencyScanningIssuesBase,
}); });
...@@ -321,7 +321,7 @@ describe('security reports mutations', () => { ...@@ -321,7 +321,7 @@ describe('security reports mutations', () => {
it('should set new issues', () => { it('should set new issues', () => {
mutations[types.SET_HEAD_BLOB_PATH](stateCopy, 'path'); mutations[types.SET_HEAD_BLOB_PATH](stateCopy, 'path');
mutations[types.RECEIVE_DEPENDENCY_SCANNING_REPORTS](stateCopy, { mutations[types.RECEIVE_DEPENDENCY_SCANNING_REPORTS](stateCopy, {
head: dependencyScanningIssues, head: dependencyScanningIssuesOld,
}); });
expect(stateCopy.dependencyScanning.isLoading).toEqual(false); expect(stateCopy.dependencyScanning.isLoading).toEqual(false);
......
...@@ -15,7 +15,9 @@ import { ...@@ -15,7 +15,9 @@ import {
sastIssues, sastIssues,
sastIssuesMajor2, sastIssuesMajor2,
sastFeedbacks, sastFeedbacks,
dependencyScanningIssuesOld,
dependencyScanningIssues, dependencyScanningIssues,
dependencyScanningIssuesMajor2,
dependencyScanningFeedbacks, dependencyScanningFeedbacks,
dockerReport, dockerReport,
containerScanningFeedbacks, containerScanningFeedbacks,
...@@ -106,35 +108,59 @@ describe('security reports utils', () => { ...@@ -106,35 +108,59 @@ describe('security reports utils', () => {
describe('parseDependencyScanningIssues', () => { describe('parseDependencyScanningIssues', () => {
it('should parse the received issues', () => { it('should parse the received issues', () => {
const parsed = parseDependencyScanningIssues(dependencyScanningIssues, [], 'path')[0]; const parsed = parseDependencyScanningIssues(dependencyScanningIssuesOld, [], 'path')[0];
expect(parsed.title).toEqual(dependencyScanningIssues[0].message); expect(parsed.title).toEqual(dependencyScanningIssuesOld[0].message);
expect(parsed.path).toEqual(dependencyScanningIssues[0].file); expect(parsed.path).toEqual(dependencyScanningIssuesOld[0].file);
expect(parsed.location.start_line).toEqual(sastIssues[0].location.start_line); expect(parsed.location.start_line).toEqual(parseInt(dependencyScanningIssuesOld[0].line, 10));
expect(parsed.location.end_line).toBeUndefined(); expect(parsed.location.end_line).toBeUndefined();
expect(parsed.urlPath).toEqual('path/Gemfile.lock#L5'); expect(parsed.urlPath).toEqual('path/Gemfile.lock#L5');
expect(parsed.project_fingerprint).toEqual(sha1(dependencyScanningIssues[0].cve)); expect(parsed.project_fingerprint).toEqual(sha1(dependencyScanningIssuesOld[0].cve));
});
it('should parse the received issues with new JSON format', () => {
const raw = dependencyScanningIssues[0];
const parsed = parseDependencyScanningIssues(dependencyScanningIssues, [], 'path')[0];
expect(parsed.title).toEqual(raw.message);
expect(parsed.path).toEqual(raw.location.file);
expect(parsed.location.start_line).toBeUndefined();
expect(parsed.location.end_line).toBeUndefined();
expect(parsed.urlPath).toEqual(`path/${raw.location.file}`);
expect(parsed.project_fingerprint).toEqual(sha1(raw.cve));
});
it('should parse the received issues with new JSON format (2.0)', () => {
const raw = dependencyScanningIssues[0];
const parsed = parseDependencyScanningIssues(dependencyScanningIssuesMajor2, [], 'path')[0];
expect(parsed.title).toEqual(raw.message);
expect(parsed.path).toEqual(raw.location.file);
expect(parsed.location.start_line).toBeUndefined();
expect(parsed.location.end_line).toBeUndefined();
expect(parsed.urlPath).toEqual(`path/${raw.location.file}`);
expect(parsed.project_fingerprint).toEqual(sha1(raw.cve));
}); });
it('generate correct path to file when there is no line', () => { it('generate correct path to file when there is no line', () => {
const parsed = parseDependencyScanningIssues(dependencyScanningIssues, [], 'path')[1]; const parsed = parseDependencyScanningIssues(dependencyScanningIssuesOld, [], 'path')[1];
expect(parsed.urlPath).toEqual('path/Gemfile.lock'); expect(parsed.urlPath).toEqual('path/Gemfile.lock');
}); });
it('uses message to generate sha1 when cve is undefined', () => { it('uses message to generate sha1 when cve is undefined', () => {
const issuesWithoutCve = dependencyScanningIssues.map(issue => ({ const issuesWithoutCve = dependencyScanningIssuesOld.map(issue => ({
...issue, ...issue,
cve: undefined, cve: undefined,
})); }));
const parsed = parseDependencyScanningIssues(issuesWithoutCve, [], 'path')[0]; const parsed = parseDependencyScanningIssues(issuesWithoutCve, [], 'path')[0];
expect(parsed.project_fingerprint).toEqual(sha1(dependencyScanningIssues[0].message)); expect(parsed.project_fingerprint).toEqual(sha1(dependencyScanningIssuesOld[0].message));
}); });
it('includes vulnerability feedbacks', () => { it('includes vulnerability feedbacks', () => {
const parsed = parseDependencyScanningIssues( const parsed = parseDependencyScanningIssues(
dependencyScanningIssues, dependencyScanningIssuesOld,
dependencyScanningFeedbacks, dependencyScanningFeedbacks,
'path', 'path',
)[0]; )[0];
......
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