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 Vue from 'vue';
import Flash from '../flash'; import Flash from '~/flash';
import PipelinesMediator from './pipeline_details_mediatior'; import Translate from '~/vue_shared/translate';
import PipelinesMediator from './pipeline_details_mediator';
import pipelineGraph from './components/graph/graph_component.vue'; import pipelineGraph from './components/graph/graph_component.vue';
import pipelineHeader from './components/header_component.vue'; import pipelineHeader from './components/header_component.vue';
import SecurityReportApp from './components/security_reports/security_report_app.vue';
import eventHub from './event_hub'; import eventHub from './event_hub';
Vue.use(Translate);
document.addEventListener('DOMContentLoaded', () => { document.addEventListener('DOMContentLoaded', () => {
const dataset = document.querySelector('.js-pipeline-details-vue').dataset; const dataset = document.querySelector('.js-pipeline-details-vue').dataset;
...@@ -66,4 +71,36 @@ document.addEventListener('DOMContentLoaded', () => { ...@@ -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 Visibility from 'visibilityjs';
import Flash from '../flash'; import Flash from '../flash';
import Poll from '../lib/utils/poll'; import Poll from '../lib/utils/poll';
import { __ } from '../locale';
import PipelineStore from './stores/pipeline_store'; import PipelineStore from './stores/pipeline_store';
import PipelineService from './services/pipeline_service'; import PipelineService from './services/pipeline_service';
...@@ -55,4 +56,16 @@ export default class pipelinesMediator { ...@@ -55,4 +56,16 @@ export default class pipelinesMediator {
.then(response => this.successCallback(response)) .then(response => this.successCallback(response))
.catch(() => this.errorCallback()); .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 { ...@@ -16,4 +16,8 @@ export default class PipelineService {
postAction(endpoint) { postAction(endpoint) {
return Vue.http.post(`${endpoint}.json`); 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 { export default class PipelineStore {
constructor() { constructor() {
this.state = {}; this.state = {};
this.state.pipeline = {}; this.state.pipeline = {};
/* EE only */
this.state.securityReports = securityState;
} }
storePipeline(pipeline = {}) { storePipeline(pipeline = {}) {
this.state.pipeline = pipeline; this.state.pipeline = pipeline;
} }
/**
* EE only
*/
storeSastReport(data) {
Object.assign(this.state.securityReports.sast, setSastReport({ head: data, headBlobPath: '' }));
}
} }
...@@ -17,7 +17,7 @@ ...@@ -17,7 +17,7 @@
Failed Jobs Failed Jobs
%span.badge.js-failures-counter= failed_builds.count %span.badge.js-failures-counter= failed_builds.count
- if sast_artifact - 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 = link_to security_project_pipeline_path(@project, @pipeline), data: {target: 'div#js-tab-security', action: 'security', toggle: 'tab' }, class: 'security-tab' do
Security Report Security Report
...@@ -61,4 +61,4 @@ ...@@ -61,4 +61,4 @@
%pre.build-log= build_summary(build, skip: index >= 10) %pre.build-log= build_summary(build, skip: index >= 10)
- if sast_artifact - if sast_artifact
#js-tab-security.build-security.tab-pane #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 CEWidgetOptions from '~/vue_merge_request_widget/mr_widget_options';
import WidgetApprovals from './components/approvals/mr_widget_approvals'; import WidgetApprovals from './components/approvals/mr_widget_approvals';
import GeoSecondaryNode from './components/states/mr_widget_secondary_geo_node'; 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 { export default {
extends: CEWidgetOptions, extends: CEWidgetOptions,
...@@ -11,6 +12,9 @@ export default { ...@@ -11,6 +12,9 @@ export default {
'mr-widget-geo-secondary-node': GeoSecondaryNode, 'mr-widget-geo-secondary-node': GeoSecondaryNode,
CollapsibleSection, CollapsibleSection,
}, },
mixins: [
securityMixin,
],
data() { data() {
return { return {
isLoadingCodequality: false, isLoadingCodequality: false,
...@@ -114,79 +118,16 @@ export default { ...@@ -114,79 +118,16 @@ export default {
securityText() { securityText() {
const { newIssues, resolvedIssues } = this.mr.securityReport; const { newIssues, resolvedIssues } = this.mr.securityReport;
const text = []; return this.sastText(newIssues, resolvedIssues);
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('');
}, },
dockerText() { dockerText() {
const { vulnerabilities, approved, unapproved } = this.mr.dockerReport; const { vulnerabilities, approved, unapproved } = this.mr.dockerReport;
return this.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() { getDastText() {
if (this.mr.dastReport.length) { return this.dastText(this.mr.dastReport);
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');
}, },
codequalityStatus() { codequalityStatus() {
...@@ -208,29 +149,8 @@ export default { ...@@ -208,29 +149,8 @@ export default {
dastStatus() { dastStatus() {
return this.checkReportStatus(this.isLoadingDast, this.loadingDastFailed); 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: { methods: {
checkReportStatus(loading, error) {
if (loading) {
return 'loading';
} else if (error) {
return 'error';
}
return 'success';
},
fetchCodeQuality() { fetchCodeQuality() {
const { head_path, base_path } = this.mr.codeclimate; const { head_path, base_path } = this.mr.codeclimate;
...@@ -347,13 +267,6 @@ export default { ...@@ -347,13 +267,6 @@ export default {
this.loadingDastFailed = true; this.loadingDastFailed = true;
}); });
}, },
translateText(type) {
return {
error: s__(`ciReport|Failed to load ${type} report`),
loading: s__(`ciReport|Loading ${type} report`),
};
},
}, },
created() { created() {
if (this.shouldRenderCodeQuality) { if (this.shouldRenderCodeQuality) {
...@@ -441,7 +354,7 @@ export default { ...@@ -441,7 +354,7 @@ export default {
:success-text="dockerText" :success-text="dockerText"
:unresolved-issues="mr.dockerReport.unapproved" :unresolved-issues="mr.dockerReport.unapproved"
:neutral-issues="mr.dockerReport.approved" :neutral-issues="mr.dockerReport.approved"
:info-text="dockerInformationText" :info-text="sastContainerInformationText()"
:has-priority="true" :has-priority="true"
/> />
<collapsible-section <collapsible-section
...@@ -451,7 +364,7 @@ export default { ...@@ -451,7 +364,7 @@ export default {
:status="dastStatus" :status="dastStatus"
:loading-text="translateText('DAST').loading" :loading-text="translateText('DAST').loading"
:error-text="translateText('DAST').error" :error-text="translateText('DAST').error"
:success-text="dastText" :success-text="getDastText"
:unresolved-issues="mr.dastReport" :unresolved-issues="mr.dastReport"
:has-priority="true" :has-priority="true"
/> />
......
import CEMergeRequestStore from '~/vue_merge_request_widget/stores/mr_widget_store'; 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 { export default class MergeRequestStore extends CEMergeRequestStore {
constructor(data) { constructor(data) {
...@@ -88,101 +94,35 @@ export default class MergeRequestStore extends CEMergeRequestStore { ...@@ -88,101 +94,35 @@ export default class MergeRequestStore extends CEMergeRequestStore {
this.dast = data.dast; this.dast = data.dast;
this.dastReport = []; 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) { setSecurityReport(data) {
if (data.base) { const report = setSastReport(data);
const filterKey = 'cve'; this.securityReport.newIssues = report.newIssues;
const parsedHead = MergeRequestStore.parseIssues(data.head, data.headBlobPath); this.securityReport.resolvedIssues = report.resolvedIssues;
const parsedBase = MergeRequestStore.parseIssues(data.base, data.baseBlobPath); this.securityReport.allIssues = report.allIssues;
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 = {}) { setDockerReport(data = {}) {
const parsedVulnerabilities = MergeRequestStore const report = setSastContainerReport(data);
.parseDockerVulnerabilities(data.vulnerabilities); this.dockerReport.approved = report.approved;
this.dockerReport.unapproved = report.unapproved;
this.dockerReport.vulnerabilities = parsedVulnerabilities || []; this.dockerReport.vulnerabilities = report.vulnerabilities;
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,
}));
} }
static parseDockerVulnerabilities(data) { setDastReport(data) {
return data.map(el => ({ this.dastReport = setDastReport(data);
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) { compareCodeclimateMetrics(headIssues, baseIssues, headBlobPath, baseBlobPath) {
const parsedHeadIssues = MergeRequestStore.parseIssues(headIssues, headBlobPath); const parsedHeadIssues = parseIssues(headIssues, headBlobPath);
const parsedBaseIssues = MergeRequestStore.parseIssues(baseIssues, baseBlobPath); const parsedBaseIssues = parseIssues(baseIssues, baseBlobPath);
this.codeclimateMetrics.newIssues = MergeRequestStore.filterByKey( this.codeclimateMetrics.newIssues = filterByKey(
parsedHeadIssues, parsedHeadIssues,
parsedBaseIssues, parsedBaseIssues,
'fingerprint', 'fingerprint',
); );
this.codeclimateMetrics.resolvedIssues = MergeRequestStore.filterByKey( this.codeclimateMetrics.resolvedIssues = filterByKey(
parsedBaseIssues, parsedBaseIssues,
parsedHeadIssues, parsedHeadIssues,
'fingerprint', 'fingerprint',
...@@ -232,64 +172,6 @@ export default class MergeRequestStore extends CEMergeRequestStore { ...@@ -232,64 +172,6 @@ export default class MergeRequestStore extends CEMergeRequestStore {
this.performanceMetrics = { improved, degraded, neutral }; 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 // normalize performance metrics by indexing on performance subject and metric name
static normalizePerformanceMetrics(performanceData) { static normalizePerformanceMetrics(performanceData) {
const indexedSubjects = {}; const indexedSubjects = {};
......
...@@ -76,15 +76,15 @@ ...@@ -76,15 +76,15 @@
<h5 class="prepend-top-20">{{ instancesLabel }}</h5> <h5 class="prepend-top-20">{{ instancesLabel }}</h5>
<ul <ul
v-if="instances" v-if="instances"
class="mr-widget-code-quality-list" class="report-block-list"
> >
<li <li
v-for="(instance, i) in instances" v-for="(instance, i) in instances"
:key="i" :key="i"
class="mr-widget-code-quality-list-item-modal failed" class="report-block-list-item-modal failed"
> >
<icon <icon
class="mr-widget-code-quality-icon" class="report-block-icon"
name="status_failed_borderless" name="status_failed_borderless"
:size="32" :size="32"
/> />
...@@ -102,7 +102,7 @@ ...@@ -102,7 +102,7 @@
<expand-button v-if="instance.evidence"> <expand-button v-if="instance.evidence">
<pre <pre
slot="expanded" 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> </expand-button>
</li> </li>
</ul> </ul>
......
...@@ -111,7 +111,7 @@ ...@@ -111,7 +111,7 @@
}; };
</script> </script>
<template> <template>
<section class="mr-widget-code-quality mr-widget-section"> <section class="report-block mr-widget-section">
<div <div
v-if="isLoading" v-if="isLoading"
...@@ -158,7 +158,7 @@ ...@@ -158,7 +158,7 @@
</div> </div>
<div <div
class="code-quality-container" class="report-block-container"
v-if="hasIssues" v-if="hasIssues"
v-show="!isCollapsed" v-show="!isCollapsed"
> >
...@@ -166,7 +166,7 @@ ...@@ -166,7 +166,7 @@
<p <p
v-if="type === 'docker' && infoText" v-if="type === 'docker' && infoText"
v-html="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> </p>
......
...@@ -117,19 +117,19 @@ ...@@ -117,19 +117,19 @@
}; };
</script> </script>
<template> <template>
<ul class="mr-widget-code-quality-list"> <ul class="report-block-list">
<li <li
:class="{ :class="{
failed: isStatusFailed, failed: isStatusFailed,
success: isStatusSuccess, success: isStatusSuccess,
neutral: isStatusNeutral neutral: isStatusNeutral
}" }"
class="mr-widget-code-quality-list-item" class="report-block-list-item"
v-for="(issue, index) in issues" v-for="(issue, index) in issues"
:key="index" :key="index"
> >
<icon <icon
class="mr-widget-code-quality-icon" class="report-block-icon"
:name="iconName" :name="iconName"
:size="32" :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 { .report-block {
.code-quality-container { .report-block-container {
border-top: 1px solid $gray-darker; border-top: 1px solid $gray-darker;
padding: $gl-padding-top; padding: $gl-padding-top;
background-color: $gray-light; background-color: $gray-light;
margin: $gl-padding -16px -16px; margin: $gl-padding -16px -16px;
.mr-widget-code-quality-info { .report-block-info {
padding-left: 10px; padding-left: 10px;
} }
.mr-widget-dast-code { .report-block-dast-code {
margin-left: 26px; margin-left: 26px;
} }
.mr-widget-code-quality-list { .report-block-list {
list-style: none; list-style: none;
padding: 0 1px; padding: 0 1px;
margin: 0; margin: 0;
...@@ -23,28 +23,28 @@ ...@@ -23,28 +23,28 @@
padding: 0 5px 4px; padding: 0 5px 4px;
} }
.mr-widget-code-quality-list-item { .report-block-list-item {
display: flex; display: flex;
} }
.mr-widget-code-quality-list-item-modal { .report-block-list-item-modal {
display: flex; display: flex;
flex-wrap: wrap; flex-wrap: wrap;
} }
.failed .mr-widget-code-quality-icon { .failed .report-block-icon {
color: $red-500; color: $red-500;
} }
.success .mr-widget-code-quality-icon { .success .report-block-icon {
color: $green-500; color: $green-500;
} }
.neutral .mr-widget-code-quality-icon { .neutral .report-block-icon {
color: $theme-gray-700; color: $theme-gray-700;
} }
.mr-widget-code-quality-icon { .report-block-icon {
margin: -5px 4px 0 0; margin: -5px 4px 0 0;
fill: currentColor; fill: currentColor;
} }
......
import _ from 'underscore'; import _ from 'underscore';
import Vue from 'vue'; import Vue from 'vue';
import PipelineMediator from '~/pipelines/pipeline_details_mediatior'; import PipelineMediator from '~/pipelines/pipeline_details_mediator';
describe('PipelineMdediator', () => { describe('PipelineMdediator', () => {
let mediator; let mediator;
......
import PipelineStore from '~/pipelines/stores/pipeline_store'; import PipelineStore from '~/pipelines/stores/pipeline_store';
import securityState from 'ee/vue_shared/security_reports/helpers/state';
describe('Pipeline Store', () => { describe('Pipeline Store', () => {
let store; let store;
...@@ -8,7 +9,6 @@ describe('Pipeline Store', () => { ...@@ -8,7 +9,6 @@ describe('Pipeline Store', () => {
}); });
it('should set defaults', () => { it('should set defaults', () => {
expect(store.state).toEqual({ pipeline: {} });
expect(store.state.pipeline).toEqual({}); expect(store.state.pipeline).toEqual({});
}); });
...@@ -24,4 +24,11 @@ describe('Pipeline Store', () => { ...@@ -24,4 +24,11 @@ describe('Pipeline Store', () => {
expect(store.state.pipeline).toEqual({ foo: 'bar' }); 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, { ...@@ -9,13 +9,16 @@ import mockData, {
headIssues, headIssues,
basePerformance, basePerformance,
headPerformance, headPerformance,
securityIssuesBase, } from './mock_data';
securityIssues, import {
sastIssues,
sastIssuesBase,
dockerReport, dockerReport,
dockerReportParsed, dockerReportParsed,
dast, dast,
parsedDast, parsedDast,
} from './mock_data'; } from '../vue_shared/security_reports/mock_data';
import mountComponent from '../helpers/vue_mount_component_helper'; import mountComponent from '../helpers/vue_mount_component_helper';
describe('ee merge request widget options', () => { describe('ee merge request widget options', () => {
...@@ -60,8 +63,8 @@ describe('ee merge request widget options', () => { ...@@ -60,8 +63,8 @@ describe('ee merge request widget options', () => {
beforeEach(() => { beforeEach(() => {
mock = mock = new MockAdapter(axios); mock = mock = new MockAdapter(axios);
mock.onGet('path.json').reply(200, securityIssuesBase); mock.onGet('path.json').reply(200, sastIssuesBase);
mock.onGet('head_path.json').reply(200, securityIssues); mock.onGet('head_path.json').reply(200, sastIssues);
vm = mountComponent(Component); vm = mountComponent(Component);
}); });
...@@ -418,13 +421,13 @@ describe('ee merge request widget options', () => { ...@@ -418,13 +421,13 @@ describe('ee merge request widget options', () => {
Vue.nextTick(() => { Vue.nextTick(() => {
expect( 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.'); ).toContain('Unapproved vulnerabilities (red) can be marked as approved.');
expect( 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'); ).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].name);
expect(firstVulnerability).toContain(dockerReportParsed.unapproved[0].path); expect(firstVulnerability).toContain(dockerReportParsed.unapproved[0].path);
...@@ -503,7 +506,7 @@ describe('ee merge request widget options', () => { ...@@ -503,7 +506,7 @@ describe('ee merge request widget options', () => {
vm.$el.querySelector('.js-dast-widget button').click(); vm.$el.querySelector('.js-dast-widget button').click();
Vue.nextTick(() => { 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].name);
expect(firstVulnerability).toContain(parsedDast[0].priority); expect(firstVulnerability).toContain(parsedDast[0].priority);
done(); done();
......
This diff is collapsed.
...@@ -3,19 +3,21 @@ import { stateKey } from '~/vue_merge_request_widget/stores/state_maps'; ...@@ -3,19 +3,21 @@ import { stateKey } from '~/vue_merge_request_widget/stores/state_maps';
import mockData, { import mockData, {
headIssues, headIssues,
baseIssues, baseIssues,
securityIssues,
securityIssuesBase,
parsedBaseIssues, parsedBaseIssues,
parsedHeadIssues, parsedHeadIssues,
parsedSecurityIssuesStore, } from '../mock_data';
parsedSecurityIssuesBaseStore, import {
sastIssues,
sastIssuesBase,
parsedSastBaseStore,
parsedSastIssuesHead,
parsedSastIssuesStore,
allIssuesParsed, allIssuesParsed,
parsedSecurityIssuesHead,
dockerReport, dockerReport,
dockerReportParsed, dockerReportParsed,
dast, dast,
parsedDast, parsedDast,
} from '../mock_data'; } from '../../vue_shared/security_reports/mock_data';
describe('MergeRequestStore', () => { describe('MergeRequestStore', () => {
let store; let store;
...@@ -98,37 +100,24 @@ describe('MergeRequestStore', () => { ...@@ -98,37 +100,24 @@ describe('MergeRequestStore', () => {
describe('setSecurityReport', () => { describe('setSecurityReport', () => {
it('should set security issues with head', () => { it('should set security issues with head', () => {
store.setSecurityReport({ head: securityIssues, headBlobPath: 'path' }); store.setSecurityReport({ head: sastIssues, headBlobPath: 'path' });
expect(store.securityReport.newIssues).toEqual(parsedSecurityIssuesStore); expect(store.securityReport.newIssues).toEqual(parsedSastIssuesStore);
}); });
it('should set security issues with head and base', () => { it('should set security issues with head and base', () => {
store.setSecurityReport({ store.setSecurityReport({
head: securityIssues, head: sastIssues,
headBlobPath: 'path', headBlobPath: 'path',
base: securityIssuesBase, base: sastIssuesBase,
baseBlobPath: 'path', baseBlobPath: 'path',
}); });
expect(store.securityReport.newIssues).toEqual(parsedSecurityIssuesHead); expect(store.securityReport.newIssues).toEqual(parsedSastIssuesHead);
expect(store.securityReport.resolvedIssues).toEqual(parsedSecurityIssuesBaseStore); expect(store.securityReport.resolvedIssues).toEqual(parsedSastBaseStore);
expect(store.securityReport.allIssues).toEqual(allIssuesParsed); 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', () => { describe('isNothingToMergeState', () => {
it('returns true when nothingToMerge', () => { it('returns true when nothingToMerge', () => {
store.state = stateKey.nothingToMerge; store.state = stateKey.nothingToMerge;
...@@ -163,16 +152,6 @@ describe('MergeRequestStore', () => { ...@@ -163,16 +152,6 @@ describe('MergeRequestStore', () => {
}); });
}); });
describe('parseDockerVulnerabilities', () => {
it('parses docker report', () => {
expect(
MergeRequestStore.parseDockerVulnerabilities(dockerReport.vulnerabilities),
).toEqual(
dockerReportParsed.vulnerabilities,
);
});
});
describe('initDastReport', () => { describe('initDastReport', () => {
it('sets the defaults', () => { it('sets the defaults', () => {
store.initDastReport({ dast: { path: 'dast.json' } }); store.initDastReport({ dast: { path: 'dast.json' } });
......
import Vue from 'vue'; 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'; import mountComponent from '../../../helpers/vue_mount_component_helper';
describe('mr widget modal', () => { describe('mr widget modal', () => {
......
import Vue from 'vue'; 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 mountComponent from '../../../helpers/vue_mount_component_helper';
import { codequalityParsedIssues } from '../../../vue_mr_widget/mock_data'; import { codequalityParsedIssues } from '../../../vue_mr_widget/mock_data';
...@@ -63,7 +63,7 @@ describe('Report Collapsible section', () => { ...@@ -63,7 +63,7 @@ describe('Report Collapsible section', () => {
Vue.nextTick(() => { Vue.nextTick(() => {
expect( expect(
vm.$el.querySelector('.code-quality-container').getAttribute('style'), vm.$el.querySelector('.report-block-container').getAttribute('style'),
).toEqual(''); ).toEqual('');
expect( expect(
vm.$el.querySelector('button').textContent.trim(), vm.$el.querySelector('button').textContent.trim(),
...@@ -73,7 +73,7 @@ describe('Report Collapsible section', () => { ...@@ -73,7 +73,7 @@ describe('Report Collapsible section', () => {
Vue.nextTick(() => { Vue.nextTick(() => {
expect( expect(
vm.$el.querySelector('.code-quality-container').getAttribute('style'), vm.$el.querySelector('.report-block-container').getAttribute('style'),
).toEqual('display: none;'); ).toEqual('display: none;');
expect( expect(
vm.$el.querySelector('button').textContent.trim(), vm.$el.querySelector('button').textContent.trim(),
......
import Vue from 'vue'; 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 mountComponent from '../../../helpers/vue_mount_component_helper';
import { import {
securityParsedIssues,
codequalityParsedIssues, codequalityParsedIssues,
} from '../../../vue_mr_widget/mock_data';
import {
sastParsedIssues,
dockerReportParsed, dockerReportParsed,
parsedDast, parsedDast,
} from '../../../vue_mr_widget/mock_data'; } from '../mock_data';
describe('Report issues', () => { describe('Report issues', () => {
let vm; let vm;
...@@ -31,13 +33,13 @@ describe('Report issues', () => { ...@@ -31,13 +33,13 @@ describe('Report issues', () => {
}); });
it('should render a list of resolved 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', () => { 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( 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'); ).toEqual('Fixed: Insecure Dependency in Gemfile.lock:12');
}); });
}); });
...@@ -52,11 +54,11 @@ describe('Report issues', () => { ...@@ -52,11 +54,11 @@ describe('Report issues', () => {
}); });
it('should render a list of unresolved 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', () => { 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', () => { ...@@ -64,7 +66,7 @@ describe('Report issues', () => {
describe('for security issues', () => { describe('for security issues', () => {
beforeEach(() => { beforeEach(() => {
vm = mountComponent(ReportIssues, { vm = mountComponent(ReportIssues, {
issues: securityParsedIssues, issues: sastParsedIssues,
type: 'security', type: 'security',
status: 'failed', status: 'failed',
hasPriority: true, hasPriority: true,
...@@ -72,24 +74,24 @@ describe('Report issues', () => { ...@@ -72,24 +74,24 @@ describe('Report issues', () => {
}); });
it('should render a list of unresolved 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', () => { 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', () => { describe('with location', () => {
it('should render location', () => { it('should render location', () => {
vm = mountComponent(ReportIssues, { vm = mountComponent(ReportIssues, {
issues: securityParsedIssues, issues: sastParsedIssues,
type: 'security', type: 'security',
status: 'failed', status: 'failed',
}); });
expect(vm.$el.querySelector('.mr-widget-code-quality-list li').textContent).toContain('in'); expect(vm.$el.querySelector('.report-block-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 a').getAttribute('href')).toEqual(sastParsedIssues[0].urlPath);
}); });
}); });
...@@ -103,8 +105,8 @@ describe('Report issues', () => { ...@@ -103,8 +105,8 @@ describe('Report issues', () => {
status: 'failed', status: 'failed',
}); });
expect(vm.$el.querySelector('.mr-widget-code-quality-list li').textContent).not.toContain('in'); expect(vm.$el.querySelector('.report-block-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 a')).toEqual(null);
}); });
}); });
...@@ -120,25 +122,25 @@ describe('Report issues', () => { ...@@ -120,25 +122,25 @@ describe('Report issues', () => {
it('renders priority', () => { it('renders priority', () => {
expect( 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); ).toContain(dockerReportParsed.unapproved[0].priority);
}); });
it('renders CVE link', () => { it('renders CVE link', () => {
expect( 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); ).toEqual(dockerReportParsed.unapproved[0].nameLink);
expect( 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); ).toEqual(dockerReportParsed.unapproved[0].name);
}); });
it('renders namespace', () => { it('renders namespace', () => {
expect( 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); ).toContain(dockerReportParsed.unapproved[0].path);
expect( expect(
vm.$el.querySelector('.mr-widget-code-quality-list li').textContent.trim(), vm.$el.querySelector('.report-block-list li').textContent.trim(),
).toContain('in'); ).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