Commit 7a6b566c authored by Filipa Lacerda's avatar Filipa Lacerda

Render dependency scanning report on CI view and MR widget

parent 5833ba11
......@@ -81,24 +81,51 @@ export default () => {
const securityTab = document.getElementById('js-security-report-app');
const sastSummary = document.querySelector('.js-sast-summary');
const updateBadgeCount = (count) => {
const badge = document.querySelector('.js-sast-counter');
if (badge.textContent !== '') {
badge.textContent = parseInt(badge.textContent, 10) + count;
} else {
badge.textContent = count;
}
badge.classList.remove('hidden');
};
// They are being rendered under the same condition
if (securityTab && sastSummary) {
const datasetOptions = securityTab.dataset;
const endpoint = datasetOptions.endpoint;
const blobPath = datasetOptions.blobPath;
const dependencyScanningEndpoint = datasetOptions.dependencyScanningEndpoint;
mediator.fetchSastReport(endpoint, blobPath)
if (endpoint) {
mediator.fetchSastReport(endpoint, blobPath)
.then(() => {
// update the badge
if (mediator.store.state.securityReports.sast.newIssues.length) {
const badge = document.querySelector('.js-sast-counter');
badge.textContent = mediator.store.state.securityReports.sast.newIssues.length;
badge.classList.remove('hidden');
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
......@@ -115,7 +142,8 @@ export default () => {
render(createElement) {
return createElement('sast-summary-widget', {
props: {
unresolvedIssues: this.mediator.store.state.securityReports.sast.newIssues,
unresolvedIssues: this.mediator.store.state.securityReports.sast.newIssues.length +
this.mediator.store.state.securityReports.dependencyScanning.newIssues.length,
},
});
},
......@@ -137,6 +165,8 @@ export default () => {
return createElement('security-report-app', {
props: {
securityReports: this.mediator.store.state.securityReports,
hasDependencyScanning: dependencyScanningEndpoint !== undefined,
hasSast: endpoint !== undefined,
},
});
},
......
......@@ -67,4 +67,12 @@ export default class pipelinesMediator {
this.store.storeSastReport(data, blobPath);
});
}
fetchDependencyScanningReport(endpoint, blobPath) {
return PipelineService.getSecurityReport(endpoint)
.then(response => response.json())
.then((data) => {
this.store.storeDependencyScanningReport(data, blobPath);
});
}
}
......@@ -26,4 +26,11 @@ export default class PipelineStore {
setSastReport({ head: data, headBlobPath: blobPath }),
);
}
storeDependencyScanningReport(data, blobPath) {
Object.assign(
this.state.securityReports.dependencyScanning,
setSastReport({ head: data, headBlobPath: blobPath }),
);
}
}
#js-pipeline-header-vue.pipeline-header-container
- sast_artifact = @pipeline.sast_artifact
- dependecy_artifact = @pipeline.dependency_scanning_artifact
- if @commit.present?
.commit-box
......@@ -35,5 +36,5 @@
= link_to @pipeline.sha, project_commit_path(@project, @pipeline.sha), class: "commit-sha commit-hash-full"
= clipboard_button(text: @pipeline.sha, title: "Copy commit SHA to clipboard")
- if sast_artifact
- if sast_artifact || dependecy_artifact
.js-sast-summary
- failed_builds = @pipeline.statuses.latest.failed
- expose_sast_data = @pipeline.expose_sast_data?
- expose_dependency_data = @pipeline.expose_dependency_scanning_data?
- blob_path = project_blob_path(@project, @pipeline.sha)
.tabs-holder
......@@ -16,7 +17,7 @@
= link_to failures_project_pipeline_path(@project, @pipeline), data: { target: '#js-tab-failures', action: 'failures', toggle: 'tab' }, class: 'failures-tab' do
= _("Failed Jobs")
%span.badge.js-failures-counter= failed_builds.count
- if expose_sast_data
- if expose_sast_data || expose_dependency_data
%li.js-security-tab-link
= link_to security_project_pipeline_path(@project, @pipeline), data: { target: '#js-tab-security', action: 'security', toggle: 'tab' }, class: 'security-tab' do
= _("Security report")
......@@ -60,6 +61,8 @@
%span.build-name
= link_to build.name, pipeline_job_url(pipeline, build)
%pre.build-log= build_summary(build, skip: index >= 10)
- if expose_sast_data
- if expose_sast_data || expose_dependency_data
#js-tab-security.build-security.tab-pane
#js-security-report-app{ data: { endpoint: sast_artifact_url(@pipeline), blob_path: blob_path } }
#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} }
<script>
import $ from 'jquery';
import { n__, s__ } from '~/locale';
import ciIcon from '~/vue_shared/components/ci_icon.vue';
import CiIcon from '~/vue_shared/components/ci_icon.vue';
export default {
name: 'SastSummaryReport',
components: {
ciIcon,
CiIcon,
},
props: {
unresolvedIssues: {
type: Array,
type: Number,
required: false,
default: () => ([]),
default: 0,
},
},
computed: {
sastText() {
if (this.unresolvedIssues.length) {
return s__('ciReport|SAST degraded on');
}
return s__('ciReport|SAST detected');
},
sastLink() {
if (this.unresolvedIssues.length) {
if (this.unresolvedIssues > 0) {
return n__(
'%d security vulnerability',
'%d security vulnerabilities',
this.unresolvedIssues.length,
this.unresolvedIssues,
);
}
return s__('ciReport|no security vulnerabilities');
},
statusIcon() {
if (this.unresolvedIssues.length) {
if (this.unresolvedIssues > 0) {
return {
group: 'warning',
icon: 'status_warning',
......@@ -65,7 +59,7 @@
<span
class="prepend-left-10 flex flex-align-self-center"
>
{{ sastText }}
{{ s__('ciReport|Security reports detected') }}
<button
type="button"
class="btn-link btn-blank prepend-left-5"
......
......@@ -17,12 +17,23 @@
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)"
......@@ -32,7 +43,23 @@
:unresolved-issues="securityReports.sast.newIssues"
:resolved-issues="securityReports.sast.resolvedIssues"
:all-issues="securityReports.sast.allIssues"
:is-collapsible="false"
/>
<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>
......@@ -17,9 +17,7 @@ export default {
'mr-widget-geo-secondary-node': GeoSecondaryNode,
ReportSection,
},
mixins: [
securityMixin,
],
mixins: [securityMixin],
dast: DAST,
sast: SAST,
sastContainer: SAST_CONTAINER,
......@@ -30,11 +28,13 @@ export default {
isLoadingSecurity: false,
isLoadingDocker: false,
isLoadingDast: false,
isLoadingDependencyScanning: false,
loadingCodequalityFailed: false,
loadingPerformanceFailed: false,
loadingSecurityFailed: false,
loadingDockerFailed: false,
loadingDastFailed: false,
loadingDependencyScanningFailed: false,
};
},
computed: {
......@@ -53,10 +53,13 @@ export default {
return this.mr.sast && this.mr.sast.head_path;
},
shouldRenderDockerReport() {
return this.mr.sastContainer;
return this.mr.sastContainer && this.mr.sastContainer.head_path;
},
shouldRenderDastReport() {
return this.mr.dast;
return this.mr.dast && this.mr.dast.head_path;
},
shouldRenderDependencyReport() {
return this.mr.dependencyScanning && this.mr.dependencyScanning.head_path;
},
codequalityText() {
const { newIssues, resolvedIssues } = this.mr.codeclimateMetrics;
......@@ -68,11 +71,13 @@ 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) {
......@@ -80,11 +85,13 @@ 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,
),
);
}
}
......@@ -101,11 +108,13 @@ 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) {
......@@ -113,11 +122,13 @@ 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,
),
);
}
}
......@@ -129,6 +140,11 @@ export default {
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);
......@@ -139,24 +155,43 @@ export default {
},
codequalityStatus() {
return this.checkReportStatus(this.isLoadingCodequality, this.loadingCodequalityFailed);
return this.checkReportStatus(
this.isLoadingCodequality,
this.loadingCodequalityFailed,
);
},
performanceStatus() {
return this.checkReportStatus(this.isLoadingPerformance, this.loadingPerformanceFailed);
return this.checkReportStatus(
this.isLoadingPerformance,
this.loadingPerformanceFailed,
);
},
securityStatus() {
return this.checkReportStatus(this.isLoadingSecurity, this.loadingSecurityFailed);
return this.checkReportStatus(
this.isLoadingSecurity,
this.loadingSecurityFailed,
);
},
dockerStatus() {
return this.checkReportStatus(this.isLoadingDocker, this.loadingDockerFailed);
return this.checkReportStatus(
this.isLoadingDocker,
this.loadingDockerFailed,
);
},
dastStatus() {
return this.checkReportStatus(this.isLoadingDast, this.loadingDastFailed);
},
dependencyScanningStatus() {
return this.checkReportStatus(
this.isLoadingDependencyScanning,
this.loadingDependencyScanningFailed,
);
},
},
methods: {
fetchCodeQuality() {
......@@ -168,7 +203,7 @@ export default {
this.service.fetchReport(head_path),
this.service.fetchReport(base_path),
])
.then((values) => {
.then(values => {
this.mr.compareCodeclimateMetrics(
values[0],
values[1],
......@@ -192,7 +227,7 @@ export default {
this.service.fetchReport(head_path),
this.service.fetchReport(base_path),
])
.then((values) => {
.then(values => {
this.mr.comparePerformanceMetrics(values[0], values[1]);
this.isLoadingPerformance = false;
})
......@@ -216,7 +251,7 @@ export default {
this.service.fetchReport(sast.head_path),
this.service.fetchReport(sast.base_path),
])
.then((values) => {
.then(values => {
this.handleSecuritySuccess({
head: values[0],
headBlobPath: this.mr.headBlobPath,
......@@ -226,8 +261,9 @@ export default {
})
.catch(() => this.handleSecurityError());
} else if (sast.head_path) {
this.service.fetchReport(sast.head_path)
.then((data) => {
this.service
.fetchReport(sast.head_path)
.then(data => {
this.handleSecuritySuccess({
head: data,
headBlobPath: this.mr.headBlobPath,
......@@ -237,6 +273,45 @@ export default {
}
},
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,
});
})
.catch(() => {
this.isLoadingDependencyScanning = false;
this.loadingDependencyScanningFailed = true;
});
}
},
handleSecuritySuccess(data) {
this.mr.setSecurityReport(data);
this.isLoadingSecurity = false;
......@@ -251,8 +326,9 @@ export default {
const { head_path } = this.mr.sastContainer;
this.isLoadingDocker = true;
this.service.fetchReport(head_path)
.then((data) => {
this.service
.fetchReport(head_path)
.then(data => {
this.mr.setDockerReport(data);
this.isLoadingDocker = false;
})
......@@ -265,8 +341,9 @@ export default {
fetchDastReport() {
this.isLoadingDast = true;
this.service.fetchReport(this.mr.dast.head_path)
.then((data) => {
this.service
.fetchReport(this.mr.dast.head_path)
.then(data => {
this.mr.setDastReport(data);
this.isLoadingDast = false;
})
......@@ -296,6 +373,10 @@ export default {
if (this.shouldRenderDastReport) {
this.fetchDastReport();
}
if (this.shouldRenderDependencyReport) {
this.fetchDependencyScanning();
}
},
template: `
<div class="mr-state-widget prepend-top-default">
......@@ -351,6 +432,18 @@ export default {
: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"
......
......@@ -20,6 +20,7 @@ export default class MergeRequestStore extends CEMergeRequestStore {
this.initSecurityReport(data);
this.initDockerReport(data);
this.initDastReport(data);
this.initDependencyScanningReport(data);
}
setData(data) {
......@@ -95,6 +96,15 @@ export default class MergeRequestStore extends CEMergeRequestStore {
this.dastReport = [];
}
initDependencyScanningReport(data) {
this.dependencyScanning = data.dependencyScanning;
this.dependencyScanningReport = {
newIssues: [],
resolvedIssues: [],
allIssues: [],
};
}
setSecurityReport(data) {
const report = setSastReport(data);
this.securityReport.newIssues = report.newIssues;
......@@ -113,6 +123,13 @@ export default class MergeRequestStore extends CEMergeRequestStore {
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 = parseIssues(headIssues, headBlobPath);
const parsedBaseIssues = parseIssues(baseIssues, baseBlobPath);
......
......@@ -155,7 +155,7 @@
class="media-body space-children"
>
<span
class="js-code-text"
class="js-code-text code-text"
>
{{ successText }}
</span>
......
......@@ -16,4 +16,11 @@ export default {
newIssues: [],
resolvedIssues: [],
},
dependencyScanning: {
isLoading: false,
hasError: false,
newIssues: [],
resolvedIssues: [],
allIssues: [],
},
};
......@@ -36,6 +36,40 @@ export default {
return text.join('');
},
depedencyScanningText(newIssues = [], resolvedIssues = [], allIssues = []) {
const text = [];
if (!newIssues.length && !resolvedIssues.length && !allIssues.length) {
text.push(s__('ciReport|Dependency scanning detected no security vulnerabilities'));
} else if (!newIssues.length && !resolvedIssues.length && allIssues.length) {
text.push(s__('ciReport|Dependency scanning detected no new security vulnerabilities'));
} else if (newIssues.length || resolvedIssues.length) {
text.push(s__('ciReport|Dependency scanning'));
}
if (resolvedIssues.length) {
text.push(n__(
' improved on %d security vulnerability',
' improved on %d security vulnerabilities',
resolvedIssues.length,
));
}
if (newIssues.length > 0 && resolvedIssues.length > 0) {
text.push(__(' and'));
}
if (newIssues.length) {
text.push(n__(
' degraded on %d security vulnerability',
' degraded on %d security vulnerabilities',
newIssues.length,
));
}
return text.join('');
},
translateText(type) {
return {
error: sprintf(s__('ciReport|Failed to load %{reportName} report'), { reportName: type }),
......
......@@ -2,11 +2,16 @@
.space-children,
.space-children > span {
display: flex;
align-self: center;
}
.media {
align-items: center;
}
.code-text {
width: 100%;
}
}
.report-block-container {
......
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