Commit 2b8dadaf authored by Lukas Eipert's avatar Lukas Eipert

Support remediations in Dependency Scanning report

Dependency Scanning vulnerabilities are now enriched with remidiations
that fix them. This is purely the support for the store, the UI will be
handled later.
parent 73310aec
...@@ -11,6 +11,32 @@ import { n__, s__, sprintf } from '~/locale'; ...@@ -11,6 +11,32 @@ import { n__, s__, sprintf } from '~/locale';
export const findIssueIndex = (issues, issue) => export const findIssueIndex = (issues, issue) =>
issues.findIndex(el => el.project_fingerprint === issue.project_fingerprint); issues.findIndex(el => el.project_fingerprint === issue.project_fingerprint);
/**
*
* Returns whether a vulnerability has a match in an array of fixes
*
* @param fixes {Array} Array of fixes (vulnerability identifiers) of a remediation
* @param vulnerability {Object} Vulnerability
* @returns {boolean}
*/
const hasMatchingFix = (fixes, vulnerability) =>
Array.isArray(fixes) ? fixes.some(fix => _.isMatch(vulnerability, fix)) : false;
/**
*
* Returns the first remediation that fixes the given vulnerability or null
*
* @param {Array} remediations
* @param {Object} vulnerability
* @returns {Object|null}
*/
export const findMatchingRemediation = (remediations, vulnerability) => {
if (!Array.isArray(remediations)) {
return null;
}
return remediations.find(rem => hasMatchingFix(rem.fixes, vulnerability)) || null;
};
/** /**
* Returns given vulnerability enriched with the corresponding * Returns given vulnerability enriched with the corresponding
* feedback (`dismissal` or `issue` type) * feedback (`dismissal` or `issue` type)
...@@ -101,6 +127,7 @@ function adaptDeprecatedReportFormat(report) { ...@@ -101,6 +127,7 @@ function adaptDeprecatedReportFormat(report) {
if (Array.isArray(report)) { if (Array.isArray(report)) {
return { return {
vulnerabilities: report, vulnerabilities: report,
remediations: [],
}; };
} }
...@@ -140,8 +167,9 @@ export const parseSastIssues = (report = [], feedback = [], path = '') => ...@@ -140,8 +167,9 @@ export const parseSastIssues = (report = [], feedback = [], path = '') =>
* @param {String} path * @param {String} path
* @returns {Array} * @returns {Array}
*/ */
export const parseDependencyScanningIssues = (report = [], feedback = [], path = '') => export const parseDependencyScanningIssues = (report = [], feedback = [], path = '') => {
adaptDeprecatedReportFormat(report).vulnerabilities.map(issue => { const { vulnerabilities, remediations } = adaptDeprecatedReportFormat(report);
return vulnerabilities.map(issue => {
const parsed = { const parsed = {
...adaptDeprecatedIssueFormat(issue), ...adaptDeprecatedIssueFormat(issue),
category: 'dependency_scanning', category: 'dependency_scanning',
...@@ -149,6 +177,12 @@ export const parseDependencyScanningIssues = (report = [], feedback = [], path = ...@@ -149,6 +177,12 @@ export const parseDependencyScanningIssues = (report = [], feedback = [], path =
title: issue.message, title: issue.message,
}; };
const remediation = findMatchingRemediation(remediations, parsed);
if (remediation) {
parsed.remediation = remediation;
}
return { return {
...parsed, ...parsed,
path: parsed.location.file, path: parsed.location.file,
...@@ -156,6 +190,7 @@ export const parseDependencyScanningIssues = (report = [], feedback = [], path = ...@@ -156,6 +190,7 @@ export const parseDependencyScanningIssues = (report = [], feedback = [], path =
...enrichVulnerabilityWithfeedback(parsed, feedback), ...enrichVulnerabilityWithfeedback(parsed, feedback),
}; };
}); });
};
/** /**
* Parses Container Scanning results into a common format to allow to use the same Vue component. * Parses Container Scanning results into a common format to allow to use the same Vue component.
...@@ -164,7 +199,6 @@ export const parseDependencyScanningIssues = (report = [], feedback = [], path = ...@@ -164,7 +199,6 @@ export const parseDependencyScanningIssues = (report = [], feedback = [], path =
* *
* @param {Array} issues * @param {Array} issues
* @param {Array} feedback * @param {Array} feedback
* @param {String} path
* @returns {Array} * @returns {Array}
*/ */
export const parseSastContainer = (issues = [], feedback = []) => export const parseSastContainer = (issues = [], feedback = []) =>
......
...@@ -497,6 +497,12 @@ export const dependencyScanningIssues = [ ...@@ -497,6 +497,12 @@ export const dependencyScanningIssues = [
export const dependencyScanningIssuesMajor2 = { export const dependencyScanningIssuesMajor2 = {
version: '2.0', version: '2.0',
vulnerabilities: dependencyScanningIssues, vulnerabilities: dependencyScanningIssues,
remediations: [
{
fixes: [{ cve: dependencyScanningIssues[0].cve }],
summary: 'Fixes the first dependency Scanning issue',
},
],
}; };
export const dependencyScanningIssuesBase = [ export const dependencyScanningIssuesBase = [
......
import sha1 from 'sha1'; import sha1 from 'sha1';
import { import {
findIssueIndex, findIssueIndex,
findMatchingRemediation,
parseSastIssues, parseSastIssues,
parseDependencyScanningIssues, parseDependencyScanningIssues,
parseSastContainer, parseSastContainer,
...@@ -55,6 +56,50 @@ describe('security reports utils', () => { ...@@ -55,6 +56,50 @@ describe('security reports utils', () => {
}); });
}); });
describe('findMatchingRemediation', () => {
const remediation1 = {
fixes: [
{
cve: '123',
},
{
foobar: 'baz',
},
],
summary: 'Update to x.y.z',
};
const remediation2 = { ...remediation1, summary: 'Remediation2' };
const impossibleRemediation = {
fixes: [],
summary: 'Impossible',
};
const remediations = [impossibleRemediation, remediation1, remediation2];
it('returns null for empty vulnerability', () => {
expect(findMatchingRemediation(remediations, {})).toBeNull();
expect(findMatchingRemediation(remediations, null)).toBeNull();
expect(findMatchingRemediation(remediations, undefined)).toBeNull();
});
it('returns null for empty remediations', () => {
expect(findMatchingRemediation([], { cve: '123' })).toBeNull();
expect(findMatchingRemediation(null, { cve: '123' })).toBeNull();
expect(findMatchingRemediation(undefined, { cve: '123' })).toBeNull();
});
it('returns null for vulnerabilities without remediation', () => {
expect(findMatchingRemediation(remediations, { cve: 'NOT_FOUND' })).toBeNull();
});
it('returns first matching remediation for a vulnerability', () => {
expect(findMatchingRemediation(remediations, { cve: '123' })).toEqual(remediation1);
expect(findMatchingRemediation(remediations, { foobar: 'baz' })).toEqual(remediation1);
});
});
describe('parseSastIssues', () => { describe('parseSastIssues', () => {
it('should parse the received issues with old JSON format', () => { it('should parse the received issues with old JSON format', () => {
const parsed = parseSastIssues(oldSastIssues, [], 'path')[0]; const parsed = parseSastIssues(oldSastIssues, [], 'path')[0];
...@@ -140,6 +185,7 @@ describe('security reports utils', () => { ...@@ -140,6 +185,7 @@ describe('security reports utils', () => {
expect(parsed.location.end_line).toBeUndefined(); expect(parsed.location.end_line).toBeUndefined();
expect(parsed.urlPath).toEqual(`path/${raw.location.file}`); expect(parsed.urlPath).toEqual(`path/${raw.location.file}`);
expect(parsed.project_fingerprint).toEqual(sha1(raw.cve)); expect(parsed.project_fingerprint).toEqual(sha1(raw.cve));
expect(parsed.remediation).toEqual(dependencyScanningIssuesMajor2.remediations[0]);
}); });
it('generate correct path to file when there is no line', () => { it('generate correct path to file when there is no line', () => {
......
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