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 {
shouldRenderDast() {
const { head, diffEndpoint } = this.dast.paths;
return head || diffEndpoint;
},
shouldRenderSast() {
const { head, diffEndpoint } = this.sast.paths;
return head || diffEndpoint;
},
},
......@@ -202,7 +207,12 @@ export default {
this.setCanCreateIssuePermission(this.canCreateIssue);
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);
if (this.sastBasePath) {
......@@ -307,7 +317,9 @@ export default {
...mapActions('sast', {
setSastHeadPath: 'setHeadPath',
setSastBasePath: 'setBasePath',
setSastDiffEndpoint: 'setDiffEndpoint',
fetchSastReports: 'fetchReports',
fetchSastDiff: 'fetchDiff',
}),
},
};
......@@ -333,7 +345,7 @@ export default {
</div>
<div slot="body" class="mr-widget-grouped-section report-block">
<template v-if="sastHeadPath">
<template v-if="shouldRenderSast">
<summary-row
:summary="groupedSastText"
:status-icon="sastStatusIcon"
......
......@@ -4,8 +4,7 @@ import { s__ } from '~/locale';
import { visitUrl } from '~/lib/utils/url_utility';
import * as types from './mutation_types';
import downloadPatchHelper from './utils/download_patch_helper';
import Poll from '~/lib/utils/poll';
import httpStatusCodes from '~/lib/utils/http_status';
import { pollUntilComplete } from './utils';
/**
* A lot of this file has duplicate actions to
......@@ -18,28 +17,6 @@ import httpStatusCodes from '~/lib/utils/http_status';
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 setBaseBlobPath = ({ commit }, blobPath) => commit(types.SET_BASE_BLOB_PATH, blobPath);
......
import axios from '~/lib/utils/axios_utils';
import * as types from './mutation_types';
import { pollUntilComplete } from '../../utils';
export const setHeadPath = ({ commit }, path) => commit(types.SET_HEAD_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 receiveReports = ({ commit }, response) => commit(types.RECEIVE_REPORTS, response);
......@@ -44,4 +47,32 @@ export const fetchReports = ({ state, rootState, dispatch }) => {
export const updateVulnerability = ({ commit }, 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 const SET_HEAD_PATH = 'SET_HEAD_PATH';
export const SET_BASE_PATH = 'SET_BASE_PATH';
export const REQUEST_REPORTS = 'REQUEST_REPORTS';
export const RECEIVE_DIFF_SUCCESS = 'RECEIVE_DIFF_SUCCESS';
export const RECEIVE_DIFF_ERROR = 'RECEIVE_DIFF_ERROR';
export const RECEIVE_REPORTS = 'RECEIVE_REPORTS';
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';
import Vue from 'vue';
import * as types from './mutation_types';
import { parseSastIssues, findIssueIndex } from '../../utils';
import { parseSastIssues, findIssueIndex, parseDiff } from '../../utils';
import filterByKey from '../../utils/filter_by_key';
export default {
......@@ -12,6 +12,10 @@ export default {
Vue.set(state.paths, 'base', path);
},
[types.SET_DIFF_ENDPOINT](state, path) {
Vue.set(state.paths, 'diffEndpoint', path);
},
[types.REQUEST_REPORTS](state) {
state.isLoading = true;
},
......@@ -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) {
state.isLoading = false;
state.hasError = true;
......
......@@ -2,6 +2,7 @@ export default () => ({
paths: {
head: null,
base: null,
diffEndpoint: null,
},
isLoading: false,
......
import sha1 from 'sha1';
import _ from 'underscore';
import axios from 'axios';
import { stripHtml } from '~/lib/utils/text_utility';
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
......@@ -441,3 +444,32 @@ export const parseDiff = (diff, enrichData) => {
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
const headPath = 'head-path.json';
const basePath = 'base-path.json';
const diffEndpoint = 'diff-endpoint.json';
const blobPath = 'blob-path.json';
const reports = {
base: 'base',
head: 'head',
enrichData: 'enrichData',
diff: 'diff',
};
const error = 'Something went wrong';
const issue = {};
......@@ -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', () => {
it(`should commit ${types.REQUEST_REPORTS}`, done => {
testAction(actions.requestReports, {}, state, [{ type: types.REQUEST_REPORTS }], [], done);
......@@ -129,6 +149,8 @@ describe('sast report actions', () => {
});
it('should dispatch the `receiveReports` action', done => {
const { head, base, enrichData } = reports;
testAction(
actions.fetchReports,
{},
......@@ -140,7 +162,7 @@ describe('sast report actions', () => {
type: 'receiveReports',
payload: {
blobPath,
reports,
reports: { head, base, enrichData },
},
},
],
......@@ -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', () => {
it(`should commit ${types.UPDATE_VULNERABILITY} with the correct response`, done => {
testAction(
......
......@@ -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, () => {
it('should set the `isLoading` status to `true`', () => {
mutations[types.REQUEST_REPORTS](state);
......@@ -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', () => {
expect(vm.sastContainer.paths.diffEndpoint).toEqual(sastContainerEndpoint);
});
it('should call `fetchSastContainerDiff`', () => {
it('should display the correct numbers of vulnerabilities', () => {
expect(vm.$el.textContent).toContain(
'Container scanning detected 2 new, and 1 fixed vulnerabilities',
);
......@@ -362,7 +362,7 @@ describe('Grouped security reports app', () => {
expect(vm.dependencyScanning.paths.diffEndpoint).toEqual(dependencyScanningEndpoint);
});
it('should call `fetchDependencyScanningDiff`', () => {
it('should display the correct numbers of vulnerabilities', () => {
expect(vm.$el.textContent).toContain(
'Dependency scanning detected 2 new, and 1 fixed vulnerabilities',
);
......@@ -410,9 +410,56 @@ describe('Grouped security reports app', () => {
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');
});
});
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