From bbbdb1c984217a594715dbf17cff260bb1c0ed49 Mon Sep 17 00:00:00 2001 From: Lukas Eipert Date: Mon, 1 Apr 2019 23:53:07 +0200 Subject: [PATCH 1/9] Container Scanning: Messages and description This improves messages and descriptions of container scanning. In order to align with backend, we remove inherit copying of attributes with spreading and explicitly build the same structure backend provides for us --- .../components/sast_container_issue_body.vue | 2 - .../security_reports/store/utils.js | 73 +++++++++++++++---- .../sast_container_issue_body_spec.js | 11 --- .../vue_shared/security_reports/mock_data.js | 40 +++++----- .../security_reports/store/mutations_spec.js | 7 +- .../security_reports/store/utils_spec.js | 48 +++++++++++- 6 files changed, 128 insertions(+), 53 deletions(-) diff --git a/ee/app/assets/javascripts/vue_shared/security_reports/components/sast_container_issue_body.vue b/ee/app/assets/javascripts/vue_shared/security_reports/components/sast_container_issue_body.vue index 7b8554395924b2..abcf0b2a4b7086 100644 --- a/ee/app/assets/javascripts/vue_shared/security_reports/components/sast_container_issue_body.vue +++ b/ee/app/assets/javascripts/vue_shared/security_reports/components/sast_container_issue_body.vue @@ -34,7 +34,5 @@ export default { - - diff --git a/ee/app/assets/javascripts/vue_shared/security_reports/store/utils.js b/ee/app/assets/javascripts/vue_shared/security_reports/store/utils.js index 1d942fa3fda3c5..9ad3211e50e7f0 100644 --- a/ee/app/assets/javascripts/vue_shared/security_reports/store/utils.js +++ b/ee/app/assets/javascripts/vue_shared/security_reports/store/utils.js @@ -198,6 +198,43 @@ export const parseDependencyScanningIssues = (report = [], feedback = [], path = }); }; +export const formatContainerScanningDescription = ({ + description, + namespace, + vulnerability, + featurename, + featureversion, +}) => { + if (!_.isEmpty(description)) { + return description; + } + + let generated; + + if (featurename && featureversion) { + generated = `${featurename}:${featureversion}`; + } else if (featurename) { + generated = featurename; + } else { + generated = namespace; + } + + return sprintf(s__('ciReport|%{namespace} is affected by %{vulnerability}.'), { + namespace: generated, + vulnerability, + }); +}; + +export const formatContainerScanningMessage = ({ vulnerability, featurename }) => { + if (featurename) { + return sprintf(s__('ciReport|%{vulnerability} in %{featurename}'), { + vulnerability, + featurename, + }); + } + return vulnerability; +}; + /** * Parses Container Scanning results into a common format to allow to use the same Vue component. * Container Scanning report is currently the straigh output from the underlying tool @@ -209,20 +246,16 @@ export const parseDependencyScanningIssues = (report = [], feedback = [], path = */ export const parseSastContainer = (issues = [], feedback = []) => issues.map(issue => { + const message = formatContainerScanningMessage(issue); + + /* + The following fields are copying the backend data structure, as can be found in: + https://gitlab.com/gitlab-org/gitlab-ee/blob/f8f5724bb47712df0a618ae0a447b69a6ef47c0c/ee/lib/gitlab/ci/parsers/security/container_scanning.rb#L42-72 + */ const parsed = { - ...issue, category: 'container_scanning', - project_fingerprint: sha1( - `${issue.namespace}:${issue.vulnerability}:${issue.featurename}:${issue.featureversion}`, - ), - title: issue.vulnerability, - description: !_.isEmpty(issue.description) - ? issue.description - : sprintf(s__('ciReport|%{namespace} is affected by %{vulnerability}.'), { - namespace: issue.namespace, - vulnerability: issue.vulnerability, - }), - path: issue.namespace, + message, + description: formatContainerScanningDescription(issue), identifiers: [ { type: 'CVE', @@ -248,9 +281,23 @@ export const parseSastContainer = (issues = [], feedback = []) => }); } + /* + The following properties are set only created in the frontend. + This is done for legacy reasons and they should be made obsolete, + before switching to the Backend implementation + */ + const frontendOnly = { + project_fingerprint: sha1( + `${issue.namespace}:${issue.vulnerability}:${issue.featurename}:${issue.featureversion}`, + ), + title: message, + vulnerability: issue.vulnerability, + }; + return { ...parsed, - ...enrichVulnerabilityWithfeedback(parsed, feedback), + ...frontendOnly, + ...enrichVulnerabilityWithfeedback(frontendOnly, feedback), }; }); diff --git a/ee/spec/javascripts/vue_shared/security_reports/components/sast_container_issue_body_spec.js b/ee/spec/javascripts/vue_shared/security_reports/components/sast_container_issue_body_spec.js index 72731e3c4d00b8..adee93a8b3737a 100644 --- a/ee/spec/javascripts/vue_shared/security_reports/components/sast_container_issue_body_spec.js +++ b/ee/spec/javascripts/vue_shared/security_reports/components/sast_container_issue_body_spec.js @@ -54,15 +54,4 @@ describe('sast container issue body', () => { expect(vm.$el.querySelector('button').textContent.trim()).toEqual(sastContainerIssue.title); }); - - describe('path', () => { - it('renders path', () => { - vm = mountComponent(Component, { - issue: sastContainerIssue, - status, - }); - - expect(vm.$el.textContent.trim()).toContain(sastContainerIssue.path); - }); - }); }); diff --git a/ee/spec/javascripts/vue_shared/security_reports/mock_data.js b/ee/spec/javascripts/vue_shared/security_reports/mock_data.js index 81645ee158f310..530f79eb2892f0 100644 --- a/ee/spec/javascripts/vue_shared/security_reports/mock_data.js +++ b/ee/spec/javascripts/vue_shared/security_reports/mock_data.js @@ -682,6 +682,7 @@ export const parsedDependencyScanningBaseStore = [ export const parsedSastContainerBaseStore = [ { category: 'container_scanning', + message: 'CVE-2014-8130', description: 'debian:8 is affected by CVE-2014-8130.', identifiers: [ { @@ -691,10 +692,7 @@ export const parsedSastContainerBaseStore = [ value: 'CVE-2014-8130', }, ], - namespace: 'debian:8', - path: 'debian:8', project_fingerprint: '20a19f706d82cec1c04d1c9a8858e89b142d602f', - severity: 'Negligible', title: 'CVE-2014-8130', vulnerability: 'CVE-2014-8130', }, @@ -759,11 +757,9 @@ export const dockerBaseReport = { export const dockerNewIssues = [ { - vulnerability: 'CVE-2017-16232', - namespace: 'debian:8', - severity: 'Negligible', - title: 'CVE-2017-16232', - path: 'debian:8', + category: 'container_scanning', + message: 'CVE-2017-16232', + description: 'debian:8 is affected by CVE-2017-16232.', identifiers: [ { type: 'CVE', @@ -772,19 +768,19 @@ export const dockerNewIssues = [ url: 'https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2017-16232', }, ], - category: 'container_scanning', project_fingerprint: '4e010f6d292364a42c6bb05dbd2cc788c2e5e408', - description: 'debian:8 is affected by CVE-2017-16232.', + title: 'CVE-2017-16232', + vulnerability: 'CVE-2017-16232', }, ]; export const dockerOnlyHeadParsed = [ { - vulnerability: 'CVE-2017-12944', - namespace: 'debian:8', + category: 'container_scanning', + message: 'CVE-2017-12944', + description: 'debian:8 is affected by CVE-2017-12944.', + cve: 'CVE-2017-12944', severity: 'Medium', - title: 'CVE-2017-12944', - path: 'debian:8', identifiers: [ { type: 'CVE', @@ -793,16 +789,14 @@ export const dockerOnlyHeadParsed = [ url: 'https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2017-12944', }, ], - category: 'container_scanning', project_fingerprint: '0693a82ef93c5e9d98c23a35ddcd8ed2cbd047d9', - description: 'debian:8 is affected by CVE-2017-12944.', + title: 'CVE-2017-12944', + vulnerability: 'CVE-2017-12944', }, { - vulnerability: 'CVE-2017-16232', - namespace: 'debian:8', - severity: 'Negligible', - title: 'CVE-2017-16232', - path: 'debian:8', + category: 'container_scanning', + message: 'CVE-2017-16232', + description: 'debian:8 is affected by CVE-2017-16232.', identifiers: [ { type: 'CVE', @@ -811,9 +805,9 @@ export const dockerOnlyHeadParsed = [ url: 'https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2017-16232', }, ], - category: 'container_scanning', project_fingerprint: '4e010f6d292364a42c6bb05dbd2cc788c2e5e408', - description: 'debian:8 is affected by CVE-2017-16232.', + title: 'CVE-2017-16232', + vulnerability: 'CVE-2017-16232', }, ]; diff --git a/ee/spec/javascripts/vue_shared/security_reports/store/mutations_spec.js b/ee/spec/javascripts/vue_shared/security_reports/store/mutations_spec.js index 146edd14f01401..15494da404e306 100644 --- a/ee/spec/javascripts/vue_shared/security_reports/store/mutations_spec.js +++ b/ee/spec/javascripts/vue_shared/security_reports/store/mutations_spec.js @@ -396,11 +396,12 @@ describe('security reports mutations', () => { title: 'Arbitrary file existence disclosure in Action Pack', path: 'Gemfile.lock', urlPath: 'path/Gemfile.lock', - namespace: 'debian:8', location: { file: 'Gemfile.lock', class: 'User', method: 'do_something', + image: 'https://example.org/docker/example:v1.2.3', + operating_system: 'debian:8', }, links: [ { @@ -434,7 +435,8 @@ describe('security reports mutations', () => { expect(stateCopy.modal.data.file.url).toEqual(issue.urlPath); expect(stateCopy.modal.data.className.value).toEqual(issue.location.class); expect(stateCopy.modal.data.methodName.value).toEqual(issue.location.method); - expect(stateCopy.modal.data.namespace.value).toEqual(issue.namespace); + expect(stateCopy.modal.data.namespace.value).toEqual(issue.location.operating_system); + expect(stateCopy.modal.data.image.value).toEqual(issue.location.image); expect(stateCopy.modal.data.identifiers.value).toEqual(issue.identifiers); expect(stateCopy.modal.data.severity.value).toEqual(issue.severity); expect(stateCopy.modal.data.confidence.value).toEqual(issue.confidence); @@ -615,7 +617,6 @@ describe('security reports mutations', () => { describe('UPDATE_CONTAINER_SCANNING_ISSUE', () => { it('updates issue in the new issues list', () => { - // TODO pas dast stateCopy.sastContainer.newIssues = dockerNewIssues; stateCopy.sastContainer.resolvedIssues = []; const updatedIssue = { diff --git a/ee/spec/javascripts/vue_shared/security_reports/store/utils_spec.js b/ee/spec/javascripts/vue_shared/security_reports/store/utils_spec.js index 28ea872ac088b9..dd33e114fabca0 100644 --- a/ee/spec/javascripts/vue_shared/security_reports/store/utils_spec.js +++ b/ee/spec/javascripts/vue_shared/security_reports/store/utils_spec.js @@ -11,6 +11,8 @@ import { groupedTextBuilder, statusIcon, countIssues, + formatContainerScanningDescription, + formatContainerScanningMessage, } from 'ee/vue_shared/security_reports/store/utils'; import { oldSastIssues, @@ -226,13 +228,57 @@ describe('security reports utils', () => { }); }); + describe('container scanning utils', () => { + describe('formatContainerScanningMessage', () => { + it('should return concatenated message if vulnerability and featurename are provided', () => { + expect( + formatContainerScanningMessage({ vulnerability: 'CVE-124', featurename: 'grep' }), + ).toBe('CVE-124 in grep'); + }); + + it('should return vulnerability if only that is provided', () => { + expect(formatContainerScanningMessage({ vulnerability: 'Foo' })).toBe('Foo'); + }); + }); + + describe('formatContainerScanningDescription', () => { + it('should return description', () => { + expect(formatContainerScanningDescription({ description: 'Foobar' })).toBe('Foobar'); + }); + + it('should build description from available fields', () => { + const featurename = 'Dependency'; + const featureversion = '1.0'; + const namespace = 'debian:8'; + const vulnerability = 'CVE-123'; + + expect( + formatContainerScanningDescription({ + featurename, + featureversion, + namespace, + vulnerability, + }), + ).toBe('Dependency:1.0 is affected by CVE-123.'); + + expect(formatContainerScanningDescription({ featurename, namespace, vulnerability })).toBe( + 'Dependency is affected by CVE-123.', + ); + + expect(formatContainerScanningDescription({ namespace, vulnerability })).toBe( + 'debian:8 is affected by CVE-123.', + ); + }); + }); + + }); + describe('parseSastContainer', () => { it('parses sast container issues', () => { const parsed = parseSastContainer(dockerReport.vulnerabilities)[0]; const issue = dockerReport.vulnerabilities[0]; expect(parsed.title).toEqual(issue.vulnerability); - expect(parsed.path).toEqual(issue.namespace); expect(parsed.identifiers).toEqual([ { type: 'CVE', -- GitLab From 09159f231c28d2d348083176a949bce4dfa849c3 Mon Sep 17 00:00:00 2001 From: Lukas Eipert Date: Mon, 1 Apr 2019 23:56:48 +0200 Subject: [PATCH 2/9] Container Scanning: Severity and Solution Implement Solution generation and Severity Mapping akin to backend --- .../security_reports/store/utils.js | 52 ++++++++++++++----- .../vue_shared/security_reports/mock_data.js | 3 ++ .../security_reports/store/utils_spec.js | 38 ++++++++++++++ 3 files changed, 80 insertions(+), 13 deletions(-) diff --git a/ee/app/assets/javascripts/vue_shared/security_reports/store/utils.js b/ee/app/assets/javascripts/vue_shared/security_reports/store/utils.js index 9ad3211e50e7f0..ae6bf99a191e62 100644 --- a/ee/app/assets/javascripts/vue_shared/security_reports/store/utils.js +++ b/ee/app/assets/javascripts/vue_shared/security_reports/store/utils.js @@ -235,6 +235,40 @@ export const formatContainerScanningMessage = ({ vulnerability, featurename }) = return vulnerability; }; +export const formatContainerScanningSolution = ({ fixedby, featurename, featureversion }) => { + if (!_.isEmpty(fixedby)) { + if (!_.isEmpty(featurename)) { + if (!_.isEmpty(featureversion)) { + return sprintf(s__('ciReport|Upgrade %{name} from %{version} to %{fixed}.'), { + name: featurename, + version: featureversion, + fixed: fixedby, + }); + } + + return sprintf(s__('ciReport|Upgrade %{name} to %{fixed}.'), { + name: featurename, + fixed: fixedby, + }); + } + + return sprintf(s__('ciReport|Upgrade to %{fixed}.'), { + fixed: fixedby, + }); + } + + return false; +}; + +export const parseContainerScanningSeverity = severity => { + if (severity === 'Defcon1') { + return 'Critical'; + } else if (severity === 'Negligible') { + return 'Low'; + } + return severity; +}; + /** * Parses Container Scanning results into a common format to allow to use the same Vue component. * Container Scanning report is currently the straigh output from the underlying tool @@ -256,6 +290,7 @@ export const parseSastContainer = (issues = [], feedback = []) => category: 'container_scanning', message, description: formatContainerScanningDescription(issue), + severity: parseContainerScanningSeverity(issue.severity), identifiers: [ { type: 'CVE', @@ -266,19 +301,10 @@ export const parseSastContainer = (issues = [], feedback = []) => ], }; - // Generate solution - if ( - !_.isEmpty(issue.fixedby) && - !_.isEmpty(issue.featurename) && - !_.isEmpty(issue.featureversion) - ) { - Object.assign(parsed, { - solution: sprintf(s__('ciReport|Upgrade %{name} from %{version} to %{fixed}.'), { - name: issue.featurename, - version: issue.featureversion, - fixed: issue.fixedby, - }), - }); + const solution = formatContainerScanningSolution(issue); + + if (solution) { + parsed.solution = solution; } /* diff --git a/ee/spec/javascripts/vue_shared/security_reports/mock_data.js b/ee/spec/javascripts/vue_shared/security_reports/mock_data.js index 530f79eb2892f0..b310b48081d399 100644 --- a/ee/spec/javascripts/vue_shared/security_reports/mock_data.js +++ b/ee/spec/javascripts/vue_shared/security_reports/mock_data.js @@ -684,6 +684,7 @@ export const parsedSastContainerBaseStore = [ category: 'container_scanning', message: 'CVE-2014-8130', description: 'debian:8 is affected by CVE-2014-8130.', + severity: 'Low', identifiers: [ { name: 'CVE-2014-8130', @@ -760,6 +761,7 @@ export const dockerNewIssues = [ category: 'container_scanning', message: 'CVE-2017-16232', description: 'debian:8 is affected by CVE-2017-16232.', + severity: 'Low', identifiers: [ { type: 'CVE', @@ -797,6 +799,7 @@ export const dockerOnlyHeadParsed = [ category: 'container_scanning', message: 'CVE-2017-16232', description: 'debian:8 is affected by CVE-2017-16232.', + severity: 'Low', identifiers: [ { type: 'CVE', diff --git a/ee/spec/javascripts/vue_shared/security_reports/store/utils_spec.js b/ee/spec/javascripts/vue_shared/security_reports/store/utils_spec.js index dd33e114fabca0..98c3c820fc8ead 100644 --- a/ee/spec/javascripts/vue_shared/security_reports/store/utils_spec.js +++ b/ee/spec/javascripts/vue_shared/security_reports/store/utils_spec.js @@ -13,6 +13,8 @@ import { countIssues, formatContainerScanningDescription, formatContainerScanningMessage, + formatContainerScanningSolution, + parseContainerScanningSeverity, } from 'ee/vue_shared/security_reports/store/utils'; import { oldSastIssues, @@ -229,6 +231,27 @@ describe('security reports utils', () => { }); describe('container scanning utils', () => { + describe('formatContainerScanningSolution', () => { + it('should return false if there is no data', () => { + expect(formatContainerScanningSolution({})).toBe(false); + }); + + it('should return the correct sentence', () => { + expect(formatContainerScanningSolution({ fixedby: 'v9000' })).toBe('Upgrade to v9000.'); + expect( + formatContainerScanningSolution({ fixedby: 'v9000', featurename: 'Dependency' }), + ).toBe('Upgrade Dependency to v9000.'); + + expect( + formatContainerScanningSolution({ + fixedby: 'v9000', + featurename: 'Dependency', + featureversion: '1.0-beta', + }), + ).toBe('Upgrade Dependency from 1.0-beta to v9000.'); + }); + }); + describe('formatContainerScanningMessage', () => { it('should return concatenated message if vulnerability and featurename are provided', () => { expect( @@ -271,6 +294,21 @@ describe('security reports utils', () => { }); }); + describe('parseContainerScanningSeverity', () => { + it('should return `Critical` for `Defcon1`', () => { + expect(parseContainerScanningSeverity('Defcon1')).toBe('Critical'); + }); + + it('should return `Low` for `Negligible`', () => { + expect(parseContainerScanningSeverity('Negligible')).toBe('Low'); + }); + + it('should not touch other severities', () => { + expect(parseContainerScanningSeverity('oxofrmbl')).toBe('oxofrmbl'); + expect(parseContainerScanningSeverity('Medium')).toBe('Medium'); + expect(parseContainerScanningSeverity('High')).toBe('High'); + }); + }); }); describe('parseSastContainer', () => { -- GitLab From f550f4a05e2421bae494d79c79b9fcc773aacac7 Mon Sep 17 00:00:00 2001 From: Lukas Eipert Date: Mon, 1 Apr 2019 23:58:38 +0200 Subject: [PATCH 3/9] Container Scanning: Location data (image/OS) Adds docker image and location metadata to the Container Scanning model. --- .../security_reports/store/mutations.js | 9 +++--- .../security_reports/store/state.js | 5 ++++ .../security_reports/store/utils.js | 28 ++++++++++++++++++- .../vue_shared/security_reports/mock_data.js | 6 ++++ .../security_reports/store/utils_spec.js | 23 +++++++++++++++ 5 files changed, 66 insertions(+), 5 deletions(-) diff --git a/ee/app/assets/javascripts/vue_shared/security_reports/store/mutations.js b/ee/app/assets/javascripts/vue_shared/security_reports/store/mutations.js index 227be687d0b760..c8eb648748b0e2 100644 --- a/ee/app/assets/javascripts/vue_shared/security_reports/store/mutations.js +++ b/ee/app/assets/javascripts/vue_shared/security_reports/store/mutations.js @@ -118,11 +118,11 @@ export default { [types.RECEIVE_SAST_CONTAINER_REPORTS](state, reports) { if (reports.base && reports.head) { const headIssues = getUnapprovedVulnerabilities( - parseSastContainer(reports.head.vulnerabilities, reports.enrichData), + parseSastContainer(reports.head.vulnerabilities, reports.enrichData, reports.head.image), reports.head.unapproved, ); const baseIssues = getUnapprovedVulnerabilities( - parseSastContainer(reports.base.vulnerabilities, reports.enrichData), + parseSastContainer(reports.base.vulnerabilities, reports.enrichData, reports.base.image), reports.base.unapproved, ); const filterKey = 'vulnerability'; @@ -135,7 +135,7 @@ export default { Vue.set(state.sastContainer, 'isLoading', false); } else if (reports.head && !reports.base) { const newIssues = getUnapprovedVulnerabilities( - parseSastContainer(reports.head.vulnerabilities, reports.enrichData), + parseSastContainer(reports.head.vulnerabilities, reports.enrichData, reports.head.image), reports.head.unapproved, ); @@ -265,7 +265,8 @@ export default { Vue.set(state.modal.data.file, 'url', issue.urlPath); Vue.set(state.modal.data.className, 'value', issue.location && issue.location.class); Vue.set(state.modal.data.methodName, 'value', issue.location && issue.location.method); - Vue.set(state.modal.data.namespace, 'value', issue.namespace); + Vue.set(state.modal.data.image, 'value', issue.location && issue.location.image); + Vue.set(state.modal.data.namespace, 'value', issue.location && issue.location.operating_system); if (issue.identifiers && issue.identifiers.length > 0) { Vue.set(state.modal.data.identifiers, 'value', issue.identifiers); diff --git a/ee/app/assets/javascripts/vue_shared/security_reports/store/state.js b/ee/app/assets/javascripts/vue_shared/security_reports/store/state.js index 873f7611dd8045..ae5c24d9c1ff2c 100644 --- a/ee/app/assets/javascripts/vue_shared/security_reports/store/state.js +++ b/ee/app/assets/javascripts/vue_shared/security_reports/store/state.js @@ -96,6 +96,11 @@ export default () => ({ text: s__('ciReport|Method'), isLink: false, }, + image: { + value: null, + text: s__('ciReport|Image'), + isLink: false, + }, namespace: { value: null, text: s__('ciReport|Namespace'), diff --git a/ee/app/assets/javascripts/vue_shared/security_reports/store/utils.js b/ee/app/assets/javascripts/vue_shared/security_reports/store/utils.js index ae6bf99a191e62..157ad677c6da2c 100644 --- a/ee/app/assets/javascripts/vue_shared/security_reports/store/utils.js +++ b/ee/app/assets/javascripts/vue_shared/security_reports/store/utils.js @@ -260,6 +260,21 @@ export const formatContainerScanningSolution = ({ fixedby, featurename, featurev return false; }; +export const formatContainerScanningDependency = ({ featurename, featureversion }) => { + if (featurename) { + const dependency = { + package: { + name: featurename, + }, + }; + if (featureversion) { + dependency.version = featureversion; + } + return dependency; + } + return false; +}; + export const parseContainerScanningSeverity = severity => { if (severity === 'Defcon1') { return 'Critical'; @@ -276,9 +291,10 @@ export const parseContainerScanningSeverity = severity => { * * @param {Array} issues * @param {Array} feedback + * @param {String} image name * @returns {Array} */ -export const parseSastContainer = (issues = [], feedback = []) => +export const parseSastContainer = (issues = [], feedback = [], image) => issues.map(issue => { const message = formatContainerScanningMessage(issue); @@ -291,6 +307,10 @@ export const parseSastContainer = (issues = [], feedback = []) => message, description: formatContainerScanningDescription(issue), severity: parseContainerScanningSeverity(issue.severity), + location: { + image, + operating_system: issue.namespace, + }, identifiers: [ { type: 'CVE', @@ -307,6 +327,12 @@ export const parseSastContainer = (issues = [], feedback = []) => parsed.solution = solution; } + const dependency = formatContainerScanningDependency(issue); + + if (dependency) { + parsed.location.dependency = dependency; + } + /* The following properties are set only created in the frontend. This is done for legacy reasons and they should be made obsolete, diff --git a/ee/spec/javascripts/vue_shared/security_reports/mock_data.js b/ee/spec/javascripts/vue_shared/security_reports/mock_data.js index b310b48081d399..04b22bb16dcea7 100644 --- a/ee/spec/javascripts/vue_shared/security_reports/mock_data.js +++ b/ee/spec/javascripts/vue_shared/security_reports/mock_data.js @@ -685,6 +685,7 @@ export const parsedSastContainerBaseStore = [ message: 'CVE-2014-8130', description: 'debian:8 is affected by CVE-2014-8130.', severity: 'Low', + location: { image: 'registry.example.com/example/master:1234', operating_system: 'debian:8' }, identifiers: [ { name: 'CVE-2014-8130', @@ -715,6 +716,7 @@ export const allIssuesParsed = [ ]; export const dockerReport = { + image: 'registry.example.com/example/master:1234', unapproved: ['CVE-2017-12944', 'CVE-2017-16232'], vulnerabilities: [ { @@ -736,6 +738,7 @@ export const dockerReport = { }; export const dockerBaseReport = { + image: 'registry.example.com/example/master:1234', unapproved: ['CVE-2017-12944', 'CVE-2014-8130'], vulnerabilities: [ { @@ -762,6 +765,7 @@ export const dockerNewIssues = [ message: 'CVE-2017-16232', description: 'debian:8 is affected by CVE-2017-16232.', severity: 'Low', + location: { image: 'registry.example.com/example/master:1234', operating_system: 'debian:8' }, identifiers: [ { type: 'CVE', @@ -783,6 +787,7 @@ export const dockerOnlyHeadParsed = [ description: 'debian:8 is affected by CVE-2017-12944.', cve: 'CVE-2017-12944', severity: 'Medium', + location: { image: 'registry.example.com/example/master:1234', operating_system: 'debian:8' }, identifiers: [ { type: 'CVE', @@ -800,6 +805,7 @@ export const dockerOnlyHeadParsed = [ message: 'CVE-2017-16232', description: 'debian:8 is affected by CVE-2017-16232.', severity: 'Low', + location: { image: 'registry.example.com/example/master:1234', operating_system: 'debian:8' }, identifiers: [ { type: 'CVE', diff --git a/ee/spec/javascripts/vue_shared/security_reports/store/utils_spec.js b/ee/spec/javascripts/vue_shared/security_reports/store/utils_spec.js index 98c3c820fc8ead..f803a1b78fda8e 100644 --- a/ee/spec/javascripts/vue_shared/security_reports/store/utils_spec.js +++ b/ee/spec/javascripts/vue_shared/security_reports/store/utils_spec.js @@ -11,6 +11,7 @@ import { groupedTextBuilder, statusIcon, countIssues, + formatContainerScanningDependency, formatContainerScanningDescription, formatContainerScanningMessage, formatContainerScanningSolution, @@ -252,6 +253,28 @@ describe('security reports utils', () => { }); }); + describe('formatContainerScanningDependency', () => { + it('should return false if there is data', () => { + expect(formatContainerScanningDependency({})).toBe(false); + }); + + it('should return the correct object', () => { + const name = 'Dependency'; + const version = '10'; + + expect(formatContainerScanningDependency({ featurename: name })).toEqual({ + package: { name }, + }); + + expect( + formatContainerScanningDependency({ featurename: name, featureversion: version }), + ).toEqual({ + package: { name }, + version, + }); + }); + }); + describe('formatContainerScanningMessage', () => { it('should return concatenated message if vulnerability and featurename are provided', () => { expect( -- GitLab From 02397f2f5605d990c7d7408cde3a7fbe8562c231 Mon Sep 17 00:00:00 2001 From: Lukas Eipert Date: Tue, 2 Apr 2019 00:43:40 +0200 Subject: [PATCH 4/9] Container Scanning: Add missing BE metadata This adds all the missing fields that the backend provides --- .../vue_shared/security_reports/store/utils.js | 7 +++++++ .../vue_shared/security_reports/mock_data.js | 11 +++++++++++ 2 files changed, 18 insertions(+) diff --git a/ee/app/assets/javascripts/vue_shared/security_reports/store/utils.js b/ee/app/assets/javascripts/vue_shared/security_reports/store/utils.js index 157ad677c6da2c..ce4af1f5b6744b 100644 --- a/ee/app/assets/javascripts/vue_shared/security_reports/store/utils.js +++ b/ee/app/assets/javascripts/vue_shared/security_reports/store/utils.js @@ -306,11 +306,14 @@ export const parseSastContainer = (issues = [], feedback = [], image) => category: 'container_scanning', message, description: formatContainerScanningDescription(issue), + cve: issue.vulnerability, severity: parseContainerScanningSeverity(issue.severity), + confidence: 'Medium', location: { image, operating_system: issue.namespace, }, + scanner: { id: 'clair', name: 'Clair' }, identifiers: [ { type: 'CVE', @@ -333,6 +336,10 @@ export const parseSastContainer = (issues = [], feedback = [], image) => parsed.location.dependency = dependency; } + if (issue.link) { + parsed.links = [{ url: issue.link }]; + } + /* The following properties are set only created in the frontend. This is done for legacy reasons and they should be made obsolete, diff --git a/ee/spec/javascripts/vue_shared/security_reports/mock_data.js b/ee/spec/javascripts/vue_shared/security_reports/mock_data.js index 04b22bb16dcea7..a03c927b7dadfd 100644 --- a/ee/spec/javascripts/vue_shared/security_reports/mock_data.js +++ b/ee/spec/javascripts/vue_shared/security_reports/mock_data.js @@ -684,8 +684,11 @@ export const parsedSastContainerBaseStore = [ category: 'container_scanning', message: 'CVE-2014-8130', description: 'debian:8 is affected by CVE-2014-8130.', + cve: 'CVE-2014-8130', severity: 'Low', + confidence: 'Medium', location: { image: 'registry.example.com/example/master:1234', operating_system: 'debian:8' }, + scanner: { id: 'clair', name: 'Clair' }, identifiers: [ { name: 'CVE-2014-8130', @@ -764,8 +767,11 @@ export const dockerNewIssues = [ category: 'container_scanning', message: 'CVE-2017-16232', description: 'debian:8 is affected by CVE-2017-16232.', + cve: 'CVE-2017-16232', severity: 'Low', + confidence: 'Medium', location: { image: 'registry.example.com/example/master:1234', operating_system: 'debian:8' }, + scanner: { id: 'clair', name: 'Clair' }, identifiers: [ { type: 'CVE', @@ -787,7 +793,9 @@ export const dockerOnlyHeadParsed = [ description: 'debian:8 is affected by CVE-2017-12944.', cve: 'CVE-2017-12944', severity: 'Medium', + confidence: 'Medium', location: { image: 'registry.example.com/example/master:1234', operating_system: 'debian:8' }, + scanner: { id: 'clair', name: 'Clair' }, identifiers: [ { type: 'CVE', @@ -804,8 +812,11 @@ export const dockerOnlyHeadParsed = [ category: 'container_scanning', message: 'CVE-2017-16232', description: 'debian:8 is affected by CVE-2017-16232.', + cve: 'CVE-2017-16232', severity: 'Low', + confidence: 'Medium', location: { image: 'registry.example.com/example/master:1234', operating_system: 'debian:8' }, + scanner: { id: 'clair', name: 'Clair' }, identifiers: [ { type: 'CVE', -- GitLab From 3a3814a11c380646e97a7c5c1335b435114a2c31 Mon Sep 17 00:00:00 2001 From: Lukas Eipert Date: Tue, 2 Apr 2019 01:07:01 +0200 Subject: [PATCH 5/9] Add changelog and update externalized strings --- .../unreleased/5528-container-scanning-fe.yml | 5 +++++ locale/gitlab.pot | 12 ++++++++++++ 2 files changed, 17 insertions(+) create mode 100644 ee/changelogs/unreleased/5528-container-scanning-fe.yml diff --git a/ee/changelogs/unreleased/5528-container-scanning-fe.yml b/ee/changelogs/unreleased/5528-container-scanning-fe.yml new file mode 100644 index 00000000000000..f802abd1d63fab --- /dev/null +++ b/ee/changelogs/unreleased/5528-container-scanning-fe.yml @@ -0,0 +1,5 @@ +--- +title: Enrich container scanning with more data on the frontend +merge_request: 10526 +author: +type: changed diff --git a/locale/gitlab.pot b/locale/gitlab.pot index a1ae7bb25922d0..498ac9f2209da2 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -12407,6 +12407,9 @@ msgstr "" msgid "ciReport|%{reportType}: Loading resulted in an error" msgstr "" +msgid "ciReport|%{vulnerability} in %{featurename}" +msgstr "" + msgid "ciReport|(errors when loading results)" msgstr "" @@ -12494,6 +12497,9 @@ msgstr "" msgid "ciReport|Identifiers" msgstr "" +msgid "ciReport|Image" +msgstr "" + msgid "ciReport|Implement this solution by creating a merge request" msgstr "" @@ -12594,6 +12600,12 @@ msgstr "" msgid "ciReport|Upgrade %{name} from %{version} to %{fixed}." msgstr "" +msgid "ciReport|Upgrade %{name} to %{fixed}." +msgstr "" + +msgid "ciReport|Upgrade to %{fixed}." +msgstr "" + msgid "ciReport|Used by %{packagesString}" msgid_plural "ciReport|Used by %{packagesString}, and %{lastPackage}" msgstr[0] "" -- GitLab From 5bbbed741b1d3a918f9852b14cb611ad87e50759 Mon Sep 17 00:00:00 2001 From: Lukas Eipert Date: Tue, 2 Apr 2019 10:49:39 +0200 Subject: [PATCH 6/9] Incorporate feedback from reviewer Moved one function inline, using constants where possible, fixed some typos, made function signatures more consistent. --- .../security_reports/store/utils.js | 44 +++++++++---------- .../components/reports/report_issues_spec.js | 6 --- .../components/reports/report_item_spec.js | 6 --- .../security_reports/store/utils_spec.js | 28 ++---------- 4 files changed, 23 insertions(+), 61 deletions(-) diff --git a/ee/app/assets/javascripts/vue_shared/security_reports/store/utils.js b/ee/app/assets/javascripts/vue_shared/security_reports/store/utils.js index ce4af1f5b6744b..2cb859145d1d24 100644 --- a/ee/app/assets/javascripts/vue_shared/security_reports/store/utils.js +++ b/ee/app/assets/javascripts/vue_shared/security_reports/store/utils.js @@ -2,6 +2,7 @@ import sha1 from 'sha1'; import _ from 'underscore'; import { stripHtml } from '~/lib/utils/text_utility'; import { n__, s__, sprintf } from '~/locale'; +import { SEVERITY_LEVELS } from 'ee/security_dashboard/store/constants'; /** * Returns the index of an issue in given list @@ -198,6 +199,10 @@ export const parseDependencyScanningIssues = (report = [], feedback = [], path = }); }; +/* + CONTAINER SCANNING MAPPING: + */ + export const formatContainerScanningDescription = ({ description, namespace, @@ -257,37 +262,22 @@ export const formatContainerScanningSolution = ({ fixedby, featurename, featurev }); } - return false; -}; - -export const formatContainerScanningDependency = ({ featurename, featureversion }) => { - if (featurename) { - const dependency = { - package: { - name: featurename, - }, - }; - if (featureversion) { - dependency.version = featureversion; - } - return dependency; - } - return false; + return null; }; export const parseContainerScanningSeverity = severity => { if (severity === 'Defcon1') { - return 'Critical'; + return SEVERITY_LEVELS.critical; } else if (severity === 'Negligible') { - return 'Low'; + return SEVERITY_LEVELS.low; } return severity; }; /** * Parses Container Scanning results into a common format to allow to use the same Vue component. - * Container Scanning report is currently the straigh output from the underlying tool - * (clair scanner) hence the formatting happenning here. + * Container Scanning report is currently the straight output from the underlying tool + * (clair scanner) hence the formatting happening here. * * @param {Array} issues * @param {Array} feedback @@ -308,7 +298,7 @@ export const parseSastContainer = (issues = [], feedback = [], image) => description: formatContainerScanningDescription(issue), cve: issue.vulnerability, severity: parseContainerScanningSeverity(issue.severity), - confidence: 'Medium', + confidence: SEVERITY_LEVELS.medium, location: { image, operating_system: issue.namespace, @@ -330,9 +320,15 @@ export const parseSastContainer = (issues = [], feedback = [], image) => parsed.solution = solution; } - const dependency = formatContainerScanningDependency(issue); - - if (dependency) { + if (issue.featurename) { + const dependency = { + package: { + name: issue.featurename, + }, + }; + if (issue.featureversion) { + dependency.version = issue.featureversion; + } parsed.location.dependency = dependency; } diff --git a/ee/spec/javascripts/vue_shared/components/reports/report_issues_spec.js b/ee/spec/javascripts/vue_shared/components/reports/report_issues_spec.js index 30931023ea057e..1dac472a5945e3 100644 --- a/ee/spec/javascripts/vue_shared/components/reports/report_issues_spec.js +++ b/ee/spec/javascripts/vue_shared/components/reports/report_issues_spec.js @@ -104,12 +104,6 @@ describe('Report issues', () => { dockerReportParsed.unapproved[0].title, ); }); - - it('renders namespace', () => { - expect(vm.$el.textContent.trim()).toContain(dockerReportParsed.unapproved[0].path); - - expect(vm.$el.textContent.trim()).toContain('in'); - }); }); describe('for dast issues', () => { diff --git a/ee/spec/javascripts/vue_shared/components/reports/report_item_spec.js b/ee/spec/javascripts/vue_shared/components/reports/report_item_spec.js index cd05bd968f31aa..fd0c0b5b322086 100644 --- a/ee/spec/javascripts/vue_shared/components/reports/report_item_spec.js +++ b/ee/spec/javascripts/vue_shared/components/reports/report_item_spec.js @@ -104,12 +104,6 @@ describe('Report issue', () => { dockerReportParsed.unapproved[0].title, ); }); - - it('renders namespace', () => { - expect(vm.$el.textContent.trim()).toContain(dockerReportParsed.unapproved[0].path); - - expect(vm.$el.textContent.trim()).toContain('in'); - }); }); describe('for dast issue', () => { diff --git a/ee/spec/javascripts/vue_shared/security_reports/store/utils_spec.js b/ee/spec/javascripts/vue_shared/security_reports/store/utils_spec.js index f803a1b78fda8e..c9c39e69777b40 100644 --- a/ee/spec/javascripts/vue_shared/security_reports/store/utils_spec.js +++ b/ee/spec/javascripts/vue_shared/security_reports/store/utils_spec.js @@ -11,12 +11,12 @@ import { groupedTextBuilder, statusIcon, countIssues, - formatContainerScanningDependency, formatContainerScanningDescription, formatContainerScanningMessage, formatContainerScanningSolution, parseContainerScanningSeverity, } from 'ee/vue_shared/security_reports/store/utils'; +import { SEVERITY_LEVELS } from 'ee/security_dashboard/store/constants'; import { oldSastIssues, sastIssues, @@ -234,7 +234,7 @@ describe('security reports utils', () => { describe('container scanning utils', () => { describe('formatContainerScanningSolution', () => { it('should return false if there is no data', () => { - expect(formatContainerScanningSolution({})).toBe(false); + expect(formatContainerScanningSolution({})).toBe(null); }); it('should return the correct sentence', () => { @@ -253,28 +253,6 @@ describe('security reports utils', () => { }); }); - describe('formatContainerScanningDependency', () => { - it('should return false if there is data', () => { - expect(formatContainerScanningDependency({})).toBe(false); - }); - - it('should return the correct object', () => { - const name = 'Dependency'; - const version = '10'; - - expect(formatContainerScanningDependency({ featurename: name })).toEqual({ - package: { name }, - }); - - expect( - formatContainerScanningDependency({ featurename: name, featureversion: version }), - ).toEqual({ - package: { name }, - version, - }); - }); - }); - describe('formatContainerScanningMessage', () => { it('should return concatenated message if vulnerability and featurename are provided', () => { expect( @@ -319,7 +297,7 @@ describe('security reports utils', () => { describe('parseContainerScanningSeverity', () => { it('should return `Critical` for `Defcon1`', () => { - expect(parseContainerScanningSeverity('Defcon1')).toBe('Critical'); + expect(parseContainerScanningSeverity('Defcon1')).toBe(SEVERITY_LEVELS.critical); }); it('should return `Low` for `Negligible`', () => { -- GitLab From 7d056936e8b46b52da35144f264cf861b1728ef3 Mon Sep 17 00:00:00 2001 From: Lukas Eipert Date: Tue, 2 Apr 2019 10:58:38 +0200 Subject: [PATCH 7/9] Remove container scanning utils to own file Moves the container scanning into it's own separate file, this has the big benefit that everything concerning container scanning is now in one file, so it is easier to see what belongs together. --- .../security_reports/store/mutations.js | 2 +- .../security_reports/store/utils.js | 163 +---------------- .../store/utils/container_scanning.js | 168 ++++++++++++++++++ .../security_reports/store/utils_spec.js | 6 +- 4 files changed, 175 insertions(+), 164 deletions(-) create mode 100644 ee/app/assets/javascripts/vue_shared/security_reports/store/utils/container_scanning.js diff --git a/ee/app/assets/javascripts/vue_shared/security_reports/store/mutations.js b/ee/app/assets/javascripts/vue_shared/security_reports/store/mutations.js index c8eb648748b0e2..75eaf2151f0f1a 100644 --- a/ee/app/assets/javascripts/vue_shared/security_reports/store/mutations.js +++ b/ee/app/assets/javascripts/vue_shared/security_reports/store/mutations.js @@ -4,11 +4,11 @@ import { parseSastIssues, parseDependencyScanningIssues, filterByKey, - parseSastContainer, parseDastIssues, getUnapprovedVulnerabilities, findIssueIndex, } from './utils'; +import { parseSastContainer } from './utils/container_scanning'; import { visitUrl } from '~/lib/utils/url_utility'; export default { diff --git a/ee/app/assets/javascripts/vue_shared/security_reports/store/utils.js b/ee/app/assets/javascripts/vue_shared/security_reports/store/utils.js index 2cb859145d1d24..46ca8f8c89ce07 100644 --- a/ee/app/assets/javascripts/vue_shared/security_reports/store/utils.js +++ b/ee/app/assets/javascripts/vue_shared/security_reports/store/utils.js @@ -2,7 +2,6 @@ import sha1 from 'sha1'; import _ from 'underscore'; import { stripHtml } from '~/lib/utils/text_utility'; import { n__, s__, sprintf } from '~/locale'; -import { SEVERITY_LEVELS } from 'ee/security_dashboard/store/constants'; /** * Returns the index of an issue in given list @@ -44,8 +43,8 @@ export const findMatchingRemediations = (remediations, vulnerability) => { * @param {Object} vulnerability * @param {Array} feedback */ -function enrichVulnerabilityWithfeedback(vulnerability, feedback = []) { - return feedback +export const enrichVulnerabilityWithfeedback = (vulnerability, feedback = []) => + feedback .filter(fb => fb.project_fingerprint === vulnerability.project_fingerprint) .reduce((vuln, fb) => { if (fb.feedback_type === 'dismissal') { @@ -69,7 +68,6 @@ function enrichVulnerabilityWithfeedback(vulnerability, feedback = []) { } return vuln; }, vulnerability); -} /** * Generates url to repository file and highlight section between start and end lines. @@ -199,163 +197,6 @@ export const parseDependencyScanningIssues = (report = [], feedback = [], path = }); }; -/* - CONTAINER SCANNING MAPPING: - */ - -export const formatContainerScanningDescription = ({ - description, - namespace, - vulnerability, - featurename, - featureversion, -}) => { - if (!_.isEmpty(description)) { - return description; - } - - let generated; - - if (featurename && featureversion) { - generated = `${featurename}:${featureversion}`; - } else if (featurename) { - generated = featurename; - } else { - generated = namespace; - } - - return sprintf(s__('ciReport|%{namespace} is affected by %{vulnerability}.'), { - namespace: generated, - vulnerability, - }); -}; - -export const formatContainerScanningMessage = ({ vulnerability, featurename }) => { - if (featurename) { - return sprintf(s__('ciReport|%{vulnerability} in %{featurename}'), { - vulnerability, - featurename, - }); - } - return vulnerability; -}; - -export const formatContainerScanningSolution = ({ fixedby, featurename, featureversion }) => { - if (!_.isEmpty(fixedby)) { - if (!_.isEmpty(featurename)) { - if (!_.isEmpty(featureversion)) { - return sprintf(s__('ciReport|Upgrade %{name} from %{version} to %{fixed}.'), { - name: featurename, - version: featureversion, - fixed: fixedby, - }); - } - - return sprintf(s__('ciReport|Upgrade %{name} to %{fixed}.'), { - name: featurename, - fixed: fixedby, - }); - } - - return sprintf(s__('ciReport|Upgrade to %{fixed}.'), { - fixed: fixedby, - }); - } - - return null; -}; - -export const parseContainerScanningSeverity = severity => { - if (severity === 'Defcon1') { - return SEVERITY_LEVELS.critical; - } else if (severity === 'Negligible') { - return SEVERITY_LEVELS.low; - } - return severity; -}; - -/** - * Parses Container Scanning results into a common format to allow to use the same Vue component. - * Container Scanning report is currently the straight output from the underlying tool - * (clair scanner) hence the formatting happening here. - * - * @param {Array} issues - * @param {Array} feedback - * @param {String} image name - * @returns {Array} - */ -export const parseSastContainer = (issues = [], feedback = [], image) => - issues.map(issue => { - const message = formatContainerScanningMessage(issue); - - /* - The following fields are copying the backend data structure, as can be found in: - https://gitlab.com/gitlab-org/gitlab-ee/blob/f8f5724bb47712df0a618ae0a447b69a6ef47c0c/ee/lib/gitlab/ci/parsers/security/container_scanning.rb#L42-72 - */ - const parsed = { - category: 'container_scanning', - message, - description: formatContainerScanningDescription(issue), - cve: issue.vulnerability, - severity: parseContainerScanningSeverity(issue.severity), - confidence: SEVERITY_LEVELS.medium, - location: { - image, - operating_system: issue.namespace, - }, - scanner: { id: 'clair', name: 'Clair' }, - identifiers: [ - { - type: 'CVE', - name: issue.vulnerability, - value: issue.vulnerability, - url: `https://cve.mitre.org/cgi-bin/cvename.cgi?name=${issue.vulnerability}`, - }, - ], - }; - - const solution = formatContainerScanningSolution(issue); - - if (solution) { - parsed.solution = solution; - } - - if (issue.featurename) { - const dependency = { - package: { - name: issue.featurename, - }, - }; - if (issue.featureversion) { - dependency.version = issue.featureversion; - } - parsed.location.dependency = dependency; - } - - if (issue.link) { - parsed.links = [{ url: issue.link }]; - } - - /* - The following properties are set only created in the frontend. - This is done for legacy reasons and they should be made obsolete, - before switching to the Backend implementation - */ - const frontendOnly = { - project_fingerprint: sha1( - `${issue.namespace}:${issue.vulnerability}:${issue.featurename}:${issue.featureversion}`, - ), - title: message, - vulnerability: issue.vulnerability, - }; - - return { - ...parsed, - ...frontendOnly, - ...enrichVulnerabilityWithfeedback(frontendOnly, feedback), - }; - }); - /** * 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) diff --git a/ee/app/assets/javascripts/vue_shared/security_reports/store/utils/container_scanning.js b/ee/app/assets/javascripts/vue_shared/security_reports/store/utils/container_scanning.js new file mode 100644 index 00000000000000..e8376ef301bb51 --- /dev/null +++ b/ee/app/assets/javascripts/vue_shared/security_reports/store/utils/container_scanning.js @@ -0,0 +1,168 @@ +import { SEVERITY_LEVELS } from 'ee/security_dashboard/store/constants'; +import { s__, sprintf } from '~/locale'; +import sha1 from 'sha1'; +import _ from 'underscore'; +import { enrichVulnerabilityWithfeedback } from '../utils'; + +/* + Container scanning mapping utils + This file contains all functions for mapping container scanning vulnerabilities + to match the representation that we are building in the backend: + + https://gitlab.com/gitlab-org/gitlab-ee/blob/bbcd07475f0334/ee/lib/gitlab/ci/parsers/security/container_scanning.rb + + All these function can hopefully be removed as soon as we retrieve the data from the backend. + */ + +export const formatContainerScanningDescription = ({ + description, + namespace, + vulnerability, + featurename, + featureversion, +}) => { + if (!_.isEmpty(description)) { + return description; + } + + let generated; + + if (featurename && featureversion) { + generated = `${featurename}:${featureversion}`; + } else if (featurename) { + generated = featurename; + } else { + generated = namespace; + } + + return sprintf(s__('ciReport|%{namespace} is affected by %{vulnerability}.'), { + namespace: generated, + vulnerability, + }); +}; + +export const formatContainerScanningMessage = ({ vulnerability, featurename }) => { + if (featurename) { + return sprintf(s__('ciReport|%{vulnerability} in %{featurename}'), { + vulnerability, + featurename, + }); + } + return vulnerability; +}; + +export const formatContainerScanningSolution = ({ fixedby, featurename, featureversion }) => { + if (!_.isEmpty(fixedby)) { + if (!_.isEmpty(featurename)) { + if (!_.isEmpty(featureversion)) { + return sprintf(s__('ciReport|Upgrade %{name} from %{version} to %{fixed}.'), { + name: featurename, + version: featureversion, + fixed: fixedby, + }); + } + + return sprintf(s__('ciReport|Upgrade %{name} to %{fixed}.'), { + name: featurename, + fixed: fixedby, + }); + } + + return sprintf(s__('ciReport|Upgrade to %{fixed}.'), { + fixed: fixedby, + }); + } + + return null; +}; + +export const parseContainerScanningSeverity = severity => { + if (severity === 'Defcon1') { + return SEVERITY_LEVELS.critical; + } else if (severity === 'Negligible') { + return SEVERITY_LEVELS.low; + } + return severity; +}; + +/** + * Parses Container Scanning results into a common format to allow to use the same Vue component. + * Container Scanning report is currently the straight output from the underlying tool + * (clair scanner) hence the formatting happening here. + * + * @param {Array} issues + * @param {Array} feedback + * @param {String} image name + * @returns {Array} + */ +export const parseSastContainer = (issues = [], feedback = [], image) => + issues.map(issue => { + const message = formatContainerScanningMessage(issue); + + /* + The following fields are copying the backend data structure, as can be found in: + https://gitlab.com/gitlab-org/gitlab-ee/blob/f8f5724bb47712df0a618ae0a447b69a6ef47c0c/ee/lib/gitlab/ci/parsers/security/container_scanning.rb#L42-72 + */ + const parsed = { + category: 'container_scanning', + message, + description: formatContainerScanningDescription(issue), + cve: issue.vulnerability, + severity: parseContainerScanningSeverity(issue.severity), + confidence: SEVERITY_LEVELS.medium, + location: { + image, + operating_system: issue.namespace, + }, + scanner: { id: 'clair', name: 'Clair' }, + identifiers: [ + { + type: 'CVE', + name: issue.vulnerability, + value: issue.vulnerability, + url: `https://cve.mitre.org/cgi-bin/cvename.cgi?name=${issue.vulnerability}`, + }, + ], + }; + + const solution = formatContainerScanningSolution(issue); + + if (solution) { + parsed.solution = solution; + } + + if (issue.featurename) { + const dependency = { + package: { + name: issue.featurename, + }, + }; + if (issue.featureversion) { + dependency.version = issue.featureversion; + } + parsed.location.dependency = dependency; + } + + if (issue.link) { + parsed.links = [{ url: issue.link }]; + } + + /* + The following properties are set only created in the frontend. + This is done for legacy reasons and they should be made obsolete, + before switching to the Backend implementation + */ + const frontendOnly = { + project_fingerprint: sha1( + `${issue.namespace}:${issue.vulnerability}:${issue.featurename}:${issue.featureversion}`, + ), + title: message, + vulnerability: issue.vulnerability, + }; + + return { + ...parsed, + ...frontendOnly, + ...enrichVulnerabilityWithfeedback(frontendOnly, feedback), + }; + }); diff --git a/ee/spec/javascripts/vue_shared/security_reports/store/utils_spec.js b/ee/spec/javascripts/vue_shared/security_reports/store/utils_spec.js index c9c39e69777b40..c8469fd867cd83 100644 --- a/ee/spec/javascripts/vue_shared/security_reports/store/utils_spec.js +++ b/ee/spec/javascripts/vue_shared/security_reports/store/utils_spec.js @@ -4,18 +4,20 @@ import { findMatchingRemediations, parseSastIssues, parseDependencyScanningIssues, - parseSastContainer, parseDastIssues, filterByKey, getUnapprovedVulnerabilities, groupedTextBuilder, statusIcon, countIssues, +} from 'ee/vue_shared/security_reports/store/utils'; +import { formatContainerScanningDescription, formatContainerScanningMessage, formatContainerScanningSolution, parseContainerScanningSeverity, -} from 'ee/vue_shared/security_reports/store/utils'; + parseSastContainer, +} from 'ee/vue_shared/security_reports/store/utils/container_scanning'; import { SEVERITY_LEVELS } from 'ee/security_dashboard/store/constants'; import { oldSastIssues, -- GitLab From ce4caec8b1938cb9056d0d79ac44d208f9abdcfd Mon Sep 17 00:00:00 2001 From: Lukas Eipert Date: Tue, 2 Apr 2019 21:13:16 +0200 Subject: [PATCH 8/9] Container Scanning: Metadata for the GSD Ensure that the container scanning metadata is shown on the Group Security Dashboard modal as well. --- .../modules/vulnerabilities/mutations.js | 39 +++++++++++++------ .../store/modules/vulnerabilities/state.js | 4 +- .../store/vulnerabilities/mutations_spec.js | 12 ++++++ locale/gitlab.pot | 6 +++ 4 files changed, 49 insertions(+), 12 deletions(-) diff --git a/ee/app/assets/javascripts/security_dashboard/store/modules/vulnerabilities/mutations.js b/ee/app/assets/javascripts/security_dashboard/store/modules/vulnerabilities/mutations.js index 5cc4056a9d49be..791972fb247bd2 100644 --- a/ee/app/assets/javascripts/security_dashboard/store/modules/vulnerabilities/mutations.js +++ b/ee/app/assets/javascripts/security_dashboard/store/modules/vulnerabilities/mutations.js @@ -68,6 +68,7 @@ export default { }, [types.SET_MODAL_DATA](state, payload) { const { vulnerability } = payload; + const { location } = vulnerability; Vue.set(state.modal, 'title', vulnerability.name); Vue.set(state.modal.data.description, 'value', vulnerability.description); @@ -81,22 +82,38 @@ export default { 'url', vulnerability.project && vulnerability.project.full_path, ); - Vue.set( - state.modal.data.file, - 'value', - vulnerability.location && - `${vulnerability.location.file}:${vulnerability.location.start_line}`, - ); + Vue.set( state.modal.data.identifiers, 'value', vulnerability.identifiers.length && vulnerability.identifiers, ); - Vue.set( - state.modal.data.className, - 'value', - vulnerability.location && vulnerability.location.class, - ); + + if (location) { + const { + file, + start_line: startLine, + end_line: endLine, + image, + operating_system: namespace, + class: className, + } = location; + + let lineSuffix = ''; + + if (startLine) { + lineSuffix += `:${startLine}`; + if (endLine && startLine !== endLine) { + lineSuffix += `-${endLine}`; + } + } + + Vue.set(state.modal.data.className, 'value', className); + Vue.set(state.modal.data.file, 'value', `${file}${lineSuffix}`); + Vue.set(state.modal.data.image, 'value', image); + Vue.set(state.modal.data.namespace, 'value', namespace); + } + Vue.set(state.modal.data.severity, 'value', vulnerability.severity); Vue.set(state.modal.data.reportType, 'value', vulnerability.report_type); Vue.set(state.modal.data.confidence, 'value', vulnerability.confidence); diff --git a/ee/app/assets/javascripts/security_dashboard/store/modules/vulnerabilities/state.js b/ee/app/assets/javascripts/security_dashboard/store/modules/vulnerabilities/state.js index 2f93a3b015fab5..f8e90bd0795ec6 100644 --- a/ee/app/assets/javascripts/security_dashboard/store/modules/vulnerabilities/state.js +++ b/ee/app/assets/javascripts/security_dashboard/store/modules/vulnerabilities/state.js @@ -28,9 +28,11 @@ export default () => ({ file: { text: s__('Vulnerability|File') }, identifiers: { text: s__('Vulnerability|Identifiers') }, severity: { text: s__('Vulnerability|Severity') }, - reportType: { text: s__('Vulnerability|Report Type') }, confidence: { text: s__('Vulnerability|Confidence') }, + reportType: { text: s__('Vulnerability|Report Type') }, className: { text: s__('Vulnerability|Class') }, + image: { text: s__('Vulnerability|Image') }, + namespace: { text: s__('Vulnerability|Namespace') }, links: { text: s__('Vulnerability|Links') }, instances: { text: s__('Vulnerability|Instances') }, }, diff --git a/ee/spec/javascripts/security_dashboard/store/vulnerabilities/mutations_spec.js b/ee/spec/javascripts/security_dashboard/store/vulnerabilities/mutations_spec.js index 3d4dbdeb732a6c..1cba29f977ab8b 100644 --- a/ee/spec/javascripts/security_dashboard/store/vulnerabilities/mutations_spec.js +++ b/ee/spec/javascripts/security_dashboard/store/vulnerabilities/mutations_spec.js @@ -280,6 +280,18 @@ describe('vulnerabilities module mutations', () => { ); }); + it('should set the modal className', () => { + expect(state.modal.data.className.value).toEqual(vulnerability.location.class); + }); + + it('should set the modal image', () => { + expect(state.modal.data.image.value).toEqual(vulnerability.location.image); + }); + + it('should set the modal namespace', () => { + expect(state.modal.data.namespace.value).toEqual(vulnerability.location.operating_system); + }); + it('should set the modal identifiers', () => { expect(state.modal.data.identifiers.value).toEqual(vulnerability.identifiers); }); diff --git a/locale/gitlab.pot b/locale/gitlab.pot index 498ac9f2209da2..d0fcdd1a9af8dc 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -11747,12 +11747,18 @@ msgstr "" msgid "Vulnerability|Identifiers" msgstr "" +msgid "Vulnerability|Image" +msgstr "" + msgid "Vulnerability|Instances" msgstr "" msgid "Vulnerability|Links" msgstr "" +msgid "Vulnerability|Namespace" +msgstr "" + msgid "Vulnerability|Project" msgstr "" -- GitLab From 7ecf49d444af9e1fd3af9e1d01e9d7fd23094140 Mon Sep 17 00:00:00 2001 From: Lukas Eipert Date: Tue, 2 Apr 2019 21:14:18 +0200 Subject: [PATCH 9/9] Sync up the order of the two modal properties --- .../security_reports/store/state.js | 28 +++++++++---------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/ee/app/assets/javascripts/vue_shared/security_reports/store/state.js b/ee/app/assets/javascripts/vue_shared/security_reports/store/state.js index ae5c24d9c1ff2c..3e5c08f2183b5f 100644 --- a/ee/app/assets/javascripts/vue_shared/security_reports/store/state.js +++ b/ee/app/assets/javascripts/vue_shared/security_reports/store/state.js @@ -75,16 +75,26 @@ export default () => ({ text: s__('ciReport|Description'), isLink: false, }, + file: { + value: null, + url: null, + text: s__('ciReport|File'), + isLink: true, + }, identifiers: { value: [], text: s__('ciReport|Identifiers'), isLink: false, }, - file: { + severity: { value: null, - url: null, - text: s__('ciReport|File'), - isLink: true, + text: s__('ciReport|Severity'), + isLink: false, + }, + confidence: { + value: null, + text: s__('ciReport|Confidence'), + isLink: false, }, className: { value: null, @@ -106,16 +116,6 @@ export default () => ({ text: s__('ciReport|Namespace'), isLink: false, }, - severity: { - value: null, - text: s__('ciReport|Severity'), - isLink: false, - }, - confidence: { - value: null, - text: s__('ciReport|Confidence'), - isLink: false, - }, links: { value: [], text: s__('ciReport|Links'), -- GitLab