Commit 6b338ad7 authored by Tim Zallmann's avatar Tim Zallmann

Merge branch '4913-frontend-outdated-security-report' into 'master'

Frontend - Resolve "Display in MR if security report is outdated"

See merge request gitlab-org/gitlab!20954
parents 2c11ce5a 9cbae06e
......@@ -165,21 +165,23 @@ export default {
<template>
<section class="media-section">
<div class="media">
<status-icon :status="statusIconName" :size="24" />
<div class="media-body d-flex flex-align-self-center">
<span class="js-code-text code-text">
<status-icon :status="statusIconName" :size="24" class="align-self-center" />
<div class="media-body d-flex flex-align-self-center align-items-center">
<div class="js-code-text code-text">
<div>
{{ headerText }}
<slot :name="slotName"></slot>
<popover v-if="hasPopover" :options="popoverOptions" class="prepend-left-5" />
</span>
</div>
<slot name="subHeading"></slot>
</div>
<slot name="actionButtons"></slot>
<button
v-if="isCollapsible"
type="button"
class="js-collapse-btn btn float-right btn-sm align-self-start qa-expand-report-button"
class="js-collapse-btn btn float-right btn-sm align-self-center qa-expand-report-button"
@click="toggleCollapsed"
>
{{ collapseText }}
......
---
title: Display in MR if security report is outdated
merge_request: 20954
author:
type: other
......@@ -274,6 +274,7 @@ export default {
v-if="shouldRenderSecurityReport"
:head-blob-path="mr.headBlobPath"
:source-branch="mr.sourceBranch"
:target-branch="mr.targetBranch"
:base-blob-path="mr.baseBlobPath"
:enabled-reports="mr.enabledSecurityReports"
:sast-head-path="mr.sast.head_path"
......
......@@ -9,6 +9,7 @@ import Icon from '~/vue_shared/components/icon.vue';
import IssueModal from './components/modal.vue';
import securityReportsMixin from './mixins/security_report_mixin';
import createStore from './store';
import { s__, sprintf } from '~/locale';
export default {
store: createStore(),
......@@ -40,6 +41,11 @@ export default {
required: false,
default: null,
},
targetBranch: {
type: String,
required: false,
default: null,
},
sastHeadPath: {
type: String,
required: false,
......@@ -169,6 +175,7 @@ export default {
'sastContainerStatusIcon',
'dastStatusIcon',
'dependencyScanningStatusIcon',
'isBaseSecurityReportOutOfDate',
]),
...mapGetters('sast', ['groupedSastText', 'sastStatusIcon']),
securityTab() {
......@@ -191,6 +198,25 @@ export default {
hasSastReports() {
return this.hasReportsType('sast');
},
subHeadingText() {
const mrDivergedCommitsCount =
(gl && gl.mrWidgetData && gl.mrWidgetData.diverged_commits_count) || 0;
const isMRBranchOutdated = mrDivergedCommitsCount > 0;
if (isMRBranchOutdated) {
return sprintf(
s__(
'Security report is out of date. Please incorporate latest changes from %{targetBranchName}',
),
{
targetBranchName: this.targetBranch,
},
);
}
return sprintf(
s__('Security report is out of date. Retry the pipeline for the target branch.'),
);
},
},
created() {
......@@ -358,6 +384,10 @@ export default {
</a>
</div>
<div v-if="isBaseSecurityReportOutOfDate" slot="subHeading" class="text-secondary-700 text-1">
<span>{{ subHeadingText }}</span>
</div>
<div slot="body" class="mr-widget-grouped-section report-block">
<template v-if="hasSastReports">
<summary-row
......
......@@ -130,5 +130,11 @@ export const anyReportHasIssues = state =>
state.sastContainer.newIssues.length > 0 ||
state.dependencyScanning.newIssues.length > 0;
export const isBaseSecurityReportOutOfDate = state =>
state.sast.baseReportOutofDate ||
state.dast.baseReportOutofDate ||
state.sastContainer.baseReportOutofDate ||
state.dependencyScanning.baseReportOutofDate;
// prevent babel-plugin-rewire from generating an invalid default during karma tests
export default () => {};
......@@ -61,11 +61,13 @@ export default {
[types.RECEIVE_DIFF_SUCCESS](state, { diff, enrichData }) {
const { added, fixed, existing } = parseDiff(diff, enrichData);
const baseReportOutofDate = diff.base_report_out_of_date || false;
state.isLoading = false;
state.newIssues = added;
state.resolvedIssues = fixed;
state.allIssues = existing;
state.baseReportOutofDate = baseReportOutofDate;
},
[types.RECEIVE_DIFF_ERROR](state) {
......
......@@ -11,4 +11,5 @@ export default () => ({
newIssues: [],
resolvedIssues: [],
allIssues: [],
baseReportOutofDate: false,
});
......@@ -108,11 +108,13 @@ export default {
[types.RECEIVE_SAST_CONTAINER_DIFF_SUCCESS](state, { diff, enrichData }) {
const { added, fixed, existing } = parseDiff(diff, enrichData);
const baseReportOutofDate = diff.base_report_out_of_date || false;
Vue.set(state.sastContainer, 'isLoading', false);
Vue.set(state.sastContainer, 'newIssues', added);
Vue.set(state.sastContainer, 'resolvedIssues', fixed);
Vue.set(state.sastContainer, 'allIssues', existing);
Vue.set(state.sastContainer, 'baseReportOutofDate', baseReportOutofDate);
},
[types.RECEIVE_SAST_CONTAINER_DIFF_ERROR](state) {
......@@ -164,11 +166,13 @@ export default {
[types.RECEIVE_DAST_DIFF_SUCCESS](state, { diff, enrichData }) {
const { added, fixed, existing } = parseDiff(diff, enrichData);
const baseReportOutofDate = diff.base_report_out_of_date || false;
Vue.set(state.dast, 'isLoading', false);
Vue.set(state.dast, 'newIssues', added);
Vue.set(state.dast, 'resolvedIssues', fixed);
Vue.set(state.dast, 'allIssues', existing);
Vue.set(state.dast, 'baseReportOutofDate', baseReportOutofDate);
},
[types.RECEIVE_DAST_DIFF_ERROR](state) {
......@@ -251,11 +255,13 @@ export default {
[types.RECEIVE_DEPENDENCY_SCANNING_DIFF_SUCCESS](state, { diff, enrichData }) {
const { added, fixed, existing } = parseDiff(diff, enrichData);
const baseReportOutofDate = diff.base_report_out_of_date || false;
Vue.set(state.dependencyScanning, 'isLoading', false);
Vue.set(state.dependencyScanning, 'newIssues', added);
Vue.set(state.dependencyScanning, 'resolvedIssues', fixed);
Vue.set(state.dependencyScanning, 'allIssues', existing);
Vue.set(state.dependencyScanning, 'baseReportOutofDate', baseReportOutofDate);
},
[types.RECEIVE_DEPENDENCY_SCANNING_DIFF_ERROR](state) {
......
......@@ -28,6 +28,7 @@ export default () => ({
newIssues: [],
resolvedIssues: [],
baseReportOutofDate: false,
},
dast: {
paths: {
......@@ -41,6 +42,7 @@ export default () => ({
newIssues: [],
resolvedIssues: [],
baseReportOutofDate: false,
},
dependencyScanning: {
......@@ -56,6 +58,7 @@ export default () => ({
newIssues: [],
resolvedIssues: [],
allIssues: [],
baseReportOutofDate: false,
},
modal: {
......
......@@ -13,6 +13,7 @@ import {
dependencyScanningStatusIcon,
anyReportHasError,
summaryCounts,
isBaseSecurityReportOutOfDate,
} from 'ee/vue_shared/security_reports/store/getters';
const BASE_PATH = 'fake/base/path.json';
......@@ -530,4 +531,15 @@ describe('Security reports getters', () => {
expect(noBaseInAllReports(state)).toEqual(false);
});
});
describe('isBaseSecurityReportOutOfDate', () => {
it('returns false when none reports are out of date', () => {
expect(isBaseSecurityReportOutOfDate(state)).toEqual(false);
});
it('returns true when any of the reports is out of date', () => {
state.dast.baseReportOutofDate = true;
expect(isBaseSecurityReportOutOfDate(state)).toEqual(true);
});
});
});
......@@ -197,6 +197,7 @@ describe('sast module mutations', () => {
],
fixed: [createIssue({ cve: 'CVE-4' }), createIssue({ cve: 'CVE-5' })],
existing: [createIssue({ cve: 'CVE-6' })],
base_report_out_of_date: true,
},
};
state.isLoading = true;
......@@ -207,6 +208,10 @@ describe('sast module mutations', () => {
expect(state.isLoading).toBe(false);
});
it('should set the `baseReportOutofDate` status to `false`', () => {
expect(state.baseReportOutofDate).toBe(true);
});
it('should have the relevant `new` issues', () => {
expect(state.newIssues.length).toBe(3);
});
......
......@@ -816,6 +816,7 @@ describe('security reports mutations', () => {
{ name: 'added vuln 2', report_type: 'container_scanning' },
],
fixed: [{ name: 'fixed vuln 1', report_type: 'container_scanning' }],
base_report_out_of_date: true,
},
};
......@@ -827,6 +828,10 @@ describe('security reports mutations', () => {
expect(stateCopy.sastContainer.isLoading).toBe(false);
});
it('should set baseReportOutofDate to true', () => {
expect(stateCopy.sastContainer.baseReportOutofDate).toBe(true);
});
it('should parse and set the added vulnerabilities', () => {
reports.diff.added.forEach((vuln, i) => {
expect(stateCopy.sastContainer.newIssues[i]).toEqual(
......@@ -885,6 +890,7 @@ describe('security reports mutations', () => {
],
fixed: [{ name: 'fixed vuln 1', report_type: 'dependency_scanning' }],
existing: [{ name: 'existing vuln 1', report_type: 'dependency_scanning' }],
base_report_out_of_date: true,
},
};
mutations[types.RECEIVE_DEPENDENCY_SCANNING_DIFF_SUCCESS](stateCopy, reports);
......@@ -894,6 +900,10 @@ describe('security reports mutations', () => {
expect(stateCopy.dependencyScanning.isLoading).toBe(false);
});
it('should set baseReportOutofDate to true', () => {
expect(stateCopy.dependencyScanning.baseReportOutofDate).toBe(true);
});
it('should parse and set the added vulnerabilities', () => {
reports.diff.added.forEach((vuln, i) => {
expect(stateCopy.dependencyScanning.newIssues[i]).toEqual(
......@@ -952,6 +962,7 @@ describe('security reports mutations', () => {
],
fixed: [{ name: 'fixed vuln 1', report_type: 'dast' }],
existing: [{ name: 'existing vuln 1', report_type: 'dast' }],
base_report_out_of_date: true,
},
};
mutations[types.RECEIVE_DAST_DIFF_SUCCESS](stateCopy, reports);
......@@ -961,6 +972,10 @@ describe('security reports mutations', () => {
expect(stateCopy.dast.isLoading).toBe(false);
});
it('should set baseReportOutofDate to true', () => {
expect(stateCopy.dast.baseReportOutofDate).toBe(true);
});
it('should parse and set the added vulnerabilities', () => {
reports.diff.added.forEach((vuln, i) => {
expect(stateCopy.dast.newIssues[i]).toEqual(
......
......@@ -478,6 +478,7 @@ describe('Grouped security reports app', () => {
mock.onGet(dastEndpoint).reply(200, {
added: [dockerReport.vulnerabilities[0]],
fixed: [dockerReport.vulnerabilities[1], dockerReport.vulnerabilities[2]],
base_report_out_of_date: true,
});
mock.onGet('vulnerability_feedback_path.json').reply(200, []);
......@@ -527,6 +528,12 @@ describe('Grouped security reports app', () => {
'DAST detected 1 new, and 2 fixed vulnerabilities',
);
});
it('should display out of date message', () => {
expect(wrapper.vm.$el.textContent).toContain(
'Security report is out of date. Retry the pipeline for the target branch',
);
});
});
});
......@@ -545,6 +552,7 @@ describe('Grouped security reports app', () => {
canCreateIssue: true,
canCreateMergeRequest: true,
canDismissVulnerability: true,
targetBranch: 'master',
};
const provide = {
glFeatures: {
......@@ -555,11 +563,13 @@ describe('Grouped security reports app', () => {
beforeEach(() => {
gl.mrWidgetData = gl.mrWidgetData || {};
gl.mrWidgetData.sast_comparison_path = sastEndpoint;
gl.mrWidgetData.diverged_commits_count = 100;
mock.onGet(sastEndpoint).reply(200, {
added: [dockerReport.vulnerabilities[0]],
fixed: [dockerReport.vulnerabilities[1], dockerReport.vulnerabilities[2]],
existing: [dockerReport.vulnerabilities[2]],
base_report_out_of_date: true,
});
mock.onGet('vulnerability_feedback_path.json').reply(200, []);
......@@ -609,6 +619,12 @@ describe('Grouped security reports app', () => {
'SAST detected 1 new, and 2 fixed vulnerabilities',
);
});
it('should display out of date message for Outdated MR ', () => {
expect(wrapper.vm.$el.textContent).toContain(
'Security report is out of date. Please incorporate latest changes from master',
);
});
});
});
});
......
......@@ -16100,6 +16100,12 @@ msgstr ""
msgid "Security dashboard"
msgstr ""
msgid "Security report is out of date. Please incorporate latest changes from %{targetBranchName}"
msgstr ""
msgid "Security report is out of date. Retry the pipeline for the target branch."
msgstr ""
msgid "SecurityConfiguration|Configured"
msgstr ""
......
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