Commit 0499b49c authored by Filipa Lacerda's avatar Filipa Lacerda

Parses docker report into the needed data strucutre

Adds tests
Adds links
parent 24d4e462
......@@ -784,6 +784,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;
......
......@@ -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__, s__ } 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';
......@@ -128,15 +128,13 @@ export default {
return s__('ciReport|No vulnerabilities were found');
}
if (!unapproved.length) {
if (!unapproved.length && approved.length) {
return n__(
'Found %d approved vulnerability',
'Found %d approved vulnerabilities',
approved.length,
);
}
if (unapproved.length && !approved.length) {
} else if (unapproved.length && !approved.length) {
return n__(
'Found %d vulnerability',
'Found %d vulnerabilities',
......@@ -147,7 +145,7 @@ export default {
return `${n__(
'Found %d vulnerability,',
'Found %d vulnerabilities,',
unapproved.length,
vulnerabilities.length,
)} ${n__(
'of which %d is approved',
'of which %d are approved',
......@@ -170,6 +168,17 @@ export default {
dockerStatus() {
return this.checkReportStatus(this.isLoadingDocker, this.loadingDockerFailed);
},
dockerInformationText() {
return sprintf(
_.escape(s__('ciReport|Unapproved vulnerabilities (red) can be marked as approved. %{helpLink}')), {
helpLink: `<a href="todo" _target="blank" >
${_.escape(s__('ciReport|Learn more about whitelisting'))}
</a>`,
},
false,
);
},
},
methods: {
checkReportStatus(loading, error) {
......@@ -294,8 +303,8 @@ export default {
loading-text="Loading codeclimate report"
error-text="Failed to load codeclimate report"
: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"
......@@ -305,9 +314,9 @@ export default {
loading-text="Loading performance report"
error-text="Failed to load performance report"
: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"
......@@ -317,18 +326,19 @@ export default {
loading-text="Loading security report"
error-text="Failed to load security report"
:success-text="securityText"
:unresolvedIssues="mr.securityReport"
:unresolved-issues="mr.securityReport"
/>
<collapsible-section
class="js-docker-widget"
v-if="shouldRenderDockerReport"
type="codequality"
type="docker"
:status="dockerStatus"
loading-text="Loading clair report"
error-text="Failed to load clair report"
:success-text="dockerText"
:unresolvedIssues="mr.dockerReport.unapproved"
:resolvedIssues="mr.dockerReport.approved"
:unresolved-issues="mr.dockerReport.unapproved"
:neutral-issues="mr.dockerReport.approved"
:info-text="dockerInformationText"
/>
<div class="mr-widget-section">
<component
......
......@@ -91,20 +91,26 @@ export default class MergeRequestStore extends CEMergeRequestStore {
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 => !data.unapproved.find(el => el === item.vulnerability)) || [];
.filter(item => !unapproved.find(el => el === item.vulnerability)) || [];
this.dockerReport.unapproved = parsedVulnerabilities
.filter(item => data.unapproved.find(el => el === item.vulnerability)) || [];
.filter(item => unapproved.find(el => el === item.vulnerability)) || [];
}
// TODO: Add links
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,
}));
}
......
......@@ -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');
});
});
});
......@@ -8,6 +8,8 @@ import mockData, {
basePerformance,
headPerformance,
securityIssues,
dockerReport,
dockerReportParsed,
} from './mock_data';
import mountComponent from '../helpers/vue_mount_component_helper';
......@@ -437,6 +439,19 @@ 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);
......@@ -447,13 +462,74 @@ describe('ee merge request widget options', () => {
});
describe('with successful request', () => {
it('should render provided data', () => {
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', () => {
it('should render error indicator', () => {
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);
});
});
});
......@@ -497,31 +573,6 @@ describe('ee merge request widget options', () => {
});
});
describe('shouldRenderDockerReport', () => {
it('returns undefined when clair is not set up', () => {
vm = mountComponent(Component, {
mrData: {
...mockData,
},
});
expect(vm.shouldRenderDockerReport).toEqual(undefined);
});
it('returns clair object when clair is set up', () => {
vm = mountComponent(Component, {
mrData: {
...mockData,
clair: {
path: 'foo',
},
},
});
expect(vm.shouldRenderDockerReport).toEqual({ path: 'foo' });
});
});
describe('dockerText', () => {
beforeEach(() => {
vm = mountComponent(Component, {
......@@ -555,7 +606,7 @@ describe('ee merge request widget options', () => {
}],
unapproved: [],
};
expect(vm.dockerText).toEqual('Found 1 vulnerability');
expect(vm.dockerText).toEqual('Found 1 approved vulnerability');
});
it('returns approved information - plural', () => {
......
......@@ -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