Commit c25232f9 authored by Dmitriy Zaporozhets's avatar Dmitriy Zaporozhets

Merge branch '3995-improve-sast' into 'master'

Resolve "Improved visualization of SAST results in MR widget"

Closes #3995 and #4358

See merge request gitlab-org/gitlab-ee!4017
parents 85802afc 1b40dd88
......@@ -801,7 +801,7 @@
.mr-widget-code-quality-list {
list-style: none;
padding: 0 2px 0 0;
padding: 0 1px;
margin: 0;
line-height: $code_line_height;
......
---
title: Adds support to show added, fixed and all vulnerabilties for SAST in merge
request widget
merge_request:
author:
type: changed
......@@ -5,7 +5,7 @@
import issuesBlock from './mr_widget_report_issues.vue';
export default {
name: 'MRWidgetCodeQuality',
name: 'MRWidgetCodeQualityCollapsible',
components: {
issuesBlock,
loadingIcon,
......@@ -49,10 +49,15 @@
required: false,
default: () => [],
},
allIssues: {
type: Array,
required: false,
default: () => [],
},
infoText: {
type: String,
type: [String, Boolean],
required: false,
default: null,
default: false,
},
hasPriority: {
type: Boolean,
......@@ -65,6 +70,7 @@
return {
collapseText: __('Expand'),
isCollapsed: true,
isFullReportVisible: false,
};
},
......@@ -85,7 +91,9 @@
return 'success';
},
hasIssues() {
return this.unresolvedIssues.length || this.resolvedIssues.length;
return this.unresolvedIssues.length ||
this.resolvedIssues.length ||
this.allIssues.length;
},
},
......@@ -96,6 +104,9 @@
const text = this.isCollapsed ? __('Expand') : __('Collapse');
this.collapseText = text;
},
openFullReport() {
this.isFullReportVisible = true;
},
},
};
</script>
......@@ -168,6 +179,15 @@
:has-priority="hasPriority"
/>
<issues-block
class="js-mr-code-all-issues"
v-if="isFullReportVisible"
:type="type"
status="failed"
:issues="allIssues"
:has-priority="hasPriority"
/>
<issues-block
class="js-mr-code-non-issues"
v-if="neutralIssues.length"
......@@ -185,6 +205,15 @@
:issues="resolvedIssues"
:has-priority="hasPriority"
/>
<button
v-if="allIssues.length && !isFullReportVisible"
type="button"
class="btn-link btn-blank prepend-left-10 js-expand-full-list"
@click="openFullReport"
>
{{ s__("ciReport|Show complete code vulnerabilities report") }}
</button>
</div>
<div
v-else-if="loadingFailed"
......
......@@ -171,6 +171,7 @@
:href="issue.urlPath"
target="_blank"
rel="noopener noreferrer nofollow"
class="prepend-left-5"
>
{{ issue.path }}<template v-if="issue.line">:{{ issue.line }}</template>
</a>
......
import { n__, s__, sprintf } from '~/locale';
import { n__, s__, __, sprintf } from '~/locale';
import CEWidgetOptions from '~/vue_merge_request_widget/mr_widget_options';
import WidgetApprovals from './components/approvals/mr_widget_approvals';
import GeoSecondaryNode from './components/states/mr_widget_secondary_geo_node';
......@@ -38,7 +38,7 @@ export default {
return performance && performance.head_path && performance.base_path;
},
shouldRenderSecurityReport() {
return this.mr.sast;
return this.mr.sast && this.mr.sast.head_path;
},
shouldRenderDockerReport() {
return this.mr.sastContainer;
......@@ -51,9 +51,9 @@ export default {
const text = [];
if (!newIssues.length && !resolvedIssues.length) {
text.push('No changes to code quality');
text.push(s__('ciReport|No changes to code quality'));
} else if (newIssues.length || resolvedIssues.length) {
text.push('Code quality');
text.push(s__('ciReport|Code quality'));
if (resolvedIssues.length) {
text.push(n__(
......@@ -64,7 +64,7 @@ export default {
}
if (newIssues.length > 0 && resolvedIssues.length > 0) {
text.push(' and');
text.push(__(' and'));
}
if (newIssues.length) {
......@@ -84,9 +84,9 @@ export default {
const text = [];
if (!improved.length && !degraded.length) {
text.push('No changes to performance metrics');
text.push(s__('ciReport|No changes to performance metrics'));
} else if (improved.length || degraded.length) {
text.push('Performance metrics');
text.push(s__('ciReport|Performance metrics'));
if (improved.length) {
text.push(n__(
......@@ -97,7 +97,7 @@ export default {
}
if (improved.length > 0 && degraded.length > 0) {
text.push(' and');
text.push(__(' and'));
}
if (degraded.length) {
......@@ -113,15 +113,36 @@ export default {
},
securityText() {
if (this.mr.securityReport.length) {
return n__(
'SAST detected %d security vulnerability',
'SAST detected %d security vulnerabilities',
this.mr.securityReport.length,
);
const { newIssues, resolvedIssues } = this.mr.securityReport;
const text = [];
if (!newIssues.length && !resolvedIssues.length) {
text.push(s__('ciReport|SAST detected no security vulnerabilities'));
} else if (newIssues.length || resolvedIssues.length) {
text.push(s__('ciReport|SAST'));
}
return 'SAST detected no security vulnerabilities';
if (resolvedIssues.length) {
text.push(n__(
' improved on %d security vulnerability',
' improved on %d security vulnerabilities',
resolvedIssues.length,
));
}
if (newIssues.length > 0 && resolvedIssues.length > 0) {
text.push(__(' and'));
}
if (newIssues.length) {
text.push(n__(
' degraded on %d security vulnerability',
' degraded on %d security vulnerabilities',
newIssues.length,
));
}
return text.join('');
},
dockerText() {
......@@ -211,7 +232,7 @@ export default {
},
fetchCodeQuality() {
const { head_path, head_blob_path, base_path, base_blob_path } = this.mr.codeclimate;
const { head_path, base_path } = this.mr.codeclimate;
this.isLoadingCodequality = true;
......@@ -220,7 +241,12 @@ export default {
this.service.fetchReport(base_path),
])
.then((values) => {
this.mr.compareCodeclimateMetrics(values[0], values[1], head_blob_path, base_blob_path);
this.mr.compareCodeclimateMetrics(
values[0],
values[1],
this.mr.headBlobPath,
this.mr.baseBlobPath,
);
this.isLoadingCodequality = false;
})
.catch(() => {
......@@ -247,20 +273,50 @@ export default {
this.loadingPerformanceFailed = true;
});
},
/**
* Sast report can either have 2 reports or just 1
* When it has 2 we need to compare them
* When it has 1 we render the output given
*/
fetchSecurity() {
const { path, blob_path } = this.mr.sast;
const { sast } = this.mr;
this.isLoadingSecurity = true;
this.service.fetchReport(path)
.then((data) => {
this.mr.setSecurityReport(data, blob_path);
this.isLoadingSecurity = false;
})
.catch(() => {
this.isLoadingSecurity = false;
this.loadingSecurityFailed = true;
});
if (sast.base_path && sast.head_path) {
Promise.all([
this.service.fetchReport(sast.head_path),
this.service.fetchReport(sast.base_path),
])
.then((values) => {
this.handleSecuritySuccess({
head: values[0],
headBlobPath: this.mr.headBlobPath,
base: values[1],
baseBlobPath: this.mr.baseBlobPath,
});
})
.catch(() => this.handleSecurityError());
} else if (sast.head_path) {
this.service.fetchReport(sast.head_path)
.then((data) => {
this.handleSecuritySuccess({
head: data,
headBlobPath: this.mr.headBlobPath,
});
})
.catch(() => this.handleSecurityError());
}
},
handleSecuritySuccess(data) {
this.mr.setSecurityReport(data);
this.isLoadingSecurity = false;
},
handleSecurityError() {
this.isLoadingSecurity = false;
this.loadingSecurityFailed = true;
},
fetchDockerReport() {
......@@ -370,7 +426,9 @@ export default {
:loading-text="translateText('security').loading"
:error-text="translateText('security').error"
:success-text="securityText"
:unresolved-issues="mr.securityReport"
:unresolved-issues="mr.securityReport.newIssues"
:resolved-issues="mr.securityReport.resolvedIssues"
:all-issues="mr.securityReport.allIssues"
:has-priority="true"
/>
<collapsible-section
......
......@@ -4,6 +4,11 @@ import { stripHtml } from '~/lib/utils/text_utility';
export default class MergeRequestStore extends CEMergeRequestStore {
constructor(data) {
super(data);
const blobPath = data.blob_path || {};
this.headBlobPath = blobPath.head_path || '';
this.baseBlobPath = blobPath.base_path || '';
this.initCodeclimate(data);
this.initPerformanceReport(data);
this.initSecurityReport(data);
......@@ -63,7 +68,11 @@ export default class MergeRequestStore extends CEMergeRequestStore {
initSecurityReport(data) {
this.sast = data.sast;
this.securityReport = [];
this.securityReport = {
newIssues: [],
resolvedIssues: [],
allIssues: [],
};
}
initDockerReport(data) {
......@@ -79,9 +88,47 @@ export default class MergeRequestStore extends CEMergeRequestStore {
this.dast = data.dast;
this.dastReport = [];
}
/**
* Security report has 3 types of issues, newIssues, resolvedIssues and allIssues.
*
* When we have both base and head:
* - newIssues = head - base
* - resolvedIssues = base - head
* - allIssues = head - newIssues - resolvedIssues
*
* When we only have head
* - newIssues = head
* - resolvedIssues = 0
* - allIssues = 0
* @param {*} data
*/
setSecurityReport(issues, path) {
this.securityReport = MergeRequestStore.parseIssues(issues, path);
setSecurityReport(data) {
if (data.base) {
const filterKey = 'cve';
const parsedHead = MergeRequestStore.parseIssues(data.head, data.headBlobPath);
const parsedBase = MergeRequestStore.parseIssues(data.base, data.baseBlobPath);
this.securityReport.newIssues = MergeRequestStore.filterByKey(
parsedHead,
parsedBase,
filterKey,
);
this.securityReport.resolvedIssues = MergeRequestStore.filterByKey(
parsedBase,
parsedHead,
filterKey,
);
// Remove the new Issues and the added issues
this.securityReport.allIssues = MergeRequestStore.filterByKey(
parsedHead,
this.securityReport.newIssues.concat(this.securityReport.resolvedIssues),
filterKey,
);
} else {
this.securityReport.newIssues = MergeRequestStore.parseIssues(data.head, data.headBlobPath);
}
}
setDockerReport(data = {}) {
......@@ -130,13 +177,15 @@ export default class MergeRequestStore extends CEMergeRequestStore {
const parsedHeadIssues = MergeRequestStore.parseIssues(headIssues, headBlobPath);
const parsedBaseIssues = MergeRequestStore.parseIssues(baseIssues, baseBlobPath);
this.codeclimateMetrics.newIssues = MergeRequestStore.filterByFingerprint(
this.codeclimateMetrics.newIssues = MergeRequestStore.filterByKey(
parsedHeadIssues,
parsedBaseIssues,
'fingerprint',
);
this.codeclimateMetrics.resolvedIssues = MergeRequestStore.filterByFingerprint(
this.codeclimateMetrics.resolvedIssues = MergeRequestStore.filterByKey(
parsedBaseIssues,
parsedHeadIssues,
'fingerprint',
);
}
......@@ -198,7 +247,7 @@ export default class MergeRequestStore extends CEMergeRequestStore {
* @param {array} issues
* @return {array}
*/
static parseIssues(issues, path) {
static parseIssues(issues, path = '') {
return issues.map((issue) => {
const parsedIssue = {
name: issue.check_name || issue.message,
......@@ -236,8 +285,8 @@ export default class MergeRequestStore extends CEMergeRequestStore {
});
}
static filterByFingerprint(firstArray, secondArray) {
return firstArray.filter(item => !secondArray.find(el => el.fingerprint === item.fingerprint));
static filterByKey(firstArray, secondArray, key) {
return firstArray.filter(item => !secondArray.find(el => el[key] === item[key]));
}
// normalize performance metrics by indexing on performance subject and metric name
......
......@@ -13,7 +13,8 @@ module EE
delegate :codeclimate_artifact, to: :base_pipeline, prefix: :base, allow_nil: true
delegate :performance_artifact, to: :head_pipeline, prefix: :head, allow_nil: true
delegate :performance_artifact, to: :base_pipeline, prefix: :base, allow_nil: true
delegate :sast_artifact, to: :head_pipeline, allow_nil: true
delegate :sast_artifact, to: :head_pipeline, prefix: :head, allow_nil: true
delegate :sast_artifact, to: :base_pipeline, prefix: :base, allow_nil: true
delegate :sast_container_artifact, to: :head_pipeline, allow_nil: true
delegate :dast_artifact, to: :head_pipeline, allow_nil: true
delegate :sha, to: :head_pipeline, prefix: :head_pipeline, allow_nil: true
......@@ -47,7 +48,11 @@ module EE
end
def has_sast_data?
sast_artifact&.success?
head_sast_artifact&.success?
end
def has_base_sast_data?
base_sast_artifact&.success?
end
def has_sast_container_data?
......
......@@ -3,6 +3,16 @@ module EE
extend ActiveSupport::Concern
prepended do
expose :blob_path do
expose :head_path, if: -> (mr, _) { mr.head_pipeline_sha } do |merge_request|
project_blob_path(merge_request.project, merge_request.head_pipeline_sha)
end
expose :base_path, if: -> (mr, _) { mr.base_pipeline_sha } do |merge_request|
project_blob_path(merge_request.project, merge_request.base_pipeline_sha)
end
end
expose :codeclimate, if: -> (mr, _) { mr.has_codeclimate_data? } do
expose :head_path, if: -> (mr, _) { can?(current_user, :read_build, mr.head_codeclimate_artifact) } do |merge_request|
raw_project_build_artifacts_url(merge_request.source_project,
......@@ -10,19 +20,11 @@ module EE
path: Ci::Build::CODEQUALITY_FILE)
end
expose :head_blob_path, if: -> (mr, _) { mr.head_pipeline_sha } do |merge_request|
project_blob_path(merge_request.project, merge_request.head_pipeline_sha)
end
expose :base_path, if: -> (mr, _) { can?(current_user, :read_build, mr.base_codeclimate_artifact) } do |merge_request|
raw_project_build_artifacts_url(merge_request.target_project,
merge_request.base_codeclimate_artifact,
path: Ci::Build::CODEQUALITY_FILE)
end
expose :base_blob_path, if: -> (mr, _) { mr.base_pipeline_sha } do |merge_request|
project_blob_path(merge_request.project, merge_request.base_pipeline_sha)
end
end
expose :performance, if: -> (mr, _) { expose_performance_data?(mr) } do
......@@ -40,14 +42,16 @@ module EE
end
expose :sast, if: -> (mr, _) { expose_sast_data?(mr, current_user) } do
expose :path do |merge_request|
expose :head_path do |merge_request|
raw_project_build_artifacts_url(merge_request.source_project,
merge_request.sast_artifact,
merge_request.head_sast_artifact,
path: Ci::Build::SAST_FILE)
end
expose :blob_path, if: -> (mr, _) { mr.head_pipeline_sha } do |merge_request|
project_blob_path(merge_request.project, merge_request.head_pipeline_sha)
expose :base_path, if: -> (mr, _) { mr.has_base_sast_data? } do |merge_request|
raw_project_build_artifacts_url(merge_request.target_project,
merge_request.base_sast_artifact,
path: Ci::Build::SAST_FILE)
end
end
......@@ -57,10 +61,6 @@ module EE
merge_request.sast_container_artifact,
path: Ci::Build::SAST_CONTAINER_FILE)
end
expose :blob_path, if: -> (mr, _) { mr.head_pipeline_sha } do |merge_request|
project_blob_path(merge_request.project, merge_request.head_pipeline_sha)
end
end
expose :dast, if: -> (mr, _) { expose_dast_data?(mr, current_user) } do
......@@ -77,7 +77,7 @@ module EE
def expose_sast_data?(mr, current_user)
mr.project.feature_available?(:sast) &&
mr.has_sast_data? &&
can?(current_user, :read_build, mr.sast_artifact)
can?(current_user, :read_build, mr.head_sast_artifact)
end
def expose_performance_data?(mr)
......
......@@ -173,20 +173,34 @@ describe MergeRequest do
end
end
describe '#sast_artifact' do
it { is_expected.to delegate_method(:sast_artifact).to(:head_pipeline) }
describe '#head_sast_artifact' do
it { is_expected.to delegate_method(:sast_artifact).to(:head_pipeline).with_prefix(:head) }
end
describe '#base_sast_artifact' do
it { is_expected.to delegate_method(:sast_artifact).to(:base_pipeline).with_prefix(:base) }
end
describe '#has_sast_data?' do
let(:artifact) { double(success?: true) }
before do
allow(merge_request).to receive(:sast_artifact).and_return(artifact)
allow(merge_request).to receive(:head_sast_artifact).and_return(artifact)
end
it { expect(merge_request.has_sast_data?).to be_truthy }
end
describe '#has_base_sast_data?' do
let(:artifact) { double(success?: true) }
before do
allow(merge_request).to receive(:base_sast_artifact).and_return(artifact)
end
it { expect(merge_request.has_base_sast_data?).to be_truthy }
end
describe '#sast_container_artifact' do
it { is_expected.to delegate_method(:sast_container_artifact).to(:head_pipeline) }
end
......
......@@ -5,11 +5,21 @@ describe MergeRequestWidgetEntity do
let(:project) { create :project, :repository }
let(:merge_request) { create(:merge_request, source_project: project, target_project: project) }
let(:request) { double('request', current_user: user) }
let(:pipeline) { create(:ci_empty_pipeline, project: project) }
subject do
described_class.new(merge_request, request: request)
end
it 'has blob path data' do
allow(merge_request).to receive(:base_pipeline).and_return(pipeline)
allow(merge_request).to receive(:head_pipeline).and_return(pipeline)
expect(subject.as_json).to include(:blob_path)
expect(subject.as_json[:blob_path]).to include(:base_path)
expect(subject.as_json[:blob_path]).to include(:head_path)
end
it 'has performance data' do
build = create(:ci_build, name: 'job')
......@@ -24,9 +34,13 @@ describe MergeRequestWidgetEntity do
build = create(:ci_build, name: 'sast')
allow(subject).to receive(:expose_sast_data?).and_return(true)
allow(merge_request).to receive(:sast_artifact).and_return(build)
allow(merge_request).to receive(:has_base_sast_data?).and_return(true)
allow(merge_request).to receive(:base_sast_artifact).and_return(build)
allow(merge_request).to receive(:head_sast_artifact).and_return(build)
expect(subject.as_json).to include(:sast)
expect(subject.as_json[:sast]).to include(:head_path)
expect(subject.as_json[:sast]).to include(:base_path)
end
it 'has sast_container data' do
......
......@@ -116,9 +116,11 @@
"approvals_path": { "type": ["string", "null"] },
"codeclimate": {
"head_path": { "type": "string" },
"head_blob_path": { "type": "string" },
"base_path": { "type": "string" },
"base_blob_path": { "type": "string" }
"base_path": { "type": "string" }
},
"blob_path": {
"head_path": { "type": "string" },
"base_path": { "type": "string" }
}
},
"additionalProperties": false
......
......@@ -98,4 +98,78 @@ describe('Merge Request collapsible section', () => {
expect(vm.$el.textContent.trim()).toEqual('Failed to load codeclimate report');
});
});
describe('With full report', () => {
beforeEach(() => {
vm = mountComponent(MRWidgetCodeQuality, {
status: 'success',
successText: 'SAST improved on 1 security vulnerability and degraded on 1 security vulnerability',
type: 'security',
errorText: 'Failed to load security report',
hasPriority: true,
loadingText: 'Loading security report',
resolvedIssues: [{
cve: 'CVE-2016-9999',
file: 'Gemfile.lock',
message: 'Test Information Leak Vulnerability in Action View',
name: 'Test Information Leak Vulnerability in Action View',
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',
tool: 'bundler_audit',
url: 'https://groups.google.com/forum/#!topic/rubyonrails-security/335P1DcLG00',
urlPath: '/Gemfile.lock',
}],
unresolvedIssues: [{
cve: 'CVE-2014-7829',
file: 'Gemfile.lock',
message: 'Arbitrary file existence disclosure in Action Pack',
name: 'Arbitrary file existence disclosure in Action Pack',
path: 'Gemfile.lock',
solution: 'upgrade to ~> 3.2.21, ~> 4.0.11.1, ~> 4.0.12, ~> 4.1.7.1, >= 4.1.8',
tool: 'bundler_audit',
url: 'https://groups.google.com/forum/#!topic/rubyonrails-security/rMTQy4oRCGk',
urlPath: '/Gemfile.lock',
}],
allIssues: [{
cve: 'CVE-2016-0752',
file: 'Gemfile.lock',
message: 'Possible Information Leak Vulnerability in Action View',
name: 'Possible Information Leak Vulnerability in Action View',
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',
tool: 'bundler_audit',
url: 'https://groups.google.com/forum/#!topic/rubyonrails-security/335P1DcLG00',
urlPath: '/Gemfile.lock',
}],
});
});
it('should render full report section', (done) => {
vm.$el.querySelector('button').click();
Vue.nextTick(() => {
expect(
vm.$el.querySelector('.js-expand-full-list').textContent.trim(),
).toEqual('Show complete code vulnerabilities report');
done();
});
});
it('should expand full list when clicked and hide the show all button', (done) => {
vm.$el.querySelector('button').click();
Vue.nextTick(() => {
vm.$el.querySelector('.js-expand-full-list').click();
Vue.nextTick(() => {
expect(
vm.$el.querySelector('.js-mr-code-all-issues').textContent.trim(),
).toContain('Possible Information Leak Vulnerability in Action View');
done();
});
});
});
});
});
......@@ -9,6 +9,7 @@ import mockData, {
headIssues,
basePerformance,
headPerformance,
securityIssuesBase,
securityIssues,
dockerReport,
dockerReportParsed,
......@@ -36,8 +37,8 @@ describe('ee merge request widget options', () => {
gl.mrWidgetData = {
...mockData,
sast: {
path: 'path.json',
blob_path: 'blob_path',
base_path: 'path.json',
head_path: 'head_path.json',
},
};
......@@ -59,7 +60,8 @@ describe('ee merge request widget options', () => {
beforeEach(() => {
mock = mock = new MockAdapter(axios);
mock.onGet('path.json').reply(200, securityIssues);
mock.onGet('path.json').reply(200, securityIssuesBase);
mock.onGet('head_path.json').reply(200, securityIssues);
vm = mountComponent(Component);
});
......@@ -71,7 +73,7 @@ describe('ee merge request widget options', () => {
setTimeout(() => {
expect(
vm.$el.querySelector('.js-sast-widget .js-code-text').textContent.trim(),
).toEqual('SAST detected 2 security vulnerabilities');
).toEqual('SAST improved on 1 security vulnerability and degraded on 2 security vulnerabilities');
done();
}, 0);
});
......@@ -83,6 +85,8 @@ describe('ee merge request widget options', () => {
beforeEach(() => {
mock = mock = new MockAdapter(axios);
mock.onGet('path.json').reply(200, []);
mock.onGet('head_path.json').reply(200, []);
vm = mountComponent(Component);
});
......@@ -106,6 +110,7 @@ describe('ee merge request widget options', () => {
beforeEach(() => {
mock = mock = new MockAdapter(axios);
mock.onGet('path.json').reply(500, []);
mock.onGet('head_path.json').reply(500, []);
vm = mountComponent(Component);
});
......@@ -373,7 +378,6 @@ describe('ee merge request widget options', () => {
...mockData,
sast_container: {
path: 'gl-sast-container.json',
blob_path: 'blob_path',
},
};
......
......@@ -210,11 +210,13 @@ export default {
"diverged_commits_count": 0,
"only_allow_merge_if_pipeline_succeeds": false,
"commit_change_content_path": "/root/acets-app/merge_requests/22/commit_change_content",
"codeclimate": {
"head_path": "head.json",
"head_blob_path": "/root/acets-app/blob/abcdef",
"base_path": "base.json",
"base_blob_path": "/root/acets-app/blob/abcdef"
codeclimate: {
head_path: "head.json",
base_path: "base.json",
},
blob_path: {
base_path: 'blob_path',
head_path: 'blob_path',
},
};
......@@ -379,55 +381,122 @@ export const securityParsedIssues = [
},
];
export const securityIssues = [
{
tool: 'bundler_audit',
message: 'Arbitrary file existence disclosure in Action Pack',
url: 'https://groups.google.com/forum/#!topic/rubyonrails-security/rMTQy4oRCGk',
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',
priority:'High',
line: 12,
},
{
tool: 'bundler_audit',
message: 'Possible Information Leak Vulnerability in Action View',
url: 'https://groups.google.com/forum/#!topic/rubyonrails-security/335P1DcLG00',
cve: 'CVE-2016-0752',
file: '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',
priority: 'Medium',
},
];
export const securityIssues = [{
tool: 'bundler_audit',
message: 'Arbitrary file existence disclosure in Action Pack',
url: 'https://groups.google.com/forum/#!topic/rubyonrails-security/rMTQy4oRCGk',
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'
}, {
tool: 'bundler_audit',
message: 'Possible Information Leak Vulnerability in Action View',
url: 'https://groups.google.com/forum/#!topic/rubyonrails-security/335P1DcLG00',
cve: 'CVE-2016-0752',
file: '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'
}, {
tool: 'bundler_audit',
message: 'Possible Object Leak and Denial of Service attack in Action Pack',
url: 'https://groups.google.com/forum/#!topic/rubyonrails-security/9oLY_FCzvoc',
cve: 'CVE-2016-0751',
file: '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'
}];
export const parsedSecurityIssuesStore = [
{
tool: 'bundler_audit',
message: 'Arbitrary file existence disclosure in Action Pack',
url: 'https://groups.google.com/forum/#!topic/rubyonrails-security/rMTQy4oRCGk',
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',
priority:'High',
line: 12,
name: 'Arbitrary file existence disclosure in Action Pack',
path: 'Gemfile.lock',
urlPath: 'path/Gemfile.lock#L12'
},
{
tool: 'bundler_audit',
message: 'Possible Information Leak Vulnerability in Action View',
url: 'https://groups.google.com/forum/#!topic/rubyonrails-security/335P1DcLG00',
cve: 'CVE-2016-0752',
file: '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',
priority: 'Medium',
name: 'Possible Information Leak Vulnerability in Action View',
path: 'Gemfile.lock',
urlPath: 'path/Gemfile.lock',
},
];
export const securityIssuesBase = [{
tool: 'bundler_audit',
message: 'Test Information Leak Vulnerability in Action View',
url: 'https://groups.google.com/forum/#!topic/rubyonrails-security/335P1DcLG00',
cve: 'CVE-2016-9999',
file: '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'
}, {
tool: 'bundler_audit',
message: 'Possible Information Leak Vulnerability in Action View',
url: 'https://groups.google.com/forum/#!topic/rubyonrails-security/335P1DcLG00',
cve: 'CVE-2016-0752',
file: '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'
}];
export const parsedSecurityIssuesStore = [{
tool: 'bundler_audit',
message: 'Arbitrary file existence disclosure in Action Pack',
url: 'https://groups.google.com/forum/#!topic/rubyonrails-security/rMTQy4oRCGk',
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',
name: 'Arbitrary file existence disclosure in Action Pack',
path: 'Gemfile.lock',
urlPath: 'path/Gemfile.lock'
}, {
tool: 'bundler_audit',
message: 'Possible Information Leak Vulnerability in Action View',
url: 'https://groups.google.com/forum/#!topic/rubyonrails-security/335P1DcLG00',
cve: 'CVE-2016-0752',
file: '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',
name: 'Possible Information Leak Vulnerability in Action View',
path: 'Gemfile.lock',
urlPath: 'path/Gemfile.lock'
}, {
tool: 'bundler_audit',
message: 'Possible Object Leak and Denial of Service attack in Action Pack',
url: 'https://groups.google.com/forum/#!topic/rubyonrails-security/9oLY_FCzvoc',
cve: 'CVE-2016-0751',
file: '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',
name: 'Possible Object Leak and Denial of Service attack in Action Pack',
path: 'Gemfile.lock',
urlPath: 'path/Gemfile.lock'
}];
export const parsedSecurityIssuesHead = [{
tool: 'bundler_audit',
message: 'Arbitrary file existence disclosure in Action Pack',
url: 'https://groups.google.com/forum/#!topic/rubyonrails-security/rMTQy4oRCGk',
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',
name: 'Arbitrary file existence disclosure in Action Pack',
path: 'Gemfile.lock',
urlPath: 'path/Gemfile.lock'
}, {
tool: 'bundler_audit',
message: 'Possible Object Leak and Denial of Service attack in Action Pack',
url: 'https://groups.google.com/forum/#!topic/rubyonrails-security/9oLY_FCzvoc',
cve: 'CVE-2016-0751',
file: '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',
name: 'Possible Object Leak and Denial of Service attack in Action Pack',
path: 'Gemfile.lock',
urlPath: 'path/Gemfile.lock'
}];
export const parsedSecurityIssuesBaseStore = [{
name: 'Test Information Leak Vulnerability in Action View',
tool: 'bundler_audit',
message: 'Test Information Leak Vulnerability in Action View',
url: 'https://groups.google.com/forum/#!topic/rubyonrails-security/335P1DcLG00',
cve: 'CVE-2016-9999',
file: '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',
path: 'Gemfile.lock',
urlPath: 'path/Gemfile.lock'
}];
export const allIssuesParsed = [{
name: 'Possible Information Leak Vulnerability in Action View',
tool: 'bundler_audit',
message: 'Possible Information Leak Vulnerability in Action View',
url: 'https://groups.google.com/forum/#!topic/rubyonrails-security/335P1DcLG00',
cve: 'CVE-2016-0752',
file: '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',
path: 'Gemfile.lock',
urlPath: 'path/Gemfile.lock',
}];
export const dockerReport = {
unapproved: [
......
......@@ -4,9 +4,13 @@ import mockData, {
headIssues,
baseIssues,
securityIssues,
securityIssuesBase,
parsedBaseIssues,
parsedHeadIssues,
parsedSecurityIssuesStore,
parsedSecurityIssuesBaseStore,
allIssuesParsed,
parsedSecurityIssuesHead,
dockerReport,
dockerReportParsed,
dast,
......@@ -93,10 +97,22 @@ describe('MergeRequestStore', () => {
});
describe('setSecurityReport', () => {
it('should set security issues', () => {
store.setSecurityReport(securityIssues, 'path');
it('should set security issues with head', () => {
store.setSecurityReport({ head: securityIssues, headBlobPath: 'path' });
expect(store.securityReport.newIssues).toEqual(parsedSecurityIssuesStore);
});
it('should set security issues with head and base', () => {
store.setSecurityReport({
head: securityIssues,
headBlobPath: 'path',
base: securityIssuesBase,
baseBlobPath: 'path',
});
expect(store.securityReport).toEqual(parsedSecurityIssuesStore);
expect(store.securityReport.newIssues).toEqual(parsedSecurityIssuesHead);
expect(store.securityReport.resolvedIssues).toEqual(parsedSecurityIssuesBaseStore);
expect(store.securityReport.allIssues).toEqual(allIssuesParsed);
});
});
......
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