Commit 577855df authored by Filipa Lacerda's avatar Filipa Lacerda

Extract store methods to common location

Create mixin with common vue methods
Mount security app in pipelines page
Adds tests
parent d2ebf467
<script>
import CollapsibleSection from 'ee/vue_shared/security_reports/components/report_collapsible_section.vue';
import securityMixin from 'ee/vue_shared/security_reports/mixins/security_report_mixin';
import LoadingIcon from '../../../vue_shared/components/loading_icon.vue';
export default {
name: 'SecurityReportTab',
components: {
LoadingIcon,
CollapsibleSection,
},
mixins: [
securityMixin,
],
props: {
securityReports: {
type: Object,
required: true,
},
},
};
</script>
<template>
<div class="pipeline-graph">
<collapsible-section
class="js-sast-widget"
type="security"
:status="checkReportStatus(securityReports.sast.isLoading, securityReports.sast.hasError)"
:loading-text="translateText('security').loading"
:error-text="translateText('security').error"
:success-text="sastText(securityReports.sast.newIssues, securityReports.sast.resolvedIssues)"
:unresolved-issues="securityReports.sast.newIssues"
:resolved-issues="securityReports.sast.resolvedIssues"
:all-issues="securityReports.sast.allIssues"
:has-priority="true"
/>
</div>
</template>
import Vue from 'vue';
import Flash from '../flash';
import PipelinesMediator from './pipeline_details_mediatior';
import Flash from '~/flash';
import Translate from '~/vue_shared/translate';
import PipelinesMediator from './pipeline_details_mediator';
import pipelineGraph from './components/graph/graph_component.vue';
import pipelineHeader from './components/header_component.vue';
import SecurityReportApp from './components/security_reports/security_report_app.vue';
import eventHub from './event_hub';
Vue.use(Translate);
document.addEventListener('DOMContentLoaded', () => {
const dataset = document.querySelector('.js-pipeline-details-vue').dataset;
......@@ -66,4 +71,36 @@ document.addEventListener('DOMContentLoaded', () => {
});
},
});
/**
* EE only
*/
const securityTab = document.getElementById('js-security-report-app');
if (securityTab) {
// eslint-disable-next-line no-new
new Vue({
el: securityTab,
components: {
SecurityReportApp,
},
data() {
return {
endpoint: this.$options.el.dataset.endpoint,
mediator,
};
},
created() {
this.mediator.fetchSastReport(this.endpoint);
},
render(createElement) {
return createElement('security-report-app', {
props: {
securityReports: this.mediator.store.state.securityReports,
},
});
},
});
}
});
import Visibility from 'visibilityjs';
import Flash from '../flash';
import Poll from '../lib/utils/poll';
import { __ } from '../locale';
import PipelineStore from './stores/pipeline_store';
import PipelineService from './services/pipeline_service';
......@@ -55,4 +56,16 @@ export default class pipelinesMediator {
.then(response => this.successCallback(response))
.catch(() => this.errorCallback());
}
/**
* EE only
*/
fetchSastReport(endpoint) {
PipelineService.getSecurityReport(endpoint)
.then(response => response.json())
.then((data) => {
this.store.storeSastData(data);
})
.catch(() => Flash(__('Something when wrong while fetching SAST.')));
}
}
......@@ -16,4 +16,8 @@ export default class PipelineService {
postAction(endpoint) {
return Vue.http.post(`${endpoint}.json`);
}
static getSecurityReport(endpoint) {
return Vue.http.get(`${endpoint}.json`);
}
}
import securityState from 'ee/vue_shared/security_reports/helpers/state';
import {
setSastReport,
} from 'ee/vue_shared/security_reports/helpers/utils';
export default class PipelineStore {
constructor() {
this.state = {};
this.state.pipeline = {};
/* EE only */
this.state.securityReports = securityState;
}
storePipeline(pipeline = {}) {
this.state.pipeline = pipeline;
}
/**
* EE only
*/
storeSastReport(data) {
Object.assign(this.state.securityReports.sast, setSastReport({ head: data, headBlobPath: '' }));
}
}
......@@ -17,7 +17,7 @@
Failed Jobs
%span.badge.js-failures-counter= failed_builds.count
- if sast_artifact
%li.js-security-tab-link{ data: { endpoint: sast_artifact_url } }
%li.js-security-tab-link
= link_to security_project_pipeline_path(@project, @pipeline), data: {target: 'div#js-tab-security', action: 'security', toggle: 'tab' }, class: 'security-tab' do
Security Report
......@@ -61,4 +61,4 @@
%pre.build-log= build_summary(build, skip: index >= 10)
- if sast_artifact
#js-tab-security.build-security.tab-pane
%h1 PUT SECURITY REPORT HERE
#js-security-report-app{ data: { endpoint: sast_artifact_url} }
import { n__, s__, __, sprintf } from '~/locale';
import { n__, s__, __ } 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';
import CollapsibleSection from '../vue_shared/components/security_reports/report_collapsible_section.vue';
import CollapsibleSection from '../vue_shared/security_reports/components/report_collapsible_section.vue';
import securityMixin from '../vue_shared/security_reports/mixins/security_report_mixin';
export default {
extends: CEWidgetOptions,
......@@ -11,6 +12,9 @@ export default {
'mr-widget-geo-secondary-node': GeoSecondaryNode,
CollapsibleSection,
},
mixins: [
securityMixin,
],
data() {
return {
isLoadingCodequality: false,
......@@ -114,79 +118,16 @@ export default {
securityText() {
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'));
}
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('');
return this.sastText(newIssues, resolvedIssues);
},
dockerText() {
const { vulnerabilities, approved, unapproved } = this.mr.dockerReport;
if (!vulnerabilities.length) {
return s__('ciReport|SAST:container no vulnerabilities were found');
}
if (!unapproved.length && approved.length) {
return n__(
'SAST:container found %d approved vulnerability',
'SAST:container found %d approved vulnerabilities',
approved.length,
);
} else if (unapproved.length && !approved.length) {
return n__(
'SAST:container found %d vulnerability',
'SAST:container found %d vulnerabilities',
unapproved.length,
);
}
return `${n__(
'SAST:container found %d vulnerability,',
'SAST:container found %d vulnerabilities,',
vulnerabilities.length,
)} ${n__(
'of which %d is approved',
'of which %d are approved',
approved.length,
)}`;
return this.sastContainerText(vulnerabilities, approved, unapproved);
},
dastText() {
if (this.mr.dastReport.length) {
return n__(
'DAST detected %d alert by analyzing the review app',
'DAST detected %d alerts by analyzing the review app',
this.mr.dastReport.length,
);
}
return s__('ciReport|DAST detected no alerts by analyzing the review app');
getDastText() {
return this.dastText(this.mr.dastReport);
},
codequalityStatus() {
......@@ -208,29 +149,8 @@ export default {
dastStatus() {
return this.checkReportStatus(this.isLoadingDast, this.loadingDastFailed);
},
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 (error) {
return 'error';
}
return 'success';
},
fetchCodeQuality() {
const { head_path, base_path } = this.mr.codeclimate;
......@@ -347,13 +267,6 @@ export default {
this.loadingDastFailed = true;
});
},
translateText(type) {
return {
error: s__(`ciReport|Failed to load ${type} report`),
loading: s__(`ciReport|Loading ${type} report`),
};
},
},
created() {
if (this.shouldRenderCodeQuality) {
......@@ -441,7 +354,7 @@ export default {
:success-text="dockerText"
:unresolved-issues="mr.dockerReport.unapproved"
:neutral-issues="mr.dockerReport.approved"
:info-text="dockerInformationText"
:info-text="sastContainerInformationText()"
:has-priority="true"
/>
<collapsible-section
......@@ -451,7 +364,7 @@ export default {
:status="dastStatus"
:loading-text="translateText('DAST').loading"
:error-text="translateText('DAST').error"
:success-text="dastText"
:success-text="getDastText"
:unresolved-issues="mr.dastReport"
:has-priority="true"
/>
......
import CEMergeRequestStore from '~/vue_merge_request_widget/stores/mr_widget_store';
import { stripHtml } from '~/lib/utils/text_utility';
import {
parseIssues,
filterByKey,
setSastContainerReport,
setSastReport,
setDastReport,
} from '../../vue_shared/security_reports/helpers/utils';
export default class MergeRequestStore extends CEMergeRequestStore {
constructor(data) {
......@@ -88,101 +94,35 @@ 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(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);
}
const report = setSastReport(data);
this.securityReport.newIssues = report.newIssues;
this.securityReport.resolvedIssues = report.resolvedIssues;
this.securityReport.allIssues = report.allIssues;
}
setDockerReport(data = {}) {
const parsedVulnerabilities = MergeRequestStore
.parseDockerVulnerabilities(data.vulnerabilities);
this.dockerReport.vulnerabilities = parsedVulnerabilities || [];
const unapproved = data.unapproved || [];
// 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)) || [];
}
/**
* Dast Report sends some keys in HTML, we need to strip the `<p>` tags.
* This should be moved to the backend.
*
* @param {Array} data
* @returns {Array}
*/
setDastReport(data) {
this.dastReport = data.site.alerts.map(alert => ({
name: alert.name,
parsedDescription: stripHtml(alert.desc, ' '),
priority: alert.riskdesc,
...alert,
}));
const report = setSastContainerReport(data);
this.dockerReport.approved = report.approved;
this.dockerReport.unapproved = report.unapproved;
this.dockerReport.vulnerabilities = report.vulnerabilities;
}
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,
}));
setDastReport(data) {
this.dastReport = setDastReport(data);
}
compareCodeclimateMetrics(headIssues, baseIssues, headBlobPath, baseBlobPath) {
const parsedHeadIssues = MergeRequestStore.parseIssues(headIssues, headBlobPath);
const parsedBaseIssues = MergeRequestStore.parseIssues(baseIssues, baseBlobPath);
const parsedHeadIssues = parseIssues(headIssues, headBlobPath);
const parsedBaseIssues = parseIssues(baseIssues, baseBlobPath);
this.codeclimateMetrics.newIssues = MergeRequestStore.filterByKey(
this.codeclimateMetrics.newIssues = filterByKey(
parsedHeadIssues,
parsedBaseIssues,
'fingerprint',
);
this.codeclimateMetrics.resolvedIssues = MergeRequestStore.filterByKey(
this.codeclimateMetrics.resolvedIssues = filterByKey(
parsedBaseIssues,
parsedHeadIssues,
'fingerprint',
......@@ -232,64 +172,6 @@ export default class MergeRequestStore extends CEMergeRequestStore {
this.performanceMetrics = { improved, degraded, neutral };
}
/**
* In order to reuse the same component we need
* to set both codequality and security issues to have the same data structure:
* [
* {
* name: String,
* priority: String,
* fingerprint: String,
* path: String,
* line: Number,
* urlPath: String
* }
* ]
* @param {array} issues
* @return {array}
*/
static parseIssues(issues, path = '') {
return issues.map((issue) => {
const parsedIssue = {
name: issue.check_name || issue.message,
...issue,
};
// code quality
if (issue.location) {
let parseCodeQualityUrl;
if (issue.location.path) {
parseCodeQualityUrl = `${path}/${issue.location.path}`;
parsedIssue.path = issue.location.path;
}
if (issue.location.lines && issue.location.lines.begin) {
parsedIssue.line = issue.location.lines.begin;
parseCodeQualityUrl += `#L${issue.location.lines.begin}`;
}
parsedIssue.urlPath = parseCodeQualityUrl;
// security
} else if (issue.file) {
let parsedSecurityUrl = `${path}/${issue.file}`;
parsedIssue.path = issue.file;
if (issue.line) {
parsedSecurityUrl += `#L${issue.line}`;
}
parsedIssue.urlPath = parsedSecurityUrl;
}
return parsedIssue;
});
}
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
static normalizePerformanceMetrics(performanceData) {
const indexedSubjects = {};
......
......@@ -76,15 +76,15 @@
<h5 class="prepend-top-20">{{ instancesLabel }}</h5>
<ul
v-if="instances"
class="mr-widget-code-quality-list"
class="report-block-list"
>
<li
v-for="(instance, i) in instances"
:key="i"
class="mr-widget-code-quality-list-item-modal failed"
class="report-block-list-item-modal failed"
>
<icon
class="mr-widget-code-quality-icon"
class="report-block-icon"
name="status_failed_borderless"
:size="32"
/>
......@@ -102,7 +102,7 @@
<expand-button v-if="instance.evidence">
<pre
slot="expanded"
class="block mr-widget-dast-code prepend-top-10">{{ instance.evidence }}</pre>
class="block report-block-dast-code prepend-top-10">{{ instance.evidence }}</pre>
</expand-button>
</li>
</ul>
......
......@@ -111,7 +111,7 @@
};
</script>
<template>
<section class="mr-widget-code-quality mr-widget-section">
<section class="report-block mr-widget-section">
<div
v-if="isLoading"
......@@ -158,7 +158,7 @@
</div>
<div
class="code-quality-container"
class="report-block-container"
v-if="hasIssues"
v-show="!isCollapsed"
>
......@@ -166,7 +166,7 @@
<p
v-if="type === 'docker' && infoText"
v-html="infoText"
class="js-mr-code-quality-info mr-widget-code-quality-info"
class="js-mr-code-quality-info report-block-info"
>
</p>
......
......@@ -117,19 +117,19 @@
};
</script>
<template>
<ul class="mr-widget-code-quality-list">
<ul class="report-block-list">
<li
:class="{
failed: isStatusFailed,
success: isStatusSuccess,
neutral: isStatusNeutral
}"
class="mr-widget-code-quality-list-item"
class="report-block-list-item"
v-for="(issue, index) in issues"
:key="index"
>
<icon
class="mr-widget-code-quality-icon"
class="report-block-icon"
:name="iconName"
:size="32"
/>
......
export default {
sast: {
isLoading: false,
hasError: false,
newIssues: [],
resolvedIssues: [],
allIssues: [],
},
sastContainer: {
approved: [],
unapproved: [],
vulnerabilities: [],
},
dast: [],
codeclimate: {
newIssues: [],
resolvedIssues: [],
},
};
import { stripHtml } from '~/lib/utils/text_utility';
/**
* Parses SAST and Codeclimate Issues into a common and reusable format
* to reuse the same vue component.
* [
* {
* name: String,
* priority: String,
* fingerprint: String,
* path: String,
* line: Number,
* urlPath: String
* }
* ]
* @param {array} issues
* @return {array}
*/
export const parseIssues = (issues = [], path = '') => issues.map((issue) => {
const parsedIssue = {
name: issue.check_name || issue.message,
...issue,
};
// code quality
if (issue.location) {
let parseCodeQualityUrl;
if (issue.location.path) {
parseCodeQualityUrl = `${path}/${issue.location.path}`;
parsedIssue.path = issue.location.path;
}
if (issue.location.lines && issue.location.lines.begin) {
parsedIssue.line = issue.location.lines.begin;
parseCodeQualityUrl += `#L${issue.location.lines.begin}`;
}
parsedIssue.urlPath = parseCodeQualityUrl;
// security
} else if (issue.file) {
let parsedSecurityUrl = `${path}/${issue.file}`;
parsedIssue.path = issue.file;
if (issue.line) {
parsedSecurityUrl += `#L${issue.line}`;
}
parsedIssue.urlPath = parsedSecurityUrl;
}
return parsedIssue;
});
/**
* Compares two arrays by the given key and returns the difference
*
* @param {Array} firstArray
* @param {Array} secondArray
* @param {String} key
* @returns {Array}
*/
export const filterByKey = (firstArray = [], secondArray = [], key = '') => firstArray
.filter(item => !secondArray.find(el => el[key] === item[key]));
/**
* Parses DAST results into a common format to allow to use the same Vue component
* And adds an external link
*
* @param {Array} data
* @returns {Array}
*/
export const parseSastContainer = (data = []) => 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,
}));
/**
* Utils functions to set the reports
*/
/**
* Compares sast results and returns the formatted report
*
* 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
* @returns {Object}
*/
export const setSastReport = (data = {}) => {
const securityReport = {};
if (data.base) {
const filterKey = 'cve';
const parsedHead = parseIssues(data.head, data.headBlobPath);
const parsedBase = parseIssues(data.base, data.baseBlobPath);
securityReport.newIssues = filterByKey(
parsedHead,
parsedBase,
filterKey,
);
securityReport.resolvedIssues = filterByKey(
parsedBase,
parsedHead,
filterKey,
);
// Remove the new Issues and the added issues
securityReport.allIssues = filterByKey(
parsedHead,
securityReport.newIssues.concat(securityReport.resolvedIssues),
filterKey,
);
} else {
securityReport.newIssues = parseIssues(data.head, data.headBlobPath);
}
return securityReport;
};
export const setSastContainerReport = (data = {}) => {
const unapproved = data.unapproved || [];
const parsedVulnerabilities = parseSastContainer(data.vulnerabilities);
// Approved can be calculated by subtracting unapproved from vulnerabilities.
return {
vulnerabilities: parsedVulnerabilities || [],
approved: parsedVulnerabilities
.filter(item => !unapproved.find(el => el === item.vulnerability)) || [],
unapproved: parsedVulnerabilities
.filter(item => unapproved.find(el => el === item.vulnerability)) || [],
};
};
/**
* Dast Report sends some keys in HTML, we need to strip the `<p>` tags.
* This should be moved to the backend.
*
* @param {Array} data
* @returns {Array}
*/
export const setDastReport = data => data.site.alerts.map(alert => ({
name: alert.name,
parsedDescription: stripHtml(alert.desc, ' '),
priority: alert.riskdesc,
...alert,
}));
import { s__, n__, __, sprintf } from '~/locale';
export default {
methods: {
sastText(newIssues = [], resolvedIssues = []) {
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'));
}
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('');
},
translateText(type) {
return {
error: s__(`ciReport|Failed to load ${type} report`),
loading: s__(`ciReport|Loading ${type} report`),
};
},
checkReportStatus(loading, error) {
if (loading) {
return 'loading';
} else if (error) {
return 'error';
}
return 'success';
},
sastContainerText(vulnerabilities = [], approved = [], unapproved = []) {
if (!vulnerabilities.length) {
return s__('ciReport|SAST:container no vulnerabilities were found');
}
if (!unapproved.length && approved.length) {
return n__(
'SAST:container found %d approved vulnerability',
'SAST:container found %d approved vulnerabilities',
approved.length,
);
} else if (unapproved.length && !approved.length) {
return n__(
'SAST:container found %d vulnerability',
'SAST:container found %d vulnerabilities',
unapproved.length,
);
}
return `${n__(
'SAST:container found %d vulnerability,',
'SAST:container found %d vulnerabilities,',
vulnerabilities.length,
)} ${n__(
'of which %d is approved',
'of which %d are approved',
approved.length,
)}`;
},
dastText(dast = []) {
if (dast.length) {
return n__(
'DAST detected %d alert by analyzing the review app',
'DAST detected %d alerts by analyzing the review app',
dast.length,
);
}
return s__('ciReport|DAST detected no alerts by analyzing the review app');
},
sastContainerInformationText() {
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,
);
},
},
};
.mr-widget-code-quality {
.code-quality-container {
.report-block {
.report-block-container {
border-top: 1px solid $gray-darker;
padding: $gl-padding-top;
background-color: $gray-light;
margin: $gl-padding -16px -16px;
.mr-widget-code-quality-info {
.report-block-info {
padding-left: 10px;
}
.mr-widget-dast-code {
.report-block-dast-code {
margin-left: 26px;
}
.mr-widget-code-quality-list {
.report-block-list {
list-style: none;
padding: 0 1px;
margin: 0;
......@@ -23,28 +23,28 @@
padding: 0 5px 4px;
}
.mr-widget-code-quality-list-item {
.report-block-list-item {
display: flex;
}
.mr-widget-code-quality-list-item-modal {
.report-block-list-item-modal {
display: flex;
flex-wrap: wrap;
}
.failed .mr-widget-code-quality-icon {
.failed .report-block-icon {
color: $red-500;
}
.success .mr-widget-code-quality-icon {
.success .report-block-icon {
color: $green-500;
}
.neutral .mr-widget-code-quality-icon {
.neutral .report-block-icon {
color: $theme-gray-700;
}
.mr-widget-code-quality-icon {
.report-block-icon {
margin: -5px 4px 0 0;
fill: currentColor;
}
......
import _ from 'underscore';
import Vue from 'vue';
import PipelineMediator from '~/pipelines/pipeline_details_mediatior';
import PipelineMediator from '~/pipelines/pipeline_details_mediator';
describe('PipelineMdediator', () => {
let mediator;
......
import PipelineStore from '~/pipelines/stores/pipeline_store';
import securityState from 'ee/vue_shared/security_reports/helpers/state';
describe('Pipeline Store', () => {
let store;
......@@ -8,7 +9,6 @@ describe('Pipeline Store', () => {
});
it('should set defaults', () => {
expect(store.state).toEqual({ pipeline: {} });
expect(store.state.pipeline).toEqual({});
});
......@@ -24,4 +24,11 @@ describe('Pipeline Store', () => {
expect(store.state.pipeline).toEqual({ foo: 'bar' });
});
});
/**
* EE only
*/
it('should set default security state', () => {
expect(store.state.securityReports).toEqual(securityState);
});
});
......@@ -9,13 +9,16 @@ import mockData, {
headIssues,
basePerformance,
headPerformance,
securityIssuesBase,
securityIssues,
} from './mock_data';
import {
sastIssues,
sastIssuesBase,
dockerReport,
dockerReportParsed,
dast,
parsedDast,
} from './mock_data';
} from '../vue_shared/security_reports/mock_data';
import mountComponent from '../helpers/vue_mount_component_helper';
describe('ee merge request widget options', () => {
......@@ -60,8 +63,8 @@ describe('ee merge request widget options', () => {
beforeEach(() => {
mock = mock = new MockAdapter(axios);
mock.onGet('path.json').reply(200, securityIssuesBase);
mock.onGet('head_path.json').reply(200, securityIssues);
mock.onGet('path.json').reply(200, sastIssuesBase);
mock.onGet('head_path.json').reply(200, sastIssues);
vm = mountComponent(Component);
});
......@@ -418,13 +421,13 @@ describe('ee merge request widget options', () => {
Vue.nextTick(() => {
expect(
vm.$el.querySelector('.js-docker-widget .mr-widget-code-quality-info').textContent.trim(),
vm.$el.querySelector('.js-docker-widget .report-block-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(),
vm.$el.querySelector('.js-docker-widget .report-block-info a').textContent.trim(),
).toContain('Learn more about whitelisting');
const firstVulnerability = vm.$el.querySelector('.js-docker-widget .mr-widget-code-quality-list').textContent.trim();
const firstVulnerability = vm.$el.querySelector('.js-docker-widget .report-block-list').textContent.trim();
expect(firstVulnerability).toContain(dockerReportParsed.unapproved[0].name);
expect(firstVulnerability).toContain(dockerReportParsed.unapproved[0].path);
......@@ -503,7 +506,7 @@ describe('ee merge request widget options', () => {
vm.$el.querySelector('.js-dast-widget button').click();
Vue.nextTick(() => {
const firstVulnerability = vm.$el.querySelector('.js-dast-widget .mr-widget-code-quality-list').textContent.trim();
const firstVulnerability = vm.$el.querySelector('.js-dast-widget .report-block-list').textContent.trim();
expect(firstVulnerability).toContain(parsedDast[0].name);
expect(firstVulnerability).toContain(parsedDast[0].priority);
done();
......
This diff is collapsed.
......@@ -3,19 +3,21 @@ import { stateKey } from '~/vue_merge_request_widget/stores/state_maps';
import mockData, {
headIssues,
baseIssues,
securityIssues,
securityIssuesBase,
parsedBaseIssues,
parsedHeadIssues,
parsedSecurityIssuesStore,
parsedSecurityIssuesBaseStore,
} from '../mock_data';
import {
sastIssues,
sastIssuesBase,
parsedSastBaseStore,
parsedSastIssuesHead,
parsedSastIssuesStore,
allIssuesParsed,
parsedSecurityIssuesHead,
dockerReport,
dockerReportParsed,
dast,
parsedDast,
} from '../mock_data';
} from '../../vue_shared/security_reports/mock_data';
describe('MergeRequestStore', () => {
let store;
......@@ -98,37 +100,24 @@ describe('MergeRequestStore', () => {
describe('setSecurityReport', () => {
it('should set security issues with head', () => {
store.setSecurityReport({ head: securityIssues, headBlobPath: 'path' });
expect(store.securityReport.newIssues).toEqual(parsedSecurityIssuesStore);
store.setSecurityReport({ head: sastIssues, headBlobPath: 'path' });
expect(store.securityReport.newIssues).toEqual(parsedSastIssuesStore);
});
it('should set security issues with head and base', () => {
store.setSecurityReport({
head: securityIssues,
head: sastIssues,
headBlobPath: 'path',
base: securityIssuesBase,
base: sastIssuesBase,
baseBlobPath: 'path',
});
expect(store.securityReport.newIssues).toEqual(parsedSecurityIssuesHead);
expect(store.securityReport.resolvedIssues).toEqual(parsedSecurityIssuesBaseStore);
expect(store.securityReport.newIssues).toEqual(parsedSastIssuesHead);
expect(store.securityReport.resolvedIssues).toEqual(parsedSastBaseStore);
expect(store.securityReport.allIssues).toEqual(allIssuesParsed);
});
});
describe('parseIssues', () => {
it('should parse the received issues', () => {
const codequality = MergeRequestStore.parseIssues(baseIssues, 'path')[0];
expect(codequality.name).toEqual(baseIssues[0].check_name);
expect(codequality.path).toEqual(baseIssues[0].location.path);
expect(codequality.line).toEqual(baseIssues[0].location.lines.begin);
const security = MergeRequestStore.parseIssues(securityIssues, 'path')[0];
expect(security.name).toEqual(securityIssues[0].message);
expect(security.path).toEqual(securityIssues[0].file);
});
});
describe('isNothingToMergeState', () => {
it('returns true when nothingToMerge', () => {
store.state = stateKey.nothingToMerge;
......@@ -163,16 +152,6 @@ describe('MergeRequestStore', () => {
});
});
describe('parseDockerVulnerabilities', () => {
it('parses docker report', () => {
expect(
MergeRequestStore.parseDockerVulnerabilities(dockerReport.vulnerabilities),
).toEqual(
dockerReportParsed.vulnerabilities,
);
});
});
describe('initDastReport', () => {
it('sets the defaults', () => {
store.initDastReport({ dast: { path: 'dast.json' } });
......
import Vue from 'vue';
import modal from 'ee/vue_shared/components/security_reports/dast_modal.vue';
import modal from 'ee/vue_shared/security_reports/components/dast_modal.vue';
import mountComponent from '../../../helpers/vue_mount_component_helper';
describe('mr widget modal', () => {
......
import Vue from 'vue';
import reportCollapsibleSection from 'ee/vue_shared/components/security_reports/report_collapsible_section.vue';
import reportCollapsibleSection from 'ee/vue_shared/security_reports/components/report_collapsible_section.vue';
import mountComponent from '../../../helpers/vue_mount_component_helper';
import { codequalityParsedIssues } from '../../../vue_mr_widget/mock_data';
......@@ -63,7 +63,7 @@ describe('Report Collapsible section', () => {
Vue.nextTick(() => {
expect(
vm.$el.querySelector('.code-quality-container').getAttribute('style'),
vm.$el.querySelector('.report-block-container').getAttribute('style'),
).toEqual('');
expect(
vm.$el.querySelector('button').textContent.trim(),
......@@ -73,7 +73,7 @@ describe('Report Collapsible section', () => {
Vue.nextTick(() => {
expect(
vm.$el.querySelector('.code-quality-container').getAttribute('style'),
vm.$el.querySelector('.report-block-container').getAttribute('style'),
).toEqual('display: none;');
expect(
vm.$el.querySelector('button').textContent.trim(),
......
import Vue from 'vue';
import reportIssues from 'ee/vue_shared/components/security_reports/report_issues.vue';
import reportIssues from 'ee/vue_shared/security_reports/components/report_issues.vue';
import mountComponent from '../../../helpers/vue_mount_component_helper';
import {
securityParsedIssues,
codequalityParsedIssues,
} from '../../../vue_mr_widget/mock_data';
import {
sastParsedIssues,
dockerReportParsed,
parsedDast,
} from '../../../vue_mr_widget/mock_data';
} from '../mock_data';
describe('Report issues', () => {
let vm;
......@@ -31,13 +33,13 @@ describe('Report issues', () => {
});
it('should render a list of resolved issues', () => {
expect(vm.$el.querySelectorAll('.mr-widget-code-quality-list li').length).toEqual(codequalityParsedIssues.length);
expect(vm.$el.querySelectorAll('.report-block-list li').length).toEqual(codequalityParsedIssues.length);
});
it('should render "Fixed" keyword', () => {
expect(vm.$el.querySelector('.mr-widget-code-quality-list li').textContent).toContain('Fixed');
expect(vm.$el.querySelector('.report-block-list li').textContent).toContain('Fixed');
expect(
vm.$el.querySelector('.mr-widget-code-quality-list li').textContent.replace(/\s+/g, ' ').trim(),
vm.$el.querySelector('.report-block-list li').textContent.replace(/\s+/g, ' ').trim(),
).toEqual('Fixed: Insecure Dependency in Gemfile.lock:12');
});
});
......@@ -52,11 +54,11 @@ describe('Report issues', () => {
});
it('should render a list of unresolved issues', () => {
expect(vm.$el.querySelectorAll('.mr-widget-code-quality-list li').length).toEqual(codequalityParsedIssues.length);
expect(vm.$el.querySelectorAll('.report-block-list li').length).toEqual(codequalityParsedIssues.length);
});
it('should not render "Fixed" keyword', () => {
expect(vm.$el.querySelector('.mr-widget-code-quality-list li').textContent).not.toContain('Fixed');
expect(vm.$el.querySelector('.report-block-list li').textContent).not.toContain('Fixed');
});
});
});
......@@ -64,7 +66,7 @@ describe('Report issues', () => {
describe('for security issues', () => {
beforeEach(() => {
vm = mountComponent(ReportIssues, {
issues: securityParsedIssues,
issues: sastParsedIssues,
type: 'security',
status: 'failed',
hasPriority: true,
......@@ -72,24 +74,24 @@ describe('Report issues', () => {
});
it('should render a list of unresolved issues', () => {
expect(vm.$el.querySelectorAll('.mr-widget-code-quality-list li').length).toEqual(securityParsedIssues.length);
expect(vm.$el.querySelectorAll('.report-block-list li').length).toEqual(sastParsedIssues.length);
});
it('should render priority', () => {
expect(vm.$el.querySelector('.mr-widget-code-quality-list li').textContent).toContain(securityParsedIssues[0].priority);
expect(vm.$el.querySelector('.report-block-list li').textContent).toContain(sastParsedIssues[0].priority);
});
});
describe('with location', () => {
it('should render location', () => {
vm = mountComponent(ReportIssues, {
issues: securityParsedIssues,
issues: sastParsedIssues,
type: 'security',
status: 'failed',
});
expect(vm.$el.querySelector('.mr-widget-code-quality-list li').textContent).toContain('in');
expect(vm.$el.querySelector('.mr-widget-code-quality-list li a').getAttribute('href')).toEqual(securityParsedIssues[0].urlPath);
expect(vm.$el.querySelector('.report-block-list li').textContent).toContain('in');
expect(vm.$el.querySelector('.report-block-list li a').getAttribute('href')).toEqual(sastParsedIssues[0].urlPath);
});
});
......@@ -103,8 +105,8 @@ describe('Report issues', () => {
status: 'failed',
});
expect(vm.$el.querySelector('.mr-widget-code-quality-list li').textContent).not.toContain('in');
expect(vm.$el.querySelector('.mr-widget-code-quality-list li a')).toEqual(null);
expect(vm.$el.querySelector('.report-block-list li').textContent).not.toContain('in');
expect(vm.$el.querySelector('.report-block-list li a')).toEqual(null);
});
});
......@@ -120,25 +122,25 @@ describe('Report issues', () => {
it('renders priority', () => {
expect(
vm.$el.querySelector('.mr-widget-code-quality-list li').textContent.trim(),
vm.$el.querySelector('.report-block-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'),
vm.$el.querySelector('.report-block-list a').getAttribute('href'),
).toEqual(dockerReportParsed.unapproved[0].nameLink);
expect(
vm.$el.querySelector('.mr-widget-code-quality-list a').textContent.trim(),
vm.$el.querySelector('.report-block-list a').textContent.trim(),
).toEqual(dockerReportParsed.unapproved[0].name);
});
it('renders namespace', () => {
expect(
vm.$el.querySelector('.mr-widget-code-quality-list li').textContent.trim(),
vm.$el.querySelector('.report-block-list li').textContent.trim(),
).toContain(dockerReportParsed.unapproved[0].path);
expect(
vm.$el.querySelector('.mr-widget-code-quality-list li').textContent.trim(),
vm.$el.querySelector('.report-block-list li').textContent.trim(),
).toContain('in');
});
});
......
import mixin from 'ee/vue_shared/security_reports/mixins/security_report_mixin';
import {
parsedSastBaseStore,
parsedSastIssuesHead,
dockerReportParsed,
parsedDast,
} from '../mock_data';
describe('security report mixin', () => {
describe('sastText', () => {
it('returns text for new and fixed issues', () => {
expect(mixin.methods.sastText(
parsedSastIssuesHead,
parsedSastBaseStore,
)).toEqual(
'SAST improved on 1 security vulnerability and degraded on 2 security vulnerabilities',
);
});
it('returns text for new issues', () => {
expect(mixin.methods.sastText(parsedSastIssuesHead, [])).toEqual(
'SAST degraded on 2 security vulnerabilities',
);
});
it('returns text for fixed issues', () => {
expect(mixin.methods.sastText([], parsedSastIssuesHead)).toEqual(
'SAST improved on 2 security vulnerabilities',
);
});
});
describe('translateText', () => {
it('returns loading and error text for the given value', () => {
expect(mixin.methods.translateText('sast')).toEqual({
error: 'Failed to load sast report',
loading: 'Loading sast report',
});
});
});
describe('checkReportStatus', () => {
it('returns loading when loading is true', () => {
expect(mixin.methods.checkReportStatus(true, false)).toEqual('loading');
});
it('returns error when error is true', () => {
expect(mixin.methods.checkReportStatus(false, true)).toEqual('error');
});
it('returns success when loading and error are false', () => {
expect(mixin.methods.checkReportStatus(false, false)).toEqual('success');
});
});
describe('sastContainerText', () => {
it('returns no vulnerabitilties text', () => {
expect(mixin.methods.sastContainerText()).toEqual(
'SAST:container no vulnerabilities were found',
);
});
it('returns approved vulnerabilities text', () => {
expect(
mixin.methods.sastContainerText(
dockerReportParsed.vulnerabilities,
dockerReportParsed.approved,
),
).toEqual(
'SAST:container found 1 approved vulnerability',
);
});
it('returns unnapproved vulnerabilities text', () => {
expect(
mixin.methods.sastContainerText(
dockerReportParsed.vulnerabilities,
[],
dockerReportParsed.unapproved,
),
).toEqual(
'SAST:container found 2 vulnerabilities',
);
});
it('returns approved & unapproved text', () => {
expect(mixin.methods.sastContainerText(
dockerReportParsed.vulnerabilities,
dockerReportParsed.approved,
dockerReportParsed.unapproved,
)).toEqual(
'SAST:container found 3 vulnerabilities, of which 1 is approved',
);
});
});
describe('dastText', () => {
it('returns dast text', () => {
expect(mixin.methods.dastText(parsedDast)).toEqual(
'DAST detected 2 alerts by analyzing the review app',
);
});
it('returns no alert text', () => {
expect(mixin.methods.dastText()).toEqual('DAST detected no alerts by analyzing the review app');
});
});
});
import {
parseIssues,
parseSastContainer,
setSastReport,
setDastReport,
} from 'ee/vue_shared/security_reports/helpers/utils';
import {
baseIssues,
sastIssues,
sastIssuesBase,
parsedSastIssuesStore,
parsedSastBaseStore,
allIssuesParsed,
parsedSastIssuesHead,
dockerReport,
dockerReportParsed,
dast,
parsedDast,
} from '../mock_data';
describe('security reports utils', () => {
describe('parseIssues', () => {
it('should parse the received issues', () => {
const codequality = parseIssues(baseIssues, 'path')[0];
expect(codequality.name).toEqual(baseIssues[0].check_name);
expect(codequality.path).toEqual(baseIssues[0].location.path);
expect(codequality.line).toEqual(baseIssues[0].location.lines.begin);
const security = parseIssues(sastIssues, 'path')[0];
expect(security.name).toEqual(sastIssues[0].message);
expect(security.path).toEqual(sastIssues[0].file);
});
});
describe('setSastReport', () => {
it('should set security issues with head', () => {
const securityReport = setSastReport({ head: sastIssues, headBlobPath: 'path' });
expect(securityReport.newIssues).toEqual(parsedSastIssuesStore);
});
it('should set security issues with head and base', () => {
const securityReport = setSastReport({
head: sastIssues,
headBlobPath: 'path',
base: sastIssuesBase,
baseBlobPath: 'path',
});
expect(securityReport.newIssues).toEqual(parsedSastIssuesHead);
expect(securityReport.resolvedIssues).toEqual(parsedSastBaseStore);
expect(securityReport.allIssues).toEqual(allIssuesParsed);
});
});
describe('parseSastContainer', () => {
it('parses sast container report', () => {
expect(parseSastContainer(dockerReport.vulnerabilities)).toEqual(
dockerReportParsed.vulnerabilities,
);
});
});
describe('dastReport', () => {
it('parsed dast report', () => {
expect(setDastReport(dast)).toEqual(parsedDast);
});
});
});
This diff is collapsed.
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