Commit 1ed8dd17 authored by Sam Beckham's avatar Sam Beckham Committed by Filipa Lacerda

Moves MR reports logic for SAST to the backend

- Copies over all the changes from the similar MRs for DAST, dependency
  and container scanning.
- Everything is behind an inactive feature flag.

See https://gitlab.com/gitlab-org/gitlab-ee/issues/12002 for more
information.
parent fa479537
...@@ -181,6 +181,11 @@ export default { ...@@ -181,6 +181,11 @@ export default {
shouldRenderDast() { shouldRenderDast() {
const { head, diffEndpoint } = this.dast.paths; const { head, diffEndpoint } = this.dast.paths;
return head || diffEndpoint;
},
shouldRenderSast() {
const { head, diffEndpoint } = this.sast.paths;
return head || diffEndpoint; return head || diffEndpoint;
}, },
}, },
...@@ -202,7 +207,12 @@ export default { ...@@ -202,7 +207,12 @@ export default {
this.setCanCreateIssuePermission(this.canCreateIssue); this.setCanCreateIssuePermission(this.canCreateIssue);
this.setCanCreateFeedbackPermission(this.canCreateFeedback); this.setCanCreateFeedbackPermission(this.canCreateFeedback);
if (this.sastHeadPath) { const sastDiffEndpoint = gl && gl.mrWidgetData && gl.mrWidgetData.sast_comparison_path;
if (gon.features && gon.features.sastMergeRequestReportApi && sastDiffEndpoint) {
this.setSastDiffEndpoint(sastDiffEndpoint);
this.fetchSastDiff();
} else if (this.sastHeadPath) {
this.setSastHeadPath(this.sastHeadPath); this.setSastHeadPath(this.sastHeadPath);
if (this.sastBasePath) { if (this.sastBasePath) {
...@@ -307,7 +317,9 @@ export default { ...@@ -307,7 +317,9 @@ export default {
...mapActions('sast', { ...mapActions('sast', {
setSastHeadPath: 'setHeadPath', setSastHeadPath: 'setHeadPath',
setSastBasePath: 'setBasePath', setSastBasePath: 'setBasePath',
setSastDiffEndpoint: 'setDiffEndpoint',
fetchSastReports: 'fetchReports', fetchSastReports: 'fetchReports',
fetchSastDiff: 'fetchDiff',
}), }),
}, },
}; };
...@@ -333,7 +345,7 @@ export default { ...@@ -333,7 +345,7 @@ export default {
</div> </div>
<div slot="body" class="mr-widget-grouped-section report-block"> <div slot="body" class="mr-widget-grouped-section report-block">
<template v-if="sastHeadPath"> <template v-if="shouldRenderSast">
<summary-row <summary-row
:summary="groupedSastText" :summary="groupedSastText"
:status-icon="sastStatusIcon" :status-icon="sastStatusIcon"
......
...@@ -4,8 +4,7 @@ import { s__ } from '~/locale'; ...@@ -4,8 +4,7 @@ import { s__ } from '~/locale';
import { visitUrl } from '~/lib/utils/url_utility'; import { visitUrl } from '~/lib/utils/url_utility';
import * as types from './mutation_types'; import * as types from './mutation_types';
import downloadPatchHelper from './utils/download_patch_helper'; import downloadPatchHelper from './utils/download_patch_helper';
import Poll from '~/lib/utils/poll'; import { pollUntilComplete } from './utils';
import httpStatusCodes from '~/lib/utils/http_status';
/** /**
* A lot of this file has duplicate actions to * A lot of this file has duplicate actions to
...@@ -18,28 +17,6 @@ import httpStatusCodes from '~/lib/utils/http_status'; ...@@ -18,28 +17,6 @@ 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);
......
import axios from '~/lib/utils/axios_utils'; import axios from '~/lib/utils/axios_utils';
import * as types from './mutation_types'; import * as types from './mutation_types';
import { pollUntilComplete } from '../../utils';
export const setHeadPath = ({ commit }, path) => commit(types.SET_HEAD_PATH, path); export const setHeadPath = ({ commit }, path) => commit(types.SET_HEAD_PATH, path);
export const setBasePath = ({ commit }, path) => commit(types.SET_BASE_PATH, path); export const setBasePath = ({ commit }, path) => commit(types.SET_BASE_PATH, path);
export const setDiffEndpoint = ({ commit }, path) => commit(types.SET_DIFF_ENDPOINT, path);
export const requestReports = ({ commit }) => commit(types.REQUEST_REPORTS); export const requestReports = ({ commit }) => commit(types.REQUEST_REPORTS);
export const receiveReports = ({ commit }, response) => commit(types.RECEIVE_REPORTS, response); export const receiveReports = ({ commit }, response) => commit(types.RECEIVE_REPORTS, response);
...@@ -44,4 +47,32 @@ export const fetchReports = ({ state, rootState, dispatch }) => { ...@@ -44,4 +47,32 @@ export const fetchReports = ({ state, rootState, dispatch }) => {
export const updateVulnerability = ({ commit }, vulnerability) => export const updateVulnerability = ({ commit }, vulnerability) =>
commit(types.UPDATE_VULNERABILITY, vulnerability); commit(types.UPDATE_VULNERABILITY, vulnerability);
export const receiveDiffSuccess = ({ commit }, response) =>
commit(types.RECEIVE_DIFF_SUCCESS, response);
export const receiveDiffError = ({ commit }, response) =>
commit(types.RECEIVE_DIFF_ERROR, response);
export const fetchDiff = ({ state, rootState, dispatch }) => {
dispatch('requestReports');
return Promise.all([
pollUntilComplete(state.paths.diffEndpoint),
axios.get(rootState.vulnerabilityFeedbackPath, {
params: {
category: 'sast',
},
}),
])
.then(values => {
dispatch('receiveDiffSuccess', {
diff: values[0].data,
enrichData: values[1].data,
});
})
.catch(() => {
dispatch('receiveDiffError');
});
};
export default () => {}; export default () => {};
export const SET_HEAD_PATH = 'SET_HEAD_PATH'; export const RECEIVE_DIFF_SUCCESS = 'RECEIVE_DIFF_SUCCESS';
export const SET_BASE_PATH = 'SET_BASE_PATH'; export const RECEIVE_DIFF_ERROR = 'RECEIVE_DIFF_ERROR';
export const REQUEST_REPORTS = 'REQUEST_REPORTS';
export const RECEIVE_REPORTS = 'RECEIVE_REPORTS'; export const RECEIVE_REPORTS = 'RECEIVE_REPORTS';
export const RECEIVE_REPORTS_ERROR = 'RECEIVE_REPORTS_ERROR'; export const RECEIVE_REPORTS_ERROR = 'RECEIVE_REPORTS_ERROR';
export const REQUEST_REPORTS = 'REQUEST_REPORTS';
export const SET_BASE_PATH = 'SET_BASE_PATH';
export const SET_DIFF_ENDPOINT = 'SET_DIFF_ENDPOINT';
export const SET_HEAD_PATH = 'SET_HEAD_PATH';
export const UPDATE_VULNERABILITY = 'UPDATE_VULNERABILITY'; export const UPDATE_VULNERABILITY = 'UPDATE_VULNERABILITY';
import Vue from 'vue'; import Vue from 'vue';
import * as types from './mutation_types'; import * as types from './mutation_types';
import { parseSastIssues, findIssueIndex } from '../../utils'; import { parseSastIssues, findIssueIndex, parseDiff } from '../../utils';
import filterByKey from '../../utils/filter_by_key'; import filterByKey from '../../utils/filter_by_key';
export default { export default {
...@@ -12,6 +12,10 @@ export default { ...@@ -12,6 +12,10 @@ export default {
Vue.set(state.paths, 'base', path); Vue.set(state.paths, 'base', path);
}, },
[types.SET_DIFF_ENDPOINT](state, path) {
Vue.set(state.paths, 'diffEndpoint', path);
},
[types.REQUEST_REPORTS](state) { [types.REQUEST_REPORTS](state) {
state.isLoading = true; state.isLoading = true;
}, },
...@@ -55,6 +59,20 @@ export default { ...@@ -55,6 +59,20 @@ export default {
} }
}, },
[types.RECEIVE_DIFF_SUCCESS](state, { diff, enrichData }) {
const { added, fixed, existing } = parseDiff(diff, enrichData);
state.isLoading = false;
state.newIssues = added;
state.resolvedIssues = fixed;
state.allIssues = existing;
},
[types.RECEIVE_DIFF_ERROR](state) {
state.isLoading = false;
state.hasError = true;
},
[types.RECEIVE_REPORTS_ERROR](state) { [types.RECEIVE_REPORTS_ERROR](state) {
state.isLoading = false; state.isLoading = false;
state.hasError = true; state.hasError = true;
......
...@@ -2,6 +2,7 @@ export default () => ({ ...@@ -2,6 +2,7 @@ export default () => ({
paths: { paths: {
head: null, head: null,
base: null, base: null,
diffEndpoint: null,
}, },
isLoading: false, isLoading: false,
......
import sha1 from 'sha1'; import sha1 from 'sha1';
import _ from 'underscore'; import _ from 'underscore';
import axios from 'axios';
import { stripHtml } from '~/lib/utils/text_utility'; import { stripHtml } from '~/lib/utils/text_utility';
import { n__, s__, sprintf } from '~/locale'; import { n__, s__, sprintf } from '~/locale';
import Poll from '~/lib/utils/poll';
import httpStatusCodes from '~/lib/utils/http_status';
/** /**
* Returns the index of an issue in given list * Returns the index of an issue in given list
...@@ -441,3 +444,32 @@ export const parseDiff = (diff, enrichData) => { ...@@ -441,3 +444,32 @@ export const parseDiff = (diff, enrichData) => {
existing: diff.existing ? diff.existing.map(enrichVulnerability) : [], existing: diff.existing ? diff.existing.map(enrichVulnerability) : [],
}; };
}; };
/**
* Polls an endpoint untill it returns either an error or a 200.
* Resolving or rejecting the promise accordingly.
*
* @param {String} endpoint the endpoint to poll.
* @returns {Promise}
*/
export 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();
});
...@@ -8,11 +8,13 @@ import * as actions from 'ee/vue_shared/security_reports/store/modules/sast/acti ...@@ -8,11 +8,13 @@ import * as actions from 'ee/vue_shared/security_reports/store/modules/sast/acti
const headPath = 'head-path.json'; const headPath = 'head-path.json';
const basePath = 'base-path.json'; const basePath = 'base-path.json';
const diffEndpoint = 'diff-endpoint.json';
const blobPath = 'blob-path.json'; const blobPath = 'blob-path.json';
const reports = { const reports = {
base: 'base', base: 'base',
head: 'head', head: 'head',
enrichData: 'enrichData', enrichData: 'enrichData',
diff: 'diff',
}; };
const error = 'Something went wrong'; const error = 'Something went wrong';
const issue = {}; const issue = {};
...@@ -62,6 +64,24 @@ describe('sast report actions', () => { ...@@ -62,6 +64,24 @@ describe('sast report actions', () => {
}); });
}); });
describe('setDiffEndpoint', () => {
it(`should commit ${types.SET_DIFF_ENDPOINT} with the correct path`, done => {
testAction(
actions.setDiffEndpoint,
diffEndpoint,
state,
[
{
type: types.SET_DIFF_ENDPOINT,
payload: diffEndpoint,
},
],
[],
done,
);
});
});
describe('requestReports', () => { describe('requestReports', () => {
it(`should commit ${types.REQUEST_REPORTS}`, done => { it(`should commit ${types.REQUEST_REPORTS}`, done => {
testAction(actions.requestReports, {}, state, [{ type: types.REQUEST_REPORTS }], [], done); testAction(actions.requestReports, {}, state, [{ type: types.REQUEST_REPORTS }], [], done);
...@@ -129,6 +149,8 @@ describe('sast report actions', () => { ...@@ -129,6 +149,8 @@ describe('sast report actions', () => {
}); });
it('should dispatch the `receiveReports` action', done => { it('should dispatch the `receiveReports` action', done => {
const { head, base, enrichData } = reports;
testAction( testAction(
actions.fetchReports, actions.fetchReports,
{}, {},
...@@ -140,7 +162,7 @@ describe('sast report actions', () => { ...@@ -140,7 +162,7 @@ describe('sast report actions', () => {
type: 'receiveReports', type: 'receiveReports',
payload: { payload: {
blobPath, blobPath,
reports, reports: { head, base, enrichData },
}, },
}, },
], ],
...@@ -173,6 +195,128 @@ describe('sast report actions', () => { ...@@ -173,6 +195,128 @@ describe('sast report actions', () => {
}); });
}); });
describe('receiveDiffSuccess', () => {
it(`should commit ${types.RECEIVE_DIFF_SUCCESS} with the correct response`, done => {
testAction(
actions.receiveDiffSuccess,
reports,
state,
[
{
type: types.RECEIVE_DIFF_SUCCESS,
payload: reports,
},
],
[],
done,
);
});
});
describe('receiveDiffError', () => {
it(`should commit ${types.RECEIVE_DIFF_ERROR} with the correct response`, done => {
testAction(
actions.receiveDiffError,
error,
state,
[
{
type: types.RECEIVE_DIFF_ERROR,
payload: error,
},
],
[],
done,
);
});
});
describe('fetchDiff', () => {
let mock;
beforeEach(() => {
mock = new MockAdapter(axios);
state.paths.diffEndpoint = diffEndpoint;
});
afterEach(() => {
mock.restore();
});
describe('when diff and vulnerability feedback endpoints respond successfully', () => {
beforeEach(() => {
mock
.onGet(diffEndpoint)
.replyOnce(200, reports.diff)
.onGet(vulnerabilityFeedbackPath)
.replyOnce(200, reports.enrichData);
});
it('should dispatch the `receiveDiffSuccess` action', done => {
const { diff, enrichData } = reports;
testAction(
actions.fetchDiff,
{},
{ ...rootState, ...state },
[],
[
{ type: 'requestReports' },
{
type: 'receiveDiffSuccess',
payload: {
diff,
enrichData,
},
},
],
done,
);
});
});
describe('when the vulnerability feedback endpoint fails', () => {
beforeEach(() => {
mock
.onGet(diffEndpoint)
.replyOnce(200, reports.diff)
.onGet(vulnerabilityFeedbackPath)
.replyOnce(404);
});
it('should dispatch the `receiveError` action', done => {
testAction(
actions.fetchDiff,
{},
{ ...rootState, ...state },
[],
[{ type: 'requestReports' }, { type: 'receiveDiffError' }],
done,
);
});
});
describe('when the diff endpoint fails', () => {
beforeEach(() => {
mock
.onGet(diffEndpoint)
.replyOnce(404)
.onGet(vulnerabilityFeedbackPath)
.replyOnce(200, reports.enrichData);
});
it('should dispatch the `receiveDiffError` action', done => {
testAction(
actions.fetchDiff,
{},
{ ...rootState, ...state },
[],
[{ type: 'requestReports' }, { type: 'receiveDiffError' }],
done,
);
});
});
});
describe('updateVulnerability', () => { describe('updateVulnerability', () => {
it(`should commit ${types.UPDATE_VULNERABILITY} with the correct response`, done => { it(`should commit ${types.UPDATE_VULNERABILITY} with the correct response`, done => {
testAction( testAction(
......
...@@ -28,6 +28,14 @@ describe('sast module mutations', () => { ...@@ -28,6 +28,14 @@ describe('sast module mutations', () => {
}); });
}); });
describe(types.SET_DIFF_ENDPOINT, () => {
it('should set the SAST diff endpoint', () => {
mutations[types.SET_DIFF_ENDPOINT](state, path);
expect(state.paths.diffEndpoint).toBe(path);
});
});
describe(types.REQUEST_REPORTS, () => { describe(types.REQUEST_REPORTS, () => {
it('should set the `isLoading` status to `true`', () => { it('should set the `isLoading` status to `true`', () => {
mutations[types.REQUEST_REPORTS](state); mutations[types.REQUEST_REPORTS](state);
...@@ -177,4 +185,53 @@ describe('sast module mutations', () => { ...@@ -177,4 +185,53 @@ describe('sast module mutations', () => {
}); });
}); });
}); });
describe(types.RECEIVE_DIFF_SUCCESS, () => {
beforeEach(() => {
const reports = {
diff: {
added: [
createIssue({ cve: 'CVE-1' }),
createIssue({ cve: 'CVE-2' }),
createIssue({ cve: 'CVE-3' }),
],
fixed: [createIssue({ cve: 'CVE-4' }), createIssue({ cve: 'CVE-5' })],
existing: [createIssue({ cve: 'CVE-6' })],
},
};
state.isLoading = true;
mutations[types.RECEIVE_DIFF_SUCCESS](state, reports);
});
it('should set the `isLoading` status to `false`', () => {
expect(state.isLoading).toBe(false);
});
it('should have the relevant `new` issues', () => {
expect(state.newIssues.length).toBe(3);
});
it('should have the relevant `resolved` issues', () => {
expect(state.resolvedIssues.length).toBe(2);
});
it('should have the relevant `all` issues', () => {
expect(state.allIssues.length).toBe(1);
});
});
describe(types.RECEIVE_DIFF_ERROR, () => {
beforeEach(() => {
state.isLoading = true;
mutations[types.RECEIVE_DIFF_ERROR](state);
});
it('should set the `isLoading` status to `false`', () => {
expect(state.isLoading).toBe(false);
});
it('should set the `hasError` status to `true`', () => {
expect(state.hasError).toBe(true);
});
});
}); });
...@@ -314,7 +314,7 @@ describe('Grouped security reports app', () => { ...@@ -314,7 +314,7 @@ describe('Grouped security reports app', () => {
expect(vm.sastContainer.paths.diffEndpoint).toEqual(sastContainerEndpoint); expect(vm.sastContainer.paths.diffEndpoint).toEqual(sastContainerEndpoint);
}); });
it('should call `fetchSastContainerDiff`', () => { it('should display the correct numbers of vulnerabilities', () => {
expect(vm.$el.textContent).toContain( expect(vm.$el.textContent).toContain(
'Container scanning detected 2 new, and 1 fixed vulnerabilities', 'Container scanning detected 2 new, and 1 fixed vulnerabilities',
); );
...@@ -362,7 +362,7 @@ describe('Grouped security reports app', () => { ...@@ -362,7 +362,7 @@ describe('Grouped security reports app', () => {
expect(vm.dependencyScanning.paths.diffEndpoint).toEqual(dependencyScanningEndpoint); expect(vm.dependencyScanning.paths.diffEndpoint).toEqual(dependencyScanningEndpoint);
}); });
it('should call `fetchDependencyScanningDiff`', () => { it('should display the correct numbers of vulnerabilities', () => {
expect(vm.$el.textContent).toContain( expect(vm.$el.textContent).toContain(
'Dependency scanning detected 2 new, and 1 fixed vulnerabilities', 'Dependency scanning detected 2 new, and 1 fixed vulnerabilities',
); );
...@@ -410,9 +410,56 @@ describe('Grouped security reports app', () => { ...@@ -410,9 +410,56 @@ describe('Grouped security reports app', () => {
expect(vm.dast.paths.diffEndpoint).toEqual(dastEndpoint); expect(vm.dast.paths.diffEndpoint).toEqual(dastEndpoint);
}); });
it('should call `fetchDastDiff`', () => { it('should display the correct numbers of vulnerabilities', () => {
expect(vm.$el.textContent).toContain('DAST detected 1 new, and 2 fixed vulnerabilities'); expect(vm.$el.textContent).toContain('DAST detected 1 new, and 2 fixed vulnerabilities');
}); });
}); });
describe('sast reports', () => {
const sastEndpoint = 'sast.json';
beforeEach(done => {
gon.features = gon.features || {};
gon.features.sastMergeRequestReportApi = true;
gl.mrWidgetData = gl.mrWidgetData || {};
gl.mrWidgetData.sast_comparison_path = sastEndpoint;
mock.onGet(sastEndpoint).reply(200, {
added: [dockerReport.vulnerabilities[0]],
fixed: [dockerReport.vulnerabilities[1], dockerReport.vulnerabilities[2]],
existing: [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, `sast/${sastTypes.RECEIVE_DIFF_SUCCESS}`)
.then(done)
.catch(done.fail);
});
it('should set setSastDiffEndpoint', () => {
expect(vm.sast.paths.diffEndpoint).toEqual(sastEndpoint);
});
it('should display the correct numbers of vulnerabilities', () => {
expect(vm.$el.textContent).toContain('SAST detected 1 new, and 2 fixed vulnerabilities');
});
});
}); });
}); });
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