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 5cc4056a9d49be435674bffc529b0e64c71ee63e..791972fb247bd28d7c2cd0542cac6eaf102f1191 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 2f93a3b015fab5bf45c3f5b4b159c2f507fb98b6..f8e90bd0795ec6f189fa4ef4e7ed19561c0f03fb 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/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 7b8554395924b26e0136680ea45f84db5740f8c2..abcf0b2a4b70863f374057106a2cbebf853e6a5f 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/mutations.js b/ee/app/assets/javascripts/vue_shared/security_reports/store/mutations.js index 227be687d0b760affc11c206f7667a025fb18031..75eaf2151f0f1a06120d89849e88d2b281766609 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 { @@ -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 873f7611dd804512db8937936354c2e6a11fd758..3e5c08f2183b5f5e18b478add1038a5f9aa3387d 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, @@ -96,19 +106,14 @@ export default () => ({ text: s__('ciReport|Method'), isLink: false, }, - namespace: { - value: null, - text: s__('ciReport|Namespace'), - isLink: false, - }, - severity: { + image: { value: null, - text: s__('ciReport|Severity'), + text: s__('ciReport|Image'), isLink: false, }, - confidence: { + namespace: { value: null, - text: s__('ciReport|Confidence'), + text: s__('ciReport|Namespace'), isLink: false, }, links: { 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 1d942fa3fda3c5f70e1d05d90cc085b659d8c8ac..46ca8f8c89ce077d6a60f437d04375c40c7ddeac 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 @@ -43,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') { @@ -68,7 +68,6 @@ function enrichVulnerabilityWithfeedback(vulnerability, feedback = []) { } return vuln; }, vulnerability); -} /** * Generates url to repository file and highlight section between start and end lines. @@ -198,62 +197,6 @@ export const parseDependencyScanningIssues = (report = [], feedback = [], path = }); }; -/** - * 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. - * - * @param {Array} issues - * @param {Array} feedback - * @returns {Array} - */ -export const parseSastContainer = (issues = [], feedback = []) => - issues.map(issue => { - 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, - identifiers: [ - { - type: 'CVE', - name: issue.vulnerability, - value: issue.vulnerability, - url: `https://cve.mitre.org/cgi-bin/cvename.cgi?name=${issue.vulnerability}`, - }, - ], - }; - - // 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, - }), - }); - } - - return { - ...parsed, - ...enrichVulnerabilityWithfeedback(parsed, 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 0000000000000000000000000000000000000000..e8376ef301bb51c48dac2f9c3ef6f8cc82cd772b --- /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/changelogs/unreleased/5528-container-scanning-fe.yml b/ee/changelogs/unreleased/5528-container-scanning-fe.yml new file mode 100644 index 0000000000000000000000000000000000000000..f802abd1d63fab5ecc09ccf380a0deb4044779eb --- /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/ee/spec/javascripts/security_dashboard/store/vulnerabilities/mutations_spec.js b/ee/spec/javascripts/security_dashboard/store/vulnerabilities/mutations_spec.js index 3d4dbdeb732a6c95303f7e1d91fa81fa0fa279aa..1cba29f977ab8b69ebc9398a3e7e8ed2fa08055a 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/ee/spec/javascripts/vue_shared/components/reports/report_issues_spec.js b/ee/spec/javascripts/vue_shared/components/reports/report_issues_spec.js index 30931023ea057e6bf7d07703f63565ad38eaa947..1dac472a5945e38a2082c36b93011745daaaa7f9 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 cd05bd968f31aa1aa74535937225e1c9a6c46ac7..fd0c0b5b322086a56d7177ece5594306821dc7f6 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/components/sast_container_issue_body_spec.js b/ee/spec/javascripts/vue_shared/security_reports/components/sast_container_issue_body_spec.js index 72731e3c4d00b89b5e789cf079f572c7c9e6b04e..adee93a8b3737a977957ce9f4998d22e3a372d59 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 81645ee158f3107dbca5908d55d616f0054f567f..a03c927b7dadfdf5cbf6d79e977bb2c8db44d822 100644 --- a/ee/spec/javascripts/vue_shared/security_reports/mock_data.js +++ b/ee/spec/javascripts/vue_shared/security_reports/mock_data.js @@ -682,7 +682,13 @@ export const parsedDependencyScanningBaseStore = [ 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', @@ -691,10 +697,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', }, @@ -716,6 +719,7 @@ export const allIssuesParsed = [ ]; export const dockerReport = { + image: 'registry.example.com/example/master:1234', unapproved: ['CVE-2017-12944', 'CVE-2017-16232'], vulnerabilities: [ { @@ -737,6 +741,7 @@ export const dockerReport = { }; export const dockerBaseReport = { + image: 'registry.example.com/example/master:1234', unapproved: ['CVE-2017-12944', 'CVE-2014-8130'], vulnerabilities: [ { @@ -759,11 +764,14 @@ 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.', + 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', @@ -772,19 +780,22 @@ 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', + confidence: 'Medium', + location: { image: 'registry.example.com/example/master:1234', operating_system: 'debian:8' }, + scanner: { id: 'clair', name: 'Clair' }, identifiers: [ { type: 'CVE', @@ -793,16 +804,19 @@ 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.', + 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', @@ -811,9 +825,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 146edd14f0140176bce15af7166083e285abaff8..15494da404e306230ef373b893e4d17f2800fbf7 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 28ea872ac088b9747e8ed0a01ceda3ecde071d06..c8469fd867cd83a4fad3f2c1649fd8909b2fb691 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,7 +4,6 @@ import { findMatchingRemediations, parseSastIssues, parseDependencyScanningIssues, - parseSastContainer, parseDastIssues, filterByKey, getUnapprovedVulnerabilities, @@ -12,6 +11,14 @@ import { statusIcon, countIssues, } from 'ee/vue_shared/security_reports/store/utils'; +import { + formatContainerScanningDescription, + formatContainerScanningMessage, + formatContainerScanningSolution, + parseContainerScanningSeverity, + parseSastContainer, +} from 'ee/vue_shared/security_reports/store/utils/container_scanning'; +import { SEVERITY_LEVELS } from 'ee/security_dashboard/store/constants'; import { oldSastIssues, sastIssues, @@ -226,13 +233,93 @@ describe('security reports utils', () => { }); }); + describe('container scanning utils', () => { + describe('formatContainerScanningSolution', () => { + it('should return false if there is no data', () => { + expect(formatContainerScanningSolution({})).toBe(null); + }); + + 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( + 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('parseContainerScanningSeverity', () => { + it('should return `Critical` for `Defcon1`', () => { + expect(parseContainerScanningSeverity('Defcon1')).toBe(SEVERITY_LEVELS.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', () => { 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', diff --git a/locale/gitlab.pot b/locale/gitlab.pot index a1ae7bb25922d0ae0368b5f246704a13b45263a4..d0fcdd1a9af8dcc999a07f8b637e47b359750cde 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 "" @@ -12407,6 +12413,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 +12503,9 @@ msgstr "" msgid "ciReport|Identifiers" msgstr "" +msgid "ciReport|Image" +msgstr "" + msgid "ciReport|Implement this solution by creating a merge request" msgstr "" @@ -12594,6 +12606,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] ""