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';
import pipelineHeader from './components/header_component.vue';
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 store from 'ee/vue_shared/security_reports/store'; // eslint-disable-line import/first
Vue.use(Translate);
......@@ -99,54 +100,23 @@ export default () => {
const blobPath = datasetOptions.blobPath;
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
// eslint-disable-next-line no-new
new Vue({
el: sastSummary,
store,
components: {
SastSummaryWidget,
},
data() {
return {
mediator,
};
methods: {
updateBadge(count) {
updateBadgeCount(count);
},
},
render(createElement) {
return createElement('sast-summary-widget', {
props: {
hasDependencyScanning: dependencyScanningEndpoint !== undefined,
hasSast: endpoint !== undefined,
sastIssues: this.mediator.store.state.securityReports.sast.newIssues.length,
dependencyScanningIssues:
this.mediator.store.state.securityReports.dependencyScanning.newIssues.length,
on: {
updateBadgeCount: this.updateBadge,
},
});
},
......@@ -156,20 +126,24 @@ export default () => {
// eslint-disable-next-line no-new
new Vue({
el: securityTab,
store,
components: {
SecurityReportApp,
},
data() {
return {
mediator,
};
methods: {
updateBadge(count) {
updateBadgeCount(count);
},
},
render(createElement) {
return createElement('security-report-app', {
props: {
securityReports: this.mediator.store.state.securityReports,
hasDependencyScanning: dependencyScanningEndpoint !== undefined,
hasSast: endpoint !== undefined,
headBlobPath: blobPath,
sastHeadPath: endpoint,
dependencyScanningHeadPath: dependencyScanningEndpoint,
},
on: {
updateBadgeCount: this.updateBadge,
},
});
},
......
......@@ -56,23 +56,4 @@ export default class pipelinesMediator {
.then(response => this.successCallback(response))
.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 {
constructor() {
this.state = {};
this.state.pipeline = {};
/* EE only */
this.state.securityReports = securityState;
}
storePipeline(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 @@
border-top: solid 1px $border-color;
}
.mr-widget-border-top {
border-top: 1px solid $border-color;
}
.mr-widget-footer {
padding: 0;
}
......
......@@ -27,6 +27,11 @@
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.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
.content-block.content-block-small.emoji-list-container.js-noteable-awards
......
......@@ -65,4 +65,6 @@
#js-tab-security.build-security.tab-pane
#js-security-report-app{ data: { endpoint: expose_sast_data ? sast_artifact_url(@pipeline) : nil,
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>
import $ from 'jquery';
import { n__, s__ } from '~/locale';
import CiIcon from '~/vue_shared/components/ci_icon.vue';
import { mapState } from 'vuex';
import $ from 'jquery';
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 {
name: 'SummaryReport',
components: {
CiIcon,
export default {
name: 'SummaryReport',
components: {
CiIcon,
LoadingIcon,
},
computed: {
...mapState(['sast', 'dependencyScanning']),
sastLink() {
return this.link(this.sast.newIssues.length);
},
props: {
sastIssues: {
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,
},
dependencyScanningLink() {
return this.link(this.dependencyScanning.newIssues.length);
},
computed: {
sastLink() {
return this.link(this.sastIssues);
},
dependencyScanningLink() {
return this.link(this.dependencyScanningIssues);
},
sastIcon() {
return this.statusIcon(this.sastIssues);
},
dependencyScanningIcon() {
return this.statusIcon(this.dependencyScanningIssues);
},
sastIcon() {
return this.statusIcon(this.hasSastError, this.sast.newIssues.length);
},
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(issues) {
if (issues > 0) {
return n__(
'%d vulnerability',
'%d vulnerabilities',
issues,
);
}
return s__('ciReport|no vulnerabilities');
},
statusIcon(issues) {
if (issues > 0) {
return {
group: 'warning',
icon: 'status_warning',
};
}
dependencyScanningIcon() {
return this.statusIcon(
this.hasDependencyScanningError,
this.dependencyScanning.newIssues.length,
);
},
hasSast() {
return this.sast.paths.head !== null;
},
hasDependencyScanning() {
return this.dependencyScanning.paths.head !== null;
},
isLoadingSast() {
return this.sast.isLoading;
},
isLoadingDependencyScanning() {
return this.dependencyScanning.isLoading;
},
hasSastError() {
return this.sast.hasError;
},
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 {
group: 'success',
icon: 'status_success',
group: 'warning',
icon: 'status_warning',
};
},
}
return {
group: 'success',
icon: 'status_success',
};
},
};
},
};
</script>
<template>
<div>
......@@ -82,7 +81,12 @@
class="well-segment flex js-sast-summary"
v-if="hasSast"
>
<loading-icon
v-if="isLoadingSast"
/>
<ci-icon
v-else
:status="sastIcon"
class="flex flex-align-self-center"
/>
......@@ -90,21 +94,33 @@
<span
class="prepend-left-10 flex flex-align-self-center"
>
{{ s__('ciReport|SAST detected') }}
<button
type="button"
class="btn-link btn-blank prepend-left-5"
@click="openTab"
>
{{ sastLink }}
</button>
<template v-if="hasSastError">
{{ s__('ciReport|SAST resulted in error while loading results') }}
</template>
<template v-else-if="isLoadingSast">
{{ s__('ciReport|SAST is loading') }}
</template>
<template v-else>
{{ s__('ciReport|SAST detected') }}
<button
type="button"
class="btn-link btn-blank prepend-left-5"
@click="openTab"
>
{{ sastLink }}
</button>
</template>
</span>
</div>
<div
class="well-segment flex js-dss-summary"
v-if="hasDependencyScanning"
>
<loading-icon
v-if="dependencyScanning.isLoading"
/>
<ci-icon
v-else
:status="dependencyScanningIcon"
class="flex flex-align-self-center"
/>
......@@ -112,14 +128,22 @@
<span
class="prepend-left-10 flex flex-align-self-center"
>
{{ s__('ciReport|Dependency scanning detected') }}
<button
type="button"
class="btn-link btn-blank prepend-left-5"
@click="openTab"
>
{{ dependencyScanningLink }}
</button>
<template v-if="hasDependencyScanningError">
{{ s__('ciReport|Dependency scanning resulted in error while loading results') }}
</template>
<template v-else-if="isLoadingDependencyScanning">
{{ s__('ciReport|Dependency scanning is loading') }}
</template>
<template v-else>
{{ s__('ciReport|Dependency scanning detected') }}
<button
type="button"
class="btn-link btn-blank prepend-left-5"
@click="openTab"
>
{{ dependencyScanningLink }}
</button>
</template>
</span>
</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 WidgetApprovals from './components/approvals/mr_widget_approvals';
import GeoSecondaryNode from './components/states/mr_widget_secondary_geo_node';
import ReportSection from '../vue_shared/security_reports/components/report_section.vue';
import securityMixin from '../vue_shared/security_reports/mixins/security_report_mixin';
import {
SAST,
DAST,
SAST_CONTAINER,
} from '../vue_shared/security_reports/helpers/constants';
import GroupedSecurityReportsApp from '../vue_shared/security_reports/grouped_security_reports_app.vue';
import reportsMixin from '../vue_shared/security_reports/mixins/reports_mixin';
export default {
extends: CEWidgetOptions,
components: {
'mr-widget-approvals': WidgetApprovals,
'mr-widget-geo-secondary-node': GeoSecondaryNode,
GroupedSecurityReportsApp,
ReportSection,
},
mixins: [securityMixin],
dast: DAST,
sast: SAST,
sastContainer: SAST_CONTAINER,
mixins: [reportsMixin],
data() {
return {
isLoadingCodequality: false,
isLoadingPerformance: false,
isLoadingSecurity: false,
isLoadingDocker: false,
isLoadingDast: false,
isLoadingDependencyScanning: false,
loadingCodequalityFailed: false,
loadingPerformanceFailed: false,
loadingSecurityFailed: false,
loadingDockerFailed: false,
loadingDastFailed: false,
loadingDependencyScanningFailed: false,
};
},
computed: {
......@@ -45,21 +31,34 @@ export default {
const { codeclimate } = this.mr;
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() {
const { performance } = this.mr;
return performance && performance.head_path && performance.base_path;
},
shouldRenderSecurityReport() {
return this.mr.sast && this.mr.sast.head_path;
},
shouldRenderDockerReport() {
return this.mr.sastContainer && this.mr.sastContainer.head_path;
},
shouldRenderDastReport() {
return this.mr.dast && this.mr.dast.head_path;
},
shouldRenderDependencyReport() {
return this.mr.dependencyScanning && this.mr.dependencyScanning.head_path;
return (
(this.mr.sast && this.mr.sast.head_path) ||
(this.mr.sastContainer && this.mr.sastContainer.head_path) ||
(this.mr.dast && this.mr.dast.head_path) ||
(this.mr.dependencyScanning && this.mr.dependencyScanning.head_path)
);
},
codequalityText() {
const { newIssues, resolvedIssues } = this.mr.codeclimateMetrics;
......@@ -71,13 +70,7 @@ export default {
text.push(s__('ciReport|Code quality'));
if (resolvedIssues.length) {
text.push(
n__(
' improved on %d point',
' improved on %d points',
resolvedIssues.length,
),
);
text.push(n__(' improved on %d point', ' improved on %d points', resolvedIssues.length));
}
if (newIssues.length > 0 && resolvedIssues.length > 0) {
......@@ -85,13 +78,7 @@ export default {
}
if (newIssues.length) {
text.push(
n__(
' degraded on %d point',
' degraded on %d points',
newIssues.length,
),
);
text.push(n__(' degraded on %d point', ' degraded on %d points', newIssues.length));
}
}
......@@ -108,13 +95,7 @@ export default {
text.push(s__('ciReport|Performance metrics'));
if (improved.length) {
text.push(
n__(
' improved on %d point',
' improved on %d points',
improved.length,
),
);
text.push(n__(' improved on %d point', ' improved on %d points', improved.length));
}
if (improved.length > 0 && degraded.length > 0) {
......@@ -122,75 +103,19 @@ export default {
}
if (degraded.length) {
text.push(
n__(
' degraded on %d point',
' degraded on %d points',
degraded.length,
),
);
text.push(n__(' degraded on %d point', ' degraded on %d points', degraded.length));
}
}
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() {
return this.checkReportStatus(
this.isLoadingCodequality,
this.loadingCodequalityFailed,
);
return this.checkReportStatus(this.isLoadingCodequality, this.loadingCodequalityFailed);
},
performanceStatus() {
return this.checkReportStatus(
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,
);
return this.checkReportStatus(this.isLoadingPerformance, this.loadingPerformanceFailed);
},
},
methods: {
......@@ -199,10 +124,7 @@ export default {
this.isLoadingCodequality = true;
Promise.all([
this.service.fetchReport(head_path),
this.service.fetchReport(base_path),
])
Promise.all([this.service.fetchReport(head_path), this.service.fetchReport(base_path)])
.then(values => {
this.mr.compareCodeclimateMetrics(
values[0],
......@@ -223,10 +145,7 @@ export default {
this.isLoadingPerformance = true;
Promise.all([
this.service.fetchReport(head_path),
this.service.fetchReport(base_path),
])
Promise.all([this.service.fetchReport(head_path), this.service.fetchReport(base_path)])
.then(values => {
this.mr.comparePerformanceMetrics(values[0], values[1]);
this.isLoadingPerformance = false;
......@@ -236,122 +155,16 @@ export default {
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;
if (sast.base_path && sast.head_path) {
Promise.all([
this.service.fetchReport(sast.head_path),
this.service.fetchReport(sast.base_path),
])
.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;
});
translateText(type) {
return {
error: sprintf(s__('ciReport|Failed to load %{reportName} report'), {
reportName: type,
}),
loading: sprintf(s__('ciReport|Loading %{reportName} report'), {
reportName: type,
}),
};
},
},
created() {
......@@ -362,22 +175,6 @@ export default {
if (this.shouldRenderPerformance) {
this.fetchPerformance();
}
if (this.shouldRenderSecurityReport) {
this.fetchSecurity();
}
if (this.shouldRenderDockerReport) {
this.fetchDockerReport();
}
if (this.shouldRenderDastReport) {
this.fetchDastReport();
}
if (this.shouldRenderDependencyReport) {
this.fetchDependencyScanning();
}
},
template: `
<div class="mr-state-widget prepend-top-default">
......@@ -397,7 +194,7 @@ export default {
v-if="shouldRenderApprovals"
:mr="mr"
:service="service"
/>
/>
<report-section
class="js-codequality-widget"
v-if="shouldRenderCodeQuality"
......@@ -408,6 +205,7 @@ export default {
:success-text="codequalityText"
:unresolved-issues="mr.codeclimateMetrics.newIssues"
:resolved-issues="mr.codeclimateMetrics.resolvedIssues"
:has-issues="hasCodequalityIssues"
/>
<report-section
class="js-performance-widget"
......@@ -420,52 +218,24 @@ export default {
:unresolved-issues="mr.performanceMetrics.degraded"
:resolved-issues="mr.performanceMetrics.improved"
:neutral-issues="mr.performanceMetrics.neutral"
:has-issues="hasPerformanceMetrics"
/>
<report-section
class="js-sast-widget"
<grouped-security-reports-app
v-if="shouldRenderSecurityReport"
:type="$options.sast"
:status="securityStatus"
:loading-text="translateText('security').loading"
:error-text="translateText('security').error"
:success-text="securityText"
:unresolved-issues="mr.securityReport.newIssues"
:resolved-issues="mr.securityReport.resolvedIssues"
:all-issues="mr.securityReport.allIssues"
/>
<report-section
class="js-dependency-scanning-widget"
v-if="shouldRenderDependencyReport"
:type="$options.sast"
:status="dependencyScanningStatus"
: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"
:head-blob-path="mr.headBlobPath"
:base-blob-path="mr.baseBlobPath"
:sast-head-path="mr.sast.head_path"
:sast-base-path="mr.sast.base_path"
:sast-help-path="mr.sastHelp"
:dast-head-path="mr.dast.head_path"
:dast-base-path="mr.dast.base_path"
:dast-help-path="mr.dastHelp"
:sast-container-head-path="mr.sastContainer.head_path"
:sast-container-base-path="mr.sastContainer.base_path"
:sast-container-help-path="mr.sastContainerHelp"
:dependency-scanning-head-path="mr.dependencyScanning.head_path"
:dependency-scanning-base-path="mr.dependencyScanning.base_path"
:dependency-scanning-help-path="mr.dependencyScanningHelp"
/>
<div class="mr-widget-section">
<component
......
import CEMergeRequestStore from '~/vue_merge_request_widget/stores/mr_widget_store';
import {
parseCodeclimateMetrics,
filterByKey,
setSastContainerReport,
setSastReport,
setDastReport,
} from '../../vue_shared/security_reports/helpers/utils';
import { filterByKey } from '../../vue_shared/security_reports/store/utils';
export default class MergeRequestStore extends CEMergeRequestStore {
constructor(data) {
......@@ -14,13 +8,17 @@ export default class MergeRequestStore extends CEMergeRequestStore {
const blobPath = data.blob_path || {};
this.headBlobPath = blobPath.head_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.initPerformanceReport(data);
this.initSecurityReport(data);
this.initDockerReport(data);
this.initDastReport(data);
this.initDependencyScanningReport(data);
}
setData(data) {
......@@ -70,69 +68,13 @@ export default class MergeRequestStore extends CEMergeRequestStore {
this.performanceMetrics = {
improved: [],
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) {
const parsedHeadIssues = parseCodeclimateMetrics(headIssues, headBlobPath);
const parsedBaseIssues = parseCodeclimateMetrics(baseIssues, baseBlobPath);
const parsedHeadIssues = MergeRequestStore.parseCodeclimateMetrics(headIssues, headBlobPath);
const parsedBaseIssues = MergeRequestStore.parseCodeclimateMetrics(baseIssues, baseBlobPath);
this.codeclimateMetrics.newIssues = filterByKey(
parsedHeadIssues,
......@@ -202,4 +144,30 @@ export default class MergeRequestStore extends CEMergeRequestStore {
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>
/**
* Renders DAST body text
* [priority]: [name]
*/
export default {
name: 'SastIssueBody',
props: {
issue: {
type: Object,
required: true,
},
/**
* Renders DAST body text
* [priority]: [name]
*/
issueIndex: {
type: Number,
required: true,
},
export default {
name: 'SastIssueBody',
props: {
issue: {
type: Object,
required: true,
},
issueIndex: {
type: Number,
required: true,
},
modalTargetId: {
type: String,
required: true,
},
modalTargetId: {
type: String,
required: true,
},
},
methods: {
openDastModal() {
this.$emit('openDastModal', this.issue, this.issueIndex);
},
methods: {
openDastModal() {
this.$emit('openDastModal', this.issue, this.issueIndex);
},
};
},
};
</script>
<template>
<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>
import $ from 'jquery';
import _ from 'underscore';
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 {
name: 'SecurityReportsHelpPopover',
components: {
Icon,
},
directives: {
popover,
},
props: {
options: {
type: Object,
required: true,
},
},
computed: {
popoverOptions() {
return {
mounted() {
$(this.$el)
.popover({
html: true,
trigger: 'focus',
container: 'body',
placement: 'top',
template:
'<div class="popover" role="tooltip"><div class="arrow"></div><p class="popover-title"></p><div class="popover-content"></div></div>',
...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>
<template>
<button
type="button"
class="btn btn-transparent"
v-popover="popoverOptions"
class="btn btn-blank btn-transparent btn-help"
tabindex="0"
>
<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>
import $ from 'jquery';
import Icon from '~/vue_shared/components/icon.vue';
import Modal from '~/vue_shared/components/gl_modal.vue';
import ExpandButton from '~/vue_shared/components/expand_button.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 SastIssue from './sast_issue_body.vue';
import SastContainerIssue from './sast_container_issue_body.vue';
import DastIssue from './dast_issue_body.vue';
import $ from 'jquery';
import Icon from '~/vue_shared/components/icon.vue';
import Modal from '~/vue_shared/components/gl_modal.vue';
import ExpandButton from '~/vue_shared/components/expand_button.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 SastIssue from './sast_issue_body.vue';
import SastContainerIssue from './sast_container_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 = {
modalId: 'modal-mrwidget-issue',
modalDesc: '',
modalTitle: '',
modalInstances: [],
modalTargetId: '#modal-mrwidget-issue',
};
const modalDefaultData = {
modalId: 'modal-mrwidget-issue',
modalDesc: '',
modalTitle: '',
modalInstances: [],
modalTargetId: '#modal-mrwidget-issue',
};
export default {
name: 'ReportIssues',
components: {
Modal,
Icon,
ExpandButton,
SastIssue,
SastContainerIssue,
DastIssue,
PerformanceIssue,
CodequalityIssue,
},
props: {
issues: {
type: Array,
required: true,
},
// security || codequality || performance || docker || dast
type: {
type: String,
required: true,
},
// failed || success
status: {
type: String,
required: true,
},
},
data() {
return modalDefaultData;
},
computed: {
iconName() {
if (this.isStatusFailed) {
return 'status_failed_borderless';
} else if (this.isStatusSuccess) {
return 'status_success_borderless';
}
export default {
name: 'ReportIssues',
components: {
Modal,
Icon,
ExpandButton,
SastIssue,
SastContainerIssue,
DastIssue,
PerformanceIssue,
CodequalityIssue,
},
props: {
issues: {
type: Array,
required: true,
},
// security || codequality || performance || docker || dast
type: {
type: String,
required: true,
},
// failed || success
status: {
type: String,
required: true,
},
},
data() {
return modalDefaultData;
},
computed: {
iconName() {
if (this.isStatusFailed) {
return 'status_failed_borderless';
} else if (this.isStatusSuccess) {
return 'status_success_borderless';
}
return 'status_created_borderless';
},
isStatusFailed() {
return this.status === 'failed';
},
isStatusSuccess() {
return this.status === 'success';
},
isStatusNeutral() {
return this.status === 'neutral';
},
isTypeCodequality() {
return this.type === 'codequality';
},
isTypePerformance() {
return this.type === 'performance';
},
isTypeSast() {
return this.type === SAST;
},
isTypeSastContainer() {
return this.type === SAST_CONTAINER;
},
isTypeDast() {
return this.type === DAST;
},
},
mounted() {
$(this.$refs.modal).on('hidden.bs.modal', () => {
this.clearModalData();
});
},
methods: {
getmodalId(index) {
return `modal-mrwidget-issue-${index}`;
},
modalIdTarget(index) {
return `#${this.getmodalId(index)}`;
},
openDastModal(issue, index) {
this.modalId = this.getmodalId(index);
this.modalTitle = `${issue.priority}: ${issue.name}`;
this.modalTargetId = `#${this.getmodalId(index)}`;
this.modalInstances = issue.instances;
this.modalDesc = issue.parsedDescription;
},
/**
* Because of https://vuejs.org/v2/guide/list.html#Caveats
* we need to clear the instances to make sure everything is properly reset.
*/
clearModalData() {
this.modalId = modalDefaultData.modalId;
this.modalDesc = modalDefaultData.modalDesc;
this.modalTitle = modalDefaultData.modalTitle;
this.modalInstances = modalDefaultData.modalInstances;
this.modalTargetId = modalDefaultData.modalTargetId;
},
},
};
return 'status_created_borderless';
},
isStatusFailed() {
return this.status === 'failed';
},
isStatusSuccess() {
return this.status === 'success';
},
isStatusNeutral() {
return this.status === 'neutral';
},
isTypeCodequality() {
return this.type === 'codequality';
},
isTypePerformance() {
return this.type === 'performance';
},
isTypeSast() {
return this.type === SAST;
},
isTypeSastContainer() {
return this.type === SAST_CONTAINER;
},
isTypeDast() {
return this.type === DAST;
},
},
mounted() {
$(this.$refs.modal).on('hidden.bs.modal', () => {
this.clearModalData();
});
},
methods: {
getmodalId(index) {
return `modal-mrwidget-issue-${index}`;
},
modalIdTarget(index) {
return `#${this.getmodalId(index)}`;
},
openDastModal(issue, index) {
this.modalId = this.getmodalId(index);
this.modalTitle = `${issue.priority}: ${issue.name}`;
this.modalTargetId = `#${this.getmodalId(index)}`;
this.modalInstances = issue.instances;
this.modalDesc = issue.parsedDescription;
},
/**
* Because of https://vuejs.org/v2/guide/list.html#Caveats
* we need to clear the instances to make sure everything is properly reset.
*/
clearModalData() {
this.modalId = modalDefaultData.modalId;
this.modalDesc = modalDefaultData.modalDesc;
this.modalTitle = modalDefaultData.modalTitle;
this.modalInstances = modalDefaultData.modalInstances;
this.modalTargetId = modalDefaultData.modalTargetId;
},
},
};
</script>
<template>
<div>
......
<script>
export default {
name: 'ReportIssueLink',
props: {
issue: {
type: Object,
required: true,
},
export default {
name: 'ReportIssueLink',
props: {
issue: {
type: Object,
required: true,
},
};
},
};
</script>
<template>
<div class="report-block-list-issue-description-link">
......
<script>
import { __ } from '~/locale';
import StatusIcon from '~/vue_merge_request_widget/components/mr_widget_status_icon.vue';
import LoadingIcon from '~/vue_shared/components/loading_icon.vue';
import IssuesBlock from './report_issues.vue';
export default {
name: 'ReportSection',
components: {
IssuesBlock,
LoadingIcon,
StatusIcon,
},
props: {
isCollapsible: {
type: Boolean,
required: false,
default: true,
},
// security | codequality | performance | docker
type: {
type: String,
required: true,
},
// loading | success | error
status: {
type: String,
required: true,
},
loadingText: {
type: String,
required: true,
},
errorText: {
type: String,
required: true,
},
successText: {
type: String,
required: true,
},
unresolvedIssues: {
type: Array,
required: false,
default: () => ([]),
},
resolvedIssues: {
type: Array,
required: false,
default: () => ([]),
},
neutralIssues: {
type: Array,
required: false,
default: () => ([]),
},
allIssues: {
type: Array,
required: false,
default: () => ([]),
},
infoText: {
type: [String, Boolean],
required: false,
default: false,
},
hasPriority: {
type: Boolean,
required: false,
default: false,
},
import { __ } from '~/locale';
import StatusIcon from '~/vue_merge_request_widget/components/mr_widget_status_icon.vue';
import LoadingIcon from '~/vue_shared/components/loading_icon.vue';
import IssuesList from './issues_list.vue';
import Popover from './help_popover.vue';
import { LOADING, ERROR, SUCCESS } from '../store/constants';
export default {
name: 'ReportSection',
components: {
IssuesList,
LoadingIcon,
StatusIcon,
Popover,
},
props: {
type: {
type: String,
required: false,
default: '',
},
status: {
type: String,
required: true,
},
loadingText: {
type: String,
required: false,
default: '',
},
errorText: {
type: String,
required: false,
default: '',
},
successText: {
type: String,
required: true,
},
unresolvedIssues: {
type: Array,
required: false,
default: () => [],
},
resolvedIssues: {
type: Array,
required: false,
default: () => [],
},
neutralIssues: {
type: Array,
required: false,
default: () => [],
},
allIssues: {
type: Array,
required: false,
default: () => [],
},
infoText: {
type: [String, Boolean],
required: false,
default: false,
},
hasIssues: {
type: Boolean,
required: true,
},
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.isCollapsible) {
return {
collapseText: __('Expand'),
isCollapsed: true,
isFullReportVisible: false,
};
if (this.isSuccess) {
return this.successText;
}
return {
isFullReportVisible: true,
};
},
if (this.loadingFailed) {
return this.errorText;
}
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';
},
hasIssues() {
return this.unresolvedIssues.length ||
this.resolvedIssues.length ||
this.allIssues.length ||
this.neutralIssues.length;
},
return '';
},
hasPopover() {
return Object.keys(this.popoverOptions).length > 0;
},
},
methods: {
toggleCollapsed() {
this.isCollapsed = !this.isCollapsed;
methods: {
toggleCollapsed() {
this.isCollapsed = !this.isCollapsed;
const text = this.isCollapsed ? __('Expand') : __('Collapse');
this.collapseText = text;
},
openFullReport() {
this.isFullReportVisible = true;
},
},
};
const text = this.isCollapsed ? __('Expand') : __('Collapse');
this.collapseText = text;
},
openFullReport() {
this.isFullReportVisible = true;
},
},
};
</script>
<template>
<section class="report-block mr-widget-section">
<section>
<div
v-if="isLoading"
class="media"
class="media prepend-top-default prepend-left-default
append-right-default append-bottom-default"
>
<div
<loading-icon
class="mr-widget-icon"
>
<loading-icon />
</div>
<div
class="media-body"
>
{{ loadingText }}
</div>
</div>
<div
v-else-if="isSuccess"
class="media"
>
v-if="isLoading"
/>
<status-icon
v-else
:status="statusIconName"
/>
<div
class="media-body space-children"
>
<span
class="js-code-text code-text"
>
{{ successText }}
{{ headerText }}
<popover
v-if="hasPopover"
class="prepend-left-5"
:options="popoverOptions"
/>
</span>
<button
type="button"
class="js-collapse-btn btn bt-default pull-right btn-sm"
v-if="isCollapsible && hasIssues"
v-if="hasIssues"
@click="toggleCollapsed"
>
{{ collapseText }}
......@@ -172,71 +172,28 @@
</div>
<div
class="report-block-container"
class="js-report-section-container"
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
v-if="infoText"
v-html="infoText"
class="js-mr-code-quality-info prepend-left-10 report-block-info"
>
</p>
<issues-block
class="js-mr-code-new-issues"
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>
<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>
</slot>
</div>
</section>
</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>
/**
* Renders SAST CONTAINER body text
* [priority]: [name|link] in [link]:[line]
*/
import ReportLink from './report_link.vue';
/**
* Renders SAST CONTAINER body text
* [priority]: [name|link] in [link]:[line]
*/
import ReportLink from './report_link.vue';
export default {
name: 'SastContainerIssueBody',
export default {
name: 'SastContainerIssueBody',
components: {
ReportLink,
},
components: {
ReportLink,
},
props: {
issue: {
type: Object,
required: true,
},
props: {
issue: {
type: Object,
required: true,
},
};
},
};
</script>
<template>
<div class="report-block-list-issue-description prepend-top-5 append-bottom-5">
......
<script>
/**
* Renders SAST body text
* [priority]: [name] in [link] : [line]
*/
import ReportLink from './report_link.vue';
/**
* Renders SAST body text
* [priority]: [name] in [link] : [line]
*/
import ReportLink from './report_link.vue';
export default {
name: 'SastIssueBody',
export default {
name: 'SastIssueBody',
components: {
ReportLink,
},
components: {
ReportLink,
},
props: {
issue: {
type: Object,
required: true,
},
props: {
issue: {
type: Object,
required: true,
},
};
},
};
</script>
<template>
<div class="report-block-list-issue-description prepend-top-5 append-bottom-5">
......
<script>
import CiIcon from '~/vue_shared/components/ci_icon.vue';
import LoadingIcon from '~/vue_shared/components/loading_icon.vue';
import Popover from './help_popover.vue';
/**
......@@ -10,6 +11,7 @@ export default {
name: 'SecuritySummaryRow',
components: {
CiIcon,
LoadingIcon,
Popover,
},
props: {
......@@ -37,12 +39,19 @@ export default {
};
</script>
<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">
<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 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">
{{ summary }}
</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 {
methods: {
sastText(newIssues = [], resolvedIssues = [], allIssues = []) {
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) {
computed: {
sastPopover() {
return {
error: sprintf(s__('ciReport|Failed to load %{reportName} report'), { reportName: type }),
loading: sprintf(s__('ciReport|Loading %{reportName} report'), { reportName: type }),
title: s__('ciReport|Static Application Security Testing (SAST) detects known vulnerabilities in your source code.'),
content: sprintf(
s__('ciReport|%{linkStartTag}Learn more about SAST %{linkEndTag}'),
{
linkStartTag: `<a href="${this.sastHelpPath}">`,
linkEndTag: '</a>',
},
false,
),
};
},
checkReportStatus(loading, error) {
if (loading) {
return 'loading';
} else if (error) {
return 'error';
}
return 'success';
},
sastContainerText(vulnerabilities = [], approved = [], unapproved = []) {
if (!vulnerabilities.length) {
return s__('ciReport|SAST:container no vulnerabilities were found');
}
if (!unapproved.length && approved.length) {
return n__(
'SAST:container found %d approved vulnerability',
'SAST:container found %d approved vulnerabilities',
approved.length,
);
} else if (unapproved.length && !approved.length) {
return n__(
'SAST:container found %d vulnerability',
'SAST:container found %d vulnerabilities',
unapproved.length,
);
}
return `${n__(
'SAST:container found %d vulnerability,',
'SAST:container found %d vulnerabilities,',
vulnerabilities.length,
)} ${n__(
'of which %d is approved',
'of which %d are approved',
approved.length,
)}`;
sastContainerPopover() {
return {
title: s__('ciReport|Container scanning detects known vulnerabilities in your docker images.'),
content: sprintf(
s__('ciReport|%{linkStartTag}Learn more about SAST image %{linkEndTag}'),
{
linkStartTag: `<a href="${this.sastContainerHelpPath}">`,
linkEndTag: '</a>',
},
false,
),
};
},
dastText(dast = []) {
if (dast.length) {
return n__(
'DAST detected %d alert by analyzing the review app',
'DAST detected %d alerts by analyzing the review app',
dast.length,
);
}
return s__('ciReport|DAST detected no alerts by analyzing the review app');
dastPopover() {
return {
title: s__('ciReport|Dynamic Application Security Testing (DAST) detects known vulnerabilities in your web application.'),
content: sprintf(
s__('ciReport|%{linkStartTag}Learn more about DAST %{linkEndTag}'),
{
linkStartTag: `<a href="${this.dastHelpPath}">`,
linkEndTag: '</a>',
},
false,
),
};
},
sastContainerInformationText() {
return sprintf(
s__('ciReport|Unapproved vulnerabilities (red) can be marked as approved. %{helpLink}'), {
helpLink: `<a href="https://gitlab.com/gitlab-org/clair-scanner#example-whitelist-yaml-file" target="_blank" rel="noopener noreferrer nofollow">
${s__('ciReport|Learn more about whitelisting')}
</a>`,
},
false,
);
dependencyScanningPopover() {
return {
title: s__('ciReport|Dependency Scanning detects known vulnerabilities in your source code\'s dependencies.'),
content: sprintf(
s__('ciReport|%{linkStartTag}Learn more about Dependency Scanning %{linkEndTag}'),
{
linkStartTag: `<a href="${this.dependencyScanningHelpPath}">`,
linkEndTag: '</a>',
},
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 }) => {
dispatch('requestSastReports');
Promise.all([
return Promise.all([
head ? axios.get(head) : Promise.resolve(),
base ? axios.get(base) : Promise.resolve(),
])
.then(values => {
dispatch('receiveSastReports', {
head: values[0] ? values[0].data : null,
base: values[1] ? values[1].data : null,
head: values && values[0] ? values[0].data : null,
base: values && values[1] ? values[1].data : null,
});
})
.catch(() => {
......@@ -65,7 +65,7 @@ export const fetchSastContainerReports = ({ state, dispatch }) => {
dispatch('requestSastContainerReports');
Promise.all([
return Promise.all([
head ? axios.get(head) : Promise.resolve(),
base ? axios.get(base) : Promise.resolve(),
])
......@@ -100,14 +100,14 @@ export const fetchDastReports = ({ state, dispatch }) => {
dispatch('requestDastReports');
Promise.all([
return Promise.all([
head ? axios.get(head) : Promise.resolve(),
base ? axios.get(base) : Promise.resolve(),
])
.then(values => {
dispatch('receiveDastReports', {
head: values[0] ? values[0].data : null,
base: values[1] ? values[1].data : null,
head: values && values[0] ? values[0].data : null,
base: values && values[1] ? values[1].data : null,
});
})
.catch(() => {
......@@ -139,7 +139,7 @@ export const fetchDependencyScanningReports = ({ state, dispatch }) => {
dispatch('requestDependencyScanningReports');
Promise.all([
return Promise.all([
head ? axios.get(head) : Promise.resolve(),
base ? axios.get(base) : Promise.resolve(),
])
......
export const SAST = 'SAST';
export const DAST = 'DAST';
export const SAST_CONTAINER = 'SAST_CONTAINER';
export const LOADING = 'LOADING';
export const ERROR = 'ERROR';
export const SUCCESS = 'SUCCESS';
import { n__, s__ } from '~/locale';
import { textBuilder, statusIcon } from './utils';
import { LOADING, ERROR, SUCCESS } from './constants';
export const groupedSastText = ({ sast }) =>
textBuilder(
export const groupedSastText = ({ sast }) => {
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.paths,
sast.newIssues.length,
sast.resolvedIssues.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 }) =>
textBuilder(
return textBuilder(
'Container scanning',
sastContainer.paths,
sastContainer.newIssues.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 }) =>
textBuilder('DAST', dast.paths, dast.newIssues.length, dast.resolvedIssues.length);
if (dependencyScanning.isLoading) {
return s__('ciReport|Dependency scanning is loading');
}
export const groupedDependencyText = ({ dependencyScanning }) =>
textBuilder(
return textBuilder(
'Dependency scanning',
dependencyScanning.paths,
dependencyScanning.newIssues.length,
dependencyScanning.resolvedIssues.length,
dependencyScanning.allIssues.length,
);
};
export const groupedSummaryText = (state, getters) => {
const { added, fixed } = state.summaryCounts;
// All reports are loading
if (getters.areAllReportsLoading) {
return s__('ciReport|Security scanning is loading');
}
// All reports returned error
if (getters.allReportsHaveError) {
return s__('ciReport|Security scanning failed loading any results');
......@@ -40,21 +84,25 @@ export const groupedSummaryText = (state, getters) => {
if (getters.noBaseInAllReports) {
if (added > 0) {
return n__(
'Security scanning was unable to compare existing and new vulnerabilities. It detected %d vulnerability',
'Security scanning was unable to compare existing and new vulnerabilities. It detected %d vulnerabilities',
'Security scanning detected %d vulnerability for the source branch only',
'Security scanning detected %d vulnerabilities for the source branch only',
added,
);
}
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')];
if (getters.areReportsLoading) {
text.push('(in progress)');
if (getters.areReportsLoading && getters.anyReportHasError) {
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) {
......@@ -81,15 +129,33 @@ export const groupedSummaryText = (state, getters) => {
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 }) =>
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 }) =>
statusIcon(dependencyScanning.hasError, dependencyScanning.newIssues.length);
statusIcon(
dependencyScanning.isLoading,
dependencyScanning.hasError,
dependencyScanning.newIssues.length,
);
export const areReportsLoading = state =>
state.sast.isLoading ||
......@@ -97,6 +163,12 @@ export const areReportsLoading = state =>
state.sastContainer.isLoading ||
state.dependencyScanning.isLoading;
export const areAllReportsLoading = state =>
state.sast.isLoading &&
state.dast.isLoading &&
state.sastContainer.isLoading &&
state.dependencyScanning.isLoading;
export const allReportsHaveError = state =>
state.sast.hasError &&
state.dast.hasError &&
......@@ -114,3 +186,9 @@ export const noBaseInAllReports = state =>
!state.dast.paths.base &&
!state.sastContainer.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 {
parseSastIssues,
......@@ -9,24 +11,24 @@ import {
export default {
[types.SET_HEAD_BLOB_PATH](state, path) {
Object.assign(state.blobPath, { head: path });
state.blobPath.head = path;
},
[types.SET_BASE_BLOB_PATH](state, path) {
Object.assign(state.blobPath, { base: path });
state.blobPath.base = path;
},
// SAST
[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) {
Object.assign(state.sast.paths, { base: path });
state.sast.paths.base = path;
},
[types.REQUEST_SAST_REPORTS](state) {
Object.assign(state.sast, { isLoading: true });
state.sast.isLoading = true;
},
/**
......@@ -52,50 +54,39 @@ export default {
const newIssues = filterByKey(parsedHead, parsedBase, filterKey);
const resolvedIssues = filterByKey(parsedBase, parsedHead, filterKey);
const allIssues = filterByKey(parsedHead, newIssues.concat(resolvedIssues), filterKey);
Object.assign(state, {
sast: {
...state.sast,
newIssues,
resolvedIssues,
allIssues,
isLoading: false,
},
summaryCounts: {
added: state.summaryCounts.added + newIssues.length,
fixed: state.summaryCounts.fixed + resolvedIssues.length,
},
});
state.sast.newIssues = newIssues;
state.sast.resolvedIssues = resolvedIssues;
state.sast.allIssues = allIssues;
state.sast.isLoading = false;
state.summaryCounts.added += newIssues.length;
state.summaryCounts.fixed += resolvedIssues.length;
} else if (reports.head && !reports.base) {
const newIssues = parseSastIssues(reports.head, state.blobPath.head);
Object.assign(state.sast, {
newIssues,
isLoading: false,
});
state.sast.newIssues = newIssues;
state.sast.isLoading = false;
state.summaryCounts.added += newIssues.length;
}
},
[types.RECEIVE_SAST_REPORTS_ERROR](state) {
Object.assign(state.sast, {
isLoading: false,
hasError: true,
});
state.sast.isLoading = false;
state.sast.hasError = true;
},
// SAST CONTAINER
[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) {
Object.assign(state.sastContainer.paths, { base: path });
state.sastContainer.paths.base = path;
},
[types.REQUEST_SAST_CONTAINER_REPORTS](state) {
Object.assign(state.sastContainer, { isLoading: true });
state.sastContainer.isLoading = true;
},
/**
......@@ -116,48 +107,40 @@ export default {
const newIssues = filterByKey(headIssues, baseIssues, filterKey);
const resolvedIssues = filterByKey(baseIssues, headIssues, filterKey);
Object.assign(state, {
sastContainer: {
...state.sastContainer,
isLoading: false,
newIssues,
resolvedIssues,
},
summaryCounts: {
added: state.summaryCounts.added + newIssues.length,
fixed: state.summaryCounts.fixed + resolvedIssues.length,
},
});
state.sastContainer.newIssues = newIssues;
state.sastContainer.resolvedIssues = resolvedIssues;
state.sastContainer.isLoading = false;
state.summaryCounts.added += newIssues.length;
state.summaryCounts.fixed += resolvedIssues.length;
} else if (reports.head && !reports.base) {
Object.assign(state.sastContainer, {
isLoading: false,
newIssues: getUnapprovedVulnerabilities(
parseSastContainer(reports.head.vulnerabilities),
reports.head.unapproved,
),
});
const newIssues = getUnapprovedVulnerabilities(
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) {
Object.assign(state.sastContainer, {
isLoading: false,
hasError: true,
});
state.sastContainer.isLoading = false;
state.sastContainer.hasError = true;
},
// DAST
[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) {
Object.assign(state.dast.paths, { base: path });
state.dast.paths.base = path;
},
[types.REQUEST_DAST_REPORTS](state) {
Object.assign(state.dast, { isLoading: true });
state.dast.isLoading = true;
},
[types.RECEIVE_DAST_REPORTS](state, reports) {
......@@ -168,45 +151,37 @@ export default {
const newIssues = filterByKey(headIssues, baseIssues, filterKey);
const resolvedIssues = filterByKey(baseIssues, headIssues, filterKey);
Object.assign(state, {
dast: {
...state.dast,
isLoading: false,
newIssues,
resolvedIssues,
},
summaryCounts: {
added: state.summaryCounts.added + newIssues.length,
fixed: state.summaryCounts.fixed + resolvedIssues.length,
},
});
state.dast.newIssues = newIssues;
state.dast.resolvedIssues = resolvedIssues;
state.dast.isLoading = false;
state.summaryCounts.added += newIssues.length;
state.summaryCounts.fixed += resolvedIssues.length;
} else if (reports.head && !reports.base) {
Object.assign(state.dast, {
isLoading: false,
newIssues: parseDastIssues(reports.head.site.alerts),
});
const newIssues = parseDastIssues(reports.head.site.alerts);
state.dast.newIssues = newIssues;
state.dast.isLoading = false;
state.summaryCounts.added += newIssues.length;
}
},
[types.RECEIVE_DAST_ERROR](state) {
Object.assign(state.dast, {
isLoading: false,
hasError: true,
});
state.dast.isLoading = false;
state.dast.hasError = true;
},
// DEPENDECY SCANNING
[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) {
Object.assign(state.dependencyScanning.paths, { base: path });
state.dependencyScanning.paths.base = path;
},
[types.REQUEST_DEPENDENCY_SCANNING_REPORTS](state) {
Object.assign(state.dependencyScanning, { isLoading: true });
state.dependencyScanning.isLoading = true;
},
/**
......@@ -234,31 +209,24 @@ export default {
const resolvedIssues = filterByKey(parsedBase, parsedHead, filterKey);
const allIssues = filterByKey(parsedHead, newIssues.concat(resolvedIssues), filterKey);
Object.assign(state, {
dependencyScanning: {
...state.dependencyScanning,
newIssues,
resolvedIssues,
allIssues,
isLoading: false,
},
summaryCounts: {
added: state.summaryCounts.added + newIssues.length,
fixed: state.summaryCounts.fixed + resolvedIssues.length,
},
});
} else {
Object.assign(state.dependencyScanning, {
newIssues: parseSastIssues(reports.head, state.blobPath.head),
isLoading: false,
});
state.dependencyScanning.newIssues = newIssues;
state.dependencyScanning.resolvedIssues = resolvedIssues;
state.dependencyScanning.allIssues = allIssues;
state.dependencyScanning.isLoading = false;
state.summaryCounts.added += newIssues.length;
state.summaryCounts.fixed += resolvedIssues.length;
}
if (reports.head && !reports.base) {
const newIssues = parseSastIssues(reports.head, state.blobPath.head);
state.dependencyScanning.newIssues = newIssues;
state.dependencyScanning.isLoading = false;
state.summaryCounts.added += newIssues.length;
}
},
[types.RECEIVE_DEPENDENCY_SCANNING_ERROR](state) {
Object.assign(state.dependencyScanning, {
isLoading: false,
hasError: true,
});
state.dependencyScanning.isLoading = false;
state.dependencyScanning.hasError = true;
},
};
......@@ -64,30 +64,33 @@ export const textBuilder = (
resolvedIssues = 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
if (newIssues === 0 && resolvedIssues === 0 && allIssues) {
return sprintf(s__('ciReport|%{type} detected no new security vulnerabilities'), { type });
}
// with new issues and only head
if (newIssues > 0 && !paths.base) {
if (!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(
n__(
'%{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} detected no vulnerabilities for the source branch only',
{ 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
if (newIssues > 0 && resolvedIssues === 0) {
return sprintf(
......@@ -128,7 +131,11 @@ export const textBuilder = (
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) {
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 > span {
display: flex;
......@@ -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 {
border-top: 1px solid $gray-darker;
border-top: 1px solid $border-color;
padding: $gl-padding-top;
background-color: $gray-light;
margin: $gl-padding #{-$gl-padding} #{-$gl-padding};
// Clean MR widget CSS
line-height: 20px;
......@@ -44,6 +72,15 @@
&.neutral {
color: $theme-gray-700;
}
.ci-status-icon {
svg {
width: 16px;
height: 16px;
top: 3px;
left: -2px;
}
}
}
.report-block-list-issue {
......@@ -65,6 +102,10 @@
word-wrap: break-word;
word-break: break-all;
}
.btn-help svg {
top: 5px;
}
}
.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
expect(page).to have_css('#js-tab-security')
end
it 'shows security report' do
expect(page).to have_content('SAST detected no security vulnerabilities')
it 'shows security report section' do
expect(page).to have_content('SAST is loading')
end
end
......
import _ from 'underscore';
import Vue from 'vue';
import PipelineMediator from '~/pipelines/pipeline_details_mediator';
import { sastIssues, parsedSastIssuesStore } from '../vue_shared/security_reports/mock_data';
describe('PipelineMdediator', () => {
let mediator;
......@@ -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 securityState from 'ee/vue_shared/security_reports/helpers/state';
describe('Pipeline Store', () => {
let store;
......@@ -24,11 +23,4 @@ describe('Pipeline Store', () => {
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';
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 mountComponent from 'spec/helpers/vue_mount_component_helper';
import mockData, {
baseIssues,
headIssues,
basePerformance,
headPerformance,
} from './mock_data';
import state from 'ee/vue_shared/security_reports/store/state';
import mockData, { baseIssues, headIssues, basePerformance, headPerformance } from './mock_data';
import {
sastIssues,
sastIssuesBase,
dockerReport,
dockerReportParsed,
dockerBaseReport,
dast,
parsedDast,
dastBase,
sastBaseAllIssues,
sastHeadAllIssues,
} from '../vue_shared/security_reports/mock_data';
describe('ee merge request widget options', () => {
let vm;
let mock;
let Component;
function removeBreakLine(data) {
return data
.replace(/\r?\n|\r/g, '')
.replace(/\s\s+/g, ' ')
.trim();
}
beforeEach(() => {
delete mrWidgetOptions.extends.el; // Prevent component mounting
Component = Vue.extend(mrWidgetOptions);
mock = new MockAdapter(axios);
});
afterEach(() => {
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', () => {
......@@ -52,56 +65,51 @@ describe('ee merge request widget options', () => {
describe('when it is loading', () => {
it('should render loading indicator', () => {
mock.onGet('path.json').reply(200, sastBaseAllIssues);
mock.onGet('head_path.json').reply(200, sastHeadAllIssues);
vm = mountComponent(Component);
expect(
vm.$el.querySelector('.js-sast-widget').textContent.trim(),
).toContain('Loading security report');
expect(vm.$el.querySelector('.js-sast-widget').textContent.trim()).toContain(
'SAST is loading',
);
});
});
describe('with successful request', () => {
let mock;
beforeEach(() => {
mock = mock = new MockAdapter(axios);
mock.onGet('path.json').reply(200, sastIssuesBase);
mock.onGet('head_path.json').reply(200, sastIssues);
vm = mountComponent(Component);
});
afterEach(() => {
mock.restore();
});
it('should render provided data', (done) => {
it('should render provided data', done => {
setTimeout(() => {
expect(
vm.$el.querySelector('.js-sast-widget .js-code-text').textContent.trim(),
).toEqual('SAST improved on 1 security vulnerability and degraded on 2 security vulnerabilities');
removeBreakLine(
vm.$el.querySelector('.js-sast-widget .report-block-list-issue-description')
.textContent,
),
).toEqual('SAST detected 2 new vulnerabilities and 1 fixed vulnerability');
done();
}, 0);
});
});
describe('with full report and no added or fixed issues', () => {
let mock;
beforeEach(() => {
mock = mock = new MockAdapter(axios);
mock.onGet('path.json').reply(200, sastBaseAllIssues);
mock.onGet('head_path.json').reply(200, sastHeadAllIssues);
vm = mountComponent(Component);
});
afterEach(() => {
mock.restore();
});
it('renders no new vulnerabilities message', (done) => {
it('renders no new vulnerabilities message', done => {
setTimeout(() => {
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');
done();
}, 0);
......@@ -109,24 +117,20 @@ describe('ee merge request widget options', () => {
});
describe('with empty successful request', () => {
let mock;
beforeEach(() => {
mock = mock = new MockAdapter(axios);
mock.onGet('path.json').reply(200, []);
mock.onGet('head_path.json').reply(200, []);
vm = mountComponent(Component);
});
afterEach(() => {
mock.restore();
});
it('should render provided data', (done) => {
it('should render provided data', done => {
setTimeout(() => {
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');
done();
}, 0);
......@@ -134,24 +138,17 @@ describe('ee merge request widget options', () => {
});
describe('with failed request', () => {
let mock;
beforeEach(() => {
mock = mock = new MockAdapter(axios);
mock.onGet('path.json').reply(500, []);
mock.onGet('head_path.json').reply(500, []);
vm = mountComponent(Component);
});
afterEach(() => {
mock.restore();
});
it('should render error indicator', (done) => {
it('should render error indicator', done => {
setTimeout(() => {
expect(
vm.$el.querySelector('.js-sast-widget').textContent.trim(),
).toContain('Failed to load security report');
expect(removeBreakLine(vm.$el.querySelector('.js-sast-widget').textContent)).toContain(
'SAST resulted in error while loading results',
);
done();
}, 0);
});
......@@ -174,56 +171,55 @@ describe('ee merge request widget options', () => {
describe('when it is loading', () => {
it('should render loading indicator', () => {
mock.onGet('path.json').reply(200, sastIssuesBase);
mock.onGet('head_path.json').reply(200, sastIssues);
vm = mountComponent(Component);
expect(
vm.$el.querySelector('.js-dependency-scanning-widget').textContent.trim(),
).toContain('Loading dependency scanning report');
removeBreakLine(vm.$el.querySelector('.js-dependency-scanning-widget').textContent),
).toContain('Dependency scanning is loading');
});
});
describe('with successful request', () => {
let mock;
beforeEach(() => {
mock = mock = new MockAdapter(axios);
mock.onGet('path.json').reply(200, sastIssuesBase);
mock.onGet('head_path.json').reply(200, sastIssues);
vm = mountComponent(Component);
});
afterEach(() => {
mock.restore();
});
it('should render provided data', (done) => {
it('should render provided data', done => {
setTimeout(() => {
expect(
vm.$el.querySelector('.js-dependency-scanning-widget .js-code-text').textContent.trim(),
).toEqual('Dependency scanning improved on 1 security vulnerability and degraded on 2 security vulnerabilities');
removeBreakLine(
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();
}, 0);
});
});
describe('with full report and no added or fixed issues', () => {
let mock;
beforeEach(() => {
mock = mock = new MockAdapter(axios);
mock.onGet('path.json').reply(200, sastBaseAllIssues);
mock.onGet('head_path.json').reply(200, sastHeadAllIssues);
vm = mountComponent(Component);
});
afterEach(() => {
mock.restore();
});
it('renders no new vulnerabilities message', (done) => {
it('renders no new vulnerabilities message', done => {
setTimeout(() => {
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');
done();
}, 0);
......@@ -231,24 +227,21 @@ describe('ee merge request widget options', () => {
});
describe('with empty successful request', () => {
let mock;
beforeEach(() => {
mock = mock = new MockAdapter(axios);
mock.onGet('path.json').reply(200, []);
mock.onGet('head_path.json').reply(200, []);
vm = mountComponent(Component);
});
afterEach(() => {
mock.restore();
});
it('should render provided data', (done) => {
it('should render provided data', done => {
setTimeout(() => {
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');
done();
}, 0);
......@@ -256,24 +249,17 @@ describe('ee merge request widget options', () => {
});
describe('with failed request', () => {
let mock;
beforeEach(() => {
mock = mock = new MockAdapter(axios);
mock.onGet('path.json').reply(500, []);
mock.onGet('head_path.json').reply(500, []);
vm = mountComponent(Component);
});
afterEach(() => {
mock.restore();
});
it('should render error indicator', (done) => {
it('should render error indicator', done => {
setTimeout(() => {
expect(
vm.$el.querySelector('.js-dependency-scanning-widget').textContent.trim(),
).toContain('Failed to load dependency scanning report');
removeBreakLine(vm.$el.querySelector('.js-dependency-scanning-widget').textContent),
).toContain('Dependency scanning resulted in error while loading results');
done();
}, 0);
});
......@@ -296,56 +282,57 @@ describe('ee merge request widget options', () => {
describe('when it is loading', () => {
it('should render loading indicator', () => {
mock.onGet('head.json').reply(200, headIssues);
mock.onGet('base.json').reply(200, baseIssues);
vm = mountComponent(Component);
expect(
vm.$el.querySelector('.js-codequality-widget').textContent.trim(),
removeBreakLine(vm.$el.querySelector('.js-codequality-widget').textContent),
).toContain('Loading codeclimate report');
});
});
describe('with successful request', () => {
let mock;
beforeEach(() => {
mock = mock = new MockAdapter(axios);
mock.onGet('head.json').reply(200, headIssues);
mock.onGet('base.json').reply(200, baseIssues);
vm = mountComponent(Component);
});
afterEach(() => {
mock.restore();
});
it('should render provided data', (done) => {
it('should render provided data', done => {
setTimeout(() => {
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');
done();
}, 0);
});
describe('text connector', () => {
it('should only render information about fixed issues', (done) => {
it('should only render information about fixed issues', done => {
setTimeout(() => {
vm.mr.codeclimateMetrics.newIssues = [];
Vue.nextTick(() => {
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');
done();
});
}, 0);
});
it('should only render information about added issues', (done) => {
it('should only render information about added issues', done => {
setTimeout(() => {
vm.mr.codeclimateMetrics.resolvedIssues = [];
Vue.nextTick(() => {
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');
done();
});
......@@ -355,10 +342,7 @@ describe('ee merge request widget options', () => {
});
describe('with empty successful request', () => {
let mock;
beforeEach(() => {
mock = mock = new MockAdapter(axios);
mock.onGet('head.json').reply(200, []);
mock.onGet('base.json').reply(200, []);
vm = mountComponent(Component);
......@@ -368,10 +352,12 @@ describe('ee merge request widget options', () => {
mock.restore();
});
it('should render provided data', (done) => {
it('should render provided data', done => {
setTimeout(() => {
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');
done();
}, 0);
......@@ -379,22 +365,19 @@ describe('ee merge request widget options', () => {
});
describe('with failed request', () => {
let mock;
beforeEach(() => {
mock = mock = new MockAdapter(axios);
mock.onGet('head.json').reply(500, []);
mock.onGet('base.json').reply(500, []);
vm = mountComponent(Component);
});
afterEach(() => {
mock.restore();
});
it('should render error indicator', (done) => {
it('should render error indicator', done => {
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();
}, 0);
});
......@@ -417,57 +400,63 @@ describe('ee merge request widget options', () => {
describe('when it is loading', () => {
it('should render loading indicator', () => {
mock.onGet('head.json').reply(200, headPerformance);
mock.onGet('base.json').reply(200, basePerformance);
vm = mountComponent(Component);
expect(
vm.$el.querySelector('.js-performance-widget').textContent.trim(),
removeBreakLine(vm.$el.querySelector('.js-performance-widget').textContent),
).toContain('Loading performance report');
});
});
describe('with successful request', () => {
let mock;
beforeEach(() => {
mock = mock = new MockAdapter(axios);
mock.onGet('head.json').reply(200, headPerformance);
mock.onGet('base.json').reply(200, basePerformance);
vm = mountComponent(Component);
});
afterEach(() => {
mock.restore();
});
it('should render provided data', (done) => {
it('should render provided data', done => {
setTimeout(() => {
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');
done();
}, 0);
});
describe('text connector', () => {
it('should only render information about fixed issues', (done) => {
it('should only render information about fixed issues', done => {
setTimeout(() => {
vm.mr.performanceMetrics.degraded = [];
Vue.nextTick(() => {
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');
done();
});
}, 0);
});
it('should only render information about added issues', (done) => {
it('should only render information about added issues', done => {
setTimeout(() => {
vm.mr.performanceMetrics.improved = [];
Vue.nextTick(() => {
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');
done();
});
......@@ -477,23 +466,19 @@ describe('ee merge request widget options', () => {
});
describe('with empty successful request', () => {
let mock;
beforeEach(() => {
mock = mock = new MockAdapter(axios);
mock.onGet('head.json').reply(200, []);
mock.onGet('base.json').reply(200, []);
vm = mountComponent(Component);
});
afterEach(() => {
mock.restore();
});
it('should render provided data', (done) => {
it('should render provided data', done => {
setTimeout(() => {
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');
done();
}, 0);
......@@ -501,34 +486,30 @@ describe('ee merge request widget options', () => {
});
describe('with failed request', () => {
let mock;
beforeEach(() => {
mock = mock = new MockAdapter(axios);
mock.onGet('head.json').reply(500, []);
mock.onGet('base.json').reply(500, []);
vm = mountComponent(Component);
});
afterEach(() => {
mock.restore();
});
it('should render error indicator', (done) => {
it('should render error indicator', done => {
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();
}, 0);
});
});
});
describe('docker report', () => {
describe('sast container report', () => {
beforeEach(() => {
gl.mrWidgetData = {
...mockData,
sast_container: {
head_path: 'gl-sast-container.json',
base_path: 'sast-container-base.json',
},
};
......@@ -538,71 +519,50 @@ describe('ee merge request widget options', () => {
describe('when it is loading', () => {
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);
expect(
vm.$el.querySelector('.js-docker-widget').textContent.trim(),
).toContain('Loading sast:container report');
expect(removeBreakLine(vm.$el.querySelector('.js-sast-container').textContent)).toContain(
'Container scanning is loading',
);
});
});
describe('with successful request', () => {
let mock;
beforeEach(() => {
mock = mock = new MockAdapter(axios);
mock.onGet('gl-sast-container.json').reply(200, dockerReport);
vm = mountComponent(Component);
});
mock.onGet('sast-container-base.json').reply(200, dockerBaseReport);
afterEach(() => {
mock.restore();
vm = mountComponent(Component);
});
it('should render provided data', (done) => {
it('should render provided data', done => {
setTimeout(() => {
expect(
vm.$el.querySelector('.js-docker-widget .js-code-text').textContent.trim(),
).toEqual('SAST:container found 3 vulnerabilities, of which 1 is approved');
vm.$el.querySelector('.js-docker-widget .js-collapse-btn').click();
Vue.nextTick(() => {
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();
});
removeBreakLine(
vm.$el.querySelector('.js-sast-container .report-block-list-issue-description')
.textContent,
),
).toEqual('Container scanning detected 1 new vulnerability');
done();
}, 0);
});
});
describe('with failed request', () => {
let mock;
beforeEach(() => {
mock = mock = new MockAdapter(axios);
mock.onGet('gl-sast-container.json').reply(500, {});
vm = mountComponent(Component);
});
mock.onGet('sast-container-base.json').reply(500, {});
afterEach(() => {
mock.restore();
vm = mountComponent(Component);
});
it('should render error indicator', (done) => {
it('should render error indicator', done => {
setTimeout(() => {
expect(
vm.$el.querySelector('.js-docker-widget').textContent.trim(),
).toContain('Failed to load sast:container report');
expect(vm.$el.querySelector('.js-sast-container').textContent.trim()).toContain(
'Container scanning resulted in error while loading results',
);
done();
}, 0);
});
......@@ -615,6 +575,7 @@ describe('ee merge request widget options', () => {
...mockData,
dast: {
head_path: 'dast.json',
base_path: 'dast_base.json',
},
};
......@@ -624,63 +585,49 @@ describe('ee merge request widget options', () => {
describe('when it is loading', () => {
it('should render loading indicator', () => {
mock.onGet('dast.json').reply(200, dast);
mock.onGet('dast_base.json').reply(200, dastBase);
vm = mountComponent(Component);
expect(
vm.$el.querySelector('.js-dast-widget').textContent.trim(),
).toContain('Loading DAST report');
expect(vm.$el.querySelector('.js-dast-widget').textContent.trim()).toContain(
'DAST is loading',
);
});
});
describe('with successful request', () => {
let mock;
beforeEach(() => {
mock = mock = new MockAdapter(axios);
mock.onGet('dast.json').reply(200, dast);
vm = mountComponent(Component);
});
mock.onGet('dast_base.json').reply(200, dastBase);
afterEach(() => {
mock.restore();
vm = mountComponent(Component);
});
it('should render provided data', (done) => {
it('should render provided data', done => {
setTimeout(() => {
expect(
vm.$el.querySelector('.js-dast-widget .js-code-text').textContent.trim(),
).toEqual('DAST detected 2 alerts by analyzing the review app');
vm.$el.querySelector('.js-dast-widget button').click();
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();
});
vm.$el
.querySelector('.js-dast-widget .report-block-list-issue-description')
.textContent.trim(),
).toEqual('DAST detected 1 new vulnerability');
done();
}, 0);
});
});
describe('with failed request', () => {
let mock;
beforeEach(() => {
mock = mock = new MockAdapter(axios);
mock.onGet('dast.json').reply(500, {});
vm = mountComponent(Component);
});
mock.onGet('dast_base.json').reply(500, {});
afterEach(() => {
mock.restore();
vm = mountComponent(Component);
});
it('should render error indicator', (done) => {
it('should render error indicator', done => {
setTimeout(() => {
expect(
vm.$el.querySelector('.js-dast-widget').textContent.trim(),
).toContain('Failed to load DAST report');
expect(vm.$el.querySelector('.js-dast-widget').textContent.trim()).toContain(
'DAST resulted in error while loading results',
);
done();
}, 0);
});
......@@ -725,183 +672,6 @@ describe('ee merge request widget options', () => {
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', () => {
......@@ -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.shouldRemoveSourceBranch = true;
vm.mr.state = 'readyToMerge';
......@@ -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.shouldRemoveSourceBranch = true;
vm.mr.state = 'merged';
......@@ -958,19 +728,22 @@ describe('ee merge request widget options', () => {
deployed_at_formatted: 'Mar 22, 2017 10:44pm',
};
beforeEach((done) => {
beforeEach(done => {
vm = mountComponent(Component, {
mrData: {
...mockData,
},
});
vm.mr.deployments.push({
...deploymentMockData,
}, {
...deploymentMockData,
id: deploymentMockData.id + 1,
});
vm.mr.deployments.push(
{
...deploymentMockData,
},
{
...deploymentMockData,
id: deploymentMockData.id + 1,
},
);
vm.$nextTick(done);
});
......
......@@ -6,18 +6,6 @@ import mockData, {
parsedBaseIssues,
parsedHeadIssues,
} from '../mock_data';
import {
sastIssues,
sastIssuesBase,
parsedSastBaseStore,
parsedSastIssuesHead,
parsedSastIssuesStore,
allIssuesParsed,
dockerReport,
dockerReportParsed,
dast,
parsedDast,
} from '../../vue_shared/security_reports/mock_data';
describe('MergeRequestStore', () => {
let store;
......@@ -98,43 +86,12 @@ describe('MergeRequestStore', () => {
});
});
describe('setSecurityReport', () => {
it('should set security issues with head', () => {
store.setSecurityReport({ head: sastIssues, headBlobPath: 'path' });
expect(store.securityReport.newIssues).toEqual(parsedSastIssuesStore);
});
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);
describe('parseCodeclimateMetrics', () => {
it('should parse the received issues', () => {
const codequality = MergeRequestStore.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);
});
});
......@@ -149,43 +106,4 @@ describe('MergeRequestStore', () => {
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', () => {
it('should render loading indicator', () => {
vm = mountComponent(ReportSection, {
type: 'codequality',
status: 'loading',
status: 'LOADING',
loadingText: 'Loading codeclimate report',
errorText: 'foo',
successText: 'Code quality improved on 1 point and degraded on 1 point',
hasIssues: false,
});
expect(vm.$el.textContent.trim()).toEqual('Loading codeclimate report');
});
......@@ -32,11 +33,12 @@ describe('Report section', () => {
it('should render provided data', () => {
vm = mountComponent(ReportSection, {
type: 'codequality',
status: 'success',
status: 'SUCCESS',
loadingText: 'Loading codeclimate report',
errorText: 'foo',
successText: 'Code quality improved on 1 point and degraded on 1 point',
resolvedIssues: codequalityParsedIssues,
hasIssues: true,
});
expect(
......@@ -52,18 +54,19 @@ describe('Report section', () => {
it('toggles issues', (done) => {
vm = mountComponent(ReportSection, {
type: 'codequality',
status: 'success',
status: 'SUCCESS',
loadingText: 'Loading codeclimate report',
errorText: 'foo',
successText: 'Code quality improved on 1 point and degraded on 1 point',
resolvedIssues: codequalityParsedIssues,
hasIssues: true,
});
vm.$el.querySelector('button').click();
Vue.nextTick(() => {
expect(
vm.$el.querySelector('.report-block-container').getAttribute('style'),
vm.$el.querySelector('.js-report-section-container').getAttribute('style'),
).toEqual('');
expect(
vm.$el.querySelector('button').textContent.trim(),
......@@ -73,7 +76,7 @@ describe('Report section', () => {
Vue.nextTick(() => {
expect(
vm.$el.querySelector('.report-block-container').getAttribute('style'),
vm.$el.querySelector('.js-report-section-container').getAttribute('style'),
).toEqual('display: none;');
expect(
vm.$el.querySelector('button').textContent.trim(),
......@@ -90,10 +93,11 @@ describe('Report section', () => {
it('should render error indicator', () => {
vm = mountComponent(ReportSection, {
type: 'codequality',
status: 'error',
status: 'ERROR',
loadingText: 'Loading codeclimate report',
errorText: 'Failed to load codeclimate report',
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');
});
......@@ -102,11 +106,11 @@ describe('Report section', () => {
describe('With full report', () => {
beforeEach(() => {
vm = mountComponent(ReportSection, {
status: 'success',
status: 'SUCCESS',
successText: 'SAST improved on 1 security vulnerability and degraded on 1 security vulnerability',
type: 'SAST',
errorText: 'Failed to load security report',
hasPriority: true,
hasIssues: true,
loadingText: 'Loading security report',
resolvedIssues: [{
cve: 'CVE-2016-9999',
......@@ -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', () => {
describe('groupedSastText', () => {
describe('with no issues', () => {
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', () => {
newState.sast.newIssues = [{}];
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', () => {
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('with no issues', () => {
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',
);
});
......@@ -103,7 +129,7 @@ describe('Security reports getters', () => {
newState.sastContainer.newIssues = [{}];
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', () => {
describe('groupedDastText', () => {
describe('with no issues', () => {
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', () => {
newState.dast.newIssues = [{}];
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', () => {
describe('groupedDependencyText', () => {
describe('with no issues', () => {
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',
);
});
......@@ -224,7 +258,7 @@ describe('Security reports getters', () => {
newState.dependencyScanning.newIssues = [{}];
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', () => {
areReportsLoading: false,
}),
).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(
groupedSummaryText(state(), {
allReportsHaveError: false,
noBaseInAllReports: false,
areReportsLoading: true,
}),
).toContain('(in progress)');
).toContain('(is loading)');
});
it('returns added and fixed text', () => {
......@@ -337,7 +371,7 @@ describe('Security reports getters', () => {
});
it('returns fixed text', () => {
const newState = Object.assign({}, state());
const newState = state();
newState.summaryCounts = {
added: 0,
fixed: 4,
......@@ -353,7 +387,7 @@ describe('Security reports getters', () => {
});
it('returns added and fixed while loading text', () => {
const newState = Object.assign({}, state());
const newState = state();
newState.summaryCounts = {
added: 2,
fixed: 4,
......@@ -366,20 +400,20 @@ describe('Security reports getters', () => {
areReportsLoading: true,
}),
).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', () => {
it('returns warning with new issues', () => {
const newState = Object.assign({}, state());
const newState = state();
newState.sast.newIssues = [{}];
expect(sastStatusIcon(newState)).toEqual('warning');
});
it('returns warning with failed report', () => {
const newState = Object.assign({}, state());
const newState = state();
newState.sast.hasError = true;
expect(sastStatusIcon(newState)).toEqual('warning');
});
......@@ -391,13 +425,13 @@ describe('Security reports getters', () => {
describe('dastStatusIcon', () => {
it('returns warning with new issues', () => {
const newState = Object.assign({}, state());
const newState = state();
newState.dast.newIssues = [{}];
expect(dastStatusIcon(newState)).toEqual('warning');
});
it('returns warning with failed report', () => {
const newState = Object.assign({}, state());
const newState = state();
newState.dast.hasError = true;
expect(dastStatusIcon(newState)).toEqual('warning');
});
......@@ -409,13 +443,13 @@ describe('Security reports getters', () => {
describe('sastContainerStatusIcon', () => {
it('returns warning with new issues', () => {
const newState = Object.assign({}, state());
const newState = state();
newState.sastContainer.newIssues = [{}];
expect(sastContainerStatusIcon(newState)).toEqual('warning');
});
it('returns warning with failed report', () => {
const newState = Object.assign({}, state());
const newState = state();
newState.sastContainer.hasError = true;
expect(sastContainerStatusIcon(newState)).toEqual('warning');
});
......@@ -427,13 +461,13 @@ describe('Security reports getters', () => {
describe('dependencyScanningStatusIcon', () => {
it('returns warning with new issues', () => {
const newState = Object.assign({}, state());
const newState = state();
newState.dependencyScanning.newIssues = [{}];
expect(dependencyScanningStatusIcon(newState)).toEqual('warning');
});
it('returns warning with failed report', () => {
const newState = Object.assign({}, state());
const newState = state();
newState.dependencyScanning.hasError = true;
expect(dependencyScanningStatusIcon(newState)).toEqual('warning');
});
......@@ -445,7 +479,7 @@ describe('Security reports getters', () => {
describe('areReportsLoading', () => {
it('returns true when any report is loading', () => {
const newState = Object.assign({}, state());
const newState = state();
newState.sast.isLoading = true;
expect(areReportsLoading(newState)).toEqual(true);
});
......@@ -457,7 +491,7 @@ describe('Security reports getters', () => {
describe('allReportsHaveError', () => {
it('returns true when all reports have error', () => {
const newState = Object.assign({}, state());
const newState = state();
newState.sast.hasError = true;
newState.dast.hasError = true;
newState.sastContainer.hasError = true;
......@@ -466,14 +500,24 @@ describe('Security reports getters', () => {
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);
});
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', () => {
it('returns true when any of the reports has error', () => {
const newState = Object.assign({}, state());
const newState = state();
newState.sast.hasError = true;
expect(anyReportHasError(newState)).toEqual(true);
......@@ -490,7 +534,7 @@ describe('Security reports getters', () => {
});
it('returns false when any of the reports has base', () => {
const newState = Object.assign({}, state());
const newState = state();
newState.sast.paths.base = 'foo';
expect(noBaseInAllReports(newState)).toEqual(false);
});
......
......@@ -64,7 +64,7 @@ describe('security reports utils', () => {
describe('textBuilder', () => {
describe('with no issues', () => {
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', () => {
describe('with new issues and without base', () => {
it('should return unable to compare text', () => {
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', () => {
describe('statusIcon', () => {
describe('with failed report', () => {
it('returns warning', () => {
expect(statusIcon(true)).toEqual('warning');
expect(statusIcon(false, true)).toEqual('warning');
});
});
describe('with new issues', () => {
it('returns warning', () => {
expect(statusIcon(false, 1)).toEqual('warning');
expect(statusIcon(false, false, 1)).toEqual('warning');
});
});
describe('with neutral issues', () => {
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