Commit 887b3f1a authored by Phil Hughes's avatar Phil Hughes

Merge branch '4310-security-reports-step-2' into 'master'

Groups reports in MR view and splits reports in CI view

Closes #4310, #5105, #4464, and #5129

See merge request gitlab-org/gitlab-ee!5065
parents a990e800 edeab723
...@@ -7,8 +7,9 @@ import pipelineGraph from './components/graph/graph_component.vue'; ...@@ -7,8 +7,9 @@ import pipelineGraph from './components/graph/graph_component.vue';
import pipelineHeader from './components/header_component.vue'; import pipelineHeader from './components/header_component.vue';
import eventHub from './event_hub'; import eventHub from './event_hub';
import SecurityReportApp from 'ee/pipelines/components/security_reports/security_report_app.vue'; // eslint-disable-line import/first import SecurityReportApp from 'ee/vue_shared/security_reports/split_security_reports_app.vue'; // eslint-disable-line import/first
import SastSummaryWidget from 'ee/pipelines/components/security_reports/report_summary_widget.vue'; // eslint-disable-line import/first import SastSummaryWidget from 'ee/pipelines/components/security_reports/report_summary_widget.vue'; // eslint-disable-line import/first
import store from 'ee/vue_shared/security_reports/store'; // eslint-disable-line import/first
Vue.use(Translate); Vue.use(Translate);
...@@ -99,54 +100,23 @@ export default () => { ...@@ -99,54 +100,23 @@ export default () => {
const blobPath = datasetOptions.blobPath; const blobPath = datasetOptions.blobPath;
const dependencyScanningEndpoint = datasetOptions.dependencyScanningEndpoint; const dependencyScanningEndpoint = datasetOptions.dependencyScanningEndpoint;
if (endpoint) {
mediator.fetchSastReport(endpoint, blobPath)
.then(() => {
// update the badge
if (mediator.store.state.securityReports.sast.newIssues.length) {
updateBadgeCount(mediator.store.state.securityReports.sast.newIssues.length);
}
})
.catch(() => {
Flash(__('Something went wrong while fetching SAST.'));
});
}
if (dependencyScanningEndpoint) {
mediator.fetchDependencyScanningReport(dependencyScanningEndpoint)
.then(() => {
// update the badge
if (mediator.store.state.securityReports.dependencyScanning.newIssues.length) {
updateBadgeCount(
mediator.store.state.securityReports.dependencyScanning.newIssues.length,
);
}
})
.catch(() => {
Flash(__('Something went wrong while fetching Dependency Scanning.'));
});
}
// Widget summary // Widget summary
// eslint-disable-next-line no-new // eslint-disable-next-line no-new
new Vue({ new Vue({
el: sastSummary, el: sastSummary,
store,
components: { components: {
SastSummaryWidget, SastSummaryWidget,
}, },
data() { methods: {
return { updateBadge(count) {
mediator, updateBadgeCount(count);
}; },
}, },
render(createElement) { render(createElement) {
return createElement('sast-summary-widget', { return createElement('sast-summary-widget', {
props: { on: {
hasDependencyScanning: dependencyScanningEndpoint !== undefined, updateBadgeCount: this.updateBadge,
hasSast: endpoint !== undefined,
sastIssues: this.mediator.store.state.securityReports.sast.newIssues.length,
dependencyScanningIssues:
this.mediator.store.state.securityReports.dependencyScanning.newIssues.length,
}, },
}); });
}, },
...@@ -156,20 +126,24 @@ export default () => { ...@@ -156,20 +126,24 @@ export default () => {
// eslint-disable-next-line no-new // eslint-disable-next-line no-new
new Vue({ new Vue({
el: securityTab, el: securityTab,
store,
components: { components: {
SecurityReportApp, SecurityReportApp,
}, },
data() { methods: {
return { updateBadge(count) {
mediator, updateBadgeCount(count);
}; },
}, },
render(createElement) { render(createElement) {
return createElement('security-report-app', { return createElement('security-report-app', {
props: { props: {
securityReports: this.mediator.store.state.securityReports, headBlobPath: blobPath,
hasDependencyScanning: dependencyScanningEndpoint !== undefined, sastHeadPath: endpoint,
hasSast: endpoint !== undefined, dependencyScanningHeadPath: dependencyScanningEndpoint,
},
on: {
updateBadgeCount: this.updateBadge,
}, },
}); });
}, },
......
...@@ -56,23 +56,4 @@ export default class pipelinesMediator { ...@@ -56,23 +56,4 @@ export default class pipelinesMediator {
.then(response => this.successCallback(response)) .then(response => this.successCallback(response))
.catch(() => this.errorCallback()); .catch(() => this.errorCallback());
} }
/**
* EE only
*/
fetchSastReport(endpoint, blobPath) {
return PipelineService.getSecurityReport(endpoint)
.then(response => response.json())
.then((data) => {
this.store.storeSastReport(data, blobPath);
});
}
fetchDependencyScanningReport(endpoint, blobPath) {
return PipelineService.getSecurityReport(endpoint)
.then(response => response.json())
.then((data) => {
this.store.storeDependencyScanningReport(data, blobPath);
});
}
} }
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, blobPath) {
Object.assign(
this.state.securityReports.sast,
setSastReport({ head: data, headBlobPath: blobPath }),
);
}
storeDependencyScanningReport(data, blobPath) {
Object.assign(
this.state.securityReports.dependencyScanning,
setSastReport({ head: data, headBlobPath: blobPath }),
);
}
} }
...@@ -28,6 +28,10 @@ ...@@ -28,6 +28,10 @@
border-top: solid 1px $border-color; border-top: solid 1px $border-color;
} }
.mr-widget-border-top {
border-top: 1px solid $border-color;
}
.mr-widget-footer { .mr-widget-footer {
padding: 0; padding: 0;
} }
......
...@@ -27,6 +27,11 @@ ...@@ -27,6 +27,11 @@
window.gl.mrWidgetData.enable_squash_before_merge = '#{@merge_request.project.feature_available?(:merge_request_squash)}' === 'true'; window.gl.mrWidgetData.enable_squash_before_merge = '#{@merge_request.project.feature_available?(:merge_request_squash)}' === 'true';
window.gl.mrWidgetData.squash_before_merge_help_path = '#{help_page_path("user/project/merge_requests/squash_and_merge")}'; window.gl.mrWidgetData.squash_before_merge_help_path = '#{help_page_path("user/project/merge_requests/squash_and_merge")}';
window.gl.mrWidgetData.sast_help_path = '#{help_page_path("user/project/merge_requests/sast")}';
window.gl.mrWidgetData.sast_container_help_path = '#{help_page_path("user/project/merge_requests/container_scanning")}';
window.gl.mrWidgetData.dast_help_path = '#{help_page_path("user/project/merge_requests/dast")}';
window.gl.mrWidgetData.dependency_scanning_help_path = '#{help_page_path("user/project/merge_requests/dependency_scanning")}';
#js-vue-mr-widget.mr-widget #js-vue-mr-widget.mr-widget
.content-block.content-block-small.emoji-list-container.js-noteable-awards .content-block.content-block-small.emoji-list-container.js-noteable-awards
......
...@@ -65,4 +65,6 @@ ...@@ -65,4 +65,6 @@
#js-tab-security.build-security.tab-pane #js-tab-security.build-security.tab-pane
#js-security-report-app{ data: { endpoint: expose_sast_data ? sast_artifact_url(@pipeline) : nil, #js-security-report-app{ data: { endpoint: expose_sast_data ? sast_artifact_url(@pipeline) : nil,
blob_path: blob_path, blob_path: blob_path,
dependency_scanning_endpoint: expose_dependency_data ? dependency_scanning_artifact_url(@pipeline) : nil} } dependency_scanning_endpoint: expose_dependency_data ? dependency_scanning_artifact_url(@pipeline) : nil,
sast_help_path: help_page_path('user/project/merge_requests/sast'),
dependency_scanning_help_path: help_page_path('user/project/merge_requests/dependency_scanning')} }
<script> <script>
import $ from 'jquery'; import { mapState } from 'vuex';
import { n__, s__ } from '~/locale'; import $ from 'jquery';
import CiIcon from '~/vue_shared/components/ci_icon.vue'; import { n__, s__ } from '~/locale';
import CiIcon from '~/vue_shared/components/ci_icon.vue';
import LoadingIcon from '~/vue_shared/components/loading_icon.vue';
export default { export default {
name: 'SummaryReport', name: 'SummaryReport',
components: { components: {
CiIcon, CiIcon,
LoadingIcon,
},
computed: {
...mapState(['sast', 'dependencyScanning']),
sastLink() {
return this.link(this.sast.newIssues.length);
}, },
props: { dependencyScanningLink() {
sastIssues: { return this.link(this.dependencyScanning.newIssues.length);
type: Number,
required: false,
default: 0,
},
dependencyScanningIssues: {
type: Number,
required: false,
default: 0,
},
hasDependencyScanning: {
type: Boolean,
required: false,
default: false,
},
hasSast: {
type: Boolean,
required: false,
default: false,
},
}, },
computed: { sastIcon() {
sastLink() { return this.statusIcon(this.hasSastError, this.sast.newIssues.length);
return this.link(this.sastIssues);
},
dependencyScanningLink() {
return this.link(this.dependencyScanningIssues);
},
sastIcon() {
return this.statusIcon(this.sastIssues);
},
dependencyScanningIcon() {
return this.statusIcon(this.dependencyScanningIssues);
},
}, },
methods: { dependencyScanningIcon() {
openTab() { return this.statusIcon(
// This opens a tab outside of this Vue application this.hasDependencyScanningError,
// It opens the securty report tab in the pipelines page and updates the URL this.dependencyScanning.newIssues.length,
// This is needed because the tabs are built in haml+jquery );
$('.pipelines-tabs a[data-action="security"]').tab('show'); },
}, hasSast() {
link(issues) { return this.sast.paths.head !== null;
if (issues > 0) { },
return n__( hasDependencyScanning() {
'%d vulnerability', return this.dependencyScanning.paths.head !== null;
'%d vulnerabilities', },
issues, isLoadingSast() {
); return this.sast.isLoading;
} },
return s__('ciReport|no vulnerabilities'); isLoadingDependencyScanning() {
}, return this.dependencyScanning.isLoading;
statusIcon(issues) { },
if (issues > 0) { hasSastError() {
return { return this.sast.hasError;
group: 'warning', },
icon: 'status_warning', hasDependencyScanningError() {
}; return this.dependencyScanning.hasError;
} },
},
methods: {
openTab() {
// This opens a tab outside of this Vue application
// It opens the securty report tab in the pipelines page and updates the URL
// This is needed because the tabs are built in haml+jquery
$('.pipelines-tabs a[data-action="security"]').tab('show');
},
link(issuesCount = 0) {
if (issuesCount > 0) {
return n__('%d vulnerability', '%d vulnerabilities', issuesCount);
}
return s__('ciReport|no vulnerabilities');
},
statusIcon(failed = true, issuesCount = 0) {
if (issuesCount > 0 || failed) {
return { return {
group: 'success', group: 'warning',
icon: 'status_success', icon: 'status_warning',
}; };
}, }
return {
group: 'success',
icon: 'status_success',
};
}, },
}; },
};
</script> </script>
<template> <template>
<div> <div>
...@@ -82,7 +81,12 @@ ...@@ -82,7 +81,12 @@
class="well-segment flex js-sast-summary" class="well-segment flex js-sast-summary"
v-if="hasSast" v-if="hasSast"
> >
<loading-icon
v-if="isLoadingSast"
/>
<ci-icon <ci-icon
v-else
:status="sastIcon" :status="sastIcon"
class="flex flex-align-self-center" class="flex flex-align-self-center"
/> />
...@@ -90,21 +94,33 @@ ...@@ -90,21 +94,33 @@
<span <span
class="prepend-left-10 flex flex-align-self-center" class="prepend-left-10 flex flex-align-self-center"
> >
{{ s__('ciReport|SAST detected') }} <template v-if="hasSastError">
<button {{ s__('ciReport|SAST resulted in error while loading results') }}
type="button" </template>
class="btn-link btn-blank prepend-left-5" <template v-else-if="isLoadingSast">
@click="openTab" {{ s__('ciReport|SAST is loading') }}
> </template>
{{ sastLink }} <template v-else>
</button> {{ s__('ciReport|SAST detected') }}
<button
type="button"
class="btn-link btn-blank prepend-left-5"
@click="openTab"
>
{{ sastLink }}
</button>
</template>
</span> </span>
</div> </div>
<div <div
class="well-segment flex js-dss-summary" class="well-segment flex js-dss-summary"
v-if="hasDependencyScanning" v-if="hasDependencyScanning"
> >
<loading-icon
v-if="dependencyScanning.isLoading"
/>
<ci-icon <ci-icon
v-else
:status="dependencyScanningIcon" :status="dependencyScanningIcon"
class="flex flex-align-self-center" class="flex flex-align-self-center"
/> />
...@@ -112,14 +128,22 @@ ...@@ -112,14 +128,22 @@
<span <span
class="prepend-left-10 flex flex-align-self-center" class="prepend-left-10 flex flex-align-self-center"
> >
{{ s__('ciReport|Dependency scanning detected') }} <template v-if="hasDependencyScanningError">
<button {{ s__('ciReport|Dependency scanning resulted in error while loading results') }}
type="button" </template>
class="btn-link btn-blank prepend-left-5" <template v-else-if="isLoadingDependencyScanning">
@click="openTab" {{ s__('ciReport|Dependency scanning is loading') }}
> </template>
{{ dependencyScanningLink }} <template v-else>
</button> {{ s__('ciReport|Dependency scanning detected') }}
<button
type="button"
class="btn-link btn-blank prepend-left-5"
@click="openTab"
>
{{ dependencyScanningLink }}
</button>
</template>
</span> </span>
</div> </div>
</div> </div>
......
<script>
import ReportSection from 'ee/vue_shared/security_reports/components/report_section.vue';
import securityMixin from 'ee/vue_shared/security_reports/mixins/security_report_mixin';
import LoadingIcon from '~/vue_shared/components/loading_icon.vue';
import { SAST } from 'ee/vue_shared/security_reports/helpers/constants';
export default {
name: 'SecurityReportTab',
components: {
LoadingIcon,
ReportSection,
},
mixins: [securityMixin],
sast: SAST,
props: {
securityReports: {
type: Object,
required: true,
},
hasDependencyScanning: {
type: Boolean,
required: false,
default: false,
},
hasSast: {
type: Boolean,
required: false,
default: false,
},
},
};
</script>
<template>
<div class="pipeline-tab-content">
<report-section
v-if="hasSast"
class="js-sast-widget"
:type="$options.sast"
: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"
/>
<report-section
v-if="hasDependencyScanning"
class="js-dependency-scanning-widget"
:class="{ 'prepend-top-20': hasSast }"
:type="$options.sast"
:status="checkReportStatus(
securityReports.dependencyScanning.isLoading,
securityReports.dependencyScanning.hasError
)"
:loading-text="translateText('dependency scanning').loading"
:error-text="translateText('dependency scanning').error"
:success-text="depedencyScanningText(
securityReports.dependencyScanning.newIssues,
securityReports.dependencyScanning.resolvedIssues
)"
:unresolved-issues="securityReports.dependencyScanning.newIssues"
:resolved-issues="securityReports.dependencyScanning.resolvedIssues"
:all-issues="securityReports.dependencyScanning.allIssues"
/>
</div>
</template>
import { n__, s__, __ } from '~/locale'; import { n__, s__, __, sprintf } 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 ReportSection from '../vue_shared/security_reports/components/report_section.vue'; import ReportSection from '../vue_shared/security_reports/components/report_section.vue';
import securityMixin from '../vue_shared/security_reports/mixins/security_report_mixin'; import GroupedSecurityReportsApp from '../vue_shared/security_reports/grouped_security_reports_app.vue';
import { import reportsMixin from '../vue_shared/security_reports/mixins/reports_mixin';
SAST,
DAST,
SAST_CONTAINER,
} from '../vue_shared/security_reports/helpers/constants';
export default { export default {
extends: CEWidgetOptions, extends: CEWidgetOptions,
components: { components: {
'mr-widget-approvals': WidgetApprovals, 'mr-widget-approvals': WidgetApprovals,
'mr-widget-geo-secondary-node': GeoSecondaryNode, 'mr-widget-geo-secondary-node': GeoSecondaryNode,
GroupedSecurityReportsApp,
ReportSection, ReportSection,
}, },
mixins: [securityMixin], mixins: [reportsMixin],
dast: DAST,
sast: SAST,
sastContainer: SAST_CONTAINER,
data() { data() {
return { return {
isLoadingCodequality: false, isLoadingCodequality: false,
isLoadingPerformance: false, isLoadingPerformance: false,
isLoadingSecurity: false,
isLoadingDocker: false,
isLoadingDast: false,
isLoadingDependencyScanning: false,
loadingCodequalityFailed: false, loadingCodequalityFailed: false,
loadingPerformanceFailed: false, loadingPerformanceFailed: false,
loadingSecurityFailed: false,
loadingDockerFailed: false,
loadingDastFailed: false,
loadingDependencyScanningFailed: false,
}; };
}, },
computed: { computed: {
...@@ -45,21 +31,34 @@ export default { ...@@ -45,21 +31,34 @@ export default {
const { codeclimate } = this.mr; const { codeclimate } = this.mr;
return codeclimate && codeclimate.head_path && codeclimate.base_path; return codeclimate && codeclimate.head_path && codeclimate.base_path;
}, },
hasCodequalityIssues() {
return (
this.mr.codeclimateMetrics &&
((this.mr.codeclimateMetrics.newIssues &&
this.mr.codeclimateMetrics.newIssues.length > 0) ||
(this.mr.codeclimateMetrics.resolvedIssues &&
this.mr.codeclimateMetrics.resolvedIssues.length > 0))
);
},
hasPerformanceMetrics() {
return (
this.mr.performanceMetrics &&
((this.mr.performanceMetrics.degraded && this.mr.performanceMetrics.degraded.length > 0) ||
(this.mr.performanceMetrics.improved && this.mr.performanceMetrics.improved.length > 0) ||
(this.mr.performanceMetrics.neutral && this.mr.performanceMetrics.neutral.length > 0))
);
},
shouldRenderPerformance() { shouldRenderPerformance() {
const { performance } = this.mr; const { performance } = this.mr;
return performance && performance.head_path && performance.base_path; return performance && performance.head_path && performance.base_path;
}, },
shouldRenderSecurityReport() { shouldRenderSecurityReport() {
return this.mr.sast && this.mr.sast.head_path; return (
}, (this.mr.sast && this.mr.sast.head_path) ||
shouldRenderDockerReport() { (this.mr.sastContainer && this.mr.sastContainer.head_path) ||
return this.mr.sastContainer && this.mr.sastContainer.head_path; (this.mr.dast && this.mr.dast.head_path) ||
}, (this.mr.dependencyScanning && this.mr.dependencyScanning.head_path)
shouldRenderDastReport() { );
return this.mr.dast && this.mr.dast.head_path;
},
shouldRenderDependencyReport() {
return this.mr.dependencyScanning && this.mr.dependencyScanning.head_path;
}, },
codequalityText() { codequalityText() {
const { newIssues, resolvedIssues } = this.mr.codeclimateMetrics; const { newIssues, resolvedIssues } = this.mr.codeclimateMetrics;
...@@ -71,13 +70,7 @@ export default { ...@@ -71,13 +70,7 @@ export default {
text.push(s__('ciReport|Code quality')); text.push(s__('ciReport|Code quality'));
if (resolvedIssues.length) { if (resolvedIssues.length) {
text.push( text.push(n__(' improved on %d point', ' improved on %d points', resolvedIssues.length));
n__(
' improved on %d point',
' improved on %d points',
resolvedIssues.length,
),
);
} }
if (newIssues.length > 0 && resolvedIssues.length > 0) { if (newIssues.length > 0 && resolvedIssues.length > 0) {
...@@ -85,13 +78,7 @@ export default { ...@@ -85,13 +78,7 @@ export default {
} }
if (newIssues.length) { if (newIssues.length) {
text.push( text.push(n__(' degraded on %d point', ' degraded on %d points', newIssues.length));
n__(
' degraded on %d point',
' degraded on %d points',
newIssues.length,
),
);
} }
} }
...@@ -108,13 +95,7 @@ export default { ...@@ -108,13 +95,7 @@ export default {
text.push(s__('ciReport|Performance metrics')); text.push(s__('ciReport|Performance metrics'));
if (improved.length) { if (improved.length) {
text.push( text.push(n__(' improved on %d point', ' improved on %d points', improved.length));
n__(
' improved on %d point',
' improved on %d points',
improved.length,
),
);
} }
if (improved.length > 0 && degraded.length > 0) { if (improved.length > 0 && degraded.length > 0) {
...@@ -122,75 +103,19 @@ export default { ...@@ -122,75 +103,19 @@ export default {
} }
if (degraded.length) { if (degraded.length) {
text.push( text.push(n__(' degraded on %d point', ' degraded on %d points', degraded.length));
n__(
' degraded on %d point',
' degraded on %d points',
degraded.length,
),
);
} }
} }
return text.join(''); return text.join('');
}, },
securityText() {
const { newIssues, resolvedIssues, allIssues } = this.mr.securityReport;
return this.sastText(newIssues, resolvedIssues, allIssues);
},
dependencyScanningText() {
const { newIssues, resolvedIssues, allIssues } = this.mr.dependencyScanningReport;
return this.depedencyScanningText(newIssues, resolvedIssues, allIssues);
},
dockerText() {
const { vulnerabilities, approved, unapproved } = this.mr.dockerReport;
return this.sastContainerText(vulnerabilities, approved, unapproved);
},
getDastText() {
return this.dastText(this.mr.dastReport);
},
codequalityStatus() { codequalityStatus() {
return this.checkReportStatus( return this.checkReportStatus(this.isLoadingCodequality, this.loadingCodequalityFailed);
this.isLoadingCodequality,
this.loadingCodequalityFailed,
);
}, },
performanceStatus() { performanceStatus() {
return this.checkReportStatus( return this.checkReportStatus(this.isLoadingPerformance, this.loadingPerformanceFailed);
this.isLoadingPerformance,
this.loadingPerformanceFailed,
);
},
securityStatus() {
return this.checkReportStatus(
this.isLoadingSecurity,
this.loadingSecurityFailed,
);
},
dockerStatus() {
return this.checkReportStatus(
this.isLoadingDocker,
this.loadingDockerFailed,
);
},
dastStatus() {
return this.checkReportStatus(this.isLoadingDast, this.loadingDastFailed);
},
dependencyScanningStatus() {
return this.checkReportStatus(
this.isLoadingDependencyScanning,
this.loadingDependencyScanningFailed,
);
}, },
}, },
methods: { methods: {
...@@ -199,10 +124,7 @@ export default { ...@@ -199,10 +124,7 @@ export default {
this.isLoadingCodequality = true; this.isLoadingCodequality = true;
Promise.all([ Promise.all([this.service.fetchReport(head_path), this.service.fetchReport(base_path)])
this.service.fetchReport(head_path),
this.service.fetchReport(base_path),
])
.then(values => { .then(values => {
this.mr.compareCodeclimateMetrics( this.mr.compareCodeclimateMetrics(
values[0], values[0],
...@@ -223,10 +145,7 @@ export default { ...@@ -223,10 +145,7 @@ export default {
this.isLoadingPerformance = true; this.isLoadingPerformance = true;
Promise.all([ Promise.all([this.service.fetchReport(head_path), this.service.fetchReport(base_path)])
this.service.fetchReport(head_path),
this.service.fetchReport(base_path),
])
.then(values => { .then(values => {
this.mr.comparePerformanceMetrics(values[0], values[1]); this.mr.comparePerformanceMetrics(values[0], values[1]);
this.isLoadingPerformance = false; this.isLoadingPerformance = false;
...@@ -236,122 +155,16 @@ export default { ...@@ -236,122 +155,16 @@ export default {
this.loadingPerformanceFailed = true; this.loadingPerformanceFailed = true;
}); });
}, },
/**
* Sast report can either have 2 reports or just 1
* When it has 2 we need to compare them
* When it has 1 we render the output given
*/
fetchSecurity() {
const { sast } = this.mr;
this.isLoadingSecurity = true; translateText(type) {
return {
if (sast.base_path && sast.head_path) { error: sprintf(s__('ciReport|Failed to load %{reportName} report'), {
Promise.all([ reportName: type,
this.service.fetchReport(sast.head_path), }),
this.service.fetchReport(sast.base_path), loading: sprintf(s__('ciReport|Loading %{reportName} report'), {
]) reportName: type,
.then(values => { }),
this.handleSecuritySuccess({ };
head: values[0],
headBlobPath: this.mr.headBlobPath,
base: values[1],
baseBlobPath: this.mr.baseBlobPath,
});
})
.catch(() => this.handleSecurityError());
} else if (sast.head_path) {
this.service
.fetchReport(sast.head_path)
.then(data => {
this.handleSecuritySuccess({
head: data,
headBlobPath: this.mr.headBlobPath,
});
})
.catch(() => this.handleSecurityError());
}
},
fetchDependencyScanning() {
const { dependencyScanning } = this.mr;
this.isLoadingDependencyScanning = true;
if (dependencyScanning.base_path && dependencyScanning.head_path) {
Promise.all([
this.service.fetchReport(dependencyScanning.head_path),
this.service.fetchReport(dependencyScanning.base_path),
])
.then(values => {
this.mr.setDependencyScanningReport({
head: values[0],
headBlobPath: this.mr.headBlobPath,
base: values[1],
baseBlobPath: this.mr.baseBlobPath,
});
this.isLoadingDependencyScanning = false;
})
.catch(() => {
this.isLoadingDependencyScanning = false;
this.loadingDependencyScanningFailed = true;
});
} else if (dependencyScanning.head_path) {
this.service
.fetchReport(dependencyScanning.head_path)
.then(data => {
this.mr.setDependencyScanningReport({
head: data,
headBlobPath: this.mr.headBlobPath,
});
this.isLoadingDependencyScanning = false;
})
.catch(() => {
this.isLoadingDependencyScanning = false;
this.loadingDependencyScanningFailed = true;
});
}
},
handleSecuritySuccess(data) {
this.mr.setSecurityReport(data);
this.isLoadingSecurity = false;
},
handleSecurityError() {
this.isLoadingSecurity = false;
this.loadingSecurityFailed = true;
},
fetchDockerReport() {
const { head_path } = this.mr.sastContainer;
this.isLoadingDocker = true;
this.service
.fetchReport(head_path)
.then(data => {
this.mr.setDockerReport(data);
this.isLoadingDocker = false;
})
.catch(() => {
this.isLoadingDocker = false;
this.loadingDockerFailed = true;
});
},
fetchDastReport() {
this.isLoadingDast = true;
this.service
.fetchReport(this.mr.dast.head_path)
.then(data => {
this.mr.setDastReport(data);
this.isLoadingDast = false;
})
.catch(() => {
this.isLoadingDast = false;
this.loadingDastFailed = true;
});
}, },
}, },
created() { created() {
...@@ -362,22 +175,6 @@ export default { ...@@ -362,22 +175,6 @@ export default {
if (this.shouldRenderPerformance) { if (this.shouldRenderPerformance) {
this.fetchPerformance(); this.fetchPerformance();
} }
if (this.shouldRenderSecurityReport) {
this.fetchSecurity();
}
if (this.shouldRenderDockerReport) {
this.fetchDockerReport();
}
if (this.shouldRenderDastReport) {
this.fetchDastReport();
}
if (this.shouldRenderDependencyReport) {
this.fetchDependencyScanning();
}
}, },
template: ` template: `
<div class="mr-state-widget prepend-top-default"> <div class="mr-state-widget prepend-top-default">
...@@ -397,7 +194,7 @@ export default { ...@@ -397,7 +194,7 @@ export default {
v-if="shouldRenderApprovals" v-if="shouldRenderApprovals"
:mr="mr" :mr="mr"
:service="service" :service="service"
/> />
<report-section <report-section
class="js-codequality-widget" class="js-codequality-widget"
v-if="shouldRenderCodeQuality" v-if="shouldRenderCodeQuality"
...@@ -408,6 +205,7 @@ export default { ...@@ -408,6 +205,7 @@ export default {
:success-text="codequalityText" :success-text="codequalityText"
:unresolved-issues="mr.codeclimateMetrics.newIssues" :unresolved-issues="mr.codeclimateMetrics.newIssues"
:resolved-issues="mr.codeclimateMetrics.resolvedIssues" :resolved-issues="mr.codeclimateMetrics.resolvedIssues"
:has-issues="hasCodequalityIssues"
/> />
<report-section <report-section
class="js-performance-widget" class="js-performance-widget"
...@@ -420,52 +218,24 @@ export default { ...@@ -420,52 +218,24 @@ export default {
:unresolved-issues="mr.performanceMetrics.degraded" :unresolved-issues="mr.performanceMetrics.degraded"
:resolved-issues="mr.performanceMetrics.improved" :resolved-issues="mr.performanceMetrics.improved"
:neutral-issues="mr.performanceMetrics.neutral" :neutral-issues="mr.performanceMetrics.neutral"
:has-issues="hasPerformanceMetrics"
/> />
<report-section <grouped-security-reports-app
class="js-sast-widget"
v-if="shouldRenderSecurityReport" v-if="shouldRenderSecurityReport"
:type="$options.sast" :head-blob-path="mr.headBlobPath"
:status="securityStatus" :base-blob-path="mr.baseBlobPath"
:loading-text="translateText('security').loading" :sast-head-path="mr.sast.head_path"
:error-text="translateText('security').error" :sast-base-path="mr.sast.base_path"
:success-text="securityText" :sast-help-path="mr.sastHelp"
:unresolved-issues="mr.securityReport.newIssues" :dast-head-path="mr.dast.head_path"
:resolved-issues="mr.securityReport.resolvedIssues" :dast-base-path="mr.dast.base_path"
:all-issues="mr.securityReport.allIssues" :dast-help-path="mr.dastHelp"
/> :sast-container-head-path="mr.sastContainer.head_path"
<report-section :sast-container-base-path="mr.sastContainer.base_path"
class="js-dependency-scanning-widget" :sast-container-help-path="mr.sastContainerHelp"
v-if="shouldRenderDependencyReport" :dependency-scanning-head-path="mr.dependencyScanning.head_path"
:type="$options.sast" :dependency-scanning-base-path="mr.dependencyScanning.base_path"
:status="dependencyScanningStatus" :dependency-scanning-help-path="mr.dependencyScanningHelp"
:loading-text="translateText('dependency scanning').loading"
:error-text="translateText('dependency scanning').error"
:success-text="dependencyScanningText"
:unresolved-issues="mr.dependencyScanningReport.newIssues"
:resolved-issues="mr.dependencyScanningReport.resolvedIssues"
:all-issues="mr.dependencyScanningReport.allIssues"
/>
<report-section
class="js-docker-widget"
v-if="shouldRenderDockerReport"
:type="$options.sastContainer"
:status="dockerStatus"
:loading-text="translateText('sast:container').loading"
:error-text="translateText('sast:container').error"
:success-text="dockerText"
:unresolved-issues="mr.dockerReport.unapproved"
:neutral-issues="mr.dockerReport.approved"
:info-text="sastContainerInformationText()"
/>
<report-section
class="js-dast-widget"
v-if="shouldRenderDastReport"
:type="$options.dast"
:status="dastStatus"
:loading-text="translateText('DAST').loading"
:error-text="translateText('DAST').error"
:success-text="getDastText"
:unresolved-issues="mr.dastReport"
/> />
<div class="mr-widget-section"> <div class="mr-widget-section">
<component <component
......
import CEMergeRequestStore from '~/vue_merge_request_widget/stores/mr_widget_store'; import CEMergeRequestStore from '~/vue_merge_request_widget/stores/mr_widget_store';
import { import { filterByKey } from '../../vue_shared/security_reports/store/utils';
parseCodeclimateMetrics,
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) {
...@@ -14,13 +8,17 @@ export default class MergeRequestStore extends CEMergeRequestStore { ...@@ -14,13 +8,17 @@ export default class MergeRequestStore extends CEMergeRequestStore {
const blobPath = data.blob_path || {}; const blobPath = data.blob_path || {};
this.headBlobPath = blobPath.head_path || ''; this.headBlobPath = blobPath.head_path || '';
this.baseBlobPath = blobPath.base_path || ''; this.baseBlobPath = blobPath.base_path || '';
this.sast = data.sast || {};
this.sastContainer = data.sast_container || {};
this.dast = data.dast || {};
this.dependencyScanning = data.dependency_scanning || {};
this.sastHelp = data.sast_help_path;
this.sastContainerHelp = data.sast_container_help_path;
this.dastHelp = data.dast_help_path;
this.dependencyScanningHelp = data.dependency_scanning_help_path;
this.initCodeclimate(data); this.initCodeclimate(data);
this.initPerformanceReport(data); this.initPerformanceReport(data);
this.initSecurityReport(data);
this.initDockerReport(data);
this.initDastReport(data);
this.initDependencyScanningReport(data);
} }
setData(data) { setData(data) {
...@@ -70,69 +68,13 @@ export default class MergeRequestStore extends CEMergeRequestStore { ...@@ -70,69 +68,13 @@ export default class MergeRequestStore extends CEMergeRequestStore {
this.performanceMetrics = { this.performanceMetrics = {
improved: [], improved: [],
degraded: [], degraded: [],
neutral: [],
}; };
} }
initSecurityReport(data) {
this.sast = data.sast;
this.securityReport = {
newIssues: [],
resolvedIssues: [],
allIssues: [],
};
}
initDockerReport(data) {
this.sastContainer = data.sast_container;
this.dockerReport = {
approved: [],
unapproved: [],
vulnerabilities: [],
};
}
initDastReport(data) {
this.dast = data.dast;
this.dastReport = [];
}
initDependencyScanningReport(data) {
this.dependencyScanning = data.dependency_scanning;
this.dependencyScanningReport = {
newIssues: [],
resolvedIssues: [],
allIssues: [],
};
}
setSecurityReport(data) {
const report = setSastReport(data);
this.securityReport.newIssues = report.newIssues;
this.securityReport.resolvedIssues = report.resolvedIssues;
this.securityReport.allIssues = report.allIssues;
}
setDockerReport(data = {}) {
const report = setSastContainerReport(data);
this.dockerReport.approved = report.approved;
this.dockerReport.unapproved = report.unapproved;
this.dockerReport.vulnerabilities = report.vulnerabilities;
}
setDastReport(data) {
this.dastReport = setDastReport(data);
}
setDependencyScanningReport(data) {
const report = setSastReport(data);
this.dependencyScanningReport.newIssues = report.newIssues;
this.dependencyScanningReport.resolvedIssues = report.resolvedIssues;
this.dependencyScanningReport.allIssues = report.allIssues;
}
compareCodeclimateMetrics(headIssues, baseIssues, headBlobPath, baseBlobPath) { compareCodeclimateMetrics(headIssues, baseIssues, headBlobPath, baseBlobPath) {
const parsedHeadIssues = parseCodeclimateMetrics(headIssues, headBlobPath); const parsedHeadIssues = MergeRequestStore.parseCodeclimateMetrics(headIssues, headBlobPath);
const parsedBaseIssues = parseCodeclimateMetrics(baseIssues, baseBlobPath); const parsedBaseIssues = MergeRequestStore.parseCodeclimateMetrics(baseIssues, baseBlobPath);
this.codeclimateMetrics.newIssues = filterByKey( this.codeclimateMetrics.newIssues = filterByKey(
parsedHeadIssues, parsedHeadIssues,
...@@ -202,4 +144,30 @@ export default class MergeRequestStore extends CEMergeRequestStore { ...@@ -202,4 +144,30 @@ export default class MergeRequestStore extends CEMergeRequestStore {
return indexedSubjects; return indexedSubjects;
} }
static parseCodeclimateMetrics(issues = [], path = '') {
return issues.map(issue => {
const parsedIssue = {
...issue,
name: issue.description,
};
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;
}
}
return parsedIssue;
});
}
} }
<script> <script>
/** /**
* Renders DAST body text * Renders DAST body text
* [priority]: [name] * [priority]: [name]
*/ */
export default {
name: 'SastIssueBody',
props: {
issue: {
type: Object,
required: true,
},
issueIndex: { export default {
type: Number, name: 'SastIssueBody',
required: true, props: {
}, issue: {
type: Object,
required: true,
},
issueIndex: {
type: Number,
required: true,
},
modalTargetId: { modalTargetId: {
type: String, type: String,
required: true, required: true,
},
}, },
},
methods: { methods: {
openDastModal() { openDastModal() {
this.$emit('openDastModal', this.issue, this.issueIndex); this.$emit('openDastModal', this.issue, this.issueIndex);
},
}, },
}; },
};
</script> </script>
<template> <template>
<div class="report-block-list-issue-description prepend-top-5 append-bottom-5"> <div class="report-block-list-issue-description prepend-top-5 append-bottom-5">
......
<script>
import CiIcon from '~/vue_shared/components/ci_icon.vue';
/**
* Renders the error row for each security report
*/
export default {
name: 'SecurityErrorRow',
components: {
CiIcon,
},
computed: {
iconStatus() {
return {
group: 'warning',
icon: 'status_warning',
};
},
},
};
</script>
<template>
<div class="report-block-list-issue prepend-left-default append-right-default">
<div class="report-block-list-icon append-right-10 prepend-left-5">
<ci-icon :status="iconStatus" />
</div>
<div class="report-block-list-issue-description">
{{ __("There was an error loading results") }}
</div>
</div>
</template>
<script> <script>
import $ from 'jquery';
import _ from 'underscore';
import Icon from '~/vue_shared/components/icon.vue'; import Icon from '~/vue_shared/components/icon.vue';
import popover from '~/vue_shared/directives/popover'; import {
togglePopover,
inserted,
mouseenter,
mouseleave,
} from '~/feature_highlight/feature_highlight_helper';
export default { export default {
name: 'SecurityReportsHelpPopover', name: 'SecurityReportsHelpPopover',
components: { components: {
Icon, Icon,
}, },
directives: {
popover,
},
props: { props: {
options: { options: {
type: Object, type: Object,
required: true, required: true,
}, },
}, },
computed: { mounted() {
popoverOptions() { $(this.$el)
return { .popover({
html: true, html: true,
trigger: 'focus', trigger: 'focus',
container: 'body',
placement: 'top', placement: 'top',
template: template:
'<div class="popover" role="tooltip"><div class="arrow"></div><p class="popover-title"></p><div class="popover-content"></div></div>', '<div class="popover" role="tooltip"><div class="arrow"></div><p class="popover-title"></p><div class="popover-content"></div></div>',
...this.options, ...this.options,
}; })
}, .on('mouseenter', mouseenter)
.on('mouseleave', _.debounce(mouseleave, 300))
.on('inserted.bs.popover', inserted)
.on('show.bs.popover', () => {
window.addEventListener('scroll', togglePopover.bind(this.$el, false), { once: true });
});
}, },
}; };
</script> </script>
<template> <template>
<button <button
type="button" type="button"
class="btn btn-transparent" class="btn btn-blank btn-transparent btn-help"
v-popover="popoverOptions"
tabindex="0" tabindex="0"
> >
<icon name="question" /> <icon name="question" />
......
<script>
import IssuesBlock from './report_issues.vue';
import SastContainerInfo from './sast_container_info.vue';
import { SAST_CONTAINER } from '../store/constants';
/**
* Renders block of issues
*/
export default {
components: {
IssuesBlock,
SastContainerInfo,
},
sastContainer: SAST_CONTAINER,
props: {
unresolvedIssues: {
type: Array,
required: false,
default: () => [],
},
resolvedIssues: {
type: Array,
required: false,
default: () => [],
},
neutralIssues: {
type: Array,
required: false,
default: () => [],
},
allIssues: {
type: Array,
required: false,
default: () => [],
},
isFullReportVisible: {
type: Boolean,
required: false,
default: false,
},
type: {
type: String,
required: true,
},
},
};
</script>
<template>
<div class="report-block-container">
<sast-container-info v-if="type === $options.sastContainer" />
<issues-block
class="js-mr-code-new-issues"
v-if="unresolvedIssues.length"
:type="type"
status="failed"
:issues="unresolvedIssues"
/>
<issues-block
class="js-mr-code-all-issues"
v-if="isFullReportVisible"
:type="type"
status="failed"
:issues="allIssues"
/>
<issues-block
class="js-mr-code-non-issues"
v-if="neutralIssues.length"
:type="type"
status="neutral"
:issues="neutralIssues"
/>
<issues-block
class="js-mr-code-resolved-issues"
v-if="resolvedIssues.length"
:type="type"
status="success"
:issues="resolvedIssues"
/>
</div>
</template>
<script>
import LoadingIcon from '~/vue_shared/components/loading_icon.vue';
/**
* Renders the loading row for each security report
*/
export default {
name: 'SecurityLoadingRow',
components: {
LoadingIcon,
},
};
</script>
<template>
<div class="report-block-list-issue prepend-left-default append-right-default">
<div class="report-block-list-icon append-right-10 prepend-left-5">
<loading-icon />
</div>
<div class="report-block-list-issue-description">
{{ __("in progress") }}
</div>
</div>
</template>
<script> <script>
import $ from 'jquery'; import $ from 'jquery';
import Icon from '~/vue_shared/components/icon.vue'; import Icon from '~/vue_shared/components/icon.vue';
import Modal from '~/vue_shared/components/gl_modal.vue'; import Modal from '~/vue_shared/components/gl_modal.vue';
import ExpandButton from '~/vue_shared/components/expand_button.vue'; import ExpandButton from '~/vue_shared/components/expand_button.vue';
import PerformanceIssue from 'ee/vue_merge_request_widget/components/performance_issue_body.vue'; import PerformanceIssue from 'ee/vue_merge_request_widget/components/performance_issue_body.vue';
import CodequalityIssue from 'ee/vue_merge_request_widget/components/codequality_issue_body.vue'; import CodequalityIssue from 'ee/vue_merge_request_widget/components/codequality_issue_body.vue';
import SastIssue from './sast_issue_body.vue'; import SastIssue from './sast_issue_body.vue';
import SastContainerIssue from './sast_container_issue_body.vue'; import SastContainerIssue from './sast_container_issue_body.vue';
import DastIssue from './dast_issue_body.vue'; import DastIssue from './dast_issue_body.vue';
import { SAST, DAST, SAST_CONTAINER } from '../helpers/constants'; import { SAST, DAST, SAST_CONTAINER } from '../store/constants';
const modalDefaultData = { const modalDefaultData = {
modalId: 'modal-mrwidget-issue', modalId: 'modal-mrwidget-issue',
modalDesc: '', modalDesc: '',
modalTitle: '', modalTitle: '',
modalInstances: [], modalInstances: [],
modalTargetId: '#modal-mrwidget-issue', modalTargetId: '#modal-mrwidget-issue',
}; };
export default { export default {
name: 'ReportIssues', name: 'ReportIssues',
components: { components: {
Modal, Modal,
Icon, Icon,
ExpandButton, ExpandButton,
SastIssue, SastIssue,
SastContainerIssue, SastContainerIssue,
DastIssue, DastIssue,
PerformanceIssue, PerformanceIssue,
CodequalityIssue, CodequalityIssue,
}, },
props: { props: {
issues: { issues: {
type: Array, type: Array,
required: true, required: true,
}, },
// security || codequality || performance || docker || dast // security || codequality || performance || docker || dast
type: { type: {
type: String, type: String,
required: true, required: true,
}, },
// failed || success // failed || success
status: { status: {
type: String, type: String,
required: true, required: true,
}, },
}, },
data() { data() {
return modalDefaultData; return modalDefaultData;
}, },
computed: { computed: {
iconName() { iconName() {
if (this.isStatusFailed) { if (this.isStatusFailed) {
return 'status_failed_borderless'; return 'status_failed_borderless';
} else if (this.isStatusSuccess) { } else if (this.isStatusSuccess) {
return 'status_success_borderless'; return 'status_success_borderless';
} }
return 'status_created_borderless'; return 'status_created_borderless';
}, },
isStatusFailed() { isStatusFailed() {
return this.status === 'failed'; return this.status === 'failed';
}, },
isStatusSuccess() { isStatusSuccess() {
return this.status === 'success'; return this.status === 'success';
}, },
isStatusNeutral() { isStatusNeutral() {
return this.status === 'neutral'; return this.status === 'neutral';
}, },
isTypeCodequality() { isTypeCodequality() {
return this.type === 'codequality'; return this.type === 'codequality';
}, },
isTypePerformance() { isTypePerformance() {
return this.type === 'performance'; return this.type === 'performance';
}, },
isTypeSast() { isTypeSast() {
return this.type === SAST; return this.type === SAST;
}, },
isTypeSastContainer() { isTypeSastContainer() {
return this.type === SAST_CONTAINER; return this.type === SAST_CONTAINER;
}, },
isTypeDast() { isTypeDast() {
return this.type === DAST; return this.type === DAST;
}, },
}, },
mounted() { mounted() {
$(this.$refs.modal).on('hidden.bs.modal', () => { $(this.$refs.modal).on('hidden.bs.modal', () => {
this.clearModalData(); this.clearModalData();
}); });
}, },
methods: { methods: {
getmodalId(index) { getmodalId(index) {
return `modal-mrwidget-issue-${index}`; return `modal-mrwidget-issue-${index}`;
}, },
modalIdTarget(index) { modalIdTarget(index) {
return `#${this.getmodalId(index)}`; return `#${this.getmodalId(index)}`;
}, },
openDastModal(issue, index) { openDastModal(issue, index) {
this.modalId = this.getmodalId(index); this.modalId = this.getmodalId(index);
this.modalTitle = `${issue.priority}: ${issue.name}`; this.modalTitle = `${issue.priority}: ${issue.name}`;
this.modalTargetId = `#${this.getmodalId(index)}`; this.modalTargetId = `#${this.getmodalId(index)}`;
this.modalInstances = issue.instances; this.modalInstances = issue.instances;
this.modalDesc = issue.parsedDescription; this.modalDesc = issue.parsedDescription;
}, },
/** /**
* Because of https://vuejs.org/v2/guide/list.html#Caveats * Because of https://vuejs.org/v2/guide/list.html#Caveats
* we need to clear the instances to make sure everything is properly reset. * we need to clear the instances to make sure everything is properly reset.
*/ */
clearModalData() { clearModalData() {
this.modalId = modalDefaultData.modalId; this.modalId = modalDefaultData.modalId;
this.modalDesc = modalDefaultData.modalDesc; this.modalDesc = modalDefaultData.modalDesc;
this.modalTitle = modalDefaultData.modalTitle; this.modalTitle = modalDefaultData.modalTitle;
this.modalInstances = modalDefaultData.modalInstances; this.modalInstances = modalDefaultData.modalInstances;
this.modalTargetId = modalDefaultData.modalTargetId; this.modalTargetId = modalDefaultData.modalTargetId;
}, },
}, },
}; };
</script> </script>
<template> <template>
<div> <div>
......
<script> <script>
export default { export default {
name: 'ReportIssueLink', name: 'ReportIssueLink',
props: { props: {
issue: { issue: {
type: Object, type: Object,
required: true, required: true,
},
}, },
}; },
};
</script> </script>
<template> <template>
<div class="report-block-list-issue-description-link"> <div class="report-block-list-issue-description-link">
......
<script> <script>
import { __ } from '~/locale'; import { __ } from '~/locale';
import StatusIcon from '~/vue_merge_request_widget/components/mr_widget_status_icon.vue'; import StatusIcon from '~/vue_merge_request_widget/components/mr_widget_status_icon.vue';
import LoadingIcon from '~/vue_shared/components/loading_icon.vue'; import LoadingIcon from '~/vue_shared/components/loading_icon.vue';
import IssuesBlock from './report_issues.vue'; import IssuesList from './issues_list.vue';
import Popover from './help_popover.vue';
export default { import { LOADING, ERROR, SUCCESS } from '../store/constants';
name: 'ReportSection',
components: { export default {
IssuesBlock, name: 'ReportSection',
LoadingIcon, components: {
StatusIcon, IssuesList,
}, LoadingIcon,
props: { StatusIcon,
isCollapsible: { Popover,
type: Boolean, },
required: false, props: {
default: true, type: {
}, type: String,
// security | codequality | performance | docker required: false,
type: { default: '',
type: String, },
required: true, status: {
}, type: String,
// loading | success | error required: true,
status: { },
type: String, loadingText: {
required: true, type: String,
}, required: false,
loadingText: { default: '',
type: String, },
required: true, errorText: {
}, type: String,
errorText: { required: false,
type: String, default: '',
required: true, },
}, successText: {
successText: { type: String,
type: String, required: true,
required: true, },
}, unresolvedIssues: {
unresolvedIssues: { type: Array,
type: Array, required: false,
required: false, default: () => [],
default: () => ([]), },
}, resolvedIssues: {
resolvedIssues: { type: Array,
type: Array, required: false,
required: false, default: () => [],
default: () => ([]), },
}, neutralIssues: {
neutralIssues: { type: Array,
type: Array, required: false,
required: false, default: () => [],
default: () => ([]), },
}, allIssues: {
allIssues: { type: Array,
type: Array, required: false,
required: false, default: () => [],
default: () => ([]), },
}, infoText: {
infoText: { type: [String, Boolean],
type: [String, Boolean], required: false,
required: false, default: false,
default: false, },
}, hasIssues: {
hasPriority: { type: Boolean,
type: Boolean, required: true,
required: false, },
default: false, popoverOptions: {
}, type: Object,
default: () => ({}),
required: false,
},
},
data() {
return {
collapseText: __('Expand'),
isCollapsed: true,
isFullReportVisible: false,
};
},
computed: {
isLoading() {
return this.status === LOADING;
},
loadingFailed() {
return this.status === ERROR;
}, },
isSuccess() {
return this.status === SUCCESS;
},
statusIconName() {
if (this.loadingFailed || this.unresolvedIssues.length || this.neutralIssues.length) {
return 'warning';
}
return 'success';
},
headerText() {
if (this.isLoading) {
return this.loadingText;
}
data() { if (this.isSuccess) {
if (this.isCollapsible) { return this.successText;
return {
collapseText: __('Expand'),
isCollapsed: true,
isFullReportVisible: false,
};
} }
return { if (this.loadingFailed) {
isFullReportVisible: true, return this.errorText;
}; }
},
computed: { return '';
isLoading() { },
return this.status === 'loading'; hasPopover() {
}, return Object.keys(this.popoverOptions).length > 0;
loadingFailed() {
return this.status === 'error';
},
isSuccess() {
return this.status === 'success';
},
statusIconName() {
if (this.loadingFailed ||
this.unresolvedIssues.length ||
this.neutralIssues.length) {
return 'warning';
}
return 'success';
},
hasIssues() {
return this.unresolvedIssues.length ||
this.resolvedIssues.length ||
this.allIssues.length ||
this.neutralIssues.length;
},
}, },
},
methods: { methods: {
toggleCollapsed() { toggleCollapsed() {
this.isCollapsed = !this.isCollapsed; this.isCollapsed = !this.isCollapsed;
const text = this.isCollapsed ? __('Expand') : __('Collapse'); const text = this.isCollapsed ? __('Expand') : __('Collapse');
this.collapseText = text; this.collapseText = text;
}, },
openFullReport() { openFullReport() {
this.isFullReportVisible = true; this.isFullReportVisible = true;
}, },
}, },
}; };
</script> </script>
<template> <template>
<section class="report-block mr-widget-section"> <section>
<div <div
v-if="isLoading" class="media prepend-top-default prepend-left-default
class="media" append-right-default append-bottom-default"
> >
<div <loading-icon
class="mr-widget-icon" class="mr-widget-icon"
> v-if="isLoading"
<loading-icon /> />
</div>
<div
class="media-body"
>
{{ loadingText }}
</div>
</div>
<div
v-else-if="isSuccess"
class="media"
>
<status-icon <status-icon
v-else
:status="statusIconName" :status="statusIconName"
/> />
<div <div
class="media-body space-children" class="media-body space-children"
> >
<span <span
class="js-code-text code-text" class="js-code-text code-text"
> >
{{ successText }} {{ headerText }}
<popover
v-if="hasPopover"
class="prepend-left-5"
:options="popoverOptions"
/>
</span> </span>
<button <button
type="button" type="button"
class="js-collapse-btn btn bt-default pull-right btn-sm" class="js-collapse-btn btn bt-default pull-right btn-sm"
v-if="isCollapsible && hasIssues" v-if="hasIssues"
@click="toggleCollapsed" @click="toggleCollapsed"
> >
{{ collapseText }} {{ collapseText }}
...@@ -172,71 +172,28 @@ ...@@ -172,71 +172,28 @@
</div> </div>
<div <div
class="report-block-container" class="js-report-section-container"
v-if="hasIssues" v-if="hasIssues"
v-show="!isCollapsible || (isCollapsible && !isCollapsed)" v-show="!isCollapsed"
> >
<slot name="body">
<issues-list
:unresolved-issues="unresolvedIssues"
:resolved-issues="resolvedIssues"
:all-issues="allIssues"
:type="type"
:is-full-report-visible="isFullReportVisible"
/>
<p <button
v-if="infoText" v-if="allIssues.length && !isFullReportVisible"
v-html="infoText" type="button"
class="js-mr-code-quality-info prepend-left-10 report-block-info" class="btn-link btn-blank prepend-left-10 js-expand-full-list break-link"
> @click="openFullReport"
</p> >
{{ s__("ciReport|Show complete code vulnerabilities report") }}
<issues-block </button>
class="js-mr-code-new-issues" </slot>
v-if="unresolvedIssues.length"
:type="type"
status="failed"
:issues="unresolvedIssues"
:has-priority="hasPriority"
/>
<issues-block
class="js-mr-code-all-issues"
v-if="isFullReportVisible"
:type="type"
status="failed"
:issues="allIssues"
:has-priority="hasPriority"
/>
<issues-block
class="js-mr-code-non-issues"
v-if="neutralIssues.length"
:type="type"
status="neutral"
:issues="neutralIssues"
:has-priority="hasPriority"
/>
<issues-block
class="js-mr-code-resolved-issues"
v-if="resolvedIssues.length"
:type="type"
status="success"
:issues="resolvedIssues"
:has-priority="hasPriority"
/>
<button
v-if="allIssues.length && !isFullReportVisible"
type="button"
class="btn-link btn-blank prepend-left-10 js-expand-full-list break-link"
@click="openFullReport"
>
{{ s__("ciReport|Show complete code vulnerabilities report") }}
</button>
</div>
<div
v-else-if="loadingFailed"
class="media"
>
<status-icon status="notfound" />
<div class="media-body">
{{ errorText }}
</div>
</div> </div>
</section> </section>
</template> </template>
<script>
export default {
name: 'SastContainerInfo',
};
</script>
<template>
<p
class="prepend-top-10 prepend-left-10 report-block-info js-mr-code-quality-info"
>
{{ s__('ciReport|Unapproved vulnerabilities (red) can be marked as approved.') }}
<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>
</p>
</template>
<script> <script>
/** /**
* Renders SAST CONTAINER body text * Renders SAST CONTAINER body text
* [priority]: [name|link] in [link]:[line] * [priority]: [name|link] in [link]:[line]
*/ */
import ReportLink from './report_link.vue'; import ReportLink from './report_link.vue';
export default { export default {
name: 'SastContainerIssueBody', name: 'SastContainerIssueBody',
components: { components: {
ReportLink, ReportLink,
}, },
props: { props: {
issue: { issue: {
type: Object, type: Object,
required: true, required: true,
},
}, },
}; },
};
</script> </script>
<template> <template>
<div class="report-block-list-issue-description prepend-top-5 append-bottom-5"> <div class="report-block-list-issue-description prepend-top-5 append-bottom-5">
......
<script> <script>
/** /**
* Renders SAST body text * Renders SAST body text
* [priority]: [name] in [link] : [line] * [priority]: [name] in [link] : [line]
*/ */
import ReportLink from './report_link.vue'; import ReportLink from './report_link.vue';
export default { export default {
name: 'SastIssueBody', name: 'SastIssueBody',
components: { components: {
ReportLink, ReportLink,
}, },
props: { props: {
issue: { issue: {
type: Object, type: Object,
required: true, required: true,
},
}, },
}; },
};
</script> </script>
<template> <template>
<div class="report-block-list-issue-description prepend-top-5 append-bottom-5"> <div class="report-block-list-issue-description prepend-top-5 append-bottom-5">
......
<script> <script>
import CiIcon from '~/vue_shared/components/ci_icon.vue'; import CiIcon from '~/vue_shared/components/ci_icon.vue';
import LoadingIcon from '~/vue_shared/components/loading_icon.vue';
import Popover from './help_popover.vue'; import Popover from './help_popover.vue';
/** /**
...@@ -10,6 +11,7 @@ export default { ...@@ -10,6 +11,7 @@ export default {
name: 'SecuritySummaryRow', name: 'SecuritySummaryRow',
components: { components: {
CiIcon, CiIcon,
LoadingIcon,
Popover, Popover,
}, },
props: { props: {
...@@ -37,12 +39,19 @@ export default { ...@@ -37,12 +39,19 @@ export default {
}; };
</script> </script>
<template> <template>
<div class="report-block-list-issue prepend-left-default append-right-default"> <div class="report-block-list-issue report-block-list-issue-parent">
<div class="report-block-list-icon append-right-10 prepend-left-5"> <div class="report-block-list-icon append-right-10 prepend-left-5">
<ci-icon :status="iconStatus" /> <loading-icon
v-if="statusIcon === 'loading'"
css-class="report-block-list-loading-icon"
/>
<ci-icon
v-else
:status="iconStatus"
/>
</div> </div>
<div class="report-block-list-issue-description prepend-top-5 append-bottom-5"> <div class="report-block-list-issue-description">
<div class="report-block-list-issue-description-text append-right-5"> <div class="report-block-list-issue-description-text append-right-5">
{{ summary }} {{ summary }}
</div> </div>
......
<script>
import { mapActions, mapState, mapGetters } from 'vuex';
import { SAST, DAST, SAST_CONTAINER } from './store/constants';
import store from './store';
import ReportSection from './components/report_section.vue';
import SummaryRow from './components/summary_row.vue';
import IssuesList from './components/issues_list.vue';
import securityReportsMixin from './mixins/security_report_mixin';
export default {
store,
components: {
ReportSection,
SummaryRow,
IssuesList,
},
mixins: [securityReportsMixin],
props: {
headBlobPath: {
type: String,
required: true,
},
baseBlobPath: {
type: String,
required: false,
default: null,
},
sastHeadPath: {
type: String,
required: false,
default: null,
},
sastBasePath: {
type: String,
required: false,
default: null,
},
dastHeadPath: {
type: String,
required: false,
default: null,
},
dastBasePath: {
type: String,
required: false,
default: null,
},
sastContainerHeadPath: {
type: String,
required: false,
default: null,
},
sastContainerBasePath: {
type: String,
required: false,
default: null,
},
dependencyScanningHeadPath: {
type: String,
required: false,
default: null,
},
dependencyScanningBasePath: {
type: String,
required: false,
default: null,
},
sastHelpPath: {
type: String,
required: false,
default: '',
},
sastContainerHelpPath: {
type: String,
required: false,
default: '',
},
dastHelpPath: {
type: String,
required: false,
default: '',
},
dependencyScanningHelpPath: {
type: String,
required: false,
default: '',
},
},
sast: SAST,
dast: DAST,
sastContainer: SAST_CONTAINER,
computed: {
...mapState(['sast', 'sastContainer', 'dast', 'dependencyScanning', 'summaryCounts']),
...mapGetters([
'groupedSastText',
'groupedSummaryText',
'summaryStatus',
'groupedSastContainerText',
'groupedDastText',
'groupedDependencyText',
'sastStatusIcon',
'sastContainerStatusIcon',
'dastStatusIcon',
'dependencyScanningStatusIcon',
]),
},
created() {
this.setHeadBlobPath(this.headBlobPath);
this.setBaseBlobPath(this.baseBlobPath);
if (this.sastHeadPath) {
this.setSastHeadPath(this.sastHeadPath);
if (this.sastBasePath) {
this.setSastBasePath(this.sastBasePath);
}
this.fetchSastReports();
}
if (this.sastContainerHeadPath) {
this.setSastContainerHeadPath(this.sastContainerHeadPath);
if (this.sastContainerBasePath) {
this.setSastContainerBasePath(this.sastContainerBasePath);
}
this.fetchSastContainerReports();
}
if (this.dastHeadPath) {
this.setDastHeadPath(this.dastHeadPath);
if (this.dastBasePath) {
this.setDastBasePath(this.dastBasePath);
}
this.fetchDastReports();
}
if (this.dependencyScanningHeadPath) {
this.setDependencyScanningHeadPath(this.dependencyScanningHeadPath);
if (this.dependencyScanningBasePath) {
this.setDependencyScanningBasePath(this.dependencyScanningBasePath);
}
this.fetchDependencyScanningReports();
}
},
methods: {
...mapActions([
'setAppType',
'setHeadBlobPath',
'setBaseBlobPath',
'setSastHeadPath',
'setSastBasePath',
'setSastContainerHeadPath',
'setSastContainerBasePath',
'setDastHeadPath',
'setDastBasePath',
'setDependencyScanningHeadPath',
'setDependencyScanningBasePath',
'fetchSastReports',
'fetchSastContainerReports',
'fetchDastReports',
'fetchDependencyScanningReports',
]),
},
};
</script>
<template>
<report-section
class="mr-widget-border-top"
:status="summaryStatus"
:success-text="groupedSummaryText"
:loading-text="groupedSummaryText"
:error-text="groupedSummaryText"
:has-issues="true"
>
<div
slot="body"
class="mr-widget-grouped-section report-block"
>
<template v-if="sastHeadPath">
<summary-row
class="js-sast-widget"
:summary="groupedSastText"
:status-icon="sastStatusIcon"
:popover-options="sastPopover"
/>
<issues-list
class="report-block-group-list"
v-if="sast.newIssues.length"
:unresolved-issues="sast.newIssues"
:resolved-issues="sast.resolvedIssues"
:all-issues="sast.allIssues"
:type="$options.sast"
/>
</template>
<template v-if="dependencyScanningHeadPath">
<summary-row
class="js-dependency-scanning-widget"
:summary="groupedDependencyText"
:status-icon="dependencyScanningStatusIcon"
:popover-options="dependencyScanningPopover"
/>
<issues-list
class="report-block-group-list"
v-if="dependencyScanning.newIssues.length"
:unresolved-issues="dependencyScanning.newIssues"
:resolved-issues="dependencyScanning.resolvedIssues"
:all-issues="dependencyScanning.allIssues"
:type="$options.sast"
/>
</template>
<template v-if="sastContainerHeadPath">
<summary-row
class="js-sast-container"
:summary="groupedSastContainerText"
:status-icon="sastContainerStatusIcon"
:popover-options="sastContainerPopover"
/>
<issues-list
class="report-block-group-list"
v-if="sastContainer.newIssues.length"
:unresolved-issues="sastContainer.newIssues"
:neutral-issues="sastContainer.resolvedIssues"
:type="$options.sastContainer"
/>
</template>
<template v-if="dastHeadPath">
<summary-row
class="js-dast-widget"
:summary="groupedDastText"
:status-icon="dastStatusIcon"
:popover-options="dastPopover"
/>
<issues-list
class="report-block-group-list"
v-if="dast.newIssues.length"
:unresolved-issues="dast.newIssues"
:resolved-issues="dast.resolvedIssues"
:type="$options.dast"
/>
</template>
</div>
</report-section>
</template>
export default {
sast: {
isLoading: false,
hasError: false,
newIssues: [],
resolvedIssues: [],
allIssues: [],
},
sastContainer: {
approved: [],
unapproved: [],
vulnerabilities: [],
},
dast: [],
codeclimate: {
newIssues: [],
resolvedIssues: [],
},
dependencyScanning: {
isLoading: false,
hasError: false,
newIssues: [],
resolvedIssues: [],
allIssues: [],
},
};
import { stripHtml } from '~/lib/utils/text_utility';
export const parseCodeclimateMetrics = (issues = [], path = '') =>
issues.map(issue => {
const parsedIssue = {
...issue,
name: issue.description,
};
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;
}
}
return parsedIssue;
});
/**
* Maps SAST & Dependency scanning issues:
* { tool: String, message: String, url: String , cve: String ,
* file: String , solution: String, priority: String }
* to contain:
* { name: String, path: String, line: String, urlPath: String, priority: String }
* @param {Array} issues
* @param {String} path
*/
export const parseSastIssues = (issues = [], path = '') =>
issues.map(issue =>
Object.assign({}, issue, {
name: issue.message,
path: issue.file,
urlPath: issue.line
? `${path}/${issue.file}#L${issue.line}`
: `${path}/${issue.file}`,
}),
);
/**
* 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 = parseSastIssues(data.head, data.headBlobPath);
const parsedBase = parseSastIssues(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 = parseSastIssues(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 {
LOADING,
ERROR,
SUCCESS,
} from '../store/constants';
export default {
methods: {
checkReportStatus(loading, error) {
if (loading) {
return LOADING;
} else if (error) {
return ERROR;
}
return SUCCESS;
},
},
};
import { s__, n__, __, sprintf } from '~/locale'; import { sprintf, s__ } from '~/locale';
export default { export default {
methods: { computed: {
sastText(newIssues = [], resolvedIssues = [], allIssues = []) { sastPopover() {
const text = [];
if (!newIssues.length && !resolvedIssues.length && !allIssues.length) {
text.push(s__('ciReport|SAST detected no security vulnerabilities'));
} else if (!newIssues.length && !resolvedIssues.length && allIssues.length) {
text.push(s__('ciReport|SAST detected no new 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('');
},
depedencyScanningText(newIssues = [], resolvedIssues = [], allIssues = []) {
const text = [];
if (!newIssues.length && !resolvedIssues.length && !allIssues.length) {
text.push(s__('ciReport|Dependency scanning detected no security vulnerabilities'));
} else if (!newIssues.length && !resolvedIssues.length && allIssues.length) {
text.push(s__('ciReport|Dependency scanning detected no new security vulnerabilities'));
} else if (newIssues.length || resolvedIssues.length) {
text.push(s__('ciReport|Dependency scanning'));
}
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 { return {
error: sprintf(s__('ciReport|Failed to load %{reportName} report'), { reportName: type }), title: s__('ciReport|Static Application Security Testing (SAST) detects known vulnerabilities in your source code.'),
loading: sprintf(s__('ciReport|Loading %{reportName} report'), { reportName: type }), content: sprintf(
s__('ciReport|%{linkStartTag}Learn more about SAST %{linkEndTag}'),
{
linkStartTag: `<a href="${this.sastHelpPath}">`,
linkEndTag: '</a>',
},
false,
),
}; };
}, },
sastContainerPopover() {
checkReportStatus(loading, error) { return {
if (loading) { title: s__('ciReport|Container scanning detects known vulnerabilities in your docker images.'),
return 'loading'; content: sprintf(
} else if (error) { s__('ciReport|%{linkStartTag}Learn more about SAST image %{linkEndTag}'),
return 'error'; {
} linkStartTag: `<a href="${this.sastContainerHelpPath}">`,
linkEndTag: '</a>',
return 'success'; },
}, false,
),
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,
)}`;
}, },
dastPopover() {
dastText(dast = []) { return {
if (dast.length) { title: s__('ciReport|Dynamic Application Security Testing (DAST) detects known vulnerabilities in your web application.'),
return n__( content: sprintf(
'DAST detected %d alert by analyzing the review app', s__('ciReport|%{linkStartTag}Learn more about DAST %{linkEndTag}'),
'DAST detected %d alerts by analyzing the review app', {
dast.length, linkStartTag: `<a href="${this.dastHelpPath}">`,
); linkEndTag: '</a>',
} },
false,
return s__('ciReport|DAST detected no alerts by analyzing the review app'); ),
};
}, },
dependencyScanningPopover() {
sastContainerInformationText() { return {
return sprintf( title: s__('ciReport|Dependency Scanning detects known vulnerabilities in your source code\'s dependencies.'),
s__('ciReport|Unapproved vulnerabilities (red) can be marked as approved. %{helpLink}'), { content: sprintf(
helpLink: `<a href="https://gitlab.com/gitlab-org/clair-scanner#example-whitelist-yaml-file" target="_blank" rel="noopener noreferrer nofollow"> s__('ciReport|%{linkStartTag}Learn more about Dependency Scanning %{linkEndTag}'),
${s__('ciReport|Learn more about whitelisting')} {
</a>`, linkStartTag: `<a href="${this.dependencyScanningHelpPath}">`,
}, linkEndTag: '</a>',
false, },
); false,
),
};
}, },
}, },
}; };
<script>
import { mapActions, mapState } from 'vuex';
import { s__, sprintf, n__ } from '~/locale';
import createFlash from '~/flash';
import { SAST } from './store/constants';
import store from './store';
import ReportSection from './components/report_section.vue';
import mixin from './mixins/security_report_mixin';
import reportsMixin from './mixins/reports_mixin';
export default {
store,
components: {
ReportSection,
},
mixins: [mixin, reportsMixin],
props: {
headBlobPath: {
type: String,
required: true,
},
sastHeadPath: {
type: String,
required: false,
default: null,
},
dependencyScanningHeadPath: {
type: String,
required: false,
default: null,
},
sastHelpPath: {
type: String,
required: false,
default: null,
},
dependencyScanningHelpPath: {
type: String,
required: false,
default: null,
},
},
sast: SAST,
computed: {
...mapState(['sast', 'dependencyScanning']),
sastText() {
return this.summaryTextBuilder('SAST', this.sast.newIssues.length);
},
dependencyScanningText() {
return this.summaryTextBuilder(
'Dependency scanning',
this.dependencyScanning.newIssues.length,
);
},
},
created() {
// update the store with the received props
this.setHeadBlobPath(this.headBlobPath);
if (this.sastHeadPath) {
this.setSastHeadPath(this.sastHeadPath);
this.fetchSastReports()
.then(() => {
this.$emit('updateBadgeCount', this.sast.newIssues.length);
})
.catch(() => createFlash(s__('ciReport|There was an error loading SAST report')));
}
if (this.dependencyScanningHeadPath) {
this.setDependencyScanningHeadPath(this.dependencyScanningHeadPath);
this.fetchDependencyScanningReports()
.then(() => {
this.$emit('updateBadgeCount', this.dependencyScanning.newIssues.length);
})
.catch(() =>
createFlash(s__('ciReport|There was an error loading dependency scanning report')),
);
}
},
methods: {
...mapActions([
'setHeadBlobPath',
'setSastHeadPath',
'setDependencyScanningHeadPath',
'fetchSastReports',
'fetchDependencyScanningReports',
]),
summaryTextBuilder(type, issuesCount = 0) {
if (issuesCount === 0) {
return sprintf(s__('ciReport|%{type} detected no vulnerabilities'), {
type,
});
}
return sprintf(
n__('%{type} detected %d vulnerability', '%{type} detected %d vulnerabilities', issuesCount),
{ type },
);
},
translateText(type) {
return {
error: sprintf(s__('ciReport|%{reportName} resulted in error while loading results'), {
reportName: type,
}),
loading: sprintf(s__('ciReport|%{reportName} is loading'), {
reportName: type,
}),
};
},
},
};
</script>
<template>
<div>
<report-section
v-if="sastHeadPath"
class="js-sast-widget split-report-section"
:type="$options.sast"
:status="checkReportStatus(sast.isLoading, sast.hasError)"
:loading-text="translateText('SAST').loading"
:error-text="translateText('SAST').error"
:success-text="sastText"
:unresolved-issues="sast.newIssues"
:has-issues="sast.newIssues.length > 0"
:popover-options="sastPopover"
/>
<report-section
v-if="dependencyScanningHeadPath"
class="js-dss-widget split-report-section"
:type="$options.sast"
:status="checkReportStatus(dependencyScanning.isLoading, dependencyScanning.hasError)"
:loading-text="translateText('Dependency scanning').loading"
:error-text="translateText('Dependency scanning').error"
:success-text="dependencyScanningText"
:unresolved-issues="dependencyScanning.newIssues"
:has-issues="dependencyScanning.newIssues.length > 0"
:popover-options="dependencyScanningPopover"
/>
</div>
</template>
...@@ -26,14 +26,14 @@ export const fetchSastReports = ({ state, dispatch }) => { ...@@ -26,14 +26,14 @@ export const fetchSastReports = ({ state, dispatch }) => {
dispatch('requestSastReports'); dispatch('requestSastReports');
Promise.all([ return Promise.all([
head ? axios.get(head) : Promise.resolve(), head ? axios.get(head) : Promise.resolve(),
base ? axios.get(base) : Promise.resolve(), base ? axios.get(base) : Promise.resolve(),
]) ])
.then(values => { .then(values => {
dispatch('receiveSastReports', { dispatch('receiveSastReports', {
head: values[0] ? values[0].data : null, head: values && values[0] ? values[0].data : null,
base: values[1] ? values[1].data : null, base: values && values[1] ? values[1].data : null,
}); });
}) })
.catch(() => { .catch(() => {
...@@ -65,7 +65,7 @@ export const fetchSastContainerReports = ({ state, dispatch }) => { ...@@ -65,7 +65,7 @@ export const fetchSastContainerReports = ({ state, dispatch }) => {
dispatch('requestSastContainerReports'); dispatch('requestSastContainerReports');
Promise.all([ return Promise.all([
head ? axios.get(head) : Promise.resolve(), head ? axios.get(head) : Promise.resolve(),
base ? axios.get(base) : Promise.resolve(), base ? axios.get(base) : Promise.resolve(),
]) ])
...@@ -100,14 +100,14 @@ export const fetchDastReports = ({ state, dispatch }) => { ...@@ -100,14 +100,14 @@ export const fetchDastReports = ({ state, dispatch }) => {
dispatch('requestDastReports'); dispatch('requestDastReports');
Promise.all([ return Promise.all([
head ? axios.get(head) : Promise.resolve(), head ? axios.get(head) : Promise.resolve(),
base ? axios.get(base) : Promise.resolve(), base ? axios.get(base) : Promise.resolve(),
]) ])
.then(values => { .then(values => {
dispatch('receiveDastReports', { dispatch('receiveDastReports', {
head: values[0] ? values[0].data : null, head: values && values[0] ? values[0].data : null,
base: values[1] ? values[1].data : null, base: values && values[1] ? values[1].data : null,
}); });
}) })
.catch(() => { .catch(() => {
...@@ -139,7 +139,7 @@ export const fetchDependencyScanningReports = ({ state, dispatch }) => { ...@@ -139,7 +139,7 @@ export const fetchDependencyScanningReports = ({ state, dispatch }) => {
dispatch('requestDependencyScanningReports'); dispatch('requestDependencyScanningReports');
Promise.all([ return Promise.all([
head ? axios.get(head) : Promise.resolve(), head ? axios.get(head) : Promise.resolve(),
base ? axios.get(base) : Promise.resolve(), base ? axios.get(base) : Promise.resolve(),
]) ])
......
export const SAST = 'SAST'; export const SAST = 'SAST';
export const DAST = 'DAST'; export const DAST = 'DAST';
export const SAST_CONTAINER = 'SAST_CONTAINER'; export const SAST_CONTAINER = 'SAST_CONTAINER';
export const LOADING = 'LOADING';
export const ERROR = 'ERROR';
export const SUCCESS = 'SUCCESS';
import { n__, s__ } from '~/locale'; import { n__, s__ } from '~/locale';
import { textBuilder, statusIcon } from './utils'; import { textBuilder, statusIcon } from './utils';
import { LOADING, ERROR, SUCCESS } from './constants';
export const groupedSastText = ({ sast }) => export const groupedSastText = ({ sast }) => {
textBuilder( if (sast.hasError) {
return s__('ciReport|SAST resulted in error while loading results');
}
if (sast.isLoading) {
return s__('ciReport|SAST is loading');
}
return textBuilder(
'SAST', 'SAST',
sast.paths, sast.paths,
sast.newIssues.length, sast.newIssues.length,
sast.resolvedIssues.length, sast.resolvedIssues.length,
sast.allIssues.length, sast.allIssues.length,
); );
};
export const groupedSastContainerText = ({ sastContainer }) => {
if (sastContainer.hasError) {
return s__('ciReport|Container scanning resulted in error while loading results');
}
if (sastContainer.isLoading) {
return s__('ciReport|Container scanning is loading');
}
export const groupedSastContainerText = ({ sastContainer }) => return textBuilder(
textBuilder(
'Container scanning', 'Container scanning',
sastContainer.paths, sastContainer.paths,
sastContainer.newIssues.length, sastContainer.newIssues.length,
sastContainer.resolvedIssues.length, sastContainer.resolvedIssues.length,
); );
};
export const groupedDastText = ({ dast }) => {
if (dast.hasError) {
return s__('ciReport|DAST resulted in error while loading results');
}
if (dast.isLoading) {
return s__('ciReport|DAST is loading');
}
return textBuilder('DAST', dast.paths, dast.newIssues.length, dast.resolvedIssues.length);
};
export const groupedDependencyText = ({ dependencyScanning }) => {
if (dependencyScanning.hasError) {
return s__('ciReport|Dependency scanning resulted in error while loading results');
}
export const groupedDastText = ({ dast }) => if (dependencyScanning.isLoading) {
textBuilder('DAST', dast.paths, dast.newIssues.length, dast.resolvedIssues.length); return s__('ciReport|Dependency scanning is loading');
}
export const groupedDependencyText = ({ dependencyScanning }) => return textBuilder(
textBuilder(
'Dependency scanning', 'Dependency scanning',
dependencyScanning.paths, dependencyScanning.paths,
dependencyScanning.newIssues.length, dependencyScanning.newIssues.length,
dependencyScanning.resolvedIssues.length, dependencyScanning.resolvedIssues.length,
dependencyScanning.allIssues.length,
); );
};
export const groupedSummaryText = (state, getters) => { export const groupedSummaryText = (state, getters) => {
const { added, fixed } = state.summaryCounts; const { added, fixed } = state.summaryCounts;
// All reports are loading
if (getters.areAllReportsLoading) {
return s__('ciReport|Security scanning is loading');
}
// All reports returned error // All reports returned error
if (getters.allReportsHaveError) { if (getters.allReportsHaveError) {
return s__('ciReport|Security scanning failed loading any results'); return s__('ciReport|Security scanning failed loading any results');
...@@ -40,21 +84,25 @@ export const groupedSummaryText = (state, getters) => { ...@@ -40,21 +84,25 @@ export const groupedSummaryText = (state, getters) => {
if (getters.noBaseInAllReports) { if (getters.noBaseInAllReports) {
if (added > 0) { if (added > 0) {
return n__( return n__(
'Security scanning was unable to compare existing and new vulnerabilities. It detected %d vulnerability', 'Security scanning detected %d vulnerability for the source branch only',
'Security scanning was unable to compare existing and new vulnerabilities. It detected %d vulnerabilities', 'Security scanning detected %d vulnerabilities for the source branch only',
added, added,
); );
} }
return s__( return s__(
'Security scanning was unable to compare existing and new vulnerabilities. It detected no vulnerabilities.', 'Security scanning detected no vulnerabilities for the source branch only',
); );
} }
const text = [s__('ciReport|Security scanning')]; const text = [s__('ciReport|Security scanning')];
if (getters.areReportsLoading) { if (getters.areReportsLoading && getters.anyReportHasError) {
text.push('(in progress)'); text.push('(is loading, errors when loading results)');
} else if (getters.areReportsLoading && !getters.anyReportHasError) {
text.push('(is loading)');
} else if (!getters.areReportsLoading && getters.anyReportHasError) {
text.push('(errors when loading results)');
} }
if (added > 0 && fixed === 0) { if (added > 0 && fixed === 0) {
...@@ -81,15 +129,33 @@ export const groupedSummaryText = (state, getters) => { ...@@ -81,15 +129,33 @@ export const groupedSummaryText = (state, getters) => {
return text.join(' '); return text.join(' ');
}; };
export const sastStatusIcon = ({ sast }) => statusIcon(sast.hasError, sast.newIssues.length); export const summaryStatus = (state, getters) => {
if (getters.areReportsLoading) {
return LOADING;
}
if (getters.anyReportHasError || getters.anyReportHasIssues) {
return ERROR;
}
return SUCCESS;
};
export const sastStatusIcon = ({ sast }) =>
statusIcon(sast.isLoading, sast.hasError, sast.newIssues.length);
export const sastContainerStatusIcon = ({ sastContainer }) => export const sastContainerStatusIcon = ({ sastContainer }) =>
statusIcon(sastContainer.hasError, sastContainer.newIssues.length); statusIcon(sastContainer.isLoading, sastContainer.hasError, sastContainer.newIssues.length);
export const dastStatusIcon = ({ dast }) => statusIcon(dast.hasError, dast.newIssues.length); export const dastStatusIcon = ({ dast }) =>
statusIcon(dast.isLoading, dast.hasError, dast.newIssues.length);
export const dependencyScanningStatusIcon = ({ dependencyScanning }) => export const dependencyScanningStatusIcon = ({ dependencyScanning }) =>
statusIcon(dependencyScanning.hasError, dependencyScanning.newIssues.length); statusIcon(
dependencyScanning.isLoading,
dependencyScanning.hasError,
dependencyScanning.newIssues.length,
);
export const areReportsLoading = state => export const areReportsLoading = state =>
state.sast.isLoading || state.sast.isLoading ||
...@@ -97,6 +163,12 @@ export const areReportsLoading = state => ...@@ -97,6 +163,12 @@ export const areReportsLoading = state =>
state.sastContainer.isLoading || state.sastContainer.isLoading ||
state.dependencyScanning.isLoading; state.dependencyScanning.isLoading;
export const areAllReportsLoading = state =>
state.sast.isLoading &&
state.dast.isLoading &&
state.sastContainer.isLoading &&
state.dependencyScanning.isLoading;
export const allReportsHaveError = state => export const allReportsHaveError = state =>
state.sast.hasError && state.sast.hasError &&
state.dast.hasError && state.dast.hasError &&
...@@ -114,3 +186,9 @@ export const noBaseInAllReports = state => ...@@ -114,3 +186,9 @@ export const noBaseInAllReports = state =>
!state.dast.paths.base && !state.dast.paths.base &&
!state.sastContainer.paths.base && !state.sastContainer.paths.base &&
!state.dependencyScanning.paths.base; !state.dependencyScanning.paths.base;
export const anyReportHasIssues = state =>
state.sast.newIssues.length > 0 ||
state.dast.newIssues.length > 0 ||
state.sastContainer.newIssues.length > 0 ||
state.dependencyScanning.newIssues.length > 0;
/* eslint-disable no-param-reassign */
import * as types from './mutation_types'; import * as types from './mutation_types';
import { import {
parseSastIssues, parseSastIssues,
...@@ -9,24 +11,24 @@ import { ...@@ -9,24 +11,24 @@ import {
export default { export default {
[types.SET_HEAD_BLOB_PATH](state, path) { [types.SET_HEAD_BLOB_PATH](state, path) {
Object.assign(state.blobPath, { head: path }); state.blobPath.head = path;
}, },
[types.SET_BASE_BLOB_PATH](state, path) { [types.SET_BASE_BLOB_PATH](state, path) {
Object.assign(state.blobPath, { base: path }); state.blobPath.base = path;
}, },
// SAST // SAST
[types.SET_SAST_HEAD_PATH](state, path) { [types.SET_SAST_HEAD_PATH](state, path) {
Object.assign(state.sast.paths, { head: path }); state.sast.paths.head = path;
}, },
[types.SET_SAST_BASE_PATH](state, path) { [types.SET_SAST_BASE_PATH](state, path) {
Object.assign(state.sast.paths, { base: path }); state.sast.paths.base = path;
}, },
[types.REQUEST_SAST_REPORTS](state) { [types.REQUEST_SAST_REPORTS](state) {
Object.assign(state.sast, { isLoading: true }); state.sast.isLoading = true;
}, },
/** /**
...@@ -52,50 +54,39 @@ export default { ...@@ -52,50 +54,39 @@ export default {
const newIssues = filterByKey(parsedHead, parsedBase, filterKey); const newIssues = filterByKey(parsedHead, parsedBase, filterKey);
const resolvedIssues = filterByKey(parsedBase, parsedHead, filterKey); const resolvedIssues = filterByKey(parsedBase, parsedHead, filterKey);
const allIssues = filterByKey(parsedHead, newIssues.concat(resolvedIssues), filterKey); const allIssues = filterByKey(parsedHead, newIssues.concat(resolvedIssues), filterKey);
Object.assign(state, { state.sast.newIssues = newIssues;
sast: { state.sast.resolvedIssues = resolvedIssues;
...state.sast, state.sast.allIssues = allIssues;
newIssues, state.sast.isLoading = false;
resolvedIssues, state.summaryCounts.added += newIssues.length;
allIssues, state.summaryCounts.fixed += resolvedIssues.length;
isLoading: false,
},
summaryCounts: {
added: state.summaryCounts.added + newIssues.length,
fixed: state.summaryCounts.fixed + resolvedIssues.length,
},
});
} else if (reports.head && !reports.base) { } else if (reports.head && !reports.base) {
const newIssues = parseSastIssues(reports.head, state.blobPath.head); const newIssues = parseSastIssues(reports.head, state.blobPath.head);
Object.assign(state.sast, { state.sast.newIssues = newIssues;
newIssues, state.sast.isLoading = false;
isLoading: false, state.summaryCounts.added += newIssues.length;
});
} }
}, },
[types.RECEIVE_SAST_REPORTS_ERROR](state) { [types.RECEIVE_SAST_REPORTS_ERROR](state) {
Object.assign(state.sast, { state.sast.isLoading = false;
isLoading: false, state.sast.hasError = true;
hasError: true,
});
}, },
// SAST CONTAINER // SAST CONTAINER
[types.SET_SAST_CONTAINER_HEAD_PATH](state, path) { [types.SET_SAST_CONTAINER_HEAD_PATH](state, path) {
Object.assign(state.sastContainer.paths, { head: path }); state.sastContainer.paths.head = path;
}, },
[types.SET_SAST_CONTAINER_BASE_PATH](state, path) { [types.SET_SAST_CONTAINER_BASE_PATH](state, path) {
Object.assign(state.sastContainer.paths, { base: path }); state.sastContainer.paths.base = path;
}, },
[types.REQUEST_SAST_CONTAINER_REPORTS](state) { [types.REQUEST_SAST_CONTAINER_REPORTS](state) {
Object.assign(state.sastContainer, { isLoading: true }); state.sastContainer.isLoading = true;
}, },
/** /**
...@@ -116,48 +107,40 @@ export default { ...@@ -116,48 +107,40 @@ export default {
const newIssues = filterByKey(headIssues, baseIssues, filterKey); const newIssues = filterByKey(headIssues, baseIssues, filterKey);
const resolvedIssues = filterByKey(baseIssues, headIssues, filterKey); const resolvedIssues = filterByKey(baseIssues, headIssues, filterKey);
Object.assign(state, { state.sastContainer.newIssues = newIssues;
sastContainer: { state.sastContainer.resolvedIssues = resolvedIssues;
...state.sastContainer, state.sastContainer.isLoading = false;
isLoading: false, state.summaryCounts.added += newIssues.length;
newIssues, state.summaryCounts.fixed += resolvedIssues.length;
resolvedIssues,
},
summaryCounts: {
added: state.summaryCounts.added + newIssues.length,
fixed: state.summaryCounts.fixed + resolvedIssues.length,
},
});
} else if (reports.head && !reports.base) { } else if (reports.head && !reports.base) {
Object.assign(state.sastContainer, { const newIssues = getUnapprovedVulnerabilities(
isLoading: false, parseSastContainer(reports.head.vulnerabilities),
newIssues: getUnapprovedVulnerabilities( reports.head.unapproved,
parseSastContainer(reports.head.vulnerabilities), );
reports.head.unapproved,
), state.sastContainer.newIssues = newIssues;
}); state.sastContainer.isLoading = false;
state.summaryCounts.added += newIssues.length;
} }
}, },
[types.RECEIVE_SAST_CONTAINER_ERROR](state) { [types.RECEIVE_SAST_CONTAINER_ERROR](state) {
Object.assign(state.sastContainer, { state.sastContainer.isLoading = false;
isLoading: false, state.sastContainer.hasError = true;
hasError: true,
});
}, },
// DAST // DAST
[types.SET_DAST_HEAD_PATH](state, path) { [types.SET_DAST_HEAD_PATH](state, path) {
Object.assign(state.dast.paths, { head: path }); state.dast.paths.head = path;
}, },
[types.SET_DAST_BASE_PATH](state, path) { [types.SET_DAST_BASE_PATH](state, path) {
Object.assign(state.dast.paths, { base: path }); state.dast.paths.base = path;
}, },
[types.REQUEST_DAST_REPORTS](state) { [types.REQUEST_DAST_REPORTS](state) {
Object.assign(state.dast, { isLoading: true }); state.dast.isLoading = true;
}, },
[types.RECEIVE_DAST_REPORTS](state, reports) { [types.RECEIVE_DAST_REPORTS](state, reports) {
...@@ -168,45 +151,37 @@ export default { ...@@ -168,45 +151,37 @@ export default {
const newIssues = filterByKey(headIssues, baseIssues, filterKey); const newIssues = filterByKey(headIssues, baseIssues, filterKey);
const resolvedIssues = filterByKey(baseIssues, headIssues, filterKey); const resolvedIssues = filterByKey(baseIssues, headIssues, filterKey);
Object.assign(state, { state.dast.newIssues = newIssues;
dast: { state.dast.resolvedIssues = resolvedIssues;
...state.dast, state.dast.isLoading = false;
isLoading: false, state.summaryCounts.added += newIssues.length;
newIssues, state.summaryCounts.fixed += resolvedIssues.length;
resolvedIssues,
},
summaryCounts: {
added: state.summaryCounts.added + newIssues.length,
fixed: state.summaryCounts.fixed + resolvedIssues.length,
},
});
} else if (reports.head && !reports.base) { } else if (reports.head && !reports.base) {
Object.assign(state.dast, { const newIssues = parseDastIssues(reports.head.site.alerts);
isLoading: false,
newIssues: parseDastIssues(reports.head.site.alerts), state.dast.newIssues = newIssues;
}); state.dast.isLoading = false;
state.summaryCounts.added += newIssues.length;
} }
}, },
[types.RECEIVE_DAST_ERROR](state) { [types.RECEIVE_DAST_ERROR](state) {
Object.assign(state.dast, { state.dast.isLoading = false;
isLoading: false, state.dast.hasError = true;
hasError: true,
});
}, },
// DEPENDECY SCANNING // DEPENDECY SCANNING
[types.SET_DEPENDENCY_SCANNING_HEAD_PATH](state, path) { [types.SET_DEPENDENCY_SCANNING_HEAD_PATH](state, path) {
Object.assign(state.dependencyScanning.paths, { head: path }); state.dependencyScanning.paths.head = path;
}, },
[types.SET_DEPENDENCY_SCANNING_BASE_PATH](state, path) { [types.SET_DEPENDENCY_SCANNING_BASE_PATH](state, path) {
Object.assign(state.dependencyScanning.paths, { base: path }); state.dependencyScanning.paths.base = path;
}, },
[types.REQUEST_DEPENDENCY_SCANNING_REPORTS](state) { [types.REQUEST_DEPENDENCY_SCANNING_REPORTS](state) {
Object.assign(state.dependencyScanning, { isLoading: true }); state.dependencyScanning.isLoading = true;
}, },
/** /**
...@@ -234,31 +209,24 @@ export default { ...@@ -234,31 +209,24 @@ export default {
const resolvedIssues = filterByKey(parsedBase, parsedHead, filterKey); const resolvedIssues = filterByKey(parsedBase, parsedHead, filterKey);
const allIssues = filterByKey(parsedHead, newIssues.concat(resolvedIssues), filterKey); const allIssues = filterByKey(parsedHead, newIssues.concat(resolvedIssues), filterKey);
Object.assign(state, { state.dependencyScanning.newIssues = newIssues;
dependencyScanning: { state.dependencyScanning.resolvedIssues = resolvedIssues;
...state.dependencyScanning, state.dependencyScanning.allIssues = allIssues;
newIssues, state.dependencyScanning.isLoading = false;
resolvedIssues, state.summaryCounts.added += newIssues.length;
allIssues, state.summaryCounts.fixed += resolvedIssues.length;
isLoading: false, }
},
summaryCounts: { if (reports.head && !reports.base) {
added: state.summaryCounts.added + newIssues.length, const newIssues = parseSastIssues(reports.head, state.blobPath.head);
fixed: state.summaryCounts.fixed + resolvedIssues.length, state.dependencyScanning.newIssues = newIssues;
}, state.dependencyScanning.isLoading = false;
}); state.summaryCounts.added += newIssues.length;
} else {
Object.assign(state.dependencyScanning, {
newIssues: parseSastIssues(reports.head, state.blobPath.head),
isLoading: false,
});
} }
}, },
[types.RECEIVE_DEPENDENCY_SCANNING_ERROR](state) { [types.RECEIVE_DEPENDENCY_SCANNING_ERROR](state) {
Object.assign(state.dependencyScanning, { state.dependencyScanning.isLoading = false;
isLoading: false, state.dependencyScanning.hasError = true;
hasError: true,
});
}, },
}; };
...@@ -64,30 +64,33 @@ export const textBuilder = ( ...@@ -64,30 +64,33 @@ export const textBuilder = (
resolvedIssues = 0, resolvedIssues = 0,
allIssues = 0, allIssues = 0,
) => { ) => {
// With no issues
if (newIssues === 0 && resolvedIssues === 0 && allIssues === 0) {
return sprintf(s__('ciReport|%{type} detected no security vulnerabilities'), { type });
}
// with no new or fixed but with vulnerabilities // with no new or fixed but with vulnerabilities
if (newIssues === 0 && resolvedIssues === 0 && allIssues) { if (newIssues === 0 && resolvedIssues === 0 && allIssues) {
return sprintf(s__('ciReport|%{type} detected no new security vulnerabilities'), { type }); return sprintf(s__('ciReport|%{type} detected no new security vulnerabilities'), { type });
} }
// with new issues and only head if (!paths.base) {
if (newIssues > 0 && !paths.base) { if (newIssues > 0) {
return sprintf(
n__(
'%{type} detected %d vulnerability for the source branch only',
'%{type} detected %d vulnerabilities for the source branch only',
newIssues,
),
{ type },
);
}
return sprintf( return sprintf(
n__( '%{type} detected no vulnerabilities for the source branch only',
'%{type} was unable to compare existing and new vulnerabilities. It detected %d vulnerability',
'%{type} was unable to compare existing and new vulnerabilities. It detected %d vulnerabilities',
newIssues,
),
{ type }, { type },
); );
} } else if (paths.base && paths.head) {
// With no issues
if (newIssues === 0 && resolvedIssues === 0 && allIssues === 0) {
return sprintf(s__('ciReport|%{type} detected no security vulnerabilities'), { type });
}
// with head + base
if (paths.base && paths.head) {
// with only new issues // with only new issues
if (newIssues > 0 && resolvedIssues === 0) { if (newIssues > 0 && resolvedIssues === 0) {
return sprintf( return sprintf(
...@@ -128,7 +131,11 @@ export const textBuilder = ( ...@@ -128,7 +131,11 @@ export const textBuilder = (
return ''; return '';
}; };
export const statusIcon = (failed = false, newIssues = 0, neutralIssues = 0) => { export const statusIcon = (loading = false, failed = false, newIssues = 0, neutralIssues = 0) => {
if (loading) {
return 'loading';
}
if (failed || newIssues > 0 || neutralIssues > 0) { if (failed || newIssues > 0 || neutralIssues > 0) {
return 'warning'; return 'warning';
} }
......
.pipeline-tab-content { .split-report-section {
border-bottom: 1px solid $gray-darker;
.report-block-list {
max-height: 500px;
overflow: auto;
}
.space-children, .space-children,
.space-children > span { .space-children > span {
display: flex; display: flex;
...@@ -14,11 +21,32 @@ ...@@ -14,11 +21,32 @@
} }
} }
.mr-widget-grouped-section {
.report-block-container {
max-height: 170px;
overflow: auto;
}
.report-block-list-issue-parent {
padding: $gl-padding-top $gl-padding;
border-top: 1px solid $border-color;
}
.report-block-list-icon .loading-container {
position: relative;
left: -2px;
// needed to make the next element align with the
// elements below that have a svg with 16px width
.fa-spinner {
width: 16px;
}
}
}
.report-block-container { .report-block-container {
border-top: 1px solid $gray-darker; border-top: 1px solid $border-color;
padding: $gl-padding-top; padding: $gl-padding-top;
background-color: $gray-light; background-color: $gray-light;
margin: $gl-padding #{-$gl-padding} #{-$gl-padding};
// Clean MR widget CSS // Clean MR widget CSS
line-height: 20px; line-height: 20px;
...@@ -44,6 +72,15 @@ ...@@ -44,6 +72,15 @@
&.neutral { &.neutral {
color: $theme-gray-700; color: $theme-gray-700;
} }
.ci-status-icon {
svg {
width: 16px;
height: 16px;
top: 3px;
left: -2px;
}
}
} }
.report-block-list-issue { .report-block-list-issue {
...@@ -65,6 +102,10 @@ ...@@ -65,6 +102,10 @@
word-wrap: break-word; word-wrap: break-word;
word-break: break-all; word-break: break-all;
} }
.btn-help svg {
top: 5px;
}
} }
.report-block-issue-code { .report-block-issue-code {
......
---
title: Renders grouped security reports in MR widget & split security reports in CI
view
merge_request:
author:
type: changed
...@@ -37,8 +37,8 @@ describe 'Pipeline', :js do ...@@ -37,8 +37,8 @@ describe 'Pipeline', :js do
expect(page).to have_css('#js-tab-security') expect(page).to have_css('#js-tab-security')
end end
it 'shows security report' do it 'shows security report section' do
expect(page).to have_content('SAST detected no security vulnerabilities') expect(page).to have_content('SAST is loading')
end end
end end
......
import _ from 'underscore'; import _ from 'underscore';
import Vue from 'vue'; import Vue from 'vue';
import PipelineMediator from '~/pipelines/pipeline_details_mediator'; import PipelineMediator from '~/pipelines/pipeline_details_mediator';
import { sastIssues, parsedSastIssuesStore } from '../vue_shared/security_reports/mock_data';
describe('PipelineMdediator', () => { describe('PipelineMdediator', () => {
let mediator; let mediator;
...@@ -40,29 +39,4 @@ describe('PipelineMdediator', () => { ...@@ -40,29 +39,4 @@ describe('PipelineMdediator', () => {
}); });
}); });
}); });
describe('security reports', () => {
const interceptor = (request, next) => {
next(request.respondWith(JSON.stringify(sastIssues), {
status: 200,
}));
};
beforeEach(() => {
Vue.http.interceptors.push(interceptor);
});
afterEach(() => {
Vue.http.interceptors = _.without(Vue.http.interceptor, interceptor);
});
it('fetches the requests endpoint and stores the data', (done) => {
mediator.fetchSastReport('sast.json', 'path');
setTimeout(() => {
expect(mediator.store.state.securityReports.sast.newIssues).toEqual(parsedSastIssuesStore);
done();
}, 0);
});
});
}); });
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;
...@@ -24,11 +23,4 @@ describe('Pipeline Store', () => { ...@@ -24,11 +23,4 @@ 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);
});
}); });
import Vue from 'vue';
import store from 'ee/vue_shared/security_reports/store';
import state from 'ee/vue_shared/security_reports/store/state';
import reportSummary from 'ee/pipelines/components/security_reports/report_summary_widget.vue';
import { createComponentWithStore } from 'spec/helpers/vue_mount_component_helper';
import { sastIssues } from '../../vue_shared/security_reports/mock_data';
describe('Report summary widget', () => {
const Component = Vue.extend(reportSummary);
let vm;
beforeEach(() => {
vm = createComponentWithStore(Component, store).$mount();
});
afterEach(() => {
vm.$destroy();
// clean up the error state
vm.$store.replaceState(state());
});
describe('without paths', () => {
it('does not render any summary', () => {
expect(vm.$el.querySelector('.js-sast-summary')).toBeNull();
expect(vm.$el.querySelector('.js-dss-summary')).toBeNull();
});
});
describe('while loading', () => {
beforeEach(() => {
vm.$store.dispatch('setSastHeadPath', 'head.json');
vm.$store.dispatch('setDependencyScanningHeadPath', 'head.json');
vm.$store.dispatch('requestSastReports');
vm.$store.dispatch('requestDependencyScanningReports');
});
it('renders loading icon and text for sast', done => {
vm.$nextTick(() => {
expect(
vm.$el
.querySelector('.js-sast-summary')
.textContent.trim()
.replace(/\s\s+/g, ' '),
).toEqual('SAST is loading');
expect(vm.$el.querySelector('.js-sast-summary .fa-spinner')).not.toBeNull();
done();
});
});
it('renders loading icon and text for dependency scanning', done => {
vm.$nextTick(() => {
expect(
vm.$el
.querySelector('.js-dss-summary')
.textContent.trim()
.replace(/\s\s+/g, ' '),
).toEqual('Dependency scanning is loading');
expect(vm.$el.querySelector('.js-dss-summary .fa-spinner')).not.toBeNull();
done();
});
});
});
describe('with error', () => {
beforeEach(() => {
vm.$store.dispatch('setSastHeadPath', 'head.json');
vm.$store.dispatch('setDependencyScanningHeadPath', 'head.json');
vm.$store.dispatch('receiveSastError');
vm.$store.dispatch('receiveDependencyScanningError');
});
it('renders warning icon and error text for sast', done => {
vm.$nextTick(() => {
expect(
vm.$el
.querySelector('.js-sast-summary')
.textContent.trim()
.replace(/\s\s+/g, ' '),
).toEqual('SAST resulted in error while loading results');
expect(vm.$el.querySelector('.js-sast-summary .js-ci-status-icon-warning')).not.toBeNull();
done();
});
});
it('renders warnin icon and error text for dependency scanning', done => {
vm.$nextTick()
.then(() => {
expect(
vm.$el
.querySelector('.js-dss-summary')
.textContent.trim()
.replace(/\s\s+/g, ' '),
).toEqual('Dependency scanning resulted in error while loading results');
expect(vm.$el.querySelector('.js-dss-summary .js-ci-status-icon-warning')).not.toBeNull();
})
.then(done)
.catch(done.fail);
});
});
describe('with vulnerabilities', () => {
beforeEach(() => {
vm.$store.dispatch('setSastHeadPath', 'head.json');
vm.$store.dispatch('setDependencyScanningHeadPath', 'head.json');
vm.$store.dispatch('receiveSastReports', {
head: sastIssues,
});
vm.$store.dispatch('receiveDependencyScanningReports', {
head: sastIssues,
});
});
it('renders warning icon and vulnerabilities text for sast', done => {
vm.$nextTick(() => {
expect(
vm.$el
.querySelector('.js-sast-summary')
.textContent.trim()
.replace(/\s\s+/g, ' '),
).toEqual('SAST detected 3 vulnerabilities');
expect(vm.$el.querySelector('.js-sast-summary .js-ci-status-icon-warning')).not.toBeNull();
done();
});
});
it('renders warning icon and vulnerabilities text for dependency scanning', done => {
vm.$nextTick(() => {
expect(
vm.$el
.querySelector('.js-dss-summary')
.textContent.trim()
.replace(/\s\s+/g, ' '),
).toEqual('Dependency scanning detected 3 vulnerabilities');
expect(vm.$el.querySelector('.js-dss-summary .js-ci-status-icon-warning')).not.toBeNull();
done();
});
});
});
describe('without vulnerabilities', () => {
beforeEach(() => {
vm.$store.dispatch('setSastHeadPath', 'head.json');
vm.$store.dispatch('setDependencyScanningHeadPath', 'head.json');
vm.$store.dispatch('receiveSastReports', {
head: [],
});
vm.$store.dispatch('receiveDependencyScanningReports', {
head: [],
});
});
it('renders success icon and vulnerabilities text for sast', done => {
vm.$nextTick(() => {
expect(
vm.$el
.querySelector('.js-sast-summary')
.textContent.trim()
.replace(/\s\s+/g, ' '),
).toEqual('SAST detected no vulnerabilities');
expect(vm.$el.querySelector('.js-sast-summary .js-ci-status-icon-success')).not.toBeNull();
done();
});
});
it('renders success icon and vulnerabilities text for dependency scanning', done => {
vm.$nextTick(() => {
expect(
vm.$el
.querySelector('.js-dss-summary')
.textContent.trim()
.replace(/\s\s+/g, ' '),
).toEqual('Dependency scanning detected no vulnerabilities');
expect(vm.$el.querySelector('.js-dss-summary .js-ci-status-icon-success')).not.toBeNull();
done();
});
});
});
});
import Vue from 'vue';
import reportSummary from 'ee/pipelines/components/security_reports/report_summary_widget.vue';
import mountComponent from 'spec/helpers/vue_mount_component_helper';
describe('Report summary widget', () => {
let vm;
let Component;
beforeEach(() => {
Component = Vue.extend(reportSummary);
});
afterEach(() => {
vm.$destroy();
});
describe('with vulnerabilities', () => {
beforeEach(() => {
vm = mountComponent(Component, {
sastIssues: 2,
dependencyScanningIssues: 4,
hasSast: true,
hasDependencyScanning: true,
});
});
it('renders summary text with warning icon for sast', () => {
expect(vm.$el.querySelector('.js-sast-summary').textContent.trim().replace(/\s\s+/g, ' ')).toEqual('SAST detected 2 vulnerabilities');
expect(vm.$el.querySelector('.js-sast-summary span').classList).toContain('ci-status-icon-warning');
});
it('renders summary text with warning icon for dependency scanning', () => {
expect(vm.$el.querySelector('.js-dss-summary').textContent.trim().replace(/\s\s+/g, ' ')).toEqual('Dependency scanning detected 4 vulnerabilities');
expect(vm.$el.querySelector('.js-dss-summary span').classList).toContain('ci-status-icon-warning');
});
});
describe('without vulnerabilities', () => {
beforeEach(() => {
vm = mountComponent(Component, {
hasSast: true,
hasDependencyScanning: true,
});
});
it('render summary text with success icon for sast', () => {
expect(vm.$el.querySelector('.js-sast-summary').textContent.trim().replace(/\s\s+/g, ' ')).toEqual('SAST detected no vulnerabilities');
expect(vm.$el.querySelector('.js-sast-summary span').classList).toContain('ci-status-icon-success');
});
it('render summary text with success icon for dependecy scanning', () => {
expect(vm.$el.querySelector('.js-dss-summary').textContent.trim().replace(/\s\s+/g, ' ')).toEqual('Dependency scanning detected no vulnerabilities');
expect(vm.$el.querySelector('.js-dss-summary span').classList).toContain('ci-status-icon-success');
});
});
});
import Vue from 'vue';
import securityReportApp from 'ee/pipelines/components/security_reports/security_report_app.vue';
import mountComponent from 'spec/helpers/vue_mount_component_helper';
import { parsedSastIssuesHead } from 'spec/vue_shared/security_reports/mock_data';
describe('Security Report App', () => {
let vm;
let Component;
beforeEach(() => {
Component = Vue.extend(securityReportApp);
});
afterEach(() => {
vm.$destroy();
});
describe('sast report', () => {
beforeEach(() => {
vm = mountComponent(Component, {
securityReports: {
sast: {
isLoading: false,
hasError: false,
newIssues: parsedSastIssuesHead,
resolvedIssues: [],
allIssues: [],
},
dependencyScanning: {
isLoading: false,
hasError: false,
newIssues: parsedSastIssuesHead,
resolvedIssues: [],
allIssues: [],
},
},
hasDependencyScanning: true,
hasSast: true,
});
});
it('renders the sast report', () => {
expect(vm.$el.querySelector('.js-sast-widget .js-code-text').textContent.trim()).toEqual('SAST degraded on 2 security vulnerabilities');
expect(vm.$el.querySelectorAll('.js-sast-widget .js-mr-code-new-issues li').length).toEqual(parsedSastIssuesHead.length);
const issue = vm.$el.querySelector('.js-sast-widget .js-mr-code-new-issues li').textContent;
expect(issue).toContain(parsedSastIssuesHead[0].message);
expect(issue).toContain(parsedSastIssuesHead[0].path);
});
it('renders the dependency scanning report', () => {
expect(vm.$el.querySelector('.js-dependency-scanning-widget .js-code-text').textContent.trim()).toEqual('Dependency scanning degraded on 2 security vulnerabilities');
expect(vm.$el.querySelectorAll('.js-dependency-scanning-widget .js-mr-code-new-issues li').length).toEqual(parsedSastIssuesHead.length);
const issue = vm.$el.querySelector('.js-dependency-scanning-widget .js-mr-code-new-issues li').textContent;
expect(issue).toContain(parsedSastIssuesHead[0].message);
expect(issue).toContain(parsedSastIssuesHead[0].path);
});
});
});
...@@ -5,35 +5,48 @@ import mrWidgetOptions from 'ee/vue_merge_request_widget/mr_widget_options'; ...@@ -5,35 +5,48 @@ import mrWidgetOptions from 'ee/vue_merge_request_widget/mr_widget_options';
import MRWidgetService from 'ee/vue_merge_request_widget/services/mr_widget_service'; import MRWidgetService from 'ee/vue_merge_request_widget/services/mr_widget_service';
import MRWidgetStore from 'ee/vue_merge_request_widget/stores/mr_widget_store'; import MRWidgetStore from 'ee/vue_merge_request_widget/stores/mr_widget_store';
import mountComponent from 'spec/helpers/vue_mount_component_helper'; import mountComponent from 'spec/helpers/vue_mount_component_helper';
import mockData, { import state from 'ee/vue_shared/security_reports/store/state';
baseIssues, import mockData, { baseIssues, headIssues, basePerformance, headPerformance } from './mock_data';
headIssues,
basePerformance,
headPerformance,
} from './mock_data';
import { import {
sastIssues, sastIssues,
sastIssuesBase, sastIssuesBase,
dockerReport, dockerReport,
dockerReportParsed, dockerBaseReport,
dast, dast,
parsedDast, dastBase,
sastBaseAllIssues, sastBaseAllIssues,
sastHeadAllIssues, sastHeadAllIssues,
} from '../vue_shared/security_reports/mock_data'; } from '../vue_shared/security_reports/mock_data';
describe('ee merge request widget options', () => { describe('ee merge request widget options', () => {
let vm; let vm;
let mock;
let Component; let Component;
function removeBreakLine(data) {
return data
.replace(/\r?\n|\r/g, '')
.replace(/\s\s+/g, ' ')
.trim();
}
beforeEach(() => { beforeEach(() => {
delete mrWidgetOptions.extends.el; // Prevent component mounting delete mrWidgetOptions.extends.el; // Prevent component mounting
Component = Vue.extend(mrWidgetOptions); Component = Vue.extend(mrWidgetOptions);
mock = new MockAdapter(axios);
}); });
afterEach(() => { afterEach(() => {
vm.$destroy(); vm.$destroy();
mock.restore();
// Clean security reports state
Component.mr.sast = state().sast;
Component.mr.sastContainer = state().sastContainer;
Component.mr.dast = state().dast;
Component.mr.dependencyScanning = state().dependencyScanning;
}); });
describe('security widget', () => { describe('security widget', () => {
...@@ -52,56 +65,51 @@ describe('ee merge request widget options', () => { ...@@ -52,56 +65,51 @@ describe('ee merge request widget options', () => {
describe('when it is loading', () => { describe('when it is loading', () => {
it('should render loading indicator', () => { it('should render loading indicator', () => {
mock.onGet('path.json').reply(200, sastBaseAllIssues);
mock.onGet('head_path.json').reply(200, sastHeadAllIssues);
vm = mountComponent(Component); vm = mountComponent(Component);
expect( expect(vm.$el.querySelector('.js-sast-widget').textContent.trim()).toContain(
vm.$el.querySelector('.js-sast-widget').textContent.trim(), 'SAST is loading',
).toContain('Loading security report'); );
}); });
}); });
describe('with successful request', () => { describe('with successful request', () => {
let mock;
beforeEach(() => { beforeEach(() => {
mock = mock = new MockAdapter(axios);
mock.onGet('path.json').reply(200, sastIssuesBase); mock.onGet('path.json').reply(200, sastIssuesBase);
mock.onGet('head_path.json').reply(200, sastIssues); mock.onGet('head_path.json').reply(200, sastIssues);
vm = mountComponent(Component); vm = mountComponent(Component);
}); });
afterEach(() => { it('should render provided data', done => {
mock.restore();
});
it('should render provided data', (done) => {
setTimeout(() => { setTimeout(() => {
expect( expect(
vm.$el.querySelector('.js-sast-widget .js-code-text').textContent.trim(), removeBreakLine(
).toEqual('SAST improved on 1 security vulnerability and degraded on 2 security vulnerabilities'); vm.$el.querySelector('.js-sast-widget .report-block-list-issue-description')
.textContent,
),
).toEqual('SAST detected 2 new vulnerabilities and 1 fixed vulnerability');
done(); done();
}, 0); }, 0);
}); });
}); });
describe('with full report and no added or fixed issues', () => { describe('with full report and no added or fixed issues', () => {
let mock;
beforeEach(() => { beforeEach(() => {
mock = mock = new MockAdapter(axios);
mock.onGet('path.json').reply(200, sastBaseAllIssues); mock.onGet('path.json').reply(200, sastBaseAllIssues);
mock.onGet('head_path.json').reply(200, sastHeadAllIssues); mock.onGet('head_path.json').reply(200, sastHeadAllIssues);
vm = mountComponent(Component); vm = mountComponent(Component);
}); });
afterEach(() => { it('renders no new vulnerabilities message', done => {
mock.restore();
});
it('renders no new vulnerabilities message', (done) => {
setTimeout(() => { setTimeout(() => {
expect( expect(
vm.$el.querySelector('.js-sast-widget .js-code-text').textContent.trim(), removeBreakLine(
vm.$el.querySelector('.js-sast-widget .report-block-list-issue-description')
.textContent,
),
).toEqual('SAST detected no new security vulnerabilities'); ).toEqual('SAST detected no new security vulnerabilities');
done(); done();
}, 0); }, 0);
...@@ -109,24 +117,20 @@ describe('ee merge request widget options', () => { ...@@ -109,24 +117,20 @@ describe('ee merge request widget options', () => {
}); });
describe('with empty successful request', () => { describe('with empty successful request', () => {
let mock;
beforeEach(() => { beforeEach(() => {
mock = mock = new MockAdapter(axios);
mock.onGet('path.json').reply(200, []); mock.onGet('path.json').reply(200, []);
mock.onGet('head_path.json').reply(200, []); mock.onGet('head_path.json').reply(200, []);
vm = mountComponent(Component); vm = mountComponent(Component);
}); });
afterEach(() => { it('should render provided data', done => {
mock.restore();
});
it('should render provided data', (done) => {
setTimeout(() => { setTimeout(() => {
expect( expect(
vm.$el.querySelector('.js-sast-widget .js-code-text').textContent.trim(), removeBreakLine(
vm.$el.querySelector('.js-sast-widget .report-block-list-issue-description')
.textContent,
).trim(),
).toEqual('SAST detected no security vulnerabilities'); ).toEqual('SAST detected no security vulnerabilities');
done(); done();
}, 0); }, 0);
...@@ -134,24 +138,17 @@ describe('ee merge request widget options', () => { ...@@ -134,24 +138,17 @@ describe('ee merge request widget options', () => {
}); });
describe('with failed request', () => { describe('with failed request', () => {
let mock;
beforeEach(() => { beforeEach(() => {
mock = mock = new MockAdapter(axios);
mock.onGet('path.json').reply(500, []); mock.onGet('path.json').reply(500, []);
mock.onGet('head_path.json').reply(500, []); mock.onGet('head_path.json').reply(500, []);
vm = mountComponent(Component); vm = mountComponent(Component);
}); });
afterEach(() => { it('should render error indicator', done => {
mock.restore();
});
it('should render error indicator', (done) => {
setTimeout(() => { setTimeout(() => {
expect( expect(removeBreakLine(vm.$el.querySelector('.js-sast-widget').textContent)).toContain(
vm.$el.querySelector('.js-sast-widget').textContent.trim(), 'SAST resulted in error while loading results',
).toContain('Failed to load security report'); );
done(); done();
}, 0); }, 0);
}); });
...@@ -174,56 +171,55 @@ describe('ee merge request widget options', () => { ...@@ -174,56 +171,55 @@ describe('ee merge request widget options', () => {
describe('when it is loading', () => { describe('when it is loading', () => {
it('should render loading indicator', () => { it('should render loading indicator', () => {
mock.onGet('path.json').reply(200, sastIssuesBase);
mock.onGet('head_path.json').reply(200, sastIssues);
vm = mountComponent(Component); vm = mountComponent(Component);
expect( expect(
vm.$el.querySelector('.js-dependency-scanning-widget').textContent.trim(), removeBreakLine(vm.$el.querySelector('.js-dependency-scanning-widget').textContent),
).toContain('Loading dependency scanning report'); ).toContain('Dependency scanning is loading');
}); });
}); });
describe('with successful request', () => { describe('with successful request', () => {
let mock;
beforeEach(() => { beforeEach(() => {
mock = mock = new MockAdapter(axios);
mock.onGet('path.json').reply(200, sastIssuesBase); mock.onGet('path.json').reply(200, sastIssuesBase);
mock.onGet('head_path.json').reply(200, sastIssues); mock.onGet('head_path.json').reply(200, sastIssues);
vm = mountComponent(Component); vm = mountComponent(Component);
}); });
afterEach(() => { it('should render provided data', done => {
mock.restore();
});
it('should render provided data', (done) => {
setTimeout(() => { setTimeout(() => {
expect( expect(
vm.$el.querySelector('.js-dependency-scanning-widget .js-code-text').textContent.trim(), removeBreakLine(
).toEqual('Dependency scanning improved on 1 security vulnerability and degraded on 2 security vulnerabilities'); vm.$el.querySelector(
'.js-dependency-scanning-widget .report-block-list-issue-description',
).textContent,
),
).toEqual(
'Dependency scanning detected 2 new vulnerabilities and 1 fixed vulnerability',
);
done(); done();
}, 0); }, 0);
}); });
}); });
describe('with full report and no added or fixed issues', () => { describe('with full report and no added or fixed issues', () => {
let mock;
beforeEach(() => { beforeEach(() => {
mock = mock = new MockAdapter(axios);
mock.onGet('path.json').reply(200, sastBaseAllIssues); mock.onGet('path.json').reply(200, sastBaseAllIssues);
mock.onGet('head_path.json').reply(200, sastHeadAllIssues); mock.onGet('head_path.json').reply(200, sastHeadAllIssues);
vm = mountComponent(Component); vm = mountComponent(Component);
}); });
afterEach(() => { it('renders no new vulnerabilities message', done => {
mock.restore();
});
it('renders no new vulnerabilities message', (done) => {
setTimeout(() => { setTimeout(() => {
expect( expect(
vm.$el.querySelector('.js-dependency-scanning-widget .js-code-text').textContent.trim(), removeBreakLine(
vm.$el.querySelector(
'.js-dependency-scanning-widget .report-block-list-issue-description',
).textContent,
),
).toEqual('Dependency scanning detected no new security vulnerabilities'); ).toEqual('Dependency scanning detected no new security vulnerabilities');
done(); done();
}, 0); }, 0);
...@@ -231,24 +227,21 @@ describe('ee merge request widget options', () => { ...@@ -231,24 +227,21 @@ describe('ee merge request widget options', () => {
}); });
describe('with empty successful request', () => { describe('with empty successful request', () => {
let mock;
beforeEach(() => { beforeEach(() => {
mock = mock = new MockAdapter(axios);
mock.onGet('path.json').reply(200, []); mock.onGet('path.json').reply(200, []);
mock.onGet('head_path.json').reply(200, []); mock.onGet('head_path.json').reply(200, []);
vm = mountComponent(Component); vm = mountComponent(Component);
}); });
afterEach(() => { it('should render provided data', done => {
mock.restore();
});
it('should render provided data', (done) => {
setTimeout(() => { setTimeout(() => {
expect( expect(
vm.$el.querySelector('.js-dependency-scanning-widget .js-code-text').textContent.trim(), removeBreakLine(
vm.$el.querySelector(
'.js-dependency-scanning-widget .report-block-list-issue-description',
).textContent,
),
).toEqual('Dependency scanning detected no security vulnerabilities'); ).toEqual('Dependency scanning detected no security vulnerabilities');
done(); done();
}, 0); }, 0);
...@@ -256,24 +249,17 @@ describe('ee merge request widget options', () => { ...@@ -256,24 +249,17 @@ describe('ee merge request widget options', () => {
}); });
describe('with failed request', () => { describe('with failed request', () => {
let mock;
beforeEach(() => { beforeEach(() => {
mock = mock = new MockAdapter(axios);
mock.onGet('path.json').reply(500, []); mock.onGet('path.json').reply(500, []);
mock.onGet('head_path.json').reply(500, []); mock.onGet('head_path.json').reply(500, []);
vm = mountComponent(Component); vm = mountComponent(Component);
}); });
afterEach(() => { it('should render error indicator', done => {
mock.restore();
});
it('should render error indicator', (done) => {
setTimeout(() => { setTimeout(() => {
expect( expect(
vm.$el.querySelector('.js-dependency-scanning-widget').textContent.trim(), removeBreakLine(vm.$el.querySelector('.js-dependency-scanning-widget').textContent),
).toContain('Failed to load dependency scanning report'); ).toContain('Dependency scanning resulted in error while loading results');
done(); done();
}, 0); }, 0);
}); });
...@@ -296,56 +282,57 @@ describe('ee merge request widget options', () => { ...@@ -296,56 +282,57 @@ describe('ee merge request widget options', () => {
describe('when it is loading', () => { describe('when it is loading', () => {
it('should render loading indicator', () => { it('should render loading indicator', () => {
mock.onGet('head.json').reply(200, headIssues);
mock.onGet('base.json').reply(200, baseIssues);
vm = mountComponent(Component); vm = mountComponent(Component);
expect( expect(
vm.$el.querySelector('.js-codequality-widget').textContent.trim(), removeBreakLine(vm.$el.querySelector('.js-codequality-widget').textContent),
).toContain('Loading codeclimate report'); ).toContain('Loading codeclimate report');
}); });
}); });
describe('with successful request', () => { describe('with successful request', () => {
let mock;
beforeEach(() => { beforeEach(() => {
mock = mock = new MockAdapter(axios);
mock.onGet('head.json').reply(200, headIssues); mock.onGet('head.json').reply(200, headIssues);
mock.onGet('base.json').reply(200, baseIssues); mock.onGet('base.json').reply(200, baseIssues);
vm = mountComponent(Component); vm = mountComponent(Component);
}); });
afterEach(() => { it('should render provided data', done => {
mock.restore();
});
it('should render provided data', (done) => {
setTimeout(() => { setTimeout(() => {
expect( expect(
vm.$el.querySelector('.js-code-text').textContent.trim(), removeBreakLine(
vm.$el.querySelector('.js-codequality-widget .js-code-text').textContent,
),
).toEqual('Code quality improved on 1 point and degraded on 1 point'); ).toEqual('Code quality improved on 1 point and degraded on 1 point');
done(); done();
}, 0); }, 0);
}); });
describe('text connector', () => { describe('text connector', () => {
it('should only render information about fixed issues', (done) => { it('should only render information about fixed issues', done => {
setTimeout(() => { setTimeout(() => {
vm.mr.codeclimateMetrics.newIssues = []; vm.mr.codeclimateMetrics.newIssues = [];
Vue.nextTick(() => { Vue.nextTick(() => {
expect( expect(
vm.$el.querySelector('.js-code-text').textContent.trim(), removeBreakLine(
vm.$el.querySelector('.js-codequality-widget .js-code-text').textContent,
),
).toEqual('Code quality improved on 1 point'); ).toEqual('Code quality improved on 1 point');
done(); done();
}); });
}, 0); }, 0);
}); });
it('should only render information about added issues', (done) => { it('should only render information about added issues', done => {
setTimeout(() => { setTimeout(() => {
vm.mr.codeclimateMetrics.resolvedIssues = []; vm.mr.codeclimateMetrics.resolvedIssues = [];
Vue.nextTick(() => { Vue.nextTick(() => {
expect( expect(
vm.$el.querySelector('.js-code-text').textContent.trim(), removeBreakLine(
vm.$el.querySelector('.js-codequality-widget .js-code-text').textContent,
),
).toEqual('Code quality degraded on 1 point'); ).toEqual('Code quality degraded on 1 point');
done(); done();
}); });
...@@ -355,10 +342,7 @@ describe('ee merge request widget options', () => { ...@@ -355,10 +342,7 @@ describe('ee merge request widget options', () => {
}); });
describe('with empty successful request', () => { describe('with empty successful request', () => {
let mock;
beforeEach(() => { beforeEach(() => {
mock = mock = new MockAdapter(axios);
mock.onGet('head.json').reply(200, []); mock.onGet('head.json').reply(200, []);
mock.onGet('base.json').reply(200, []); mock.onGet('base.json').reply(200, []);
vm = mountComponent(Component); vm = mountComponent(Component);
...@@ -368,10 +352,12 @@ describe('ee merge request widget options', () => { ...@@ -368,10 +352,12 @@ describe('ee merge request widget options', () => {
mock.restore(); mock.restore();
}); });
it('should render provided data', (done) => { it('should render provided data', done => {
setTimeout(() => { setTimeout(() => {
expect( expect(
vm.$el.querySelector('.js-code-text').textContent.trim(), removeBreakLine(
vm.$el.querySelector('.js-codequality-widget .js-code-text').textContent,
),
).toEqual('No changes to code quality'); ).toEqual('No changes to code quality');
done(); done();
}, 0); }, 0);
...@@ -379,22 +365,19 @@ describe('ee merge request widget options', () => { ...@@ -379,22 +365,19 @@ describe('ee merge request widget options', () => {
}); });
describe('with failed request', () => { describe('with failed request', () => {
let mock;
beforeEach(() => { beforeEach(() => {
mock = mock = new MockAdapter(axios);
mock.onGet('head.json').reply(500, []); mock.onGet('head.json').reply(500, []);
mock.onGet('base.json').reply(500, []); mock.onGet('base.json').reply(500, []);
vm = mountComponent(Component); vm = mountComponent(Component);
}); });
afterEach(() => { it('should render error indicator', done => {
mock.restore();
});
it('should render error indicator', (done) => {
setTimeout(() => { setTimeout(() => {
expect(vm.$el.querySelector('.js-codequality-widget').textContent.trim()).toContain('Failed to load codeclimate report'); expect(
removeBreakLine(
vm.$el.querySelector('.js-codequality-widget .js-code-text').textContent,
),
).toContain('Failed to load codeclimate report');
done(); done();
}, 0); }, 0);
}); });
...@@ -417,57 +400,63 @@ describe('ee merge request widget options', () => { ...@@ -417,57 +400,63 @@ describe('ee merge request widget options', () => {
describe('when it is loading', () => { describe('when it is loading', () => {
it('should render loading indicator', () => { it('should render loading indicator', () => {
mock.onGet('head.json').reply(200, headPerformance);
mock.onGet('base.json').reply(200, basePerformance);
vm = mountComponent(Component); vm = mountComponent(Component);
expect( expect(
vm.$el.querySelector('.js-performance-widget').textContent.trim(), removeBreakLine(vm.$el.querySelector('.js-performance-widget').textContent),
).toContain('Loading performance report'); ).toContain('Loading performance report');
}); });
}); });
describe('with successful request', () => { describe('with successful request', () => {
let mock;
beforeEach(() => { beforeEach(() => {
mock = mock = new MockAdapter(axios);
mock.onGet('head.json').reply(200, headPerformance); mock.onGet('head.json').reply(200, headPerformance);
mock.onGet('base.json').reply(200, basePerformance); mock.onGet('base.json').reply(200, basePerformance);
vm = mountComponent(Component); vm = mountComponent(Component);
}); });
afterEach(() => { it('should render provided data', done => {
mock.restore();
});
it('should render provided data', (done) => {
setTimeout(() => { setTimeout(() => {
expect( expect(
vm.$el.querySelector('.js-performance-widget .js-code-text').textContent.trim(), removeBreakLine(
vm.$el.querySelector('.js-performance-widget .js-code-text')
.textContent,
),
).toEqual('Performance metrics improved on 2 points and degraded on 1 point'); ).toEqual('Performance metrics improved on 2 points and degraded on 1 point');
done(); done();
}, 0); }, 0);
}); });
describe('text connector', () => { describe('text connector', () => {
it('should only render information about fixed issues', (done) => { it('should only render information about fixed issues', done => {
setTimeout(() => { setTimeout(() => {
vm.mr.performanceMetrics.degraded = []; vm.mr.performanceMetrics.degraded = [];
Vue.nextTick(() => { Vue.nextTick(() => {
expect( expect(
vm.$el.querySelector('.js-performance-widget .js-code-text').textContent.trim(), removeBreakLine(
vm.$el.querySelector(
'.js-performance-widget .js-code-text',
).textContent,
),
).toEqual('Performance metrics improved on 2 points'); ).toEqual('Performance metrics improved on 2 points');
done(); done();
}); });
}, 0); }, 0);
}); });
it('should only render information about added issues', (done) => { it('should only render information about added issues', done => {
setTimeout(() => { setTimeout(() => {
vm.mr.performanceMetrics.improved = []; vm.mr.performanceMetrics.improved = [];
Vue.nextTick(() => { Vue.nextTick(() => {
expect( expect(
vm.$el.querySelector('.js-performance-widget .js-code-text').textContent.trim(), removeBreakLine(
vm.$el.querySelector(
'.js-performance-widget .js-code-text',
).textContent,
),
).toEqual('Performance metrics degraded on 1 point'); ).toEqual('Performance metrics degraded on 1 point');
done(); done();
}); });
...@@ -477,23 +466,19 @@ describe('ee merge request widget options', () => { ...@@ -477,23 +466,19 @@ describe('ee merge request widget options', () => {
}); });
describe('with empty successful request', () => { describe('with empty successful request', () => {
let mock;
beforeEach(() => { beforeEach(() => {
mock = mock = new MockAdapter(axios);
mock.onGet('head.json').reply(200, []); mock.onGet('head.json').reply(200, []);
mock.onGet('base.json').reply(200, []); mock.onGet('base.json').reply(200, []);
vm = mountComponent(Component); vm = mountComponent(Component);
}); });
afterEach(() => { it('should render provided data', done => {
mock.restore();
});
it('should render provided data', (done) => {
setTimeout(() => { setTimeout(() => {
expect( expect(
vm.$el.querySelector('.js-performance-widget .js-code-text').textContent.trim(), removeBreakLine(
vm.$el.querySelector('.js-performance-widget .js-code-text')
.textContent,
),
).toEqual('No changes to performance metrics'); ).toEqual('No changes to performance metrics');
done(); done();
}, 0); }, 0);
...@@ -501,34 +486,30 @@ describe('ee merge request widget options', () => { ...@@ -501,34 +486,30 @@ describe('ee merge request widget options', () => {
}); });
describe('with failed request', () => { describe('with failed request', () => {
let mock;
beforeEach(() => { beforeEach(() => {
mock = mock = new MockAdapter(axios);
mock.onGet('head.json').reply(500, []); mock.onGet('head.json').reply(500, []);
mock.onGet('base.json').reply(500, []); mock.onGet('base.json').reply(500, []);
vm = mountComponent(Component); vm = mountComponent(Component);
}); });
afterEach(() => { it('should render error indicator', done => {
mock.restore();
});
it('should render error indicator', (done) => {
setTimeout(() => { setTimeout(() => {
expect(vm.$el.querySelector('.js-performance-widget').textContent.trim()).toContain('Failed to load performance report'); expect(
removeBreakLine(vm.$el.querySelector('.js-performance-widget .js-code-text').textContent),
).toContain('Failed to load performance report');
done(); done();
}, 0); }, 0);
}); });
}); });
}); });
describe('docker report', () => { describe('sast container report', () => {
beforeEach(() => { beforeEach(() => {
gl.mrWidgetData = { gl.mrWidgetData = {
...mockData, ...mockData,
sast_container: { sast_container: {
head_path: 'gl-sast-container.json', head_path: 'gl-sast-container.json',
base_path: 'sast-container-base.json',
}, },
}; };
...@@ -538,71 +519,50 @@ describe('ee merge request widget options', () => { ...@@ -538,71 +519,50 @@ describe('ee merge request widget options', () => {
describe('when it is loading', () => { describe('when it is loading', () => {
it('should render loading indicator', () => { it('should render loading indicator', () => {
mock.onGet('gl-sast-container.json').reply(200, dockerReport);
mock.onGet('sast-container-base.json').reply(200, dockerBaseReport);
vm = mountComponent(Component); vm = mountComponent(Component);
expect( expect(removeBreakLine(vm.$el.querySelector('.js-sast-container').textContent)).toContain(
vm.$el.querySelector('.js-docker-widget').textContent.trim(), 'Container scanning is loading',
).toContain('Loading sast:container report'); );
}); });
}); });
describe('with successful request', () => { describe('with successful request', () => {
let mock;
beforeEach(() => { beforeEach(() => {
mock = mock = new MockAdapter(axios);
mock.onGet('gl-sast-container.json').reply(200, dockerReport); mock.onGet('gl-sast-container.json').reply(200, dockerReport);
vm = mountComponent(Component); mock.onGet('sast-container-base.json').reply(200, dockerBaseReport);
});
afterEach(() => { vm = mountComponent(Component);
mock.restore();
}); });
it('should render provided data', (done) => { it('should render provided data', done => {
setTimeout(() => { setTimeout(() => {
expect( expect(
vm.$el.querySelector('.js-docker-widget .js-code-text').textContent.trim(), removeBreakLine(
).toEqual('SAST:container found 3 vulnerabilities, of which 1 is approved'); vm.$el.querySelector('.js-sast-container .report-block-list-issue-description')
.textContent,
vm.$el.querySelector('.js-docker-widget .js-collapse-btn').click(); ),
).toEqual('Container scanning detected 1 new vulnerability');
Vue.nextTick(() => { done();
expect(
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 .report-block-info a').textContent.trim(),
).toContain('Learn more about whitelisting');
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);
done();
});
}, 0); }, 0);
}); });
}); });
describe('with failed request', () => { describe('with failed request', () => {
let mock;
beforeEach(() => { beforeEach(() => {
mock = mock = new MockAdapter(axios);
mock.onGet('gl-sast-container.json').reply(500, {}); mock.onGet('gl-sast-container.json').reply(500, {});
vm = mountComponent(Component); mock.onGet('sast-container-base.json').reply(500, {});
});
afterEach(() => { vm = mountComponent(Component);
mock.restore();
}); });
it('should render error indicator', (done) => { it('should render error indicator', done => {
setTimeout(() => { setTimeout(() => {
expect( expect(vm.$el.querySelector('.js-sast-container').textContent.trim()).toContain(
vm.$el.querySelector('.js-docker-widget').textContent.trim(), 'Container scanning resulted in error while loading results',
).toContain('Failed to load sast:container report'); );
done(); done();
}, 0); }, 0);
}); });
...@@ -615,6 +575,7 @@ describe('ee merge request widget options', () => { ...@@ -615,6 +575,7 @@ describe('ee merge request widget options', () => {
...mockData, ...mockData,
dast: { dast: {
head_path: 'dast.json', head_path: 'dast.json',
base_path: 'dast_base.json',
}, },
}; };
...@@ -624,63 +585,49 @@ describe('ee merge request widget options', () => { ...@@ -624,63 +585,49 @@ describe('ee merge request widget options', () => {
describe('when it is loading', () => { describe('when it is loading', () => {
it('should render loading indicator', () => { it('should render loading indicator', () => {
mock.onGet('dast.json').reply(200, dast);
mock.onGet('dast_base.json').reply(200, dastBase);
vm = mountComponent(Component); vm = mountComponent(Component);
expect( expect(vm.$el.querySelector('.js-dast-widget').textContent.trim()).toContain(
vm.$el.querySelector('.js-dast-widget').textContent.trim(), 'DAST is loading',
).toContain('Loading DAST report'); );
}); });
}); });
describe('with successful request', () => { describe('with successful request', () => {
let mock;
beforeEach(() => { beforeEach(() => {
mock = mock = new MockAdapter(axios);
mock.onGet('dast.json').reply(200, dast); mock.onGet('dast.json').reply(200, dast);
vm = mountComponent(Component); mock.onGet('dast_base.json').reply(200, dastBase);
});
afterEach(() => { vm = mountComponent(Component);
mock.restore();
}); });
it('should render provided data', (done) => { it('should render provided data', done => {
setTimeout(() => { setTimeout(() => {
expect( expect(
vm.$el.querySelector('.js-dast-widget .js-code-text').textContent.trim(), vm.$el
).toEqual('DAST detected 2 alerts by analyzing the review app'); .querySelector('.js-dast-widget .report-block-list-issue-description')
.textContent.trim(),
vm.$el.querySelector('.js-dast-widget button').click(); ).toEqual('DAST detected 1 new vulnerability');
done();
Vue.nextTick(() => {
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();
});
}, 0); }, 0);
}); });
}); });
describe('with failed request', () => { describe('with failed request', () => {
let mock;
beforeEach(() => { beforeEach(() => {
mock = mock = new MockAdapter(axios);
mock.onGet('dast.json').reply(500, {}); mock.onGet('dast.json').reply(500, {});
vm = mountComponent(Component); mock.onGet('dast_base.json').reply(500, {});
});
afterEach(() => { vm = mountComponent(Component);
mock.restore();
}); });
it('should render error indicator', (done) => { it('should render error indicator', done => {
setTimeout(() => { setTimeout(() => {
expect( expect(vm.$el.querySelector('.js-dast-widget').textContent.trim()).toContain(
vm.$el.querySelector('.js-dast-widget').textContent.trim(), 'DAST resulted in error while loading results',
).toContain('Failed to load DAST report'); );
done(); done();
}, 0); }, 0);
}); });
...@@ -725,183 +672,6 @@ describe('ee merge request widget options', () => { ...@@ -725,183 +672,6 @@ describe('ee merge request widget options', () => {
expect(vm.shouldRenderApprovals).toBeTruthy(); expect(vm.shouldRenderApprovals).toBeTruthy();
}); });
}); });
describe('dockerText', () => {
beforeEach(() => {
vm = mountComponent(Component, {
mrData: {
...mockData,
sast_container: {
path: 'foo',
},
},
});
});
describe('with no vulnerabilities', () => {
it('returns No vulnerabilities found', () => {
expect(vm.dockerText).toEqual('SAST:container no vulnerabilities were found');
});
});
describe('without unapproved vulnerabilities', () => {
it('returns approved information - single', () => {
vm.mr.dockerReport = {
vulnerabilities: [{
vulnerability: 'CVE-2017-12944',
namespace: 'debian:8',
severity: 'Medium',
}],
approved: [{
vulnerability: 'CVE-2017-12944',
namespace: 'debian:8',
severity: 'Medium',
}],
unapproved: [],
};
expect(vm.dockerText).toEqual('SAST:container found 1 approved vulnerability');
});
it('returns approved information - plural', () => {
vm.mr.dockerReport = {
vulnerabilities: [{
vulnerability: 'CVE-2017-12944',
namespace: 'debian:8',
severity: 'Medium',
}],
approved: [
{
vulnerability: 'CVE-2017-12944',
namespace: 'debian:8',
severity: 'Medium',
},
{
vulnerability: 'CVE-2017-13726',
namespace: 'debian:8',
severity: 'Medium',
},
],
unapproved: [],
};
expect(vm.dockerText).toEqual('SAST:container found 2 approved vulnerabilities');
});
});
describe('with only unapproved vulnerabilities', () => {
it('returns number of vulnerabilities - single', () => {
vm.mr.dockerReport = {
vulnerabilities: [{
vulnerability: 'CVE-2017-12944',
namespace: 'debian:8',
severity: 'Medium',
}],
unapproved: [
{
vulnerability: 'CVE-2017-12944',
namespace: 'debian:8',
severity: 'Medium',
},
],
approved: [],
};
expect(vm.dockerText).toEqual('SAST:container found 1 vulnerability');
});
it('returns number of vulnerabilities - plural', () => {
vm.mr.dockerReport = {
vulnerabilities: [{
vulnerability: 'CVE-2017-12944',
namespace: 'debian:8',
severity: 'Medium',
}],
unapproved: [
{
vulnerability: 'CVE-2017-12944',
namespace: 'debian:8',
severity: 'Medium',
},
{
vulnerability: 'CVE-2017-12944',
namespace: 'debian:8',
severity: 'Medium',
},
],
approved: [],
};
expect(vm.dockerText).toEqual('SAST:container found 2 vulnerabilities');
});
});
describe('with approved and unapproved vulnerabilities', () => {
it('returns message with information about both - single', () => {
vm.mr.dockerReport = {
vulnerabilities: [{
vulnerability: 'CVE-2017-12944',
namespace: 'debian:8',
severity: 'Medium',
}],
unapproved: [
{
vulnerability: 'CVE-2017-12944',
namespace: 'debian:8',
severity: 'Medium',
},
],
approved: [
{
vulnerability: 'CVE-2017-12944',
namespace: 'debian:8',
severity: 'Medium',
},
],
};
expect(vm.dockerText).toEqual('SAST:container found 1 vulnerability, of which 1 is approved');
});
it('returns message with information about both - plural', () => {
vm.mr.dockerReport = {
vulnerabilities: [
{
vulnerability: 'CVE-2017-12944',
namespace: 'debian:8',
severity: 'Medium',
},
{
vulnerability: 'CVE-2017-12944',
namespace: 'debian:8',
severity: 'Medium',
},
],
unapproved: [
{
vulnerability: 'CVE-2017-12944',
namespace: 'debian:8',
severity: 'Medium',
},
{
vulnerability: 'CVE-2017-12923',
namespace: 'debian:8',
severity: 'Medium',
},
],
approved: [
{
vulnerability: 'CVE-2017-12944',
namespace: 'debian:8',
severity: 'Medium',
},
{
vulnerability: 'CVE-2017-13944',
namespace: 'debian:8',
severity: 'Medium',
},
],
};
expect(vm.dockerText).toEqual('SAST:container found 2 vulnerabilities, of which 2 are approved');
});
});
});
}); });
describe('rendering source branch removal status', () => { describe('rendering source branch removal status', () => {
...@@ -913,7 +683,7 @@ describe('ee merge request widget options', () => { ...@@ -913,7 +683,7 @@ describe('ee merge request widget options', () => {
}); });
}); });
it('renders when user cannot remove branch and branch should be removed', (done) => { it('renders when user cannot remove branch and branch should be removed', done => {
vm.mr.canRemoveSourceBranch = false; vm.mr.canRemoveSourceBranch = false;
vm.mr.shouldRemoveSourceBranch = true; vm.mr.shouldRemoveSourceBranch = true;
vm.mr.state = 'readyToMerge'; vm.mr.state = 'readyToMerge';
...@@ -930,7 +700,7 @@ describe('ee merge request widget options', () => { ...@@ -930,7 +700,7 @@ describe('ee merge request widget options', () => {
}); });
}); });
it('does not render in merged state', (done) => { it('does not render in merged state', done => {
vm.mr.canRemoveSourceBranch = false; vm.mr.canRemoveSourceBranch = false;
vm.mr.shouldRemoveSourceBranch = true; vm.mr.shouldRemoveSourceBranch = true;
vm.mr.state = 'merged'; vm.mr.state = 'merged';
...@@ -958,19 +728,22 @@ describe('ee merge request widget options', () => { ...@@ -958,19 +728,22 @@ describe('ee merge request widget options', () => {
deployed_at_formatted: 'Mar 22, 2017 10:44pm', deployed_at_formatted: 'Mar 22, 2017 10:44pm',
}; };
beforeEach((done) => { beforeEach(done => {
vm = mountComponent(Component, { vm = mountComponent(Component, {
mrData: { mrData: {
...mockData, ...mockData,
}, },
}); });
vm.mr.deployments.push({ vm.mr.deployments.push(
...deploymentMockData, {
}, { ...deploymentMockData,
...deploymentMockData, },
id: deploymentMockData.id + 1, {
}); ...deploymentMockData,
id: deploymentMockData.id + 1,
},
);
vm.$nextTick(done); vm.$nextTick(done);
}); });
......
...@@ -6,18 +6,6 @@ import mockData, { ...@@ -6,18 +6,6 @@ import mockData, {
parsedBaseIssues, parsedBaseIssues,
parsedHeadIssues, parsedHeadIssues,
} from '../mock_data'; } from '../mock_data';
import {
sastIssues,
sastIssuesBase,
parsedSastBaseStore,
parsedSastIssuesHead,
parsedSastIssuesStore,
allIssuesParsed,
dockerReport,
dockerReportParsed,
dast,
parsedDast,
} from '../../vue_shared/security_reports/mock_data';
describe('MergeRequestStore', () => { describe('MergeRequestStore', () => {
let store; let store;
...@@ -98,43 +86,12 @@ describe('MergeRequestStore', () => { ...@@ -98,43 +86,12 @@ describe('MergeRequestStore', () => {
}); });
}); });
describe('setSecurityReport', () => { describe('parseCodeclimateMetrics', () => {
it('should set security issues with head', () => { it('should parse the received issues', () => {
store.setSecurityReport({ head: sastIssues, headBlobPath: 'path' }); const codequality = MergeRequestStore.parseCodeclimateMetrics(baseIssues, 'path')[0];
expect(store.securityReport.newIssues).toEqual(parsedSastIssuesStore); 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);
it('should set security issues with head and base', () => {
store.setSecurityReport({
head: sastIssues,
headBlobPath: 'path',
base: sastIssuesBase,
baseBlobPath: 'path',
});
expect(store.securityReport.newIssues).toEqual(parsedSastIssuesHead);
expect(store.securityReport.resolvedIssues).toEqual(parsedSastBaseStore);
expect(store.securityReport.allIssues).toEqual(allIssuesParsed);
});
});
describe('setDependencyScanningReport', () => {
it('should set security issues with head', () => {
store.setDependencyScanningReport({ head: sastIssues, headBlobPath: 'path' });
expect(store.dependencyScanningReport.newIssues).toEqual(parsedSastIssuesStore);
});
it('should set security issues with head and base', () => {
store.setDependencyScanningReport({
head: sastIssues,
headBlobPath: 'path',
base: sastIssuesBase,
baseBlobPath: 'path',
});
expect(store.dependencyScanningReport.newIssues).toEqual(parsedSastIssuesHead);
expect(store.dependencyScanningReport.resolvedIssues).toEqual(parsedSastBaseStore);
expect(store.dependencyScanningReport.allIssues).toEqual(allIssuesParsed);
}); });
}); });
...@@ -149,43 +106,4 @@ describe('MergeRequestStore', () => { ...@@ -149,43 +106,4 @@ describe('MergeRequestStore', () => {
expect(store.isNothingToMergeState).toEqual(false); expect(store.isNothingToMergeState).toEqual(false);
}); });
}); });
describe('initDockerReport', () => {
it('sets the defaults', () => {
store.initDockerReport({ sast_container: { path: 'gl-sast-container.json' } });
expect(store.sastContainer).toEqual({ path: 'gl-sast-container.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);
});
});
describe('initDastReport', () => {
it('sets the defaults', () => {
store.initDastReport({ dast: { path: 'dast.json' } });
expect(store.dast).toEqual({ path: 'dast.json' });
expect(store.dastReport).toEqual([]);
});
});
describe('setDastReport', () => {
it('parsed data and sets the report', () => {
store.setDastReport(dast);
expect(store.dastReport).toEqual(parsedDast);
});
});
}); });
import Vue from 'vue';
import component from 'ee/vue_shared/security_reports/components/error_row.vue';
import mountComponent from 'spec/helpers/vue_mount_component_helper';
describe('loading row', () => {
const Component = Vue.extend(component);
let vm;
beforeEach(() => {
vm = mountComponent(Component);
});
afterEach(() => {
vm.$destroy();
});
it('renders warning icon with error message', () => {
expect(vm.$el.querySelector('.report-block-list-icon span').classList).toContain(
'js-ci-status-icon-warning',
);
expect(vm.$el.querySelector('.report-block-list-issue-description').textContent.trim()).toEqual(
'There was an error loading results',
);
});
});
import Vue from 'vue';
import component from 'ee/vue_shared/security_reports/components/loading_row.vue';
import mountComponent from 'spec/helpers/vue_mount_component_helper';
describe('loading row', () => {
const Component = Vue.extend(component);
let vm;
beforeEach(() => {
vm = mountComponent(Component);
});
afterEach(() => {
vm.$destroy();
});
it('renders loading icon with message', () => {
expect(vm.$el.querySelector('.report-block-list-icon i').classList).toContain('fa-spin');
expect(vm.$el.querySelector('.report-block-list-issue-description').textContent.trim()).toEqual(
'in progress',
);
});
});
...@@ -19,10 +19,11 @@ describe('Report section', () => { ...@@ -19,10 +19,11 @@ describe('Report section', () => {
it('should render loading indicator', () => { it('should render loading indicator', () => {
vm = mountComponent(ReportSection, { vm = mountComponent(ReportSection, {
type: 'codequality', type: 'codequality',
status: 'loading', status: 'LOADING',
loadingText: 'Loading codeclimate report', loadingText: 'Loading codeclimate report',
errorText: 'foo', errorText: 'foo',
successText: 'Code quality improved on 1 point and degraded on 1 point', successText: 'Code quality improved on 1 point and degraded on 1 point',
hasIssues: false,
}); });
expect(vm.$el.textContent.trim()).toEqual('Loading codeclimate report'); expect(vm.$el.textContent.trim()).toEqual('Loading codeclimate report');
}); });
...@@ -32,11 +33,12 @@ describe('Report section', () => { ...@@ -32,11 +33,12 @@ describe('Report section', () => {
it('should render provided data', () => { it('should render provided data', () => {
vm = mountComponent(ReportSection, { vm = mountComponent(ReportSection, {
type: 'codequality', type: 'codequality',
status: 'success', status: 'SUCCESS',
loadingText: 'Loading codeclimate report', loadingText: 'Loading codeclimate report',
errorText: 'foo', errorText: 'foo',
successText: 'Code quality improved on 1 point and degraded on 1 point', successText: 'Code quality improved on 1 point and degraded on 1 point',
resolvedIssues: codequalityParsedIssues, resolvedIssues: codequalityParsedIssues,
hasIssues: true,
}); });
expect( expect(
...@@ -52,18 +54,19 @@ describe('Report section', () => { ...@@ -52,18 +54,19 @@ describe('Report section', () => {
it('toggles issues', (done) => { it('toggles issues', (done) => {
vm = mountComponent(ReportSection, { vm = mountComponent(ReportSection, {
type: 'codequality', type: 'codequality',
status: 'success', status: 'SUCCESS',
loadingText: 'Loading codeclimate report', loadingText: 'Loading codeclimate report',
errorText: 'foo', errorText: 'foo',
successText: 'Code quality improved on 1 point and degraded on 1 point', successText: 'Code quality improved on 1 point and degraded on 1 point',
resolvedIssues: codequalityParsedIssues, resolvedIssues: codequalityParsedIssues,
hasIssues: true,
}); });
vm.$el.querySelector('button').click(); vm.$el.querySelector('button').click();
Vue.nextTick(() => { Vue.nextTick(() => {
expect( expect(
vm.$el.querySelector('.report-block-container').getAttribute('style'), vm.$el.querySelector('.js-report-section-container').getAttribute('style'),
).toEqual(''); ).toEqual('');
expect( expect(
vm.$el.querySelector('button').textContent.trim(), vm.$el.querySelector('button').textContent.trim(),
...@@ -73,7 +76,7 @@ describe('Report section', () => { ...@@ -73,7 +76,7 @@ describe('Report section', () => {
Vue.nextTick(() => { Vue.nextTick(() => {
expect( expect(
vm.$el.querySelector('.report-block-container').getAttribute('style'), vm.$el.querySelector('.js-report-section-container').getAttribute('style'),
).toEqual('display: none;'); ).toEqual('display: none;');
expect( expect(
vm.$el.querySelector('button').textContent.trim(), vm.$el.querySelector('button').textContent.trim(),
...@@ -90,10 +93,11 @@ describe('Report section', () => { ...@@ -90,10 +93,11 @@ describe('Report section', () => {
it('should render error indicator', () => { it('should render error indicator', () => {
vm = mountComponent(ReportSection, { vm = mountComponent(ReportSection, {
type: 'codequality', type: 'codequality',
status: 'error', status: 'ERROR',
loadingText: 'Loading codeclimate report', loadingText: 'Loading codeclimate report',
errorText: 'Failed to load codeclimate report', errorText: 'Failed to load codeclimate report',
successText: 'Code quality improved on 1 point and degraded on 1 point', successText: 'Code quality improved on 1 point and degraded on 1 point',
hasIssues: false,
}); });
expect(vm.$el.textContent.trim()).toEqual('Failed to load codeclimate report'); expect(vm.$el.textContent.trim()).toEqual('Failed to load codeclimate report');
}); });
...@@ -102,11 +106,11 @@ describe('Report section', () => { ...@@ -102,11 +106,11 @@ describe('Report section', () => {
describe('With full report', () => { describe('With full report', () => {
beforeEach(() => { beforeEach(() => {
vm = mountComponent(ReportSection, { vm = mountComponent(ReportSection, {
status: 'success', status: 'SUCCESS',
successText: 'SAST improved on 1 security vulnerability and degraded on 1 security vulnerability', successText: 'SAST improved on 1 security vulnerability and degraded on 1 security vulnerability',
type: 'SAST', type: 'SAST',
errorText: 'Failed to load security report', errorText: 'Failed to load security report',
hasPriority: true, hasIssues: true,
loadingText: 'Loading security report', loadingText: 'Loading security report',
resolvedIssues: [{ resolvedIssues: [{
cve: 'CVE-2016-9999', cve: 'CVE-2016-9999',
...@@ -172,28 +176,4 @@ describe('Report section', () => { ...@@ -172,28 +176,4 @@ describe('Report section', () => {
}); });
}); });
}); });
describe('When it is not collapsible', () => {
beforeEach(() => {
vm = mountComponent(ReportSection, {
type: 'codequality',
status: 'success',
loadingText: 'Loading codeclimate report',
errorText: 'foo',
successText: 'Code quality improved on 1 point and degraded on 1 point',
resolvedIssues: codequalityParsedIssues,
isCollapsible: false,
});
});
it('should not render collapse button', () => {
expect(vm.$el.querySelector('.js-collapse-btn')).toBe(null);
});
it('should show the report by default', () => {
expect(
vm.$el.querySelectorAll('.report-block-list .report-block-list-issue').length,
).toEqual(codequalityParsedIssues.length);
});
});
}); });
import Vue from 'vue';
import MockAdapter from 'axios-mock-adapter';
import axios from '~/lib/utils/axios_utils';
import component from 'ee/vue_shared/security_reports/grouped_security_reports_app.vue';
import state from 'ee/vue_shared/security_reports/store/state';
import mountComponent from '../../helpers/vue_mount_component_helper';
import {
sastIssues,
sastIssuesBase,
dockerReport,
dockerBaseReport,
dast,
dastBase,
} from './mock_data';
describe('Grouped security reports app', () => {
let vm;
let mock;
const Component = Vue.extend(component);
function removeBreakLine(data) {
return data
.replace(/\r?\n|\r/g, '')
.replace(/\s\s+/g, ' ')
.trim();
}
beforeEach(() => {
mock = new MockAdapter(axios);
});
afterEach(() => {
vm.$destroy();
mock.restore();
vm.$store.replaceState(state());
});
describe('with error', () => {
beforeEach(() => {
mock.onGet('sast_head.json').reply(500);
mock.onGet('sast_base.json').reply(500);
mock.onGet('dast_head.json').reply(500);
mock.onGet('dast_base.json').reply(500);
mock.onGet('sast_container_head.json').reply(500);
mock.onGet('sast_container_base.json').reply(500);
mock.onGet('dss_head.json').reply(500);
mock.onGet('dss_base.json').reply(500);
vm = mountComponent(Component, {
headBlobPath: 'path',
baseBlobPath: 'path',
sastHeadPath: 'sast_head.json',
sastBasePath: 'sast_base.json',
dastHeadPath: 'dast_head.json',
dastBasePath: 'dast_base.json',
sastContainerHeadPath: 'sast_container_head.json',
sastContainerBasePath: 'sast_container_base.json',
dependencyScanningHeadPath: 'dss_head.json',
dependencyScanningBasePath: 'dss_base.json',
sastHelpPath: 'path',
sastContainerHelpPath: 'path',
dastHelpPath: 'path',
dependencyScanningHelpPath: 'path',
});
});
it('renders loading state', done => {
setTimeout(() => {
expect(vm.$el.querySelector('.fa-spinner')).toBeNull();
expect(vm.$el.querySelector('.js-code-text').textContent.trim()).toEqual(
'Security scanning failed loading any results',
);
expect(vm.$el.querySelector('.js-collapse-btn').textContent.trim()).toEqual('Expand');
expect(removeBreakLine(vm.$el.textContent)).toContain('SAST resulted in error while loading results');
expect(removeBreakLine(vm.$el.textContent)).toContain('Dependency scanning resulted in error while loading results');
expect(vm.$el.textContent).toContain('Container scanning resulted in error while loading results');
expect(vm.$el.textContent).toContain('DAST resulted in error while loading results');
done();
}, 0);
});
});
describe('while loading', () => {
beforeEach(() => {
mock.onGet('sast_head.json').reply(200, sastIssues);
mock.onGet('sast_base.json').reply(200, sastIssuesBase);
mock.onGet('dast_head.json').reply(200, dast);
mock.onGet('dast_base.json').reply(200, dastBase);
mock.onGet('sast_container_head.json').reply(200, dockerReport);
mock.onGet('sast_container_base.json').reply(200, dockerBaseReport);
mock.onGet('dss_head.json').reply(200, sastIssues);
mock.onGet('dss_base.json').reply(200, sastIssuesBase);
vm = mountComponent(Component, {
headBlobPath: 'path',
baseBlobPath: 'path',
sastHeadPath: 'sast_head.json',
sastBasePath: 'sast_base.json',
dastHeadPath: 'dast_head.json',
dastBasePath: 'dast_base.json',
sastContainerHeadPath: 'sast_container_head.json',
sastContainerBasePath: 'sast_container_base.json',
dependencyScanningHeadPath: 'dss_head.json',
dependencyScanningBasePath: 'dss_base.json',
sastHelpPath: 'path',
sastContainerHelpPath: 'path',
dastHelpPath: 'path',
dependencyScanningHelpPath: 'path',
});
});
it('renders loading summary text + spinner', () => {
expect(vm.$el.querySelector('.fa-spinner')).not.toBeNull();
expect(vm.$el.querySelector('.js-code-text').textContent.trim()).toEqual(
'Security scanning is loading',
);
expect(vm.$el.querySelector('.js-collapse-btn').textContent.trim()).toEqual('Expand');
expect(vm.$el.textContent).toContain('SAST is loading');
expect(vm.$el.textContent).toContain('Dependency scanning is loading');
expect(vm.$el.textContent).toContain('Container scanning is loading');
expect(vm.$el.textContent).toContain('DAST is loading');
});
});
describe('with all reports', () => {
beforeEach(() => {
mock.onGet('sast_head.json').reply(200, sastIssues);
mock.onGet('sast_base.json').reply(200, sastIssuesBase);
mock.onGet('dast_head.json').reply(200, dast);
mock.onGet('dast_base.json').reply(200, dastBase);
mock.onGet('sast_container_head.json').reply(200, dockerReport);
mock.onGet('sast_container_base.json').reply(200, dockerBaseReport);
mock.onGet('dss_head.json').reply(200, sastIssues);
mock.onGet('dss_base.json').reply(200, sastIssuesBase);
vm = mountComponent(Component, {
headBlobPath: 'path',
baseBlobPath: 'path',
sastHeadPath: 'sast_head.json',
sastBasePath: 'sast_base.json',
dastHeadPath: 'dast_head.json',
dastBasePath: 'dast_base.json',
sastContainerHeadPath: 'sast_container_head.json',
sastContainerBasePath: 'sast_container_base.json',
dependencyScanningHeadPath: 'dss_head.json',
dependencyScanningBasePath: 'dss_base.json',
sastHelpPath: 'path',
sastContainerHelpPath: 'path',
dastHelpPath: 'path',
dependencyScanningHelpPath: 'path',
});
});
it('renders reports', done => {
setTimeout(() => {
expect(vm.$el.querySelector('.fa-spinner')).toBeNull();
expect(vm.$el.querySelector('.js-code-text').textContent.trim()).toEqual(
'Security scanning detected 12 new vulnerabilities and 4 fixed vulnerabilities',
);
expect(vm.$el.querySelector('.js-collapse-btn').textContent.trim()).toEqual('Expand');
expect(removeBreakLine(vm.$el.textContent)).toContain('SAST detected 2 new vulnerabilities and 1 fixed vulnerability');
expect(removeBreakLine(vm.$el.textContent)).toContain('Dependency scanning detected 2 new vulnerabilities and 1 fixed vulnerability');
expect(vm.$el.textContent).toContain('Container scanning detected 1 new vulnerability');
expect(vm.$el.textContent).toContain('DAST detected 1 new vulnerability');
done();
}, 0);
});
});
});
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 added 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',
);
});
it('returns text for full report and no added or fixed issues', () => {
expect(mixin.methods.sastText([], [], parsedSastIssuesHead)).toEqual(
'SAST detected no new 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 {
parseSastIssues,
parseCodeclimateMetrics,
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('parseSastIssues', () => {
it('should parse the received issues', () => {
const security = parseSastIssues(sastIssues, 'path')[0];
expect(security.name).toEqual(sastIssues[0].message);
expect(security.path).toEqual(sastIssues[0].file);
});
});
describe('parseCodeclimateMetrics', () => {
it('should parse the received issues', () => {
const codequality = parseCodeclimateMetrics(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);
});
});
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);
});
});
});
import Vue from 'vue';
import MockAdapter from 'axios-mock-adapter';
import axios from '~/lib/utils/axios_utils';
import component from 'ee/vue_shared/security_reports/split_security_reports_app.vue';
import state from 'ee/vue_shared/security_reports/store/state';
import mountComponent from '../../helpers/vue_mount_component_helper';
import { sastIssues } from './mock_data';
describe('Slipt security reports app', () => {
const Component = Vue.extend(component);
let vm;
let mock;
function removeBreakLine(data) {
return data
.replace(/\r?\n|\r/g, '')
.replace(/\s\s+/g, ' ')
.trim();
}
beforeEach(() => {
mock = new MockAdapter(axios);
});
afterEach(() => {
vm.$destroy();
mock.restore();
vm.$store.replaceState(state());
});
describe('while loading', () => {
beforeEach(() => {
mock.onGet('sast_head.json').reply(200, sastIssues);
mock.onGet('dss_head.json').reply(200, sastIssues);
vm = mountComponent(Component, {
headBlobPath: 'path',
baseBlobPath: 'path',
sastHeadPath: 'sast_head.json',
dependencyScanningHeadPath: 'dss_head.json',
sastHelpPath: 'path',
dependencyScanningHelpPath: 'path',
});
});
it('renders loading summary text + spinner', () => {
expect(vm.$el.querySelector('.fa-spinner')).not.toBeNull();
expect(vm.$el.textContent).toContain('SAST is loading');
expect(vm.$el.textContent).toContain('Dependency scanning is loading');
});
});
describe('with all reports', () => {
beforeEach(() => {
mock.onGet('sast_head.json').reply(200, sastIssues);
mock.onGet('dss_head.json').reply(200, sastIssues);
vm = mountComponent(Component, {
headBlobPath: 'path',
baseBlobPath: 'path',
sastHeadPath: 'sast_head.json',
dependencyScanningHeadPath: 'dss_head.json',
sastHelpPath: 'path',
dependencyScanningHelpPath: 'path',
});
});
it('renders reports', done => {
setTimeout(() => {
expect(vm.$el.querySelector('.fa-spinner')).toBeNull();
expect(vm.$el.querySelector('.js-collapse-btn').textContent.trim()).toEqual('Expand');
expect(removeBreakLine(vm.$el.textContent)).toContain('SAST detected 3 vulnerabilities');
expect(removeBreakLine(vm.$el.textContent)).toContain(
'Dependency scanning detected 3 vulnerabilities',
);
done();
}, 0);
});
});
describe('with error', () => {
beforeEach(() => {
mock.onGet('sast_head.json').reply(500);
mock.onGet('dss_head.json').reply(500);
vm = mountComponent(Component, {
headBlobPath: 'path',
baseBlobPath: 'path',
sastHeadPath: 'sast_head.json',
dependencyScanningHeadPath: 'dss_head.json',
sastHelpPath: 'path',
dependencyScanningHelpPath: 'path',
});
});
it('renders error state', done => {
setTimeout(() => {
expect(vm.$el.querySelector('.fa-spinner')).toBeNull();
expect(removeBreakLine(vm.$el.textContent)).toContain('SAST resulted in error while loading results');
expect(removeBreakLine(vm.$el.textContent)).toContain(
'Dependency scanning resulted in error while loading results',
);
done();
}, 0);
});
});
});
...@@ -23,7 +23,11 @@ describe('Security reports getters', () => { ...@@ -23,7 +23,11 @@ describe('Security reports getters', () => {
describe('groupedSastText', () => { describe('groupedSastText', () => {
describe('with no issues', () => { describe('with no issues', () => {
it('returns no issues text', () => { it('returns no issues text', () => {
expect(groupedSastText(state())).toEqual('SAST detected no security vulnerabilities'); const newState = state();
newState.sast.paths.head = 'foo';
newState.sast.paths.base = 'bar';
expect(groupedSastText(newState)).toEqual('SAST detected no security vulnerabilities');
}); });
}); });
...@@ -43,7 +47,7 @@ describe('Security reports getters', () => { ...@@ -43,7 +47,7 @@ describe('Security reports getters', () => {
newState.sast.newIssues = [{}]; newState.sast.newIssues = [{}];
expect(groupedSastText(newState)).toEqual( expect(groupedSastText(newState)).toEqual(
'SAST was unable to compare existing and new vulnerabilities. It detected 1 vulnerability', 'SAST detected 1 vulnerability for the source branch only',
); );
}); });
}); });
...@@ -84,13 +88,35 @@ describe('Security reports getters', () => { ...@@ -84,13 +88,35 @@ describe('Security reports getters', () => {
expect(groupedSastText(newState)).toEqual('SAST detected 1 fixed vulnerability'); expect(groupedSastText(newState)).toEqual('SAST detected 1 fixed vulnerability');
}); });
}); });
describe('with error', () => {
it('returns error text', () => {
const newState = state();
newState.sast.hasError = true;
expect(groupedSastText(newState)).toEqual('SAST resulted in error while loading results');
});
});
describe('while loading', () => {
it('returns loading text', () => {
const newState = state();
newState.sast.isLoading = true;
expect(groupedSastText(newState)).toEqual('SAST is loading');
});
});
}); });
}); });
describe('groupedSastContainerText', () => { describe('groupedSastContainerText', () => {
describe('with no issues', () => { describe('with no issues', () => {
it('returns no issues text', () => { it('returns no issues text', () => {
expect(groupedSastContainerText(state())).toEqual( const newState = state();
newState.sastContainer.paths.head = 'foo';
newState.sastContainer.paths.base = 'foo';
expect(groupedSastContainerText(newState)).toEqual(
'Container scanning detected no security vulnerabilities', 'Container scanning detected no security vulnerabilities',
); );
}); });
...@@ -103,7 +129,7 @@ describe('Security reports getters', () => { ...@@ -103,7 +129,7 @@ describe('Security reports getters', () => {
newState.sastContainer.newIssues = [{}]; newState.sastContainer.newIssues = [{}];
expect(groupedSastContainerText(newState)).toEqual( expect(groupedSastContainerText(newState)).toEqual(
'Container scanning was unable to compare existing and new vulnerabilities. It detected 1 vulnerability', 'Container scanning detected 1 vulnerability for the source branch only',
); );
}); });
}); });
...@@ -154,7 +180,11 @@ describe('Security reports getters', () => { ...@@ -154,7 +180,11 @@ describe('Security reports getters', () => {
describe('groupedDastText', () => { describe('groupedDastText', () => {
describe('with no issues', () => { describe('with no issues', () => {
it('returns no issues text', () => { it('returns no issues text', () => {
expect(groupedDastText(state())).toEqual('DAST detected no security vulnerabilities'); const newState = state();
newState.dast.paths.head = 'foo';
newState.dast.paths.base = 'foo';
expect(groupedDastText(newState)).toEqual('DAST detected no security vulnerabilities');
}); });
}); });
...@@ -165,7 +195,7 @@ describe('Security reports getters', () => { ...@@ -165,7 +195,7 @@ describe('Security reports getters', () => {
newState.dast.newIssues = [{}]; newState.dast.newIssues = [{}];
expect(groupedDastText(newState)).toEqual( expect(groupedDastText(newState)).toEqual(
'DAST was unable to compare existing and new vulnerabilities. It detected 1 vulnerability', 'DAST detected 1 vulnerability for the source branch only',
); );
}); });
}); });
...@@ -211,7 +241,11 @@ describe('Security reports getters', () => { ...@@ -211,7 +241,11 @@ describe('Security reports getters', () => {
describe('groupedDependencyText', () => { describe('groupedDependencyText', () => {
describe('with no issues', () => { describe('with no issues', () => {
it('returns no issues text', () => { it('returns no issues text', () => {
expect(groupedDependencyText(state())).toEqual( const newState = state();
newState.dependencyScanning.paths.head = 'foo';
newState.dependencyScanning.paths.base = 'foo';
expect(groupedDependencyText(newState)).toEqual(
'Dependency scanning detected no security vulnerabilities', 'Dependency scanning detected no security vulnerabilities',
); );
}); });
...@@ -224,7 +258,7 @@ describe('Security reports getters', () => { ...@@ -224,7 +258,7 @@ describe('Security reports getters', () => {
newState.dependencyScanning.newIssues = [{}]; newState.dependencyScanning.newIssues = [{}];
expect(groupedDependencyText(newState)).toEqual( expect(groupedDependencyText(newState)).toEqual(
'Dependency scanning was unable to compare existing and new vulnerabilities. It detected 1 vulnerability', 'Dependency scanning detected 1 vulnerability for the source branch only',
); );
}); });
}); });
...@@ -290,18 +324,18 @@ describe('Security reports getters', () => { ...@@ -290,18 +324,18 @@ describe('Security reports getters', () => {
areReportsLoading: false, areReportsLoading: false,
}), }),
).toEqual( ).toEqual(
'Security scanning was unable to compare existing and new vulnerabilities. It detected no vulnerabilities.', 'Security scanning detected no vulnerabilities for the source branch only',
); );
}); });
it('returns in progress text', () => { it('returns is loading text', () => {
expect( expect(
groupedSummaryText(state(), { groupedSummaryText(state(), {
allReportsHaveError: false, allReportsHaveError: false,
noBaseInAllReports: false, noBaseInAllReports: false,
areReportsLoading: true, areReportsLoading: true,
}), }),
).toContain('(in progress)'); ).toContain('(is loading)');
}); });
it('returns added and fixed text', () => { it('returns added and fixed text', () => {
...@@ -337,7 +371,7 @@ describe('Security reports getters', () => { ...@@ -337,7 +371,7 @@ describe('Security reports getters', () => {
}); });
it('returns fixed text', () => { it('returns fixed text', () => {
const newState = Object.assign({}, state()); const newState = state();
newState.summaryCounts = { newState.summaryCounts = {
added: 0, added: 0,
fixed: 4, fixed: 4,
...@@ -353,7 +387,7 @@ describe('Security reports getters', () => { ...@@ -353,7 +387,7 @@ describe('Security reports getters', () => {
}); });
it('returns added and fixed while loading text', () => { it('returns added and fixed while loading text', () => {
const newState = Object.assign({}, state()); const newState = state();
newState.summaryCounts = { newState.summaryCounts = {
added: 2, added: 2,
fixed: 4, fixed: 4,
...@@ -366,20 +400,20 @@ describe('Security reports getters', () => { ...@@ -366,20 +400,20 @@ describe('Security reports getters', () => {
areReportsLoading: true, areReportsLoading: true,
}), }),
).toContain( ).toContain(
'Security scanning (in progress) detected 2 new vulnerabilities and 4 fixed vulnerabilities', 'Security scanning (is loading) detected 2 new vulnerabilities and 4 fixed vulnerabilities',
); );
}); });
}); });
describe('sastStatusIcon', () => { describe('sastStatusIcon', () => {
it('returns warning with new issues', () => { it('returns warning with new issues', () => {
const newState = Object.assign({}, state()); const newState = state();
newState.sast.newIssues = [{}]; newState.sast.newIssues = [{}];
expect(sastStatusIcon(newState)).toEqual('warning'); expect(sastStatusIcon(newState)).toEqual('warning');
}); });
it('returns warning with failed report', () => { it('returns warning with failed report', () => {
const newState = Object.assign({}, state()); const newState = state();
newState.sast.hasError = true; newState.sast.hasError = true;
expect(sastStatusIcon(newState)).toEqual('warning'); expect(sastStatusIcon(newState)).toEqual('warning');
}); });
...@@ -391,13 +425,13 @@ describe('Security reports getters', () => { ...@@ -391,13 +425,13 @@ describe('Security reports getters', () => {
describe('dastStatusIcon', () => { describe('dastStatusIcon', () => {
it('returns warning with new issues', () => { it('returns warning with new issues', () => {
const newState = Object.assign({}, state()); const newState = state();
newState.dast.newIssues = [{}]; newState.dast.newIssues = [{}];
expect(dastStatusIcon(newState)).toEqual('warning'); expect(dastStatusIcon(newState)).toEqual('warning');
}); });
it('returns warning with failed report', () => { it('returns warning with failed report', () => {
const newState = Object.assign({}, state()); const newState = state();
newState.dast.hasError = true; newState.dast.hasError = true;
expect(dastStatusIcon(newState)).toEqual('warning'); expect(dastStatusIcon(newState)).toEqual('warning');
}); });
...@@ -409,13 +443,13 @@ describe('Security reports getters', () => { ...@@ -409,13 +443,13 @@ describe('Security reports getters', () => {
describe('sastContainerStatusIcon', () => { describe('sastContainerStatusIcon', () => {
it('returns warning with new issues', () => { it('returns warning with new issues', () => {
const newState = Object.assign({}, state()); const newState = state();
newState.sastContainer.newIssues = [{}]; newState.sastContainer.newIssues = [{}];
expect(sastContainerStatusIcon(newState)).toEqual('warning'); expect(sastContainerStatusIcon(newState)).toEqual('warning');
}); });
it('returns warning with failed report', () => { it('returns warning with failed report', () => {
const newState = Object.assign({}, state()); const newState = state();
newState.sastContainer.hasError = true; newState.sastContainer.hasError = true;
expect(sastContainerStatusIcon(newState)).toEqual('warning'); expect(sastContainerStatusIcon(newState)).toEqual('warning');
}); });
...@@ -427,13 +461,13 @@ describe('Security reports getters', () => { ...@@ -427,13 +461,13 @@ describe('Security reports getters', () => {
describe('dependencyScanningStatusIcon', () => { describe('dependencyScanningStatusIcon', () => {
it('returns warning with new issues', () => { it('returns warning with new issues', () => {
const newState = Object.assign({}, state()); const newState = state();
newState.dependencyScanning.newIssues = [{}]; newState.dependencyScanning.newIssues = [{}];
expect(dependencyScanningStatusIcon(newState)).toEqual('warning'); expect(dependencyScanningStatusIcon(newState)).toEqual('warning');
}); });
it('returns warning with failed report', () => { it('returns warning with failed report', () => {
const newState = Object.assign({}, state()); const newState = state();
newState.dependencyScanning.hasError = true; newState.dependencyScanning.hasError = true;
expect(dependencyScanningStatusIcon(newState)).toEqual('warning'); expect(dependencyScanningStatusIcon(newState)).toEqual('warning');
}); });
...@@ -445,7 +479,7 @@ describe('Security reports getters', () => { ...@@ -445,7 +479,7 @@ describe('Security reports getters', () => {
describe('areReportsLoading', () => { describe('areReportsLoading', () => {
it('returns true when any report is loading', () => { it('returns true when any report is loading', () => {
const newState = Object.assign({}, state()); const newState = state();
newState.sast.isLoading = true; newState.sast.isLoading = true;
expect(areReportsLoading(newState)).toEqual(true); expect(areReportsLoading(newState)).toEqual(true);
}); });
...@@ -457,7 +491,7 @@ describe('Security reports getters', () => { ...@@ -457,7 +491,7 @@ describe('Security reports getters', () => {
describe('allReportsHaveError', () => { describe('allReportsHaveError', () => {
it('returns true when all reports have error', () => { it('returns true when all reports have error', () => {
const newState = Object.assign({}, state()); const newState = state();
newState.sast.hasError = true; newState.sast.hasError = true;
newState.dast.hasError = true; newState.dast.hasError = true;
newState.sastContainer.hasError = true; newState.sastContainer.hasError = true;
...@@ -466,14 +500,24 @@ describe('Security reports getters', () => { ...@@ -466,14 +500,24 @@ describe('Security reports getters', () => {
expect(allReportsHaveError(newState)).toEqual(true); expect(allReportsHaveError(newState)).toEqual(true);
}); });
it('returns false when none of the reports has error', () => { it('returns false when none of the reports have error', () => {
expect(allReportsHaveError(state())).toEqual(false); expect(allReportsHaveError(state())).toEqual(false);
}); });
it('returns false when one of the reports does not have error', () => {
const newState = state();
newState.sast.hasError = false;
newState.dast.hasError = true;
newState.sastContainer.hasError = true;
newState.dependencyScanning.hasError = true;
expect(allReportsHaveError(newState)).toEqual(false);
});
}); });
describe('anyReportHasError', () => { describe('anyReportHasError', () => {
it('returns true when any of the reports has error', () => { it('returns true when any of the reports has error', () => {
const newState = Object.assign({}, state()); const newState = state();
newState.sast.hasError = true; newState.sast.hasError = true;
expect(anyReportHasError(newState)).toEqual(true); expect(anyReportHasError(newState)).toEqual(true);
...@@ -490,7 +534,7 @@ describe('Security reports getters', () => { ...@@ -490,7 +534,7 @@ describe('Security reports getters', () => {
}); });
it('returns false when any of the reports has base', () => { it('returns false when any of the reports has base', () => {
const newState = Object.assign({}, state()); const newState = state();
newState.sast.paths.base = 'foo'; newState.sast.paths.base = 'foo';
expect(noBaseInAllReports(newState)).toEqual(false); expect(noBaseInAllReports(newState)).toEqual(false);
}); });
......
...@@ -64,7 +64,7 @@ describe('security reports utils', () => { ...@@ -64,7 +64,7 @@ describe('security reports utils', () => {
describe('textBuilder', () => { describe('textBuilder', () => {
describe('with no issues', () => { describe('with no issues', () => {
it('should return no vulnerabiltities text', () => { it('should return no vulnerabiltities text', () => {
expect(textBuilder()).toEqual(' detected no security vulnerabilities'); expect(textBuilder('', { head: 'foo', base: 'bar' }, 0, 0, 0)).toEqual(' detected no security vulnerabilities');
}); });
}); });
...@@ -77,7 +77,13 @@ describe('security reports utils', () => { ...@@ -77,7 +77,13 @@ describe('security reports utils', () => {
describe('with new issues and without base', () => { describe('with new issues and without base', () => {
it('should return unable to compare text', () => { it('should return unable to compare text', () => {
expect(textBuilder('', { head: 'foo' }, 1, 0, 0)).toEqual( expect(textBuilder('', { head: 'foo' }, 1, 0, 0)).toEqual(
' was unable to compare existing and new vulnerabilities. It detected 1 vulnerability', ' detected 1 vulnerability for the source branch only',
);
});
it('should return unable to compare text with no vulnerability', () => {
expect(textBuilder('', { head: 'foo' }, 0, 0, 0)).toEqual(
' detected no vulnerabilities for the source branch only',
); );
}); });
}); });
...@@ -112,19 +118,19 @@ describe('security reports utils', () => { ...@@ -112,19 +118,19 @@ describe('security reports utils', () => {
describe('statusIcon', () => { describe('statusIcon', () => {
describe('with failed report', () => { describe('with failed report', () => {
it('returns warning', () => { it('returns warning', () => {
expect(statusIcon(true)).toEqual('warning'); expect(statusIcon(false, true)).toEqual('warning');
}); });
}); });
describe('with new issues', () => { describe('with new issues', () => {
it('returns warning', () => { it('returns warning', () => {
expect(statusIcon(false, 1)).toEqual('warning'); expect(statusIcon(false, false, 1)).toEqual('warning');
}); });
}); });
describe('with neutral issues', () => { describe('with neutral issues', () => {
it('returns warning', () => { it('returns warning', () => {
expect(statusIcon(false, 0, 1)).toEqual('warning'); expect(statusIcon(false, false, 0, 1)).toEqual('warning');
}); });
}); });
......
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