Commit 05ef0599 authored by Robert Speicher's avatar Robert Speicher

Merge branch '5043_enrich_sast_reports' into 'master'

Enrich Security Reports with more data.

See merge request gitlab-org/gitlab-ee!5878
parents dd78ad02 27a821ba
<script> <script>
/** /**
* Renders DAST body text * Renders DAST body text
* [priority]: [name] * [severity] ([confidence]): [name]
*/ */
import ModalOpenName from './modal_open_name.vue'; import ModalOpenName from './modal_open_name.vue';
...@@ -27,7 +27,7 @@ export default { ...@@ -27,7 +27,7 @@ export default {
<template> <template>
<div class="report-block-list-issue-description prepend-top-5 append-bottom-5"> <div class="report-block-list-issue-description prepend-top-5 append-bottom-5">
<div class="report-block-list-issue-description-text"> <div class="report-block-list-issue-description-text">
<template v-if="issue.priority">{{ issue.priority }}:</template> {{ issue.severity }} ({{ issue.confidence }}):
<modal-open-name <modal-open-name
:issue="issue" :issue="issue"
......
...@@ -35,9 +35,20 @@ export default { ...@@ -35,9 +35,20 @@ export default {
this.dismissIssue(); this.dismissIssue();
} }
}, },
isLastValue(index, values) {
return index < values.length - 1;
},
hasValue(field) {
return field.value && field.value.length > 0;
},
hasInstances(field, key) { hasInstances(field, key) {
return key === 'instances' && field.value && field.value.length > 0; return key === 'instances' && this.hasValue(field);
},
hasIdentifiers(field, key) {
return key === 'identifiers' && this.hasValue(field);
},
hasLinks(field, key) {
return key === 'links' && this.hasValue(field);
}, },
}, },
}; };
...@@ -51,7 +62,7 @@ export default { ...@@ -51,7 +62,7 @@ export default {
<slot> <slot>
<div <div
v-for="(field, key, index) in modal.data" v-for="(field, key, index) in modal.data"
v-if="field.value || hasInstances(field, key)" v-if="field.value"
class="row prepend-top-10 append-bottom-10" class="row prepend-top-10 append-bottom-10"
:key="index" :key="index"
> >
...@@ -99,6 +110,42 @@ export default { ...@@ -99,6 +110,42 @@ export default {
</li> </li>
</ul> </ul>
</div> </div>
<template v-else-if="hasIdentifiers(field, key)">
<span
v-for="(identifier, i) in field.value"
:key="i"
>
<a
:class="`js-link-${key}`"
v-if="identifier.url"
target="_blank"
:href="identifier.url"
rel="noopener noreferrer"
>
{{ identifier.name }}
</a>
<span v-else>
{{ identifier.name }}
</span>
<span v-if="isLastValue(i, field.value)">,&nbsp;</span>
</span>
</template>
<template v-else-if="hasLinks(field, key)">
<span
v-for="(link, i) in field.value"
:key="i"
>
<a
:class="`js-link-${key}`"
target="_blank"
:href="link.url"
rel="noopener noreferrer"
>
{{ link.value || link.url }}
</a>
<span v-if="isLastValue(i, field.value)">,&nbsp;</span>
</span>
</template>
<template v-else> <template v-else>
<a <a
:class="`js-link-${key}`" :class="`js-link-${key}`"
......
...@@ -22,6 +22,6 @@ export default { ...@@ -22,6 +22,6 @@ export default {
@click="handleIssueClick()" @click="handleIssueClick()"
class="btn-link btn-blank text-left break-link vulnerability-name-button" class="btn-link btn-blank text-left break-link vulnerability-name-button"
> >
{{ issue.name }} {{ issue.title }}
</button> </button>
</template> </template>
...@@ -23,7 +23,7 @@ export default { ...@@ -23,7 +23,7 @@ export default {
<template> <template>
<div class="report-block-list-issue-description prepend-top-5 append-bottom-5"> <div class="report-block-list-issue-description prepend-top-5 append-bottom-5">
<div class="report-block-list-issue-description-text"> <div class="report-block-list-issue-description-text">
<template v-if="issue.priority">{{ issue.priority }}:</template> <template v-if="issue.severity">{{ issue.severity }}:</template>
<modal-open-name :issue="issue" /> <modal-open-name :issue="issue" />
</div> </div>
......
<script> <script>
/** /**
* Renders SAST body text * Renders SAST body text
* [priority]: [name] in [link] : [line] * [severity] ([confidence]): [name] in [link] : [line]
*/ */
import ReportLink from './report_link.vue'; import ReportLink from './report_link.vue';
import ModalOpenName from './modal_open_name.vue'; import ModalOpenName from './modal_open_name.vue';
...@@ -25,7 +25,10 @@ export default { ...@@ -25,7 +25,10 @@ export default {
<template> <template>
<div class="report-block-list-issue-description prepend-top-5 append-bottom-5"> <div class="report-block-list-issue-description prepend-top-5 append-bottom-5">
<div class="report-block-list-issue-description-text"> <div class="report-block-list-issue-description-text">
<template v-if="issue.priority">{{ issue.priority }}:</template> <template v-if="issue.severity && issue.confidence">
{{ issue.severity }} ({{ issue.confidence }}):
</template>
<template v-else-if="issue.priority">{{ issue.priority }}:</template>
<modal-open-name :issue="issue" /> <modal-open-name :issue="issue" />
</div> </div>
......
...@@ -248,28 +248,30 @@ export default { ...@@ -248,28 +248,30 @@ export default {
}, },
[types.SET_ISSUE_MODAL_DATA](state, issue) { [types.SET_ISSUE_MODAL_DATA](state, issue) {
state.modal.title = issue.name; state.modal.title = issue.title;
state.modal.data.description.value = issue.description; state.modal.data.description.value = issue.description;
state.modal.data.file.value = issue.file; state.modal.data.file.value = issue.location && issue.location.file;
state.modal.data.file.url = issue.urlPath; state.modal.data.file.url = issue.urlPath;
state.modal.data.className.value = issue.location && issue.location.class;
state.modal.data.methodName.value = issue.location && issue.location.method;
state.modal.data.namespace.value = issue.namespace; state.modal.data.namespace.value = issue.namespace;
if (issue.identifiers && issue.identifiers.length > 0) {
state.modal.data.identifiers.value = issue.identifiers;
} else {
// Force a null value for identifiers to avoid showing an empty array
state.modal.data.identifiers.value = null;
}
state.modal.data.severity.value = issue.severity; state.modal.data.severity.value = issue.severity;
state.modal.data.confidence.value = issue.confidence;
state.modal.data.solution.value = issue.solution; state.modal.data.solution.value = issue.solution;
state.modal.data.confidenceLevel.value = issue.confidence; if (issue.links && issue.links.length > 0) {
state.modal.data.source.value = issue.source; state.modal.data.links.value = issue.links;
state.modal.data.instances.value = issue.instances;
state.modal.vulnerability = issue;
// Link to CVE-ID for Container Scanning
if (issue.nameLink) {
state.modal.data.identifier.value = issue.name;
state.modal.data.identifier.isLink = true;
state.modal.data.identifier.url = issue.nameLink;
} else { } else {
state.modal.data.identifier.value = issue.identifier; // Force a null value for links to avoid showing an empty array
state.modal.data.identifier.isLink = false; state.modal.data.links.value = null;
state.modal.data.identifier.url = null;
} }
state.modal.data.instances.value = issue.instances;
state.modal.vulnerability = issue;
// clear previous state // clear previous state
state.modal.error = null; state.modal.error = null;
......
...@@ -77,21 +77,30 @@ export default () => ({ ...@@ -77,21 +77,30 @@ export default () => ({
text: s__('ciReport|Description'), text: s__('ciReport|Description'),
isLink: false, isLink: false,
}, },
identifiers: {
value: [],
text: s__('ciReport|Identifiers'),
isLink: false,
},
file: { file: {
value: null, value: null,
url: null, url: null,
text: s__('ciReport|File'), text: s__('ciReport|File'),
isLink: true, isLink: true,
}, },
namespace: { className: {
value: null, value: null,
text: s__('ciReport|Namespace'), text: s__('ciReport|Class'),
isLink: false, isLink: false,
}, },
identifier: { methodName: {
value: null, value: null,
url: null, text: s__('ciReport|Method'),
text: s__('ciReport|Identifier'), isLink: false,
},
namespace: {
value: null,
text: s__('ciReport|Namespace'),
isLink: false, isLink: false,
}, },
severity: { severity: {
...@@ -99,20 +108,20 @@ export default () => ({ ...@@ -99,20 +108,20 @@ export default () => ({
text: s__('ciReport|Severity'), text: s__('ciReport|Severity'),
isLink: false, isLink: false,
}, },
solution: { confidence: {
value: null, value: null,
text: s__('ciReport|Solution'), text: s__('ciReport|Confidence'),
isLink: false, isLink: false,
}, },
confidenceLevel: { solution: {
value: null, value: null,
text: s__('ciReport|Confidence Level'), text: s__('ciReport|Solution'),
isLink: false, isLink: false,
}, },
source: { links: {
value: null, value: [],
text: s__('ciReport|Source'), text: s__('ciReport|Links'),
isLink: true, isLink: false,
}, },
instances: { instances: {
value: [], value: [],
......
import sha1 from 'sha1'; import sha1 from 'sha1';
import _ from 'underscore';
import { stripHtml } from '~/lib/utils/text_utility'; import { stripHtml } from '~/lib/utils/text_utility';
import { n__, s__, sprintf } from '~/locale'; import { n__, s__, sprintf } from '~/locale';
...@@ -12,25 +13,25 @@ export const findIssueIndex = (issues, issue) => ...@@ -12,25 +13,25 @@ export const findIssueIndex = (issues, issue) =>
/** /**
* Returns given vulnerability enriched with the corresponding * Returns given vulnerability enriched with the corresponding
* feedbacks (`dismissal` or `issue` type) * feedback (`dismissal` or `issue` type)
* @param {Object} vulnerability * @param {Object} vulnerability
* @param {Array} feedbacks * @param {Array} feedback
*/ */
function enrichVulnerabilityWithfeedbacks(vulnerability, feedbacks = []) { function enrichVulnerabilityWithfeedback(vulnerability, feedback = []) {
return feedbacks.filter( return feedback.filter(
feedback => feedback.project_fingerprint === vulnerability.project_fingerprint, fb => fb.project_fingerprint === vulnerability.project_fingerprint,
).reduce((vuln, feedback) => { ).reduce((vuln, fb) => {
if (feedback.feedback_type === 'dismissal') { if (fb.feedback_type === 'dismissal') {
return { return {
...vuln, ...vuln,
isDismissed: true, isDismissed: true,
dismissalFeedback: feedback, dismissalFeedback: fb,
}; };
} else if (feedback.feedback_type === 'issue') { } else if (fb.feedback_type === 'issue') {
return { return {
...vuln, ...vuln,
hasIssue: true, hasIssue: true,
issueFeedback: feedback, issueFeedback: fb,
}; };
} }
return vuln; return vuln;
...@@ -38,107 +39,192 @@ function enrichVulnerabilityWithfeedbacks(vulnerability, feedbacks = []) { ...@@ -38,107 +39,192 @@ function enrichVulnerabilityWithfeedbacks(vulnerability, feedbacks = []) {
} }
/** /**
* Maps SAST issues: * Generates url to repository file and highlight section between start and end lines.
* { tool: String, message: String, url: String , cve: String , *
* file: String , solution: String, priority: String } * @param {Object} location
* to contain: * @param {String} pathPrefix
* { name: String, path: String, line: String, urlPath: String, priority: String } * @returns {String}
*/
function fileUrl(location, pathPrefix) {
let lineSuffix = '';
if (!_.isEmpty(location.start_line)) {
lineSuffix += `#L${location.start_line}`;
if (!_.isEmpty(location.end_line)) {
lineSuffix += `-${location.end_line}`;
}
}
return `${pathPrefix}/${location.file}${lineSuffix}`;
}
/**
* Parses issues with deprecated JSON format and adapts it to the new one.
*
* @param {Object} issue
* @returns {Object}
*/
function adaptDeprecatedFormat(issue) {
// Skip issue with new format (old format does not have a location property)
if (issue.location) {
return issue;
}
const adapted = {
...issue,
};
// Add the new links property
const links = [];
if (!_.isEmpty(adapted.url)) {
links.push({ url: adapted.url });
}
Object.assign(adapted, {
// Add the new location property
location: {
file: adapted.file,
start_line: adapted.line,
},
links,
});
return adapted;
}
/**
* Parses SAST results into a common format to allow to use the same Vue component.
*
* @param {Array} issues * @param {Array} issues
* @param {Array} feedback
* @param {String} path * @param {String} path
* @returns {Array}
*/ */
export const parseSastIssues = (issues = [], feedbacks = [], path = '') => export const parseSastIssues = (issues = [], feedback = [], path = '') =>
issues.map(issue => { issues.map(issue => {
const parsed = { const parsed = {
...issue, ...adaptDeprecatedFormat(issue),
category: 'sast', category: 'sast',
// TODO: replace with issue.project_fingerprint
project_fingerprint: sha1(issue.cve), project_fingerprint: sha1(issue.cve),
name: issue.message, title: issue.message,
path: issue.file,
urlPath: issue.line ? `${path}/${issue.file}#L${issue.line}` : `${path}/${issue.file}`,
}; };
return { return {
...parsed, ...parsed,
...enrichVulnerabilityWithfeedbacks(parsed, feedbacks), path: parsed.location.file,
urlPath: fileUrl(parsed.location, path),
...enrichVulnerabilityWithfeedback(parsed, feedback),
}; };
}); });
/** /**
* Maps Dependency scanning issues: * Parses Dependency Scanning results into a common format to allow to use the same Vue component.
* { tool: String, message: String, url: String , cve: String , *
* file: String , solution: String, priority: String }
* to contain:
* { name: String, path: String, line: String, urlPath: String, priority: String }
* @param {Array} issues * @param {Array} issues
* @param {Array} feedback
* @param {String} path * @param {String} path
* @returns {Array}
*/ */
export const parseDependencyScanningIssues = (issues = [], feedbacks = [], path = '') => export const parseDependencyScanningIssues = (issues = [], feedback = [], path = '') =>
issues.map(issue => { issues.map(issue => {
const parsed = { const parsed = {
...issue, ...adaptDeprecatedFormat(issue),
category: 'dependency_scanning', category: 'dependency_scanning',
// TODO: replace with issue.project_fingerprint
project_fingerprint: sha1(issue.cve || issue.message), project_fingerprint: sha1(issue.cve || issue.message),
name: issue.message, title: issue.message,
path: issue.file,
urlPath: issue.line ? `${path}/${issue.file}#L${issue.line}` : `${path}/${issue.file}`,
}; };
return { return {
...parsed, ...parsed,
...enrichVulnerabilityWithfeedbacks(parsed, feedbacks), path: parsed.location.file,
urlPath: fileUrl(parsed.location, path),
...enrichVulnerabilityWithfeedback(parsed, feedback),
}; };
}); });
/** /**
* Parses Sast Container 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.
* And adds an external link * Container Scanning report is currently the straigh output from the underlying tool
* (clair scanner) hence the formatting happenning here.
* *
* @param {Array} data * @param {Array} issues
* @param {Array} feedback
* @param {String} path
* @returns {Array} * @returns {Array}
*/ */
export const parseSastContainer = (issues = [], feedbacks = []) => export const parseSastContainer = (issues = [], feedback = []) =>
issues.map(issue => { issues.map(issue => {
const parsed = { const parsed = {
...issue, ...issue,
category: 'container_scanning', category: 'container_scanning',
// TODO: replace with issue.project_fingerprint
project_fingerprint: sha1(`${issue.namespace}:${issue.vulnerability}:${issue.featurename}:${issue.featureversion}`), project_fingerprint: sha1(`${issue.namespace}:${issue.vulnerability}:${issue.featurename}:${issue.featureversion}`),
name: issue.vulnerability, title: issue.vulnerability,
priority: issue.severity, description: !_.isEmpty(issue.description) ? issue.description :
sprintf(s__('ciReport|%{namespace} is affected by %{vulnerability}.'), {
namespace: issue.namespace,
vulnerability: issue.vulnerability,
}),
path: issue.namespace, path: issue.namespace,
// external link to provide better description identifiers: [{
nameLink: `https://cve.mitre.org/cgi-bin/cvename.cgi?name=${issue.vulnerability}`, 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 { return {
...parsed, ...parsed,
...enrichVulnerabilityWithfeedbacks(parsed, feedbacks), ...enrichVulnerabilityWithfeedback(parsed, feedback),
}; };
}); });
export const parseDastIssues = (issues = [], feedbacks = []) => /**
* Parses DAST into a common format to allow to use the same Vue component.
* DAST report is currently the straigh output from the underlying tool (ZAProxy)
* hence the formatting happenning here.
*
* @param {Array} issues
* @param {Array} feedback
* @returns {Array}
*/
export const parseDastIssues = (issues = [], feedback = []) =>
issues.map(issue => { issues.map(issue => {
const parsed = { const parsed = {
...issue, ...issue,
category: 'dast', category: 'dast',
// TODO: replace with issue.project_fingerprint
project_fingerprint: sha1(issue.pluginid), project_fingerprint: sha1(issue.pluginid),
parsedDescription: stripHtml(issue.desc, ' '), title: issue.name,
priority: issue.riskdesc,
solution: stripHtml(issue.solution, ' '),
description: stripHtml(issue.desc, ' '), description: stripHtml(issue.desc, ' '),
solution: stripHtml(issue.solution, ' '),
}; };
if (issue.cweid && issue.cweid !== '') { if (!_.isEmpty(issue.cweid)) {
Object.assign(parsed, { Object.assign(parsed, {
identifier: `CWE-${issue.cweid}`, identifiers: [{
type: 'CWE',
name: `CWE-${issue.cweid}`,
value: issue.cweid,
url: `https://cwe.mitre.org/data/definitions/${issue.cweid}.html`,
}],
}); });
} }
if (issue.riskdesc && issue.riskdesc !== '') { if (issue.riskdesc && issue.riskdesc !== '') {
// Split 'severity (confidence)' // Split riskdesc into severity and confidence.
// Riskdesc format is: "severity (confidence)"
const [, severity, confidence] = issue.riskdesc.match(/(.*) \((.*)\)/); const [, severity, confidence] = issue.riskdesc.match(/(.*) \((.*)\)/);
Object.assign(parsed, { Object.assign(parsed, {
severity, severity,
...@@ -148,7 +234,7 @@ export const parseDastIssues = (issues = [], feedbacks = []) => ...@@ -148,7 +234,7 @@ export const parseDastIssues = (issues = [], feedbacks = []) =>
return { return {
...parsed, ...parsed,
...enrichVulnerabilityWithfeedbacks(parsed, feedbacks), ...enrichVulnerabilityWithfeedback(parsed, feedback),
}; };
}); });
......
...@@ -109,9 +109,22 @@ class Projects::VulnerabilityFeedbackController < Projects::ApplicationControlle ...@@ -109,9 +109,22 @@ class Projects::VulnerabilityFeedbackController < Projects::ApplicationControlle
method method
uri uri
], ],
location: %i[
file
start_line
end_line
class
method
],
identifiers: %i[ identifiers: %i[
type
name name
value value
url
],
links: %i[
name
url
] ]
] ]
end end
......
...@@ -2,14 +2,14 @@ module Issues ...@@ -2,14 +2,14 @@ module Issues
class CreateFromVulnerabilityDataService < ::BaseService class CreateFromVulnerabilityDataService < ::BaseService
def execute def execute
vulnerability = case @params[:category] vulnerability = case @params[:category]
when 'sast', 'dependency_scanning' when 'sast', 'dependency_scanning', 'dast'
Gitlab::Vulnerabilities::StandardVulnerability.new(params) Gitlab::Vulnerabilities::StandardVulnerability.new(params)
when 'container_scanning' when 'container_scanning'
Gitlab::Vulnerabilities::ContainerScanningVulnerability.new(params) Gitlab::Vulnerabilities::ContainerScanningVulnerability.new(params)
when 'dast'
Gitlab::Vulnerabilities::DastVulnerability.new(params)
end end
return error('Invalid vulnerability category') unless vulnerability
issue_params = { issue_params = {
title: "Investigate vulnerability: #{vulnerability.title}", title: "Investigate vulnerability: #{vulnerability.title}",
description: render_description(vulnerability) description: render_description(vulnerability)
......
...@@ -19,11 +19,22 @@ ...@@ -19,11 +19,22 @@
### Identifiers: ### Identifiers:
<% vulnerability.identifiers.each do |identifier| %> <% vulnerability.identifiers.each do |identifier| %>
<% if identifier[:link].present? %> <% if identifier[:url].present? %>
* [<%= identifier[:value] %>](<%= identifier[:link] %>) * [<%= identifier[:name] %>](<%= identifier[:url] %>)
<% else %> <% else %>
* <%= identifier[:value] %> * <%= identifier[:name] %>
<% end %> <% end %>
<% end %> <% end %>
<% end %>
<% if vulnerability.links.present? %>
### Links:
<% vulnerability.links.each do |link| %>
<% if link[:name].present? %>
* [<%= link[:name] %>](<%= link[:url] %>)
<% else %>
* <%= link[:url] %>
<% end %>
<% end %>
<% end %> <% end %>
---
title: Enrich Security Reports with more data
merge_request: 5878
author:
type: changed
...@@ -15,23 +15,12 @@ module Gitlab ...@@ -15,23 +15,12 @@ module Gitlab
confidence confidence
solution solution
identifiers identifiers
links
].each do |method_name| ].each do |method_name|
define_method(method_name) do define_method(method_name) do
raise NotImplementedError raise NotImplementedError
end end
end end
protected
# cve_id must be 'CVE-YYYY-XXXX' (prefix + year + digits)
def cve_link(cve_id)
"https://cve.mitre.org/cgi-bin/cvename.cgi?name=#{cve_id}"
end
# cve_id must be a number only (no 'CWE-' prefix)
def cwe_link(cwe_id)
"https://cwe.mitre.org/data/definitions/#{cwe_id}.html"
end
end end
end end
end end
...@@ -2,24 +2,24 @@ module Gitlab ...@@ -2,24 +2,24 @@ module Gitlab
module Vulnerabilities module Vulnerabilities
class ContainerScanningVulnerability < BaseVulnerability class ContainerScanningVulnerability < BaseVulnerability
def title def title
"#{@data[:name]} in #{@data[:namespace]}" "#{@data[:vulnerability]} in #{@data[:namespace]}"
end end
# Passthrough properties # Passthrough properties
%i[ %i[
confidence
severity severity
identifiers
links
].each do |method_name| ].each do |method_name|
define_method(method_name) do define_method(method_name) do
@data[method_name] @data[method_name]
end end
end end
def confidence
end
def description def description
@data[:description].presence || @data[:description].presence ||
"**#{@data[:namespace]}** is affected by #{@data[:name]}" "**#{@data[:namespace]}** is affected by #{@data[:vulnerability]}"
end end
def solution def solution
...@@ -30,13 +30,6 @@ module Gitlab ...@@ -30,13 +30,6 @@ module Gitlab
"Upgrade **#{@data[:featurename]}** from `#{@data[:featureversion]}` to `#{@data[:fixedby]}`" "Upgrade **#{@data[:featurename]}** from `#{@data[:featureversion]}` to `#{@data[:fixedby]}`"
end end
end end
def identifiers
[{
value: @data[:name],
link: cve_link(@data[:name])
}]
end
end end
end end
end end
module Gitlab
module Vulnerabilities
class DastVulnerability < BaseVulnerability
def title
@data[:name]
end
# Passthrough properties
%i[
severity
confidence
solution
].each do |method_name|
define_method(method_name) do
@data[method_name]
end
end
def description
@data[:desc]
end
def identifiers
ids = []
if @data[:cweid].present?
ids << {
value: "CWE-#{@data[:cweid]}",
link: cwe_link(@data[:cweid])
}
end
if @data[:wascid].present?
ids << {
value: "WASC-#{@data[:wascid]}"
}
end
ids
end
end
end
end
module Gitlab module Gitlab
module Vulnerabilities module Vulnerabilities
class StandardVulnerability < BaseVulnerability class StandardVulnerability < BaseVulnerability
def title
@data[:name]
end
# Passthrough properties # Passthrough properties
%i[ %i[
title
severity severity
confidence confidence
solution solution
identifiers
links
].each do |method_name| ].each do |method_name|
define_method(method_name) do define_method(method_name) do
@data[method_name] @data[method_name]
...@@ -17,30 +16,7 @@ module Gitlab ...@@ -17,30 +16,7 @@ module Gitlab
end end
def description def description
@data[:description].presence || @data[:name] @data[:description].presence || @data[:title]
end
def identifiers
return [] unless @data[:identifiers].present?
ids = []
@data[:identifiers].each do |identifier|
# Only show known identifiers
case identifier[:name]
when 'CVE'
ids << {
value: identifier[:value],
link: cve_link(identifier[:value])
}
when 'CWE'
ids << {
value: "CWE-#{identifier[:value]}",
link: cwe_link(identifier[:value])
}
end
end
ids
end end
end end
end end
......
...@@ -86,18 +86,37 @@ describe Projects::VulnerabilityFeedbackController do ...@@ -86,18 +86,37 @@ describe Projects::VulnerabilityFeedbackController do
feedback_type: 'dismissal', pipeline_id: pipeline.id, category: 'sast', feedback_type: 'dismissal', pipeline_id: pipeline.id, category: 'sast',
project_fingerprint: '418291a26024a1445b23fe64de9380cdcdfd1fa8', project_fingerprint: '418291a26024a1445b23fe64de9380cdcdfd1fa8',
vulnerability_data: { vulnerability_data: {
priority: 'Low', line: '41', category: 'sast',
file: 'subdir/src/main/java/com/gitlab/security_products/tests/App.java', severity: 'Low',
confidence: 'Medium',
cve: '818bf5dacb291e15d9e6dc3c5ac32178:PREDICTABLE_RANDOM', cve: '818bf5dacb291e15d9e6dc3c5ac32178:PREDICTABLE_RANDOM',
name: 'Predictable pseudorandom number generator', title: 'Predictable pseudorandom number generator',
description: 'Description of Predictable pseudorandom number generator', description: 'Description of Predictable pseudorandom number generator',
tool: 'find_sec_bugs' tool: 'find_sec_bugs',
location: {
file: 'subdir/src/main/java/com/gitlab/security_products/tests/App.java',
start_line: '41'
},
identifiers: [{
type: 'CVE',
name: 'CVE-2018-1234',
value: 'CVE-2018-1234',
url: 'https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2018-1234'
}],
links: [{
name: 'Awesome-security blog post',
url: 'https;//example.com/blog-post'
}]
} }
} }
end end
context 'with valid params' do context 'with valid params' do
it 'returns the created list' do it 'returns the created feedback' do
allow(VulnerabilityFeedbackModule::CreateService)
.to receive(:new).with(project, user, create_params)
.and_call_original
create_feedback user: user, project: project, params: create_params create_feedback user: user, project: project, params: create_params
expect(response).to match_response_schema('vulnerability_feedback', dir: 'ee') expect(response).to match_response_schema('vulnerability_feedback', dir: 'ee')
...@@ -142,7 +161,7 @@ describe Projects::VulnerabilityFeedbackController do ...@@ -142,7 +161,7 @@ describe Projects::VulnerabilityFeedbackController do
def create_feedback(user:, project:, params:) def create_feedback(user:, project:, params:)
sign_in(user) sign_in(user)
post :create, namespace_id: project.namespace.to_param, project_id: project, vulnerability_feedback: params post :create, namespace_id: project.namespace.to_param, project_id: project, vulnerability_feedback: params, format: :json
end end
end end
......
...@@ -29,19 +29,34 @@ describe Issues::CreateFromVulnerabilityDataService, '#execute' do ...@@ -29,19 +29,34 @@ describe Issues::CreateFromVulnerabilityDataService, '#execute' do
let(:params) do let(:params) do
{ {
category: 'sast', category: 'sast',
priority: 'Low', line: '41',
severity: 'Low', confidence: 'High', severity: 'Low', confidence: 'High',
solution: 'Please do something!', solution: 'Please do something!',
file: 'subdir/src/main/java/com/gitlab/security_products/tests/App.java', file: 'subdir/src/main/java/com/gitlab/security_products/tests/App.java',
cve: '818bf5dacb291e15d9e6dc3c5ac32178:PREDICTABLE_RANDOM', cve: '818bf5dacb291e15d9e6dc3c5ac32178:PREDICTABLE_RANDOM',
name: 'Predictable pseudorandom number generator', title: 'Predictable pseudorandom number generator',
description: 'Description of Predictable pseudorandom number generator', description: 'Description of Predictable pseudorandom number generator',
tool: 'find_sec_bugs', tool: 'find_sec_bugs',
identifiers: [ identifiers: [{
{ name: 'CVE', value: 'CVE-2017-15650' }, type: 'CVE',
{ name: 'CWE', value: '16' }, name: 'CVE-2017-15650',
{ name: 'GAS_RULE_ID', value: 'G105' } value: 'CVE-2017-15650',
] url: 'https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2017-15650'
}, {
type: 'CWE',
name: 'CWE-16',
value: '16',
url: 'https://cwe.mitre.org/data/definitions/16.html'
}, {
type: 'GAS_RULE_ID',
name: 'GAS Rule ID G105',
value: 'G105'
}],
links: [{
name: 'Awesome-security blog post',
url: 'https;//example.com/blog-post'
}, {
url: 'https://example.com/another-link'
}]
} }
end end
...@@ -63,6 +78,12 @@ describe Issues::CreateFromVulnerabilityDataService, '#execute' do ...@@ -63,6 +78,12 @@ describe Issues::CreateFromVulnerabilityDataService, '#execute' do
* [CVE-2017-15650](https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2017-15650) * [CVE-2017-15650](https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2017-15650)
* [CWE-16](https://cwe.mitre.org/data/definitions/16.html) * [CWE-16](https://cwe.mitre.org/data/definitions/16.html)
* GAS Rule ID G105
### Links:
* [Awesome-security blog post](https;//example.com/blog-post)
* https://example.com/another-link
DESC DESC
end end
...@@ -73,16 +94,12 @@ describe Issues::CreateFromVulnerabilityDataService, '#execute' do ...@@ -73,16 +94,12 @@ describe Issues::CreateFromVulnerabilityDataService, '#execute' do
let(:params) do let(:params) do
{ {
category: 'sast', category: 'sast',
priority: 'Low', line: '41',
severity: 'Low', confidence: 'High', severity: 'Low', confidence: 'High',
solution: 'Please do something!', solution: 'Please do something!',
file: 'subdir/src/main/java/com/gitlab/security_products/tests/App.java', file: 'subdir/src/main/java/com/gitlab/security_products/tests/App.java',
cve: '818bf5dacb291e15d9e6dc3c5ac32178:PREDICTABLE_RANDOM', cve: '818bf5dacb291e15d9e6dc3c5ac32178:PREDICTABLE_RANDOM',
name: 'Predictable pseudorandom number generator', title: 'Predictable pseudorandom number generator',
tool: 'find_sec_bugs', tool: 'find_sec_bugs'
identifiers: [
{ name: 'CVE', value: 'CVE-2017-15650' }
]
} }
end end
let(:expected_title) { 'Investigate vulnerability: Predictable pseudorandom number generator' } let(:expected_title) { 'Investigate vulnerability: Predictable pseudorandom number generator' }
...@@ -98,10 +115,6 @@ describe Issues::CreateFromVulnerabilityDataService, '#execute' do ...@@ -98,10 +115,6 @@ describe Issues::CreateFromVulnerabilityDataService, '#execute' do
### Solution: ### Solution:
Please do something! Please do something!
### Identifiers:
* [CVE-2017-15650](https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2017-15650)
DESC DESC
end end
...@@ -114,19 +127,34 @@ describe Issues::CreateFromVulnerabilityDataService, '#execute' do ...@@ -114,19 +127,34 @@ describe Issues::CreateFromVulnerabilityDataService, '#execute' do
let(:params) do let(:params) do
{ {
category: 'dependency_scanning', category: 'dependency_scanning',
priority: 'Low', line: '41',
severity: 'Low', confidence: 'High', severity: 'Low', confidence: 'High',
solution: 'Please do something!', solution: 'Please do something!',
file: 'subdir/src/main/java/com/gitlab/security_products/tests/App.java', file: 'subdir/src/main/java/com/gitlab/security_products/tests/App.java',
cve: '818bf5dacb291e15d9e6dc3c5ac32178:PREDICTABLE_RANDOM', cve: '818bf5dacb291e15d9e6dc3c5ac32178:PREDICTABLE_RANDOM',
name: 'Predictable pseudorandom number generator', title: 'Predictable pseudorandom number generator',
description: 'Description of Predictable pseudorandom number generator', description: 'Description of Predictable pseudorandom number generator',
tool: 'find_sec_bugs', tool: 'find_sec_bugs',
identifiers: [ identifiers: [{
{ name: 'CVE', value: 'CVE-2017-15650' }, type: 'CVE',
{ name: 'CWE', value: '16' }, name: 'CVE-2017-15650',
{ name: 'GAS_RULE_ID', value: 'G105' } value: 'CVE-2017-15650',
] url: 'https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2017-15650'
}, {
type: 'CWE',
name: 'CWE-16',
value: '16',
url: 'https://cwe.mitre.org/data/definitions/16.html'
}, {
type: 'GAS_RULE_ID',
name: 'GAS Rule ID G105',
value: 'G105'
}],
links: [{
name: 'Awesome-security blog post',
url: 'https;//example.com/blog-post'
}, {
url: 'https://example.com/another-link'
}]
} }
end end
let(:expected_title) { 'Investigate vulnerability: Predictable pseudorandom number generator' } let(:expected_title) { 'Investigate vulnerability: Predictable pseudorandom number generator' }
...@@ -147,6 +175,12 @@ describe Issues::CreateFromVulnerabilityDataService, '#execute' do ...@@ -147,6 +175,12 @@ describe Issues::CreateFromVulnerabilityDataService, '#execute' do
* [CVE-2017-15650](https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2017-15650) * [CVE-2017-15650](https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2017-15650)
* [CWE-16](https://cwe.mitre.org/data/definitions/16.html) * [CWE-16](https://cwe.mitre.org/data/definitions/16.html)
* GAS Rule ID G105
### Links:
* [Awesome-security blog post](https;//example.com/blog-post)
* https://example.com/another-link
DESC DESC
end end
...@@ -162,11 +196,8 @@ describe Issues::CreateFromVulnerabilityDataService, '#execute' do ...@@ -162,11 +196,8 @@ describe Issues::CreateFromVulnerabilityDataService, '#execute' do
solution: 'Please do something!', solution: 'Please do something!',
file: 'subdir/src/main/java/com/gitlab/security_products/tests/App.java', file: 'subdir/src/main/java/com/gitlab/security_products/tests/App.java',
cve: '818bf5dacb291e15d9e6dc3c5ac32178:PREDICTABLE_RANDOM', cve: '818bf5dacb291e15d9e6dc3c5ac32178:PREDICTABLE_RANDOM',
name: 'Predictable pseudorandom number generator', title: 'Predictable pseudorandom number generator',
tool: 'find_sec_bugs', tool: 'find_sec_bugs'
identifiers: [
{ name: 'CVE', value: 'CVE-2017-15650' }
]
} }
end end
let(:expected_title) { 'Investigate vulnerability: Predictable pseudorandom number generator' } let(:expected_title) { 'Investigate vulnerability: Predictable pseudorandom number generator' }
...@@ -182,10 +213,6 @@ describe Issues::CreateFromVulnerabilityDataService, '#execute' do ...@@ -182,10 +213,6 @@ describe Issues::CreateFromVulnerabilityDataService, '#execute' do
### Solution: ### Solution:
Please do something! Please do something!
### Identifiers:
* [CVE-2017-15650](https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2017-15650)
DESC DESC
end end
...@@ -204,10 +231,16 @@ describe Issues::CreateFromVulnerabilityDataService, '#execute' do ...@@ -204,10 +231,16 @@ describe Issues::CreateFromVulnerabilityDataService, '#execute' do
featurename: 'musl', featurename: 'musl',
featureversion: '1.1.14-r15', featureversion: '1.1.14-r15',
fixedby: '1.1.14-r16', fixedby: '1.1.14-r16',
name: 'CVE-2017-15650', title: 'CVE-2017-15650',
vulnerability: 'CVE-2017-15650', vulnerability: 'CVE-2017-15650',
description: 'This is a description for CVE-2017-15650.', description: 'This is a description for CVE-2017-15650.',
tool: 'find_sec_bugs' tool: 'find_sec_bugs',
identifiers: [{
type: 'CVE',
name: 'CVE-2017-15650',
value: 'CVE-2017-15650',
url: 'https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2017-15650'
}]
} }
end end
let(:expected_title) { 'Investigate vulnerability: CVE-2017-15650 in alpine:v3.4' } let(:expected_title) { 'Investigate vulnerability: CVE-2017-15650 in alpine:v3.4' }
...@@ -241,10 +274,16 @@ describe Issues::CreateFromVulnerabilityDataService, '#execute' do ...@@ -241,10 +274,16 @@ describe Issues::CreateFromVulnerabilityDataService, '#execute' do
featurename: 'musl', featurename: 'musl',
featureversion: '1.1.14-r15', featureversion: '1.1.14-r15',
fixedby: '1.1.14-r16', fixedby: '1.1.14-r16',
name: 'CVE-2017-15650', title: 'CVE-2017-15650',
vulnerability: 'CVE-2017-15650', vulnerability: 'CVE-2017-15650',
description: '', description: '',
tool: 'find_sec_bugs' tool: 'find_sec_bugs',
identifiers: [{
type: 'CVE',
name: 'CVE-2017-15650',
value: 'CVE-2017-15650',
url: 'https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2017-15650'
}]
} }
end end
let(:expected_title) { 'Investigate vulnerability: CVE-2017-15650 in alpine:v3.4' } let(:expected_title) { 'Investigate vulnerability: CVE-2017-15650 in alpine:v3.4' }
...@@ -276,11 +315,22 @@ describe Issues::CreateFromVulnerabilityDataService, '#execute' do ...@@ -276,11 +315,22 @@ describe Issues::CreateFromVulnerabilityDataService, '#execute' do
category: 'dast', category: 'dast',
priority: 'Low', priority: 'Low',
severity: 'Low', severity: 'Low',
name: 'X-Content-Type-Options Header Missing', title: 'X-Content-Type-Options Header Missing',
desc: 'The Anti-MIME-Sniffing header X-Content-Type-Options was not set to nosniff.', desc: '<p>The Anti-MIME-Sniffing header X-Content-Type-Options was not set to nosniff.</p>',
description: 'The Anti-MIME-Sniffing header X-Content-Type-Options was not set to nosniff.',
cweid: '123', cweid: '123',
wascid: '456', wascid: '456',
solution: 'Ensure that the application/web server sets the Content-Type header appropriately, and that it sets the X-Content-Type-Options header to nosniff for all web pages.' solution: 'Ensure that the application/web server sets the Content-Type header appropriately, and that it sets the X-Content-Type-Options header to nosniff for all web pages.',
identifiers: [{
type: 'CWE',
name: 'CWE-123',
value: '123',
url: 'https://cwe.mitre.org/data/definitions/123.html'
}, {
type: 'WASC',
name: 'WASC-456',
value: '456'
}]
} }
end end
let(:expected_title) { 'Investigate vulnerability: X-Content-Type-Options Header Missing' } let(:expected_title) { 'Investigate vulnerability: X-Content-Type-Options Header Missing' }
...@@ -306,4 +356,16 @@ describe Issues::CreateFromVulnerabilityDataService, '#execute' do ...@@ -306,4 +356,16 @@ describe Issues::CreateFromVulnerabilityDataService, '#execute' do
it_behaves_like 'a created issue' it_behaves_like 'a created issue'
end end
end end
context 'when params are invalid' do
context 'when category is unknown' do
let(:params) { { category: 'foo' } }
let(:result) { described_class.new(project, user, params).execute }
it 'return expected error' do
expect(result[:status]).to eq(:error)
expect(result[:message]).to eq('Invalid vulnerability category')
end
end
end
end end
...@@ -5922,6 +5922,9 @@ msgstr "" ...@@ -5922,6 +5922,9 @@ msgstr ""
msgid "ciReport|%{linkStartTag}Learn more about SAST image %{linkEndTag}" msgid "ciReport|%{linkStartTag}Learn more about SAST image %{linkEndTag}"
msgstr "" msgstr ""
msgid "ciReport|%{namespace} is affected by %{vulnerability}."
msgstr ""
msgid "ciReport|%{reportName} is loading" msgid "ciReport|%{reportName} is loading"
msgstr "" msgstr ""
...@@ -5937,10 +5940,13 @@ msgstr "" ...@@ -5937,10 +5940,13 @@ msgstr ""
msgid "ciReport|%{type} detected no vulnerabilities" msgid "ciReport|%{type} detected no vulnerabilities"
msgstr "" msgstr ""
msgid "ciReport|Class"
msgstr ""
msgid "ciReport|Code quality" msgid "ciReport|Code quality"
msgstr "" msgstr ""
msgid "ciReport|Confidence Level" msgid "ciReport|Confidence"
msgstr "" msgstr ""
msgid "ciReport|Container scanning detects known vulnerabilities in your docker images." msgid "ciReport|Container scanning detects known vulnerabilities in your docker images."
...@@ -5991,7 +5997,7 @@ msgstr "" ...@@ -5991,7 +5997,7 @@ msgstr ""
msgid "ciReport|Fixed:" msgid "ciReport|Fixed:"
msgstr "" msgstr ""
msgid "ciReport|Identifier" msgid "ciReport|Identifiers"
msgstr "" msgstr ""
msgid "ciReport|Instances" msgid "ciReport|Instances"
...@@ -6000,9 +6006,15 @@ msgstr "" ...@@ -6000,9 +6006,15 @@ msgstr ""
msgid "ciReport|Learn more about whitelisting" msgid "ciReport|Learn more about whitelisting"
msgstr "" msgstr ""
msgid "ciReport|Links"
msgstr ""
msgid "ciReport|Loading %{reportName} report" msgid "ciReport|Loading %{reportName} report"
msgstr "" msgstr ""
msgid "ciReport|Method"
msgstr ""
msgid "ciReport|Namespace" msgid "ciReport|Namespace"
msgstr "" msgstr ""
...@@ -6045,9 +6057,6 @@ msgstr "" ...@@ -6045,9 +6057,6 @@ msgstr ""
msgid "ciReport|Solution" msgid "ciReport|Solution"
msgstr "" msgstr ""
msgid "ciReport|Source"
msgstr ""
msgid "ciReport|Static Application Security Testing (SAST) detects known vulnerabilities in your source code." msgid "ciReport|Static Application Security Testing (SAST) detects known vulnerabilities in your source code."
msgstr "" msgstr ""
...@@ -6069,6 +6078,9 @@ msgstr "" ...@@ -6069,6 +6078,9 @@ msgstr ""
msgid "ciReport|Unapproved vulnerabilities (red) can be marked as approved." msgid "ciReport|Unapproved vulnerabilities (red) can be marked as approved."
msgstr "" msgstr ""
msgid "ciReport|Upgrade %{name} from %{version} to %{fixed}."
msgstr ""
msgid "ciReport|no vulnerabilities" msgid "ciReport|no vulnerabilities"
msgstr "" msgstr ""
......
...@@ -8,15 +8,13 @@ describe('dast issue body', () => { ...@@ -8,15 +8,13 @@ describe('dast issue body', () => {
const Component = Vue.extend(component); const Component = Vue.extend(component);
const dastIssue = { const dastIssue = {
alert: 'X-Content-Type-Options Header Missing', alert: 'X-Content-Type-Options Header Missing',
confidence: '2', severity: 'Low',
confidence: 'Medium',
count: '17', count: '17',
cweid: '16', cweid: '16',
desc: desc:
'<p>The Anti-MIME-Sniffing header X-Content-Type-Options was not set to "nosniff". </p>', '<p>The Anti-MIME-Sniffing header X-Content-Type-Options was not set to "nosniff". </p>',
name: 'X-Content-Type-Options Header Missing', title: 'X-Content-Type-Options Header Missing',
parsedDescription:
' The Anti-MIME-Sniffing header X-Content-Type-Options was not set to "nosniff". ',
priority: 'Low (Medium)',
reference: reference:
'<p>http://msdn.microsoft.com/en-us/library/ie/gg622941%28v=vs.85%29.aspx</p><p>https://www.owasp.org/index.php/List_of_useful_HTTP_headers</p>', '<p>http://msdn.microsoft.com/en-us/library/ie/gg622941%28v=vs.85%29.aspx</p><p>https://www.owasp.org/index.php/List_of_useful_HTTP_headers</p>',
riskcode: '1', riskcode: '1',
...@@ -27,34 +25,19 @@ describe('dast issue body', () => { ...@@ -27,34 +25,19 @@ describe('dast issue body', () => {
vm.$destroy(); vm.$destroy();
}); });
describe('with priority', () => { describe('severity and confidence ', () => {
it('renders priority key', () => { it('renders severity and confidence', () => {
vm = mountComponent(Component, { vm = mountComponent(Component, {
issue: dastIssue, issue: dastIssue,
issueIndex: 1, issueIndex: 1,
modalTargetId: '#modal-mrwidget-issue', modalTargetId: '#modal-mrwidget-issue',
}); });
expect(vm.$el.textContent.trim()).toContain(dastIssue.priority); expect(vm.$el.textContent.trim()).toContain(`${dastIssue.severity} (${dastIssue.confidence})`);
}); });
}); });
describe('without priority', () => { describe('issue title', () => {
it('does not rendere priority key', () => {
const issueCopy = Object.assign({}, dastIssue);
delete issueCopy.priority;
vm = mountComponent(Component, {
issue: issueCopy,
issueIndex: 1,
modalTargetId: '#modal-mrwidget-issue',
});
expect(vm.$el.textContent.trim()).not.toContain(dastIssue.priority);
});
});
describe('issue name', () => {
beforeEach(() => { beforeEach(() => {
vm = mountComponent(Component, { vm = mountComponent(Component, {
issue: dastIssue, issue: dastIssue,
...@@ -63,8 +46,8 @@ describe('dast issue body', () => { ...@@ -63,8 +46,8 @@ describe('dast issue body', () => {
}); });
}); });
it('renders button with issue name', () => { it('renders button with issue title', () => {
expect(vm.$el.textContent.trim()).toContain(dastIssue.name); expect(vm.$el.textContent.trim()).toContain(dastIssue.title);
}); });
}); });
}); });
...@@ -27,7 +27,7 @@ describe('Security Reports modal', () => { ...@@ -27,7 +27,7 @@ describe('Security Reports modal', () => {
cve: 'CVE-2014-9999', cve: 'CVE-2014-9999',
file: 'Gemfile.lock', file: 'Gemfile.lock',
solution: 'upgrade to ~> 3.2.21, ~> 4.0.11.1, ~> 4.0.12, ~> 4.1.7.1, >= 4.1.8', solution: 'upgrade to ~> 3.2.21, ~> 4.0.11.1, ~> 4.0.12, ~> 4.1.7.1, >= 4.1.8',
name: 'Arbitrary file existence disclosure in Action Pack', title: 'Arbitrary file existence disclosure in Action Pack',
path: 'Gemfile.lock', path: 'Gemfile.lock',
urlPath: 'path/Gemfile.lock', urlPath: 'path/Gemfile.lock',
isDismissed: true, isDismissed: true,
...@@ -83,7 +83,7 @@ describe('Security Reports modal', () => { ...@@ -83,7 +83,7 @@ describe('Security Reports modal', () => {
cve: 'CVE-2014-9999', cve: 'CVE-2014-9999',
file: 'Gemfile.lock', file: 'Gemfile.lock',
solution: 'upgrade to ~> 3.2.21, ~> 4.0.11.1, ~> 4.0.12, ~> 4.1.7.1, >= 4.1.8', solution: 'upgrade to ~> 3.2.21, ~> 4.0.11.1, ~> 4.0.12, ~> 4.1.7.1, >= 4.1.8',
name: 'Arbitrary file existence disclosure in Action Pack', title: 'Arbitrary file existence disclosure in Action Pack',
path: 'Gemfile.lock', path: 'Gemfile.lock',
urlPath: 'path/Gemfile.lock', urlPath: 'path/Gemfile.lock',
}); });
...@@ -112,12 +112,10 @@ describe('Security Reports modal', () => { ...@@ -112,12 +112,10 @@ describe('Security Reports modal', () => {
describe('with instances', () => { describe('with instances', () => {
beforeEach(() => { beforeEach(() => {
store.dispatch('setModalData', { store.dispatch('setModalData', {
name: 'Absence of Anti-CSRF Tokens', title: 'Absence of Anti-CSRF Tokens',
riskcode: '1', riskcode: '1',
riskdesc: 'Low (Medium)', riskdesc: 'Low (Medium)',
priority: 'Low (Medium)',
desc: '<p>No Anti-CSRF tokens were found in a HTML submission form.</p>', desc: '<p>No Anti-CSRF tokens were found in a HTML submission form.</p>',
parsedDescription: ' No Anti-CSRF tokens were found in a HTML submission form. ',
pluginid: '123', pluginid: '123',
instances: [ instances: [
{ {
...@@ -159,13 +157,17 @@ describe('Security Reports modal', () => { ...@@ -159,13 +157,17 @@ describe('Security Reports modal', () => {
store.dispatch('setModalData', { store.dispatch('setModalData', {
tool: 'bundler_audit', tool: 'bundler_audit',
message: 'Arbitrary file existence disclosure in Action Pack', message: 'Arbitrary file existence disclosure in Action Pack',
url: 'https://groups.google.com/forum/#!topic/rubyonrails-security/rMTQy4oRCGk',
cve: 'CVE-2014-9999', cve: 'CVE-2014-9999',
file: 'Gemfile.lock',
solution: 'upgrade to ~> 3.2.21, ~> 4.0.11.1, ~> 4.0.12, ~> 4.1.7.1, >= 4.1.8', solution: 'upgrade to ~> 3.2.21, ~> 4.0.11.1, ~> 4.0.12, ~> 4.1.7.1, >= 4.1.8',
name: 'Arbitrary file existence disclosure in Action Pack', title: 'Arbitrary file existence disclosure in Action Pack',
path: 'Gemfile.lock', path: 'Gemfile.lock',
urlPath: 'path/Gemfile.lock', urlPath: 'path/Gemfile.lock',
location: {
file: 'Gemfile.lock',
},
links: [{
url: 'https://groups.google.com/forum/#!topic/rubyonrails-security/rMTQy4oRCGk',
}],
}); });
vm = mountComponentWithStore(Component, { vm = mountComponentWithStore(Component, {
......
...@@ -95,7 +95,7 @@ describe('Report issues', () => { ...@@ -95,7 +95,7 @@ describe('Report issues', () => {
it('should not render location', () => { it('should not render location', () => {
vm = mountComponent(ReportIssues, { vm = mountComponent(ReportIssues, {
issues: [{ issues: [{
name: 'foo', title: 'foo',
}], }],
type: 'SAST', type: 'SAST',
status: 'failed', status: 'failed',
...@@ -106,7 +106,7 @@ describe('Report issues', () => { ...@@ -106,7 +106,7 @@ describe('Report issues', () => {
}); });
}); });
describe('for docker issues', () => { describe('for container scanning issues', () => {
beforeEach(() => { beforeEach(() => {
vm = mountComponent(ReportIssues, { vm = mountComponent(ReportIssues, {
issues: dockerReportParsed.unapproved, issues: dockerReportParsed.unapproved,
...@@ -115,16 +115,16 @@ describe('Report issues', () => { ...@@ -115,16 +115,16 @@ describe('Report issues', () => {
}); });
}); });
it('renders priority', () => { it('renders severity', () => {
expect( expect(
vm.$el.querySelector('.report-block-list li').textContent.trim(), vm.$el.querySelector('.report-block-list li').textContent.trim(),
).toContain(dockerReportParsed.unapproved[0].priority); ).toContain(dockerReportParsed.unapproved[0].severity);
}); });
it('renders CVE name', () => { it('renders CVE name', () => {
expect( expect(
vm.$el.querySelector('.report-block-list button').textContent.trim(), vm.$el.querySelector('.report-block-list button').textContent.trim(),
).toEqual(dockerReportParsed.unapproved[0].name); ).toEqual(dockerReportParsed.unapproved[0].title);
}); });
it('renders namespace', () => { it('renders namespace', () => {
...@@ -148,9 +148,9 @@ describe('Report issues', () => { ...@@ -148,9 +148,9 @@ describe('Report issues', () => {
}); });
}); });
it('renders priority and name', () => { it('renders severity (confidence) and title', () => {
expect(vm.$el.textContent).toContain(parsedDast[0].name); expect(vm.$el.textContent).toContain(parsedDast[0].title);
expect(vm.$el.textContent).toContain(parsedDast[0].priority); expect(vm.$el.textContent).toContain(`${parsedDast[0].severity} (${parsedDast[0].confidence})`);
}); });
}); });
}); });
...@@ -116,7 +116,7 @@ describe('Report section', () => { ...@@ -116,7 +116,7 @@ describe('Report section', () => {
cve: 'CVE-2016-9999', cve: 'CVE-2016-9999',
file: 'Gemfile.lock', file: 'Gemfile.lock',
message: 'Test Information Leak Vulnerability in Action View', message: 'Test Information Leak Vulnerability in Action View',
name: 'Test Information Leak Vulnerability in Action View', title: 'Test Information Leak Vulnerability in Action View',
path: 'Gemfile.lock', path: 'Gemfile.lock',
solution: 'upgrade to >= 5.0.0.beta1.1, >= 4.2.5.1, ~> 4.2.5, >= 4.1.14.1, ~> 4.1.14, ~> 3.2.22.1', solution: 'upgrade to >= 5.0.0.beta1.1, >= 4.2.5.1, ~> 4.2.5, >= 4.1.14.1, ~> 4.1.14, ~> 3.2.22.1',
tool: 'bundler_audit', tool: 'bundler_audit',
...@@ -127,7 +127,7 @@ describe('Report section', () => { ...@@ -127,7 +127,7 @@ describe('Report section', () => {
cve: 'CVE-2014-7829', cve: 'CVE-2014-7829',
file: 'Gemfile.lock', file: 'Gemfile.lock',
message: 'Arbitrary file existence disclosure in Action Pack', message: 'Arbitrary file existence disclosure in Action Pack',
name: 'Arbitrary file existence disclosure in Action Pack', title: 'Arbitrary file existence disclosure in Action Pack',
path: 'Gemfile.lock', path: 'Gemfile.lock',
solution: 'upgrade to ~> 3.2.21, ~> 4.0.11.1, ~> 4.0.12, ~> 4.1.7.1, >= 4.1.8', solution: 'upgrade to ~> 3.2.21, ~> 4.0.11.1, ~> 4.0.12, ~> 4.1.7.1, >= 4.1.8',
tool: 'bundler_audit', tool: 'bundler_audit',
...@@ -138,7 +138,7 @@ describe('Report section', () => { ...@@ -138,7 +138,7 @@ describe('Report section', () => {
cve: 'CVE-2016-0752', cve: 'CVE-2016-0752',
file: 'Gemfile.lock', file: 'Gemfile.lock',
message: 'Possible Information Leak Vulnerability in Action View', message: 'Possible Information Leak Vulnerability in Action View',
name: 'Possible Information Leak Vulnerability in Action View', title: 'Possible Information Leak Vulnerability in Action View',
path: 'Gemfile.lock', path: 'Gemfile.lock',
solution: 'upgrade to >= 5.0.0.beta1.1, >= 4.2.5.1, ~> 4.2.5, >= 4.1.14.1, ~> 4.1.14, ~> 3.2.22.1', solution: 'upgrade to >= 5.0.0.beta1.1, >= 4.2.5.1, ~> 4.2.5, >= 4.1.14.1, ~> 4.1.14, ~> 3.2.22.1',
tool: 'bundler_audit', tool: 'bundler_audit',
......
...@@ -8,11 +8,9 @@ describe('sast container issue body', () => { ...@@ -8,11 +8,9 @@ describe('sast container issue body', () => {
const Component = Vue.extend(component); const Component = Vue.extend(component);
const sastContainerIssue = { const sastContainerIssue = {
name: 'CVE-2017-11671', title: 'CVE-2017-11671',
nameLink: 'https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2017-11671',
namespace: 'debian:8', namespace: 'debian:8',
path: 'debian:8', path: 'debian:8',
priority: 'Low',
severity: 'Low', severity: 'Low',
vulnerability: 'CVE-2017-11671', vulnerability: 'CVE-2017-11671',
}; };
...@@ -21,26 +19,26 @@ describe('sast container issue body', () => { ...@@ -21,26 +19,26 @@ describe('sast container issue body', () => {
vm.$destroy(); vm.$destroy();
}); });
describe('with priority', () => { describe('with severity', () => {
it('renders priority key', () => { it('renders severity key', () => {
vm = mountComponent(Component, { vm = mountComponent(Component, {
issue: sastContainerIssue, issue: sastContainerIssue,
}); });
expect(vm.$el.textContent.trim()).toContain(sastContainerIssue.priority); expect(vm.$el.textContent.trim()).toContain(sastContainerIssue.severity);
}); });
}); });
describe('without priority', () => { describe('without severity', () => {
it('does not rendere priority key', () => { it('does not render severity key', () => {
const issueCopy = Object.assign({}, sastContainerIssue); const issueCopy = Object.assign({}, sastContainerIssue);
delete issueCopy.priority; delete issueCopy.severity;
vm = mountComponent(Component, { vm = mountComponent(Component, {
issue: issueCopy, issue: issueCopy,
}); });
expect(vm.$el.textContent.trim()).not.toContain(sastContainerIssue.priority); expect(vm.$el.textContent.trim()).not.toContain(sastContainerIssue.severity);
}); });
}); });
...@@ -49,7 +47,7 @@ describe('sast container issue body', () => { ...@@ -49,7 +47,7 @@ describe('sast container issue body', () => {
issue: sastContainerIssue, issue: sastContainerIssue,
}); });
expect(vm.$el.querySelector('button').textContent.trim()).toEqual(sastContainerIssue.name); expect(vm.$el.querySelector('button').textContent.trim()).toEqual(sastContainerIssue.title);
}); });
describe('path', () => { describe('path', () => {
......
...@@ -11,7 +11,7 @@ describe('sast issue body', () => { ...@@ -11,7 +11,7 @@ describe('sast issue body', () => {
cve: 'CVE-2016-9999', cve: 'CVE-2016-9999',
file: 'Gemfile.lock', file: 'Gemfile.lock',
message: 'Test Information Leak Vulnerability in Action View', message: 'Test Information Leak Vulnerability in Action View',
name: 'Test Information Leak Vulnerability in Action View', title: 'Test Information Leak Vulnerability in Action View',
path: 'Gemfile.lock', path: 'Gemfile.lock',
solution: solution:
'upgrade to >= 5.0.0.beta1.1, >= 4.2.5.1, ~> 4.2.5, >= 4.1.14.1, ~> 4.1.14, ~> 3.2.22.1', 'upgrade to >= 5.0.0.beta1.1, >= 4.2.5.1, ~> 4.2.5, >= 4.1.14.1, ~> 4.1.14, ~> 3.2.22.1',
...@@ -19,27 +19,57 @@ describe('sast issue body', () => { ...@@ -19,27 +19,57 @@ describe('sast issue body', () => {
url: url:
'https://groups.google.com/forum/#!topic/rubyonrails-security/335P1DcLG00', 'https://groups.google.com/forum/#!topic/rubyonrails-security/335P1DcLG00',
urlPath: '/Gemfile.lock', urlPath: '/Gemfile.lock',
priority: 'Low', severity: 'Medium',
confidence: 'Low',
}; };
afterEach(() => { afterEach(() => {
vm.$destroy(); vm.$destroy();
}); });
describe('with priority', () => { describe('with severity and confidence (new json format)', () => {
it('renders priority key', () => { it('renders severity and confidence', () => {
vm = mountComponent(Component, { vm = mountComponent(Component, {
issue: sastIssue, issue: sastIssue,
}); });
expect(vm.$el.textContent.trim()).toContain(sastIssue.priority); expect(vm.$el.textContent.trim()).toContain(`${sastIssue.severity} (${sastIssue.confidence})`);
});
});
describe('without severity', () => {
it('does not render severity nor confidence', () => {
const issueCopy = Object.assign({}, sastIssue);
delete issueCopy.severity;
vm = mountComponent(Component, {
issue: issueCopy,
});
expect(vm.$el.textContent.trim()).not.toContain(sastIssue.severity);
expect(vm.$el.textContent.trim()).not.toContain(sastIssue.confidence);
});
});
describe('with priority (old json format)', () => {
it('renders priority key', () => {
const issueCopy = Object.assign({}, sastIssue);
delete issueCopy.severity;
delete issueCopy.confidence;
issueCopy.priority = 'Low';
vm = mountComponent(Component, {
issue: issueCopy,
});
expect(vm.$el.textContent.trim()).toContain(issueCopy.priority);
}); });
}); });
describe('without priority', () => { describe('without priority', () => {
it('does not rendere priority key', () => { it('does not render priority key', () => {
const issueCopy = Object.assign({}, sastIssue); const issueCopy = Object.assign({}, sastIssue);
delete issueCopy.priority; delete issueCopy.severity;
delete issueCopy.confidence;
vm = mountComponent(Component, { vm = mountComponent(Component, {
issue: issueCopy, issue: issueCopy,
...@@ -51,20 +81,20 @@ describe('sast issue body', () => { ...@@ -51,20 +81,20 @@ describe('sast issue body', () => {
}); });
}); });
describe('name', () => { describe('title', () => {
it('renders name', () => { it('renders title', () => {
vm = mountComponent(Component, { vm = mountComponent(Component, {
issue: sastIssue, issue: sastIssue,
}); });
expect(vm.$el.textContent.trim()).toContain( expect(vm.$el.textContent.trim()).toContain(
sastIssue.name, sastIssue.title,
); );
}); });
}); });
describe('path', () => { describe('path', () => {
it('renders name', () => { it('renders path', () => {
vm = mountComponent(Component, { vm = mountComponent(Component, {
issue: sastIssue, issue: sastIssue,
}); });
......
...@@ -316,21 +316,50 @@ describe('security reports mutations', () => { ...@@ -316,21 +316,50 @@ describe('security reports mutations', () => {
const issue = { const issue = {
tool: 'bundler_audit', tool: 'bundler_audit',
message: 'Arbitrary file existence disclosure in Action Pack', message: 'Arbitrary file existence disclosure in Action Pack',
url: 'https://groups.google.com/forum/#!topic/rubyonrails-security/rMTQy4oRCGk',
cve: 'CVE-2014-7829', cve: 'CVE-2014-7829',
file: 'Gemfile.lock',
solution: 'upgrade to ~> 3.2.21, ~> 4.0.11.1, ~> 4.0.12, ~> 4.1.7.1, >= 4.1.8', solution: 'upgrade to ~> 3.2.21, ~> 4.0.11.1, ~> 4.0.12, ~> 4.1.7.1, >= 4.1.8',
name: 'Arbitrary file existence disclosure in Action Pack', title: 'Arbitrary file existence disclosure in Action Pack',
path: 'Gemfile.lock', path: 'Gemfile.lock',
urlPath: 'path/Gemfile.lock', urlPath: 'path/Gemfile.lock',
namespace: 'debian:8',
location: {
file: 'Gemfile.lock',
class: 'User',
method: 'do_something',
},
links: [{
url: 'https://groups.google.com/forum/#!topic/rubyonrails-security/rMTQy4oRCGk',
}],
identifiers: [{
type: 'CVE',
name: 'CVE-2014-9999',
value: 'CVE-2014-9999',
url: 'https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2014-9999',
}],
instances: [{
param: 'X-Content-Type-Options',
method: 'GET',
uri: 'http://example.com/some-path',
}],
isDismissed: true, isDismissed: true,
}; };
mutations[types.SET_ISSUE_MODAL_DATA](stateCopy, issue); mutations[types.SET_ISSUE_MODAL_DATA](stateCopy, issue);
expect(stateCopy.modal.title).toEqual(issue.name); expect(stateCopy.modal.title).toEqual(issue.title);
expect(stateCopy.modal.data.file.value).toEqual(issue.file); expect(stateCopy.modal.data.description.value).toEqual(issue.description);
expect(stateCopy.modal.data.file.value).toEqual(issue.location.file);
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.identifiers.value).toEqual(issue.identifiers);
expect(stateCopy.modal.data.severity.value).toEqual(issue.severity);
expect(stateCopy.modal.data.confidence.value).toEqual(issue.confidence);
expect(stateCopy.modal.data.solution.value).toEqual(issue.solution); expect(stateCopy.modal.data.solution.value).toEqual(issue.solution);
expect(stateCopy.modal.data.links.value).toEqual(issue.links);
expect(stateCopy.modal.data.instances.value).toEqual(issue.instances);
expect(stateCopy.modal.vulnerability).toEqual(issue); expect(stateCopy.modal.vulnerability).toEqual(issue);
}); });
}); });
......
...@@ -11,6 +11,7 @@ import { ...@@ -11,6 +11,7 @@ import {
statusIcon, statusIcon,
} from 'ee/vue_shared/security_reports/store/utils'; } from 'ee/vue_shared/security_reports/store/utils';
import { import {
oldSastIssues,
sastIssues, sastIssues,
sastFeedbacks, sastFeedbacks,
dependencyScanningIssues, dependencyScanningIssues,
...@@ -52,10 +53,17 @@ describe('security reports utils', () => { ...@@ -52,10 +53,17 @@ describe('security reports utils', () => {
}); });
describe('parseSastIssues', () => { describe('parseSastIssues', () => {
it('should parse the received issues', () => { it('should parse the received issues with old JSON format', () => {
const parsed = parseSastIssues(oldSastIssues, [], 'path')[0];
expect(parsed.title).toEqual(sastIssues[0].message);
expect(parsed.path).toEqual(sastIssues[0].location.file);
expect(parsed.project_fingerprint).toEqual(sha1(sastIssues[0].cve));
});
it('should parse the received issues with new JSON format', () => {
const parsed = parseSastIssues(sastIssues, [], 'path')[0]; const parsed = parseSastIssues(sastIssues, [], 'path')[0];
expect(parsed.name).toEqual(sastIssues[0].message); expect(parsed.title).toEqual(sastIssues[0].message);
expect(parsed.path).toEqual(sastIssues[0].file); expect(parsed.path).toEqual(sastIssues[0].location.file);
expect(parsed.project_fingerprint).toEqual(sha1(sastIssues[0].cve)); expect(parsed.project_fingerprint).toEqual(sha1(sastIssues[0].cve));
}); });
...@@ -75,7 +83,7 @@ describe('security reports utils', () => { ...@@ -75,7 +83,7 @@ describe('security reports utils', () => {
describe('parseDependencyScanningIssues', () => { describe('parseDependencyScanningIssues', () => {
it('should parse the received issues', () => { it('should parse the received issues', () => {
const parsed = parseDependencyScanningIssues(dependencyScanningIssues, [], 'path')[0]; const parsed = parseDependencyScanningIssues(dependencyScanningIssues, [], 'path')[0];
expect(parsed.name).toEqual(dependencyScanningIssues[0].message); expect(parsed.title).toEqual(dependencyScanningIssues[0].message);
expect(parsed.path).toEqual(dependencyScanningIssues[0].file); expect(parsed.path).toEqual(dependencyScanningIssues[0].file);
expect(parsed.project_fingerprint).toEqual(sha1(dependencyScanningIssues[0].cve)); expect(parsed.project_fingerprint).toEqual(sha1(dependencyScanningIssues[0].cve));
}); });
...@@ -107,14 +115,14 @@ describe('security reports utils', () => { ...@@ -107,14 +115,14 @@ describe('security reports utils', () => {
const parsed = parseSastContainer(dockerReport.vulnerabilities)[0]; const parsed = parseSastContainer(dockerReport.vulnerabilities)[0];
const issue = dockerReport.vulnerabilities[0]; const issue = dockerReport.vulnerabilities[0];
expect(parsed.name).toEqual(dockerReport.vulnerabilities[0].vulnerability); expect(parsed.title).toEqual(issue.vulnerability);
expect(parsed.priority).toEqual(dockerReport.vulnerabilities[0].severity); expect(parsed.path).toEqual(issue.namespace);
expect(parsed.path).toEqual(dockerReport.vulnerabilities[0].namespace); expect(parsed.identifiers).toEqual([{
expect(parsed.nameLink).toEqual( type: 'CVE',
`https://cve.mitre.org/cgi-bin/cvename.cgi?name=${ name: issue.vulnerability,
dockerReport.vulnerabilities[0].vulnerability value: issue.vulnerability,
}`, url: `https://cve.mitre.org/cgi-bin/cvename.cgi?name=${issue.vulnerability}`,
); }]);
expect(parsed.project_fingerprint).toEqual( expect(parsed.project_fingerprint).toEqual(
sha1(`${issue.namespace}:${issue.vulnerability}:${issue.featurename}:${issue.featureversion}`)); sha1(`${issue.namespace}:${issue.vulnerability}:${issue.featurename}:${issue.featureversion}`));
}); });
......
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