Commit 24652977 authored by Sam Beckham's avatar Sam Beckham Committed by Fatih Acet

Moves container scanning logic to the backend

- Uses the baceng API to parse container scanning logic on the MR page.
- Maps the slightly misaligned data in the vulnerabilities
- Uses polling because the end point is a polling one
- Adds an action for storing the endpoint in vuex
- Adds a new action for calling the endpoint
- Puts all the above behind a feature flag
- Adds action tests
- Adds mutation tests
- Adds component tests
- Fixes issues the tests brought up
- - Text generation failed without a head or base path
- - Report visibilty failed without a head or base path
parent 1c9a1820
......@@ -168,6 +168,11 @@ export default {
securityTab() {
return `${this.pipelinePath}/security`;
},
shouldRenderSastContainer() {
const { head, diffEndpoint } = this.sastContainer.paths;
return head || diffEndpoint;
},
},
created() {
......@@ -196,7 +201,17 @@ export default {
this.fetchSastReports();
}
if (this.sastContainerHeadPath) {
const sastContainerDiffEndpoint =
gl && gl.mrWidgetData && gl.mrWidgetData.container_scanning_comparison_path;
if (
gon.features &&
gon.features.containerScanningMergeRequestReportApi &&
sastContainerDiffEndpoint
) {
this.setSastContainerDiffEndpoint(sastContainerDiffEndpoint);
this.fetchSastContainerDiff();
} else if (this.sastContainerHeadPath) {
this.setSastContainerHeadPath(this.sastContainerHeadPath);
if (this.sastContainerBasePath) {
......@@ -257,6 +272,8 @@ export default {
'deleteDismissalComment',
'showDismissalDeleteButtons',
'hideDismissalDeleteButtons',
'fetchSastContainerDiff',
'setSastContainerDiffEndpoint',
]),
...mapActions('sast', {
setSastHeadPath: 'setHeadPath',
......@@ -322,7 +339,7 @@ export default {
/>
</template>
<template v-if="sastContainerHeadPath">
<template v-if="shouldRenderSastContainer">
<summary-row
:summary="groupedSastContainerText"
:status-icon="sastContainerStatusIcon"
......
......@@ -4,6 +4,8 @@ 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';
/**
* A lot of this file has duplicate actions to
......@@ -54,6 +56,9 @@ export const setSastContainerHeadPath = ({ commit }, path) =>
export const setSastContainerBasePath = ({ commit }, path) =>
commit(types.SET_SAST_CONTAINER_BASE_PATH, path);
export const setSastContainerDiffEndpoint = ({ commit }, path) =>
commit(types.SET_SAST_CONTAINER_DIFF_ENDPOINT, path);
export const requestSastContainerReports = ({ commit }) =>
commit(types.REQUEST_SAST_CONTAINER_REPORTS);
......@@ -63,6 +68,52 @@ export const receiveSastContainerReports = ({ commit }, response) =>
export const receiveSastContainerError = ({ commit }, error) =>
commit(types.RECEIVE_SAST_CONTAINER_ERROR, error);
export const receiveSastContainerDiffSuccess = ({ commit }, response) =>
commit(types.RECEIVE_SAST_CONTAINER_DIFF_SUCCESS, response);
export const fetchSastContainerDiff = ({ state, dispatch }) => {
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([
pollPromise,
axios.get(state.vulnerabilityFeedbackPath, {
params: {
category: 'container_scanning',
},
}),
])
.then(values => {
dispatch('receiveSastContainerDiffSuccess', {
diff: values[0].data,
enrichData: values[1].data,
});
})
.catch(() => {
dispatch('receiveSastContainerError');
});
};
export const fetchSastContainerReports = ({ state, dispatch }) => {
const { base, head } = state.sastContainer.paths;
......
......@@ -16,9 +16,11 @@ export const SET_CAN_CREATE_FEEDBACK_PERMISSION = 'SET_CAN_CREATE_FEEDBACK_PERMI
// SAST CONTAINER
export const SET_SAST_CONTAINER_HEAD_PATH = 'SET_SAST_CONTAINER_HEAD_PATH';
export const SET_SAST_CONTAINER_BASE_PATH = 'SET_SAST_CONTAINER_BASE_PATH';
export const SET_SAST_CONTAINER_DIFF_ENDPOINT = 'SET_SAST_CONTAINER_DIFF_ENDPOINT';
export const REQUEST_SAST_CONTAINER_REPORTS = 'REQUEST_SAST_CONTAINER_REPORTS';
export const RECEIVE_SAST_CONTAINER_REPORTS = 'RECEIVE_SAST_CONTAINER_REPORTS';
export const RECEIVE_SAST_CONTAINER_ERROR = 'RECEIVE_SAST_CONTAINER_ERROR';
export const RECEIVE_SAST_CONTAINER_DIFF_SUCCESS = 'RECEIVE_SAST_CONTAINER_DIFF_SUCCESS';
// DAST
export const SET_DAST_HEAD_PATH = 'SET_DAST_HEAD_PATH';
......
......@@ -5,6 +5,7 @@ import {
parseDastIssues,
getUnapprovedVulnerabilities,
findIssueIndex,
enrichVulnerabilityWithFeedback,
} from './utils';
import filterByKey from './utils/filter_by_key';
import { parseSastContainer } from './utils/container_scanning';
......@@ -64,6 +65,10 @@ export default {
Vue.set(state.sastContainer.paths, 'base', path);
},
[types.SET_SAST_CONTAINER_DIFF_ENDPOINT](state, path) {
Vue.set(state.sastContainer.paths, 'diffEndpoint', path);
},
[types.REQUEST_SAST_CONTAINER_REPORTS](state) {
Vue.set(state.sastContainer, 'isLoading', true);
},
......@@ -100,6 +105,21 @@ export default {
}
},
[types.RECEIVE_SAST_CONTAINER_DIFF_SUCCESS](state, { diff, enrichData }) {
const fillInTheGaps = vulnerability => ({
...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, 'newIssues', added);
Vue.set(state.sastContainer, 'resolvedIssues', fixed);
},
[types.RECEIVE_SAST_CONTAINER_ERROR](state) {
Vue.set(state.sastContainer, 'isLoading', false);
Vue.set(state.sastContainer, 'hasError', true);
......
......@@ -20,6 +20,7 @@ export default () => ({
paths: {
head: null,
base: null,
diffEndpoint: null,
},
isLoading: false,
......
......@@ -43,7 +43,7 @@ export const findMatchingRemediations = (remediations, vulnerability) => {
* @param {Object} vulnerability
* @param {Array} feedback
*/
export const enrichVulnerabilityWithfeedback = (vulnerability, feedback = []) =>
export const enrichVulnerabilityWithFeedback = (vulnerability, feedback = []) =>
feedback
.filter(fb => fb.project_fingerprint === vulnerability.project_fingerprint)
.reduce((vuln, fb) => {
......@@ -160,7 +160,7 @@ export const parseSastIssues = (report = [], feedback = [], path = '') =>
...parsed,
path: parsed.location.file,
urlPath: fileUrl(parsed.location, path),
...enrichVulnerabilityWithfeedback(parsed, feedback),
...enrichVulnerabilityWithFeedback(parsed, feedback),
};
});
......@@ -192,7 +192,7 @@ export const parseDependencyScanningIssues = (report = [], feedback = [], path =
...parsed,
path: parsed.location.file,
urlPath: fileUrl(parsed.location, path),
...enrichVulnerabilityWithfeedback(parsed, feedback),
...enrichVulnerabilityWithFeedback(parsed, feedback),
};
});
};
......@@ -253,7 +253,7 @@ export const parseDastIssues = (sites = [], feedback = []) =>
return {
...parsed,
...enrichVulnerabilityWithfeedback(parsed, feedback),
...enrichVulnerabilityWithFeedback(parsed, feedback),
};
}),
],
......@@ -274,7 +274,7 @@ export const groupedTextBuilder = ({
}) => {
let baseString = '';
if (!paths.base) {
if (!paths.base && !paths.diffEndpoint) {
if (added && !dismissed) {
// added
baseString = n__(
......@@ -300,7 +300,7 @@ export const groupedTextBuilder = ({
'ciReport|%{reportType} %{status} detected no vulnerabilities for the source branch only',
);
}
} else if (paths.head) {
} else if (paths.head || paths.diffEndpoint) {
if (added && !fixed && !dismissed) {
// added
baseString = n__(
......
......@@ -2,7 +2,7 @@ import { SEVERITY_LEVELS } from 'ee/security_dashboard/store/constants';
import { s__, sprintf } from '~/locale';
import sha1 from 'sha1';
import _ from 'underscore';
import { enrichVulnerabilityWithfeedback } from '../utils';
import { enrichVulnerabilityWithFeedback } from '../utils';
/*
Container scanning mapping utils
......@@ -166,6 +166,6 @@ export const parseSastContainer = (issues = [], feedback = [], image) =>
return {
...parsed,
...frontendOnly,
...enrichVulnerabilityWithfeedback(frontendOnly, feedback),
...enrichVulnerabilityWithFeedback(frontendOnly, feedback),
};
});
......@@ -784,4 +784,60 @@ describe('security reports mutations', () => {
expect(stateCopy.dast.resolvedIssues[0]).toEqual(updatedIssue);
});
});
describe('SET_SAST_CONTAINER_DIFF_ENDPOINT', () => {
const endpoint = 'sast_container_diff_endpoint.json';
beforeEach(() => {
mutations[types.SET_SAST_CONTAINER_DIFF_ENDPOINT](stateCopy, endpoint);
});
it('should set the correct endpoint', () => {
expect(stateCopy.sastContainer.paths.diffEndpoint).toEqual(endpoint);
});
});
describe('RECEIVE_SAST_CONTAINER_DIFF_SUCCESS', () => {
const reports = {
diff: {
added: [
{ name: 'added vuln 1', report_type: 'container_scanning' },
{ name: 'added vuln 2', report_type: 'container_scanning' },
],
fixed: [{ name: 'fixed vuln 1', report_type: 'container_scanning' }],
},
};
beforeEach(() => {
mutations[types.RECEIVE_SAST_CONTAINER_DIFF_SUCCESS](stateCopy, reports);
});
it('should set isLoading to false', () => {
expect(stateCopy.sastContainer.isLoading).toBe(false);
});
it('should parse and set the added vulnerabilities', () => {
reports.diff.added.forEach((vuln, i) => {
expect(stateCopy.sastContainer.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.sastContainer.resolvedIssues[i]).toEqual(
expect.objectContaining({
name: vuln.name,
title: vuln.name,
category: vuln.report_type,
}),
);
});
});
});
});
......@@ -271,4 +271,52 @@ describe('Grouped security reports app', () => {
expect(vm.securityTab).toEqual(`${pipelinePath}/security`);
});
});
describe('with the reports API enabled', () => {
const sastContainerEndpoint = 'sast_container.json';
beforeEach(done => {
gon.features = gon.features || {};
gon.features.containerScanningMergeRequestReportApi = true;
gl.mrWidgetData = gl.mrWidgetData || {};
gl.mrWidgetData.container_scanning_comparison_path = sastContainerEndpoint;
mock.onGet(sastContainerEndpoint).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_SAST_CONTAINER_DIFF_SUCCESS)
.then(done)
.catch();
});
it('should set setSastContainerDiffEndpoint', () => {
expect(vm.sastContainer.paths.diffEndpoint).toEqual(sastContainerEndpoint);
});
it('should call `fetchSastContainerDiff`', () => {
expect(vm.$el.textContent).toContain(
'Container scanning detected 2 new, and 1 fixed vulnerabilities',
);
});
});
});
......@@ -55,6 +55,9 @@ import actions, {
requestDeleteDismissalComment,
showDismissalDeleteButtons,
hideDismissalDeleteButtons,
setSastContainerDiffEndpoint,
receiveSastContainerDiffSuccess,
fetchSastContainerDiff,
} from 'ee/vue_shared/security_reports/store/actions';
import * as types from 'ee/vue_shared/security_reports/store/mutation_types';
import state from 'ee/vue_shared/security_reports/store/state';
......@@ -1585,4 +1588,144 @@ describe('security reports actions', () => {
);
});
});
describe('setSastContainerDiffEndpoint', () => {
it('should pass down the endpoint to the mutation', done => {
const payload = '/sast_container_endpoint.json';
testAction(
setSastContainerDiffEndpoint,
payload,
mockedState,
[
{
type: types.SET_SAST_CONTAINER_DIFF_ENDPOINT,
payload,
},
],
[],
done,
);
});
});
describe('receiveSastContainerDiffSuccess', () => {
it('should pass down the response to the mutation', done => {
const payload = { data: 'Effort yields its own rewards.' };
testAction(
receiveSastContainerDiffSuccess,
payload,
mockedState,
[
{
type: types.RECEIVE_SAST_CONTAINER_DIFF_SUCCESS,
payload,
},
],
[],
done,
);
});
});
describe('fetchSastContainerDiff', () => {
const diff = { vulnerabilities: [] };
beforeEach(() => {
mockedState.vulnerabilityFeedbackPath = 'vulnerabilities_feedback';
mockedState.sastContainer.paths.diffEndpoint = 'sast_container_diff.json';
});
describe('on success', () => {
it('should dispatch `receiveSastContainerDiffSuccess`', done => {
mock.onGet('sast_container_diff.json').reply(200, diff);
mock
.onGet('vulnerabilities_feedback', {
params: {
category: 'container_scanning',
},
})
.reply(200, containerScanningFeedbacks);
testAction(
fetchSastContainerDiff,
null,
mockedState,
[],
[
{
type: 'requestSastContainerReports',
},
{
type: 'receiveSastContainerDiffSuccess',
payload: {
diff,
enrichData: containerScanningFeedbacks,
},
},
],
done,
);
});
});
describe('when vulnerabilities path errors', () => {
it('should dispatch `receiveSastContainerError`', done => {
mock.onGet('sast_container_diff.json').reply(500);
mock
.onGet('vulnerabilities_feedback', {
params: {
category: 'container_scanning',
},
})
.reply(200, containerScanningFeedbacks);
testAction(
fetchSastContainerDiff,
null,
mockedState,
[],
[
{
type: 'requestSastContainerReports',
},
{
type: 'receiveSastContainerError',
},
],
done,
);
});
});
describe('when feedback path errors', () => {
it('should dispatch `receiveSastContainerError`', done => {
mock.onGet('sast_container_diff.json').reply(200, diff);
mock
.onGet('vulnerabilities_feedback', {
params: {
category: 'container_scanning',
},
})
.reply(500);
testAction(
fetchSastContainerDiff,
null,
mockedState,
[],
[
{
type: 'requestSastContainerReports',
},
{
type: 'receiveSastContainerError',
},
],
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