Commit a5172063 authored by Filipa Lacerda's avatar Filipa Lacerda

Merge branch...

Merge branch '13649-move-dependency-scanning-reports-logic-for-the-merge-request-widget-to-the-backend-frontend-part' into 'master'

Resolve "Move dependency scanning reports logic for the Merge Request widget to the backend - frontend part"

Closes #13649

See merge request gitlab-org/gitlab-ee!15406
parents 127acef8 b3aa2dc2
...@@ -171,6 +171,11 @@ export default { ...@@ -171,6 +171,11 @@ export default {
shouldRenderSastContainer() { shouldRenderSastContainer() {
const { head, diffEndpoint } = this.sastContainer.paths; const { head, diffEndpoint } = this.sastContainer.paths;
return head || diffEndpoint;
},
shouldRenderDependencyScanning() {
const { head, diffEndpoint } = this.dependencyScanning.paths;
return head || diffEndpoint; return head || diffEndpoint;
}, },
}, },
...@@ -229,7 +234,17 @@ export default { ...@@ -229,7 +234,17 @@ export default {
this.fetchDastReports(); this.fetchDastReports();
} }
if (this.dependencyScanningHeadPath) { const dependencyScanningDiffEndpoint =
gl && gl.mrWidgetData && gl.mrWidgetData.dependency_scanning_comparison_path;
if (
gon.features &&
gon.features.dependencyScanningMergeRequestReportApi &&
dependencyScanningDiffEndpoint
) {
this.setDependencyScanningDiffEndpoint(dependencyScanningDiffEndpoint);
this.fetchDependencyScanningDiff();
} else if (this.dependencyScanningHeadPath) {
this.setDependencyScanningHeadPath(this.dependencyScanningHeadPath); this.setDependencyScanningHeadPath(this.dependencyScanningHeadPath);
if (this.dependencyScanningBasePath) { if (this.dependencyScanningBasePath) {
...@@ -274,6 +289,8 @@ export default { ...@@ -274,6 +289,8 @@ export default {
'hideDismissalDeleteButtons', 'hideDismissalDeleteButtons',
'fetchSastContainerDiff', 'fetchSastContainerDiff',
'setSastContainerDiffEndpoint', 'setSastContainerDiffEndpoint',
'fetchDependencyScanningDiff',
'setDependencyScanningDiffEndpoint',
]), ]),
...mapActions('sast', { ...mapActions('sast', {
setSastHeadPath: 'setHeadPath', setSastHeadPath: 'setHeadPath',
...@@ -322,7 +339,7 @@ export default { ...@@ -322,7 +339,7 @@ export default {
/> />
</template> </template>
<template v-if="dependencyScanningHeadPath"> <template v-if="shouldRenderDependencyScanning">
<summary-row <summary-row
:summary="groupedDependencyText" :summary="groupedDependencyText"
:status-icon="dependencyScanningStatusIcon" :status-icon="dependencyScanningStatusIcon"
......
...@@ -18,6 +18,28 @@ import httpStatusCodes from '~/lib/utils/http_status'; ...@@ -18,6 +18,28 @@ import httpStatusCodes from '~/lib/utils/http_status';
const hideModal = () => $('#modal-mrwidget-security-issue').modal('hide'); const hideModal = () => $('#modal-mrwidget-security-issue').modal('hide');
const pollUntilComplete = endpoint =>
new Promise((resolve, reject) => {
const eTagPoll = new Poll({
resource: {
getReports(url) {
return axios.get(url);
},
},
data: endpoint,
method: 'getReports',
successCallback: response => {
if (response.status === httpStatusCodes.OK) {
resolve(response);
eTagPoll.stop();
}
},
errorCallback: reject,
});
eTagPoll.makeRequest();
});
export const setHeadBlobPath = ({ commit }, blobPath) => commit(types.SET_HEAD_BLOB_PATH, blobPath); export const setHeadBlobPath = ({ commit }, blobPath) => commit(types.SET_HEAD_BLOB_PATH, blobPath);
export const setBaseBlobPath = ({ commit }, blobPath) => commit(types.SET_BASE_BLOB_PATH, blobPath); export const setBaseBlobPath = ({ commit }, blobPath) => commit(types.SET_BASE_BLOB_PATH, blobPath);
...@@ -74,29 +96,8 @@ export const receiveSastContainerDiffSuccess = ({ commit }, response) => ...@@ -74,29 +96,8 @@ export const receiveSastContainerDiffSuccess = ({ commit }, response) =>
export const fetchSastContainerDiff = ({ state, dispatch }) => { export const fetchSastContainerDiff = ({ state, dispatch }) => {
dispatch('requestSastContainerReports'); dispatch('requestSastContainerReports');
const pollPromise = new Promise((resolve, reject) => {
const eTagPoll = new Poll({
resource: {
getReports(endpoint) {
return axios.get(endpoint);
},
},
data: state.sastContainer.paths.diffEndpoint,
method: 'getReports',
successCallback: response => {
if (response.status === httpStatusCodes.OK) {
resolve(response);
eTagPoll.stop();
}
},
errorCallback: reject,
});
eTagPoll.makeRequest();
});
return Promise.all([ return Promise.all([
pollPromise, pollUntilComplete(state.sastContainer.paths.diffEndpoint),
axios.get(state.vulnerabilityFeedbackPath, { axios.get(state.vulnerabilityFeedbackPath, {
params: { params: {
category: 'container_scanning', category: 'container_scanning',
...@@ -194,6 +195,9 @@ export const setDependencyScanningHeadPath = ({ commit }, path) => ...@@ -194,6 +195,9 @@ export const setDependencyScanningHeadPath = ({ commit }, path) =>
export const setDependencyScanningBasePath = ({ commit }, path) => export const setDependencyScanningBasePath = ({ commit }, path) =>
commit(types.SET_DEPENDENCY_SCANNING_BASE_PATH, path); commit(types.SET_DEPENDENCY_SCANNING_BASE_PATH, path);
export const setDependencyScanningDiffEndpoint = ({ commit }, path) =>
commit(types.SET_DEPENDENCY_SCANNING_DIFF_ENDPOINT, path);
export const requestDependencyScanningReports = ({ commit }) => export const requestDependencyScanningReports = ({ commit }) =>
commit(types.REQUEST_DEPENDENCY_SCANNING_REPORTS); commit(types.REQUEST_DEPENDENCY_SCANNING_REPORTS);
...@@ -203,6 +207,31 @@ export const receiveDependencyScanningReports = ({ commit }, response) => ...@@ -203,6 +207,31 @@ export const receiveDependencyScanningReports = ({ commit }, response) =>
export const receiveDependencyScanningError = ({ commit }, error) => export const receiveDependencyScanningError = ({ commit }, error) =>
commit(types.RECEIVE_DEPENDENCY_SCANNING_ERROR, error); commit(types.RECEIVE_DEPENDENCY_SCANNING_ERROR, error);
export const receiveDependencyScanningDiffSuccess = ({ commit }, response) =>
commit(types.RECEIVE_DEPENDENCY_SCANNING_DIFF_SUCCESS, response);
export const fetchDependencyScanningDiff = ({ state, dispatch }) => {
dispatch('requestDependencyScanningReports');
return Promise.all([
pollUntilComplete(state.dependencyScanning.paths.diffEndpoint),
axios.get(state.vulnerabilityFeedbackPath, {
params: {
category: 'dependency_scanning',
},
}),
])
.then(values => {
dispatch('receiveDependencyScanningDiffSuccess', {
diff: values[0].data,
enrichData: values[1].data,
});
})
.catch(() => {
dispatch('receiveDependencyScanningError');
});
};
export const fetchDependencyScanningReports = ({ state, dispatch }) => { export const fetchDependencyScanningReports = ({ state, dispatch }) => {
const { base, head } = state.dependencyScanning.paths; const { base, head } = state.dependencyScanning.paths;
......
...@@ -32,9 +32,11 @@ export const RECEIVE_DAST_ERROR = 'RECEIVE_DAST_ERROR'; ...@@ -32,9 +32,11 @@ export const RECEIVE_DAST_ERROR = 'RECEIVE_DAST_ERROR';
// DEPENDENCY_SCANNING // DEPENDENCY_SCANNING
export const SET_DEPENDENCY_SCANNING_HEAD_PATH = 'SET_DEPENDENCY_SCANNING_HEAD_PATH'; export const SET_DEPENDENCY_SCANNING_HEAD_PATH = 'SET_DEPENDENCY_SCANNING_HEAD_PATH';
export const SET_DEPENDENCY_SCANNING_BASE_PATH = 'SET_DEPENDENCY_SCANNING_BASE_PATH'; export const SET_DEPENDENCY_SCANNING_BASE_PATH = 'SET_DEPENDENCY_SCANNING_BASE_PATH';
export const SET_DEPENDENCY_SCANNING_DIFF_ENDPOINT = 'SET_DEPENDENCY_SCANNING_DIFF_ENDPOINT';
export const REQUEST_DEPENDENCY_SCANNING_REPORTS = 'REQUEST_DEPENDENCY_SCANNING_REPORTS'; export const REQUEST_DEPENDENCY_SCANNING_REPORTS = 'REQUEST_DEPENDENCY_SCANNING_REPORTS';
export const RECEIVE_DEPENDENCY_SCANNING_REPORTS = 'RECEIVE_DEPENDENCY_SCANNING_REPORTS'; export const RECEIVE_DEPENDENCY_SCANNING_REPORTS = 'RECEIVE_DEPENDENCY_SCANNING_REPORTS';
export const RECEIVE_DEPENDENCY_SCANNING_ERROR = 'RECEIVE_DEPENDENCY_SCANNING_ERROR'; export const RECEIVE_DEPENDENCY_SCANNING_ERROR = 'RECEIVE_DEPENDENCY_SCANNING_ERROR';
export const RECEIVE_DEPENDENCY_SCANNING_DIFF_SUCCESS = 'RECEIVE_DEPENDENCY_SCANNING_DIFF_SUCCESS';
// Dismiss security issue // Dismiss security issue
export const SET_ISSUE_MODAL_DATA = 'SET_ISSUE_MODAL_DATA'; export const SET_ISSUE_MODAL_DATA = 'SET_ISSUE_MODAL_DATA';
......
...@@ -5,7 +5,7 @@ import { ...@@ -5,7 +5,7 @@ import {
parseDastIssues, parseDastIssues,
getUnapprovedVulnerabilities, getUnapprovedVulnerabilities,
findIssueIndex, findIssueIndex,
enrichVulnerabilityWithFeedback, parseDiff,
} from './utils'; } from './utils';
import filterByKey from './utils/filter_by_key'; import filterByKey from './utils/filter_by_key';
import { parseSastContainer } from './utils/container_scanning'; import { parseSastContainer } from './utils/container_scanning';
...@@ -106,18 +106,12 @@ export default { ...@@ -106,18 +106,12 @@ export default {
}, },
[types.RECEIVE_SAST_CONTAINER_DIFF_SUCCESS](state, { diff, enrichData }) { [types.RECEIVE_SAST_CONTAINER_DIFF_SUCCESS](state, { diff, enrichData }) {
const fillInTheGaps = vulnerability => ({ const { added, fixed, existing } = parseDiff(diff, enrichData);
...enrichVulnerabilityWithFeedback(vulnerability, enrichData),
category: vulnerability.report_type,
title: vulnerability.message || vulnerability.name,
});
const added = diff.added ? diff.added.map(fillInTheGaps) : [];
const fixed = diff.fixed ? diff.fixed.map(fillInTheGaps) : [];
Vue.set(state.sastContainer, 'isLoading', false); Vue.set(state.sastContainer, 'isLoading', false);
Vue.set(state.sastContainer, 'newIssues', added); Vue.set(state.sastContainer, 'newIssues', added);
Vue.set(state.sastContainer, 'resolvedIssues', fixed); Vue.set(state.sastContainer, 'resolvedIssues', fixed);
Vue.set(state.sastContainer, 'allIssues', existing);
}, },
[types.RECEIVE_SAST_CONTAINER_ERROR](state) { [types.RECEIVE_SAST_CONTAINER_ERROR](state) {
...@@ -173,6 +167,10 @@ export default { ...@@ -173,6 +167,10 @@ export default {
Vue.set(state.dependencyScanning.paths, 'base', path); Vue.set(state.dependencyScanning.paths, 'base', path);
}, },
[types.SET_DEPENDENCY_SCANNING_DIFF_ENDPOINT](state, path) {
Vue.set(state.dependencyScanning.paths, 'diffEndpoint', path);
},
[types.REQUEST_DEPENDENCY_SCANNING_REPORTS](state) { [types.REQUEST_DEPENDENCY_SCANNING_REPORTS](state) {
Vue.set(state.dependencyScanning, 'isLoading', true); Vue.set(state.dependencyScanning, 'isLoading', true);
}, },
...@@ -227,6 +225,15 @@ export default { ...@@ -227,6 +225,15 @@ export default {
} }
}, },
[types.RECEIVE_DEPENDENCY_SCANNING_DIFF_SUCCESS](state, { diff, enrichData }) {
const { added, fixed, existing } = parseDiff(diff, enrichData);
Vue.set(state.dependencyScanning, 'isLoading', false);
Vue.set(state.dependencyScanning, 'newIssues', added);
Vue.set(state.dependencyScanning, 'resolvedIssues', fixed);
Vue.set(state.dependencyScanning, 'allIssues', existing);
},
[types.RECEIVE_DEPENDENCY_SCANNING_ERROR](state) { [types.RECEIVE_DEPENDENCY_SCANNING_ERROR](state) {
Vue.set(state.dependencyScanning, 'isLoading', false); Vue.set(state.dependencyScanning, 'isLoading', false);
Vue.set(state.dependencyScanning, 'hasError', true); Vue.set(state.dependencyScanning, 'hasError', true);
......
...@@ -46,6 +46,7 @@ export default () => ({ ...@@ -46,6 +46,7 @@ export default () => ({
paths: { paths: {
head: null, head: null,
base: null, base: null,
diffEndpoint: null,
}, },
isLoading: false, isLoading: false,
......
...@@ -420,3 +420,24 @@ export const groupedReportText = (report, reportType, errorMessage, loadingMessa ...@@ -420,3 +420,24 @@ export const groupedReportText = (report, reportType, errorMessage, loadingMessa
paths, paths,
}); });
}; };
/**
* Generates the added, fixed, and existing vulnerabilities from the API report.
*
* @param {Object} diff The original reports.
* @param {Object} enrichData Feedback data to add to the reports.
* @returns {Object}
*/
export const parseDiff = (diff, enrichData) => {
const enrichVulnerability = vulnerability => ({
...enrichVulnerabilityWithFeedback(vulnerability, enrichData),
category: vulnerability.report_type,
title: vulnerability.message || vulnerability.name,
});
return {
added: diff.added ? diff.added.map(enrichVulnerability) : [],
fixed: diff.fixed ? diff.fixed.map(enrichVulnerability) : [],
existing: diff.existing ? diff.existing.map(enrichVulnerability) : [],
};
};
...@@ -840,4 +840,62 @@ describe('security reports mutations', () => { ...@@ -840,4 +840,62 @@ describe('security reports mutations', () => {
}); });
}); });
}); });
describe('SET_DEPENDENCY_SCANNING_DIFF_ENDPOINT', () => {
const endpoint = 'dependency_scannning_diff_endpoint.json';
beforeEach(() => {
mutations[types.SET_DEPENDENCY_SCANNING_DIFF_ENDPOINT](stateCopy, endpoint);
});
it('should set the correct endpoint', () => {
expect(stateCopy.dependencyScanning.paths.diffEndpoint).toEqual(endpoint);
});
});
describe('RECEIVE_DEPENDENCY_SCANNING_DIFF_SUCCESS', () => {
let reports = {};
beforeEach(() => {
reports = {
diff: {
added: [
{ name: 'added vuln 1', report_type: 'dependency_scanning' },
{ name: 'added vuln 2', report_type: 'dependency_scanning' },
],
fixed: [{ name: 'fixed vuln 1', report_type: 'dependency_scanning' }],
existing: [{ name: 'existing vuln 1', report_type: 'dependency_scanning' }],
},
};
mutations[types.RECEIVE_DEPENDENCY_SCANNING_DIFF_SUCCESS](stateCopy, reports);
});
it('should set isLoading to false', () => {
expect(stateCopy.dependencyScanning.isLoading).toBe(false);
});
it('should parse and set the added vulnerabilities', () => {
reports.diff.added.forEach((vuln, i) => {
expect(stateCopy.dependencyScanning.newIssues[i]).toEqual(
expect.objectContaining({
name: vuln.name,
title: vuln.name,
category: vuln.report_type,
}),
);
});
});
it('should parse and set the fixed vulnerabilities', () => {
reports.diff.fixed.forEach((vuln, i) => {
expect(stateCopy.dependencyScanning.resolvedIssues[i]).toEqual(
expect.objectContaining({
name: vuln.name,
title: vuln.name,
category: vuln.report_type,
}),
);
});
});
});
}); });
...@@ -273,6 +273,7 @@ describe('Grouped security reports app', () => { ...@@ -273,6 +273,7 @@ describe('Grouped security reports app', () => {
}); });
describe('with the reports API enabled', () => { describe('with the reports API enabled', () => {
describe('container scanning reports', () => {
const sastContainerEndpoint = 'sast_container.json'; const sastContainerEndpoint = 'sast_container.json';
beforeEach(done => { beforeEach(done => {
...@@ -306,7 +307,7 @@ describe('Grouped security reports app', () => { ...@@ -306,7 +307,7 @@ describe('Grouped security reports app', () => {
waitForMutation(vm.$store, types.RECEIVE_SAST_CONTAINER_DIFF_SUCCESS) waitForMutation(vm.$store, types.RECEIVE_SAST_CONTAINER_DIFF_SUCCESS)
.then(done) .then(done)
.catch(); .catch(done.fail);
}); });
it('should set setSastContainerDiffEndpoint', () => { it('should set setSastContainerDiffEndpoint', () => {
...@@ -319,4 +320,53 @@ describe('Grouped security reports app', () => { ...@@ -319,4 +320,53 @@ describe('Grouped security reports app', () => {
); );
}); });
}); });
describe('dependency scanning reports', () => {
const dependencyScanningEndpoint = 'dependency_scanning.json';
beforeEach(done => {
gon.features = gon.features || {};
gon.features.dependencyScanningMergeRequestReportApi = true;
gl.mrWidgetData = gl.mrWidgetData || {};
gl.mrWidgetData.dependency_scanning_comparison_path = dependencyScanningEndpoint;
mock.onGet(dependencyScanningEndpoint).reply(200, {
added: [dockerReport.vulnerabilities[0], dockerReport.vulnerabilities[1]],
fixed: [dockerReport.vulnerabilities[2]],
});
mock.onGet('vulnerability_feedback_path.json').reply(200, []);
vm = mountComponent(Component, {
headBlobPath: 'path',
baseBlobPath: 'path',
sastHelpPath: 'path',
sastContainerHelpPath: 'path',
dastHelpPath: 'path',
dependencyScanningHelpPath: 'path',
vulnerabilityFeedbackPath: 'vulnerability_feedback_path.json',
vulnerabilityFeedbackHelpPath: 'path',
pipelineId: 123,
canCreateIssue: true,
canCreateMergeRequest: true,
canDismissVulnerability: true,
});
waitForMutation(vm.$store, types.RECEIVE_DEPENDENCY_SCANNING_DIFF_SUCCESS)
.then(done)
.catch();
});
it('should set setDependencyScanningDiffEndpoint', () => {
expect(vm.dependencyScanning.paths.diffEndpoint).toEqual(dependencyScanningEndpoint);
});
it('should call `fetchDependencyScanningDiff`', () => {
expect(vm.$el.textContent).toContain(
'Dependency scanning detected 2 new, and 1 fixed vulnerabilities',
);
});
});
});
}); });
...@@ -58,6 +58,9 @@ import actions, { ...@@ -58,6 +58,9 @@ import actions, {
setSastContainerDiffEndpoint, setSastContainerDiffEndpoint,
receiveSastContainerDiffSuccess, receiveSastContainerDiffSuccess,
fetchSastContainerDiff, fetchSastContainerDiff,
setDependencyScanningDiffEndpoint,
receiveDependencyScanningDiffSuccess,
fetchDependencyScanningDiff,
} from 'ee/vue_shared/security_reports/store/actions'; } from 'ee/vue_shared/security_reports/store/actions';
import * as types from 'ee/vue_shared/security_reports/store/mutation_types'; import * as types from 'ee/vue_shared/security_reports/store/mutation_types';
import state from 'ee/vue_shared/security_reports/store/state'; import state from 'ee/vue_shared/security_reports/store/state';
...@@ -72,6 +75,7 @@ import { ...@@ -72,6 +75,7 @@ import {
sastFeedbacks, sastFeedbacks,
dastFeedbacks, dastFeedbacks,
containerScanningFeedbacks, containerScanningFeedbacks,
dependencyScanningFeedbacks,
} from '../mock_data'; } from '../mock_data';
const createVulnerability = options => ({ const createVulnerability = options => ({
...@@ -1728,4 +1732,144 @@ describe('security reports actions', () => { ...@@ -1728,4 +1732,144 @@ describe('security reports actions', () => {
}); });
}); });
}); });
describe('setDependencyScanningDiffEndpoint', () => {
it('should pass down the endpoint to the mutation', done => {
const payload = '/dependency_scanning_endpoint.json';
testAction(
setDependencyScanningDiffEndpoint,
payload,
mockedState,
[
{
type: types.SET_DEPENDENCY_SCANNING_DIFF_ENDPOINT,
payload,
},
],
[],
done,
);
});
});
describe('receiveDependencyScanningDiffSuccess', () => {
it('should pass down the response to the mutation', done => {
const payload = { data: 'Effort yields its own rewards.' };
testAction(
receiveDependencyScanningDiffSuccess,
payload,
mockedState,
[
{
type: types.RECEIVE_DEPENDENCY_SCANNING_DIFF_SUCCESS,
payload,
},
],
[],
done,
);
});
});
describe('fetchDependencyScanningDiff', () => {
const diff = { foo: {} };
beforeEach(() => {
mockedState.vulnerabilityFeedbackPath = 'vulnerabilities_feedback';
mockedState.dependencyScanning.paths.diffEndpoint = 'dependency_scanning_diff.json';
});
describe('on success', () => {
it('should dispatch `receiveDependencyScanningDiffSuccess`', done => {
mock.onGet('dependency_scanning_diff.json').reply(200, diff);
mock
.onGet('vulnerabilities_feedback', {
params: {
category: 'dependency_scanning',
},
})
.reply(200, dependencyScanningFeedbacks);
testAction(
fetchDependencyScanningDiff,
null,
mockedState,
[],
[
{
type: 'requestDependencyScanningReports',
},
{
type: 'receiveDependencyScanningDiffSuccess',
payload: {
diff,
enrichData: dependencyScanningFeedbacks,
},
},
],
done,
);
});
});
describe('when vulnerabilities path errors', () => {
it('should dispatch `receiveDependencyScanningError`', done => {
mock.onGet('dependency_scanning_diff.json').reply(500);
mock
.onGet('vulnerabilities_feedback', {
params: {
category: 'dependency_scanning',
},
})
.reply(200, dependencyScanningFeedbacks);
testAction(
fetchDependencyScanningDiff,
null,
mockedState,
[],
[
{
type: 'requestDependencyScanningReports',
},
{
type: 'receiveDependencyScanningError',
},
],
done,
);
});
});
describe('when feedback path errors', () => {
it('should dispatch `receiveDependencyScanningError`', done => {
mock.onGet('dependency_scanning_diff.json').reply(200, diff);
mock
.onGet('vulnerabilities_feedback', {
params: {
category: 'dependency_scanning',
},
})
.reply(500);
testAction(
fetchDependencyScanningDiff,
null,
mockedState,
[],
[
{
type: 'requestDependencyScanningReports',
},
{
type: 'receiveDependencyScanningError',
},
],
done,
);
});
});
});
}); });
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