Commit 5feaa733 authored by Dmitriy Zaporozhets's avatar Dmitriy Zaporozhets

Merge branch '4249-show-results-from-docker-image-scan-in-the-merge-request-widget' into 'master'

Resolve "Show results from docker image scan in the merge request widget"

Closes #4249

See merge request gitlab-org/gitlab-ee!3672
parents 11f6e324 6c22e997
......@@ -790,6 +790,10 @@
background-color: $gray-light;
margin: $gl-padding -16px -16px;
.mr-widget-code-quality-info {
padding-left: 12px;
}
.mr-widget-code-quality-list {
list-style: none;
padding: 0 12px;
......
---
title: Show results from docker image scan in the merge request widget
merge_request: 3672
author:
type: added
......@@ -7,7 +7,7 @@ export default {
name: 'MRWidgetCodeQuality',
props: {
// security | codequality | performance
// security | codequality | performance | docker
type: {
type: String,
required: true,
......@@ -44,6 +44,10 @@ export default {
required: false,
default: () => [],
},
infoText: {
type: String,
required: false,
},
},
components: {
......@@ -128,12 +132,19 @@ export default {
class="code-quality-container"
v-if="hasIssues"
v-show="!isCollapsed">
<p
v-if="type === 'docker' && infoText"
v-html="infoText"
class="js-mr-code-quality-info mr-widget-code-quality-info">
</p>
<issues-block
class="js-mr-code-resolved-issues"
v-if="resolvedIssues.length"
class="js-mr-code-new-issues"
v-if="unresolvedIssues.length"
:type="type"
status="success"
:issues="resolvedIssues"
status="failed"
:issues="unresolvedIssues"
/>
<issues-block
......@@ -145,11 +156,11 @@ export default {
/>
<issues-block
class="js-mr-code-new-issues"
v-if="unresolvedIssues.length"
class="js-mr-code-resolved-issues"
v-if="resolvedIssues.length"
:type="type"
status="failed"
:issues="unresolvedIssues"
status="success"
:issues="resolvedIssues"
/>
</div>
<div
......
......@@ -8,7 +8,7 @@
type: Array,
required: true,
},
// security || codequality || performance
// security || codequality || performance || docker
type: {
type: String,
required: true,
......@@ -41,6 +41,14 @@
isTypeSecurity() {
return this.type === 'security';
},
isTypeDocker() {
return this.type === 'docker';
},
},
methods: {
shouldRenderPriority(issue) {
return (this.isTypeSecurity || this.isTypeDocker) && issue.priority;
},
},
};
</script>
......@@ -60,9 +68,23 @@
</span>
<template v-if="isStatusSuccess && isTypeQuality">Fixed:</template>
<template v-if="isTypeSecurity && issue.priority">{{issue.priority}}:</template>
<template v-if="shouldRenderPriority(issue)">{{issue.priority}}:</template>
{{issue.name}}<template v-if="issue.score">: <strong>{{issue.score}}</strong></template>
<template v-if="isTypeDocker">
<a
v-if="issue.nameLink"
:href="issue.nameLink"
target="_blank"
rel="noopener noreferrer nofollow">
{{issue.name}}
</a>
<template v-else>
{{issue.name}}
</template>
</template>
<template v-else>
{{issue.name}}<template v-if="issue.score">: <strong>{{issue.score}}</strong></template>
</template>
<template v-if="isTypePerformance && issue.delta != null">
({{issue.delta >= 0 ? '+' : ''}}{{issue.delta}})
......
import { n__ } 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';
......@@ -18,9 +18,11 @@ export default {
isLoadingCodequality: false,
isLoadingPerformance: false,
isLoadingSecurity: false,
isLoadingDocker: false,
loadingCodequalityFailed: false,
loadingPerformanceFailed: false,
loadingSecurityFailed: false,
loadingDockerFailed: false,
};
},
computed: {
......@@ -38,6 +40,9 @@ export default {
shouldRenderSecurityReport() {
return this.mr.sast;
},
shouldRenderDockerReport() {
return this.mr.clair;
},
codequalityText() {
const { newIssues, resolvedIssues } = this.mr.codeclimateMetrics;
const text = [];
......@@ -116,34 +121,76 @@ export default {
return 'No security vulnerabilities detected';
},
codequalityStatus() {
if (this.isLoadingCodequality) {
return 'loading';
} else if (this.loadingCodequalityFailed) {
return 'error';
dockerText() {
const { vulnerabilities, approved, unapproved } = this.mr.dockerReport;
if (!vulnerabilities.length) {
return s__('ciReport|No vulnerabilities were found');
}
return 'success';
if (!unapproved.length && approved.length) {
return n__(
'Found %d approved vulnerability',
'Found %d approved vulnerabilities',
approved.length,
);
} else if (unapproved.length && !approved.length) {
return n__(
'Found %d vulnerability',
'Found %d vulnerabilities',
unapproved.length,
);
}
return `${n__(
'Found %d vulnerability,',
'Found %d vulnerabilities,',
vulnerabilities.length,
)} ${n__(
'of which %d is approved',
'of which %d are approved',
approved.length,
)}`;
},
codequalityStatus() {
return this.checkReportStatus(this.isLoadingCodequality, this.loadingCodequalityFailed);
},
performanceStatus() {
if (this.isLoadingPerformance) {
return 'loading';
} else if (this.loadingPerformanceFailed) {
return 'error';
}
return 'success';
return this.checkReportStatus(this.isLoadingPerformance, this.loadingPerformanceFailed);
},
securityStatus() {
if (this.isLoadingSecurity) {
return this.checkReportStatus(this.isLoadingSecurity, this.loadingSecurityFailed);
},
dockerStatus() {
return this.checkReportStatus(this.isLoadingDocker, this.loadingDockerFailed);
},
dockerInformationText() {
return sprintf(
s__('ciReport|Unapproved vulnerabilities (red) can be marked as approved. %{helpLink}'), {
helpLink: `<a href="https://gitlab.com/gitlab-org/clair-scanner#example-whitelist-yaml-file" target="_blank" rel="noopener noreferrer nofollow">
${s__('ciReport|Learn more about whitelisting')}
</a>`,
},
false,
);
},
},
methods: {
checkReportStatus(loading, error) {
if (loading) {
return 'loading';
} else if (this.loadingSecurityFailed) {
} else if (error) {
return 'error';
}
return 'success';
},
},
methods: {
fetchCodeQuality() {
const { head_path, head_blob_path, base_path, base_blob_path } = this.mr.codeclimate;
......@@ -196,6 +243,28 @@ export default {
this.loadingSecurityFailed = true;
});
},
fetchDockerReport() {
const { path } = this.mr.clair;
this.isLoadingDocker = true;
this.service.fetchReport(path)
.then((data) => {
this.mr.setDockerReport(data);
this.isLoadingDocker = false;
})
.catch(() => {
this.isLoadingDocker = false;
this.loadingDockerFailed = true;
});
},
translateText(type) {
return {
error: s__(`ciReport|Failed to load ${type} report`),
loading: s__(`ciReport|Loading ${type} report`),
};
},
},
created() {
if (this.shouldRenderCodeQuality) {
......@@ -209,6 +278,10 @@ export default {
if (this.shouldRenderSecurityReport) {
this.fetchSecurity();
}
if (this.shouldRenderDockerReport) {
this.fetchDockerReport();
}
},
template: `
<div class="mr-state-widget prepend-top-default">
......@@ -222,43 +295,57 @@ export default {
<mr-widget-deployment
v-if="shouldRenderDeployments"
:mr="mr"
:service="service" />
:service="service"
/>
<mr-widget-approvals
v-if="shouldRenderApprovals"
:mr="mr"
:service="service" />
:service="service"
/>
<collapsible-section
class="js-codequality-widget"
v-if="shouldRenderCodeQuality"
type="codequality"
:status="codequalityStatus"
loading-text="Loading codeclimate report"
error-text="Failed to load codeclimate report"
:loading-text="translateText('codeclimate').loading"
:error-text="translateText('codeclimate').error"
:success-text="codequalityText"
:unresolvedIssues="mr.codeclimateMetrics.newIssues"
:resolvedIssues="mr.codeclimateMetrics.resolvedIssues"
:unresolved-issues="mr.codeclimateMetrics.newIssues"
:resolved-issues="mr.codeclimateMetrics.resolvedIssues"
/>
<collapsible-section
class="js-performance-widget"
v-if="shouldRenderPerformance"
type="performance"
:status="performanceStatus"
loading-text="Loading performance report"
error-text="Failed to load performance report"
:loading-text="translateText('performance').loading"
:error-text="translateText('performance').error"
:success-text="performanceText"
:unresolvedIssues="mr.performanceMetrics.degraded"
:resolvedIssues="mr.performanceMetrics.improved"
:neutralIssues="mr.performanceMetrics.neutral"
:unresolved-issues="mr.performanceMetrics.degraded"
:resolved-issues="mr.performanceMetrics.improved"
:neutral-issues="mr.performanceMetrics.neutral"
/>
<collapsible-section
class="js-sast-widget"
v-if="shouldRenderSecurityReport"
type="security"
:status="securityStatus"
loading-text="Loading security report"
error-text="Failed to load security report"
:loading-text="translateText('security').loading"
:error-text="translateText('security').error"
:success-text="securityText"
:unresolvedIssues="mr.securityReport"
:unresolved-issues="mr.securityReport"
/>
<collapsible-section
class="js-docker-widget"
v-if="shouldRenderDockerReport"
type="docker"
:status="dockerStatus"
:loading-text="translateText('clair').loading"
:error-text="translateText('clair').error"
:success-text="dockerText"
:unresolved-issues="mr.dockerReport.unapproved"
:neutral-issues="mr.dockerReport.approved"
:info-text="dockerInformationText"
/>
<div class="mr-widget-section">
<component
......@@ -267,7 +354,8 @@ export default {
:service="service" />
<mr-widget-related-links
v-if="shouldRenderRelatedLinks"
:related-links="mr.relatedLinks" />
:related-links="mr.relatedLinks"
/>
</div>
<div class="mr-widget-footer" v-if="shouldRenderMergeHelp">
<mr-widget-merge-help />
......
......@@ -6,6 +6,7 @@ export default class MergeRequestStore extends CEMergeRequestStore {
this.initCodeclimate(data);
this.initPerformanceReport(data);
this.initSecurityReport(data);
this.initDockerReport(data);
}
setData(data) {
......@@ -71,10 +72,49 @@ export default class MergeRequestStore extends CEMergeRequestStore {
this.securityReport = [];
}
initDockerReport(data) {
this.clair = data.clair;
this.dockerReport = {
approved: [],
unapproved: [],
vulnerabilities: [],
};
}
setSecurityReport(issues, path) {
this.securityReport = MergeRequestStore.parseIssues(issues, path);
}
setDockerReport(data = {}) {
const parsedVulnerabilities = MergeRequestStore
.parseDockerVulnerabilities(data.vulnerabilities);
this.dockerReport.vulnerabilities = parsedVulnerabilities || [];
// There is a typo in the original repo:
// https://github.com/arminc/clair-scanner/pull/39/files
// Fix this when the above PR is accepted
const unapproved = data.unapproved || data.unaproved || [];
// Approved can be calculated by subtracting unapproved from vulnerabilities.
this.dockerReport.approved = parsedVulnerabilities
.filter(item => !unapproved.find(el => el === item.vulnerability)) || [];
this.dockerReport.unapproved = parsedVulnerabilities
.filter(item => unapproved.find(el => el === item.vulnerability)) || [];
}
static parseDockerVulnerabilities(data) {
return data.map(el => ({
name: el.vulnerability,
priority: el.severity,
path: el.namespace,
// external link to provide better description
nameLink: `https://cve.mitre.org/cgi-bin/cvename.cgi?name=${el.vulnerability}`,
...el,
}));
}
compareCodeclimateMetrics(headIssues, baseIssues, headBlobPath, baseBlobPath) {
const parsedHeadIssues = MergeRequestStore.parseIssues(headIssues, headBlobPath);
const parsedBaseIssues = MergeRequestStore.parseIssues(baseIssues, baseBlobPath);
......
......@@ -10,11 +10,13 @@ module EE
CODEQUALITY_FILE = 'codeclimate.json'.freeze
SAST_FILE = 'gl-sast-report.json'.freeze
PERFORMANCE_FILE = 'performance.json'.freeze
CLAIR_FILE = 'gl-clair-report.json'.freeze
included do
scope :codequality, ->() { where(name: %w[codequality codeclimate]) }
scope :performance, ->() { where(name: %w[performance deploy]) }
scope :sast, ->() { where(name: 'sast') }
scope :clair, ->() { where(name: 'clair') }
after_save :stick_build_if_status_changed
end
......@@ -42,6 +44,10 @@ module EE
has_artifact?(SAST_FILE)
end
def has_clair_json?
has_artifact?(CLAIR_FILE)
end
private
def has_artifact?(name)
......
......@@ -24,6 +24,10 @@ module EE
def sast_artifact
artifacts.sast.find(&:has_sast_json?)
end
def clair_artifact
artifacts.clair.find(&:has_clair_json?)
end
end
end
end
......@@ -14,6 +14,7 @@ module EE
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 :clair_artifact, to: :head_pipeline, allow_nil: true
delegate :sha, to: :head_pipeline, prefix: :head_pipeline, allow_nil: true
delegate :sha, to: :base_pipeline, prefix: :base_pipeline, allow_nil: true
end
......@@ -54,5 +55,9 @@ module EE
def has_sast_data?
sast_artifact&.success?
end
def has_clair_data?
clair_artifact&.success?
end
end
end
......@@ -55,6 +55,7 @@ class License < ActiveRecord::Base
EEU_FEATURES = EEP_FEATURES + %i[
sast
sast_image
epics
].freeze
......
......@@ -50,6 +50,18 @@ module EE
project_blob_path(merge_request.project, merge_request.head_pipeline_sha)
end
end
expose :clair, if: -> (mr, _) { expose_clair_data?(mr, current_user) } do
expose :path do |merge_request|
raw_project_build_artifacts_url(merge_request.source_project,
merge_request.clair_artifact,
path: Ci::Build::CLAIR_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
end
private
......@@ -64,5 +76,11 @@ module EE
mr.project.feature_available?(:merge_request_performance_metrics) &&
mr.has_performance_data?
end
def expose_clair_data?(mr, current_user)
mr.project.feature_available?(:sast_image) &&
mr.has_clair_data? &&
can?(current_user, :read_build, mr.clair_artifact)
end
end
end
......@@ -131,7 +131,8 @@ describe Ci::Build do
ARTIFACTS_METHODS = {
has_codeclimate_json?: Ci::Build::CODEQUALITY_FILE,
has_performance_json?: Ci::Build::PERFORMANCE_FILE,
has_sast_json?: Ci::Build::SAST_FILE
has_sast_json?: Ci::Build::SAST_FILE,
has_clair_json?: Ci::Build::CLAIR_FILE
}.freeze
ARTIFACTS_METHODS.each do |method, filename|
......
......@@ -15,87 +15,40 @@ describe Ci::Pipeline do
end
end
describe '#codeclimate_artifact' do
context 'has codequality job' do
let!(:build) do
create(
:ci_build,
:artifacts,
name: 'codequality',
pipeline: pipeline,
options: {
artifacts: {
paths: ['codeclimate.json']
ARTIFACTS_METHODS = {
codeclimate_artifact: Ci::Build::CODEQUALITY_FILE,
performance_artifact: Ci::Build::PERFORMANCE_FILE,
sast_artifact: Ci::Build::SAST_FILE,
clair_artifact: Ci::Build::CLAIR_FILE
}.freeze
ARTIFACTS_METHODS.each do |method, filename|
describe method.to_s do
context 'has corresponding job' do
let!(:build) do
create(
:ci_build,
:artifacts,
name: method.to_s.sub('_artifact', ''),
pipeline: pipeline,
options: {
artifacts: {
paths: [filename]
}
}
}
)
end
it { expect(pipeline.codeclimate_artifact).to eq(build) }
end
)
end
context 'no codequality job' do
before do
create(:ci_build, pipeline: pipeline)
it { expect(pipeline.send(method)).to eq(build) }
end
it { expect(pipeline.codeclimate_artifact).to be_nil }
end
end
context 'no codequality job' do
before do
create(:ci_build, pipeline: pipeline)
end
describe '#performance_artifact' do
context 'has performance job' do
let!(:build) do
create(
:ci_build,
:artifacts,
name: 'performance',
pipeline: pipeline,
options: {
artifacts: {
paths: ['performance.json']
}
}
)
it { expect(pipeline.send(method)).to be_nil }
end
it { expect(pipeline.performance_artifact).to eq(build) }
end
context 'no performance job' do
before do
create(:ci_build, pipeline: pipeline)
end
it { expect(pipeline.performance_artifact).to be_nil }
end
end
describe '#sast_artifact' do
context 'has sast job' do
let!(:build) do
create(
:ci_build,
:artifacts,
name: 'sast',
pipeline: pipeline,
options: {
artifacts: {
paths: ['gl-sast-report.json']
}
}
)
end
it { expect(pipeline.sast_artifact).to eq(build) }
end
context 'no sast job' do
before do
create(:ci_build, pipeline: pipeline)
end
it { expect(pipeline.sast_artifact).to be_nil }
end
end
end
......@@ -246,4 +246,8 @@ describe MergeRequest do
it { expect(merge_request.has_sast_data?).to be_truthy }
end
describe '#clair_artifact' do
it { is_expected.to delegate_method(:clair_artifact).to(:head_pipeline) }
end
end
......@@ -4,7 +4,6 @@ describe MergeRequestEntity do
let(:user) { create(:user) }
let(:project) { create :project, :repository }
let(:merge_request) { create(:merge_request, source_project: project, target_project: project) }
let(:build) { create(:ci_build, name: 'job') }
let(:request) { double('request', current_user: user) }
subject do
......@@ -12,6 +11,8 @@ describe MergeRequestEntity do
end
it 'has performance data' do
build = create(:ci_build, name: 'job')
allow(subject).to receive(:expose_performance_data?).and_return(true)
allow(merge_request).to receive(:base_performance_artifact).and_return(build)
allow(merge_request).to receive(:head_performance_artifact).and_return(build)
......@@ -20,9 +21,20 @@ describe MergeRequestEntity do
end
it 'has sast data' 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)
expect(subject.as_json).to include(:sast)
end
it 'has clair data' do
build = create(:ci_build, name: 'clair')
allow(subject).to receive(:expose_clair_data?).and_return(true)
allow(merge_request).to receive(:clair_artifact).and_return(build)
expect(subject.as_json).to include(:clair)
end
end
......@@ -95,7 +95,6 @@ describe('Merge Request collapsible section', () => {
errorText: 'Failed to load codeclimate report',
successText: 'Code quality improved on 1 point and degraded on 1 point',
});
expect(vm.$el.textContent.trim()).toEqual('Failed to load codeclimate report');
});
});
......
import Vue from 'vue';
import mrWidgetCodeQualityIssues from 'ee/vue_merge_request_widget/components/mr_widget_report_issues.vue';
import mountComponent from '../../helpers/vue_mount_component_helper';
import { securityParsedIssues, codequalityParsedIssues } from '../mock_data';
import {
securityParsedIssues,
codequalityParsedIssues,
dockerReportParsed,
} from '../mock_data';
describe('merge request report issues', () => {
let vm;
......@@ -101,4 +105,38 @@ describe('merge request report issues', () => {
expect(vm.$el.querySelector('.mr-widget-code-quality-list li a')).toEqual(null);
});
});
describe('for docker issues', () => {
beforeEach(() => {
vm = mountComponent(MRWidgetCodeQualityIssues, {
issues: dockerReportParsed.unapproved,
type: 'docker',
status: 'failed',
});
});
it('renders priority', () => {
expect(
vm.$el.querySelector('.mr-widget-code-quality-list li').textContent.trim(),
).toContain(dockerReportParsed.unapproved[0].priority);
});
it('renders CVE link', () => {
expect(
vm.$el.querySelector('.mr-widget-code-quality-list a').getAttribute('href'),
).toEqual(dockerReportParsed.unapproved[0].nameLink);
expect(
vm.$el.querySelector('.mr-widget-code-quality-list a').textContent.trim(),
).toEqual(dockerReportParsed.unapproved[0].name);
});
it('renders namespace', () => {
expect(
vm.$el.querySelector('.mr-widget-code-quality-list li').textContent.trim(),
).toContain(dockerReportParsed.unapproved[0].path);
expect(
vm.$el.querySelector('.mr-widget-code-quality-list li').textContent.trim(),
).toContain('in');
});
});
});
......@@ -10,6 +10,8 @@ import mockData, {
basePerformance,
headPerformance,
securityIssues,
dockerReport,
dockerReportParsed,
} from './mock_data';
import mountComponent from '../helpers/vue_mount_component_helper';
......@@ -363,6 +365,102 @@ describe('ee merge request widget options', () => {
});
});
describe('docker report', () => {
beforeEach(() => {
gl.mrWidgetData = {
...mockData,
clair: {
path: 'clair.json',
blob_path: 'blob_path',
},
};
Component.mr = new MRWidgetStore(gl.mrWidgetData);
Component.service = new MRWidgetService({});
});
describe('when it is loading', () => {
it('should render loading indicator', () => {
vm = mountComponent(Component);
expect(
vm.$el.querySelector('.js-docker-widget').textContent.trim(),
).toContain('Loading clair report');
});
});
describe('with successful request', () => {
const interceptor = (request, next) => {
if (request.url === 'clair.json') {
next(request.respondWith(JSON.stringify(dockerReport), {
status: 200,
}));
}
};
beforeEach(() => {
Vue.http.interceptors.push(interceptor);
vm = mountComponent(Component);
});
afterEach(() => {
Vue.http.interceptors = _.without(Vue.http.interceptors, interceptor);
});
it('should render provided data', (done) => {
setTimeout(() => {
expect(
vm.$el.querySelector('.js-docker-widget .js-code-text').textContent.trim(),
).toEqual('Found 3 vulnerabilities, of which 1 is approved');
vm.$el.querySelector('.js-docker-widget button').click();
Vue.nextTick(() => {
expect(
vm.$el.querySelector('.js-docker-widget .mr-widget-code-quality-info').textContent.trim(),
).toContain('Unapproved vulnerabilities (red) can be marked as approved.');
expect(
vm.$el.querySelector('.js-docker-widget .mr-widget-code-quality-info a').textContent.trim(),
).toContain('Learn more about whitelisting');
const firstVulnerability = vm.$el.querySelector('.js-docker-widget .mr-widget-code-quality-list').textContent.trim();
expect(firstVulnerability).toContain(dockerReportParsed.unapproved[0].name);
expect(firstVulnerability).toContain(dockerReportParsed.unapproved[0].path);
done();
});
}, 0);
});
});
describe('with failed request', () => {
const interceptor = (request, next) => {
if (request.url === 'clair.json') {
next(request.respondWith({}, {
status: 500,
}));
}
};
beforeEach(() => {
Vue.http.interceptors.push(interceptor);
vm = mountComponent(Component);
});
afterEach(() => {
Vue.http.interceptors = _.without(Vue.http.interceptors, interceptor);
});
it('should render error indicator', (done) => {
setTimeout(() => {
expect(
vm.$el.querySelector('.js-docker-widget').textContent.trim(),
).toContain('Failed to load clair report');
done();
}, 0);
});
});
});
describe('computed', () => {
describe('shouldRenderApprovals', () => {
it('should return false when no approvals', () => {
......@@ -401,5 +499,182 @@ describe('ee merge request widget options', () => {
expect(vm.shouldRenderApprovals).toBeTruthy();
});
});
describe('dockerText', () => {
beforeEach(() => {
vm = mountComponent(Component, {
mrData: {
...mockData,
clair: {
path: 'foo',
},
},
});
});
describe('with no vulnerabilities', () => {
it('returns No vulnerabilities found', () => {
expect(vm.dockerText).toEqual('No vulnerabilities were found');
});
});
describe('without unapproved vulnerabilities', () => {
it('returns approved information - single', () => {
vm.mr.dockerReport = {
vulnerabilities: [{
vulnerability: 'CVE-2017-12944',
namespace: 'debian:8',
severity: 'Medium',
}],
approved: [{
vulnerability: 'CVE-2017-12944',
namespace: 'debian:8',
severity: 'Medium',
}],
unapproved: [],
};
expect(vm.dockerText).toEqual('Found 1 approved vulnerability');
});
it('returns approved information - plural', () => {
vm.mr.dockerReport = {
vulnerabilities: [{
vulnerability: 'CVE-2017-12944',
namespace: 'debian:8',
severity: 'Medium',
}],
approved: [
{
vulnerability: 'CVE-2017-12944',
namespace: 'debian:8',
severity: 'Medium',
},
{
vulnerability: 'CVE-2017-13726',
namespace: 'debian:8',
severity: 'Medium',
},
],
unapproved: [],
};
expect(vm.dockerText).toEqual('Found 2 approved vulnerabilities');
});
});
describe('with only unapproved vulnerabilities', () => {
it('returns number of vulnerabilities - single', () => {
vm.mr.dockerReport = {
vulnerabilities: [{
vulnerability: 'CVE-2017-12944',
namespace: 'debian:8',
severity: 'Medium',
}],
unapproved: [
{
vulnerability: 'CVE-2017-12944',
namespace: 'debian:8',
severity: 'Medium',
},
],
approved: [],
};
expect(vm.dockerText).toEqual('Found 1 vulnerability');
});
it('returns number of vulnerabilities - plural', () => {
vm.mr.dockerReport = {
vulnerabilities: [{
vulnerability: 'CVE-2017-12944',
namespace: 'debian:8',
severity: 'Medium',
}],
unapproved: [
{
vulnerability: 'CVE-2017-12944',
namespace: 'debian:8',
severity: 'Medium',
},
{
vulnerability: 'CVE-2017-12944',
namespace: 'debian:8',
severity: 'Medium',
},
],
approved: [],
};
expect(vm.dockerText).toEqual('Found 2 vulnerabilities');
});
});
describe('with approved and unapproved vulnerabilities', () => {
it('returns message with information about both - single', () => {
vm.mr.dockerReport = {
vulnerabilities: [{
vulnerability: 'CVE-2017-12944',
namespace: 'debian:8',
severity: 'Medium',
}],
unapproved: [
{
vulnerability: 'CVE-2017-12944',
namespace: 'debian:8',
severity: 'Medium',
},
],
approved: [
{
vulnerability: 'CVE-2017-12944',
namespace: 'debian:8',
severity: 'Medium',
},
],
};
expect(vm.dockerText).toEqual('Found 1 vulnerability, of which 1 is approved');
});
it('returns message with information about both - plural', () => {
vm.mr.dockerReport = {
vulnerabilities: [
{
vulnerability: 'CVE-2017-12944',
namespace: 'debian:8',
severity: 'Medium',
},
{
vulnerability: 'CVE-2017-12944',
namespace: 'debian:8',
severity: 'Medium',
},
],
unapproved: [
{
vulnerability: 'CVE-2017-12944',
namespace: 'debian:8',
severity: 'Medium',
},
{
vulnerability: 'CVE-2017-12923',
namespace: 'debian:8',
severity: 'Medium',
},
],
approved: [
{
vulnerability: 'CVE-2017-12944',
namespace: 'debian:8',
severity: 'Medium',
},
{
vulnerability: 'CVE-2017-13944',
namespace: 'debian:8',
severity: 'Medium',
},
],
};
expect(vm.dockerText).toEqual('Found 2 vulnerabilities, of which 2 are approved');
});
});
});
});
});
......@@ -215,7 +215,7 @@ export default {
"head_blob_path": "/root/acets-app/blob/abcdef",
"base_path": "base.json",
"base_blob_path": "/root/acets-app/blob/abcdef"
}
},
};
export const headIssues = [
......@@ -428,3 +428,90 @@ export const parsedSecurityIssuesStore = [
urlPath: 'path/Gemfile.lock',
},
];
export const dockerReport = {
unapproved: [
'CVE-2017-12944',
'CVE-2017-16232'
],
vulnerabilities: [
{
vulnerability: 'CVE-2017-12944',
namespace: 'debian:8',
severity: 'Medium'
},
{
vulnerability: 'CVE-2017-16232',
namespace: 'debian:8',
severity: 'Negligible'
},
{
vulnerability: 'CVE-2014-8130',
namespace: 'debian:8',
severity: 'Negligible'
}
]
};
export const dockerReportParsed = {
unapproved: [
{
vulnerability: 'CVE-2017-12944',
namespace: 'debian:8',
severity: 'Medium',
name: 'CVE-2017-12944',
priority: 'Medium',
path: 'debian:8',
nameLink: 'https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2017-12944'
},
{
vulnerability: 'CVE-2017-16232',
namespace: 'debian:8',
severity: 'Negligible',
name: 'CVE-2017-16232',
priority: 'Negligible',
path: 'debian:8',
nameLink: 'https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2017-16232'
},
],
approved: [
{
vulnerability: 'CVE-2014-8130',
namespace: 'debian:8',
severity: 'Negligible',
name: 'CVE-2014-8130',
priority: 'Negligible',
path: 'debian:8',
nameLink: 'https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2014-8130'
},
],
vulnerabilities: [
{
vulnerability: 'CVE-2017-12944',
namespace: 'debian:8',
severity: 'Medium',
name: 'CVE-2017-12944',
priority: 'Medium',
path: 'debian:8',
nameLink: 'https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2017-12944'
},
{
vulnerability: 'CVE-2017-16232',
namespace: 'debian:8',
severity: 'Negligible',
name: 'CVE-2017-16232',
priority: 'Negligible',
path: 'debian:8',
nameLink: 'https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2017-16232'
},
{
vulnerability: 'CVE-2014-8130',
namespace: 'debian:8',
severity: 'Negligible',
name: 'CVE-2014-8130',
priority: 'Negligible',
path: 'debian:8',
nameLink: 'https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2014-8130'
}
]
};
......@@ -6,6 +6,8 @@ import mockData, {
parsedBaseIssues,
parsedHeadIssues,
parsedSecurityIssuesStore,
dockerReport,
dockerReportParsed,
} from '../mock_data';
describe('MergeRequestStore', () => {
......@@ -95,4 +97,51 @@ describe('MergeRequestStore', () => {
expect(security.path).toEqual(securityIssues[0].file);
});
});
describe('initDockerReport', () => {
it('sets the defaults', () => {
store.initDockerReport({ clair: { path: 'clair.json' } });
expect(store.clair).toEqual({ path: 'clair.json' });
expect(store.dockerReport).toEqual({
approved: [],
unapproved: [],
vulnerabilities: [],
});
});
});
describe('setDockerReport', () => {
it('sets docker report with approved and unapproved vulnerabilities parsed', () => {
store.setDockerReport(dockerReport);
expect(store.dockerReport.vulnerabilities).toEqual(dockerReportParsed.vulnerabilities);
expect(store.dockerReport.approved).toEqual(dockerReportParsed.approved);
expect(store.dockerReport.unapproved).toEqual(dockerReportParsed.unapproved);
});
it('handles unaproved typo', () => {
store.setDockerReport({
vulnerabilities: [
{
vulnerability: 'CVE-2017-12944',
namespace: 'debian:8',
severity: 'Medium',
},
],
unaproved: ['CVE-2017-12944'],
});
expect(store.dockerReport.unapproved[0].vulnerability).toEqual('CVE-2017-12944');
});
});
describe('parseDockerVulnerabilities', () => {
it('parses docker report', () => {
expect(
MergeRequestStore.parseDockerVulnerabilities(dockerReport.vulnerabilities),
).toEqual(
dockerReportParsed.vulnerabilities,
);
});
});
});
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