Commit e681c186 authored by Dheeraj Joshi's avatar Dheeraj Joshi Committed by Mark Florian

Add secret scanning to security report

This adds vulnerability findings from secret scanning
report in Merge Request Widget
parent 2ab7e8ac
......@@ -5,6 +5,7 @@ import LicenseIssueBody from 'ee/vue_shared/license_compliance/components/licens
import SastIssueBody from 'ee/vue_shared/security_reports/components/sast_issue_body.vue';
import ContainerScanningIssueBody from 'ee/vue_shared/security_reports/components/container_scanning_issue_body.vue';
import DastIssueBody from 'ee/vue_shared/security_reports/components/dast_issue_body.vue';
import SecretScanningIssueBody from 'ee/vue_shared/security_reports/components/secret_scanning_issue_body.vue';
import MetricsReportsIssueBody from 'ee/vue_shared/metrics_reports/components/metrics_reports_issue_body.vue';
import {
components as componentsCE,
......@@ -19,6 +20,7 @@ export const components = {
ContainerScanningIssueBody,
SastIssueBody,
DastIssueBody,
SecretScanningIssueBody,
MetricsReportsIssueBody,
BlockingMergeRequestsBody,
};
......@@ -31,6 +33,7 @@ export const componentNames = {
ContainerScanningIssueBody: ContainerScanningIssueBody.name,
SastIssueBody: SastIssueBody.name,
DastIssueBody: DastIssueBody.name,
SecretScanningIssueBody: SecretScanningIssueBody.name,
MetricsReportsIssueBody: MetricsReportsIssueBody.name,
BlockingMergeRequestsBody: BlockingMergeRequestsBody.name,
};
......@@ -314,6 +314,7 @@ export default {
:dast-help-path="mr.dastHelp"
:container-scanning-help-path="mr.containerScanningHelp"
:dependency-scanning-help-path="mr.dependencyScanningHelp"
:secret-scanning-help-path="mr.secretScanningHelp"
:vulnerability-feedback-path="mr.vulnerabilityFeedbackPath"
:vulnerability-feedback-help-path="mr.vulnerabilityFeedbackHelpPath"
:create-vulnerability-feedback-issue-path="mr.createVulnerabilityFeedbackIssuePath"
......
......@@ -13,6 +13,7 @@ export default class MergeRequestStore extends CEMergeRequestStore {
this.sastHelp = data.sast_help_path;
this.containerScanningHelp = data.container_scanning_help_path;
this.dastHelp = data.dast_help_path;
this.secretScanningHelp = data.secret_scanning_help_path;
this.dependencyScanningHelp = data.dependency_scanning_help_path;
this.vulnerabilityFeedbackPath = data.vulnerability_feedback_path;
this.vulnerabilityFeedbackHelpPath = data.vulnerability_feedback_help_path;
......
<script>
/**
* Renders SECRET SCANNING body text
* [severity-badge] [name] in [link]:[line]
*/
import ModalOpenName from '~/reports/components/modal_open_name.vue';
import SeverityBadge from './severity_badge.vue';
export default {
name: 'SecretScanningIssueBody',
components: {
ModalOpenName,
SeverityBadge,
},
props: {
issue: {
type: Object,
required: true,
},
// failed || success
status: {
type: String,
required: true,
},
},
};
</script>
<template>
<div class="report-block-list-issue-description prepend-top-5 append-bottom-5">
<div class="report-block-list-issue-description-text">
<severity-badge v-if="issue.severity" class="d-inline-block" :severity="issue.severity" />
<modal-open-name :issue="issue" :status="status" />
</div>
</div>
</template>
......@@ -69,6 +69,11 @@ export default {
required: false,
default: '',
},
secretScanningHelpPath: {
type: String,
required: false,
default: '',
},
vulnerabilityFeedbackPath: {
type: String,
required: false,
......@@ -132,6 +137,7 @@ export default {
'containerScanning',
'dast',
'dependencyScanning',
'secretScanning',
'summaryCounts',
'modal',
'isCreatingIssue',
......@@ -144,9 +150,11 @@ export default {
'groupedContainerScanningText',
'groupedDastText',
'groupedDependencyText',
'groupedSecretScanningText',
'containerScanningStatusIcon',
'dastStatusIcon',
'dependencyScanningStatusIcon',
'secretScanningStatusIcon',
'isBaseSecurityReportOutOfDate',
'canCreateIssue',
'canCreateMergeRequest',
......@@ -168,6 +176,9 @@ export default {
hasSastReports() {
return this.enabledReports.sast;
},
hasSecretScanningReports() {
return this.enabledReports.secretScanning;
},
isMRActive() {
return this.mrState !== mrStates.merged && this.mrState !== mrStates.closed;
},
......@@ -220,6 +231,12 @@ export default {
this.setDependencyScanningDiffEndpoint(dependencyScanningDiffEndpoint);
this.fetchDependencyScanningDiff();
}
const secretScanningDiffEndpoint = gl?.mrWidgetData?.secret_scanning_comparison_path;
if (secretScanningDiffEndpoint && this.hasSecretScanningReports) {
this.setSecretScanningDiffEndpoint(secretScanningDiffEndpoint);
this.fetchSecretScanningDiff();
}
},
methods: {
...mapActions([
......@@ -250,6 +267,8 @@ export default {
'setDependencyScanningDiffEndpoint',
'fetchDastDiff',
'setDastDiffEndpoint',
'fetchSecretScanningDiff',
'setSecretScanningDiffEndpoint',
]),
...mapActions('sast', {
setSastDiffEndpoint: 'setDiffEndpoint',
......@@ -404,6 +423,24 @@ export default {
/>
</template>
<template v-if="hasSecretScanningReports">
<summary-row
:summary="groupedSecretScanningText"
:status-icon="secretScanningStatusIcon"
:popover-options="secretScanningPopover"
class="js-secret-scanning"
data-qa-selector="secret_scan_report"
/>
<issues-list
v-if="secretScanning.newIssues.length || secretScanning.resolvedIssues.length"
:unresolved-issues="secretScanning.newIssues"
:resolved-issues="secretScanning.resolvedIssues"
:component="$options.componentNames.SecretScanningIssueBody"
class="report-block-group-list"
/>
</template>
<issue-modal
:modal="modal"
:vulnerability-feedback-help-path="vulnerabilityFeedbackHelpPath"
......
......@@ -70,5 +70,20 @@ export default {
),
};
},
secretScanningPopover() {
return {
title: s__(
'ciReport|Secret scanning detects secrets and credentials vulnerabilities in your source code.',
),
content: sprintf(
s__('ciReport|%{linkStartTag}Learn more about Secret Scanning %{linkEndTag}'),
{
linkStartTag: getLinkStartTag(this.secretScanningHelpPath),
linkEndTag,
},
false,
),
};
},
},
};
......@@ -159,6 +159,46 @@ export const fetchDependencyScanningDiff = ({ state, dispatch }) => {
export const updateDependencyScanningIssue = ({ commit }, issue) =>
commit(types.UPDATE_DEPENDENCY_SCANNING_ISSUE, issue);
/**
* SECRET SCANNING
*/
export const setSecretScanningDiffEndpoint = ({ commit }, path) =>
commit(types.SET_SECRET_SCANNING_DIFF_ENDPOINT, path);
export const requestSecretScanningDiff = ({ commit }) => commit(types.REQUEST_SECRET_SCANNING_DIFF);
export const receiveSecretScanningDiffSuccess = ({ commit }, response) =>
commit(types.RECEIVE_SECRET_SCANNING_DIFF_SUCCESS, response);
export const receiveSecretScanningDiffError = ({ commit }) =>
commit(types.RECEIVE_SECRET_SCANNING_DIFF_ERROR);
export const fetchSecretScanningDiff = ({ state, dispatch }) => {
dispatch('requestSecretScanningDiff');
return Promise.all([
pollUntilComplete(state.secretScanning.paths.diffEndpoint),
axios.get(state.vulnerabilityFeedbackPath, {
params: {
category: 'secret_scanning',
},
}),
])
.then(values => {
dispatch('receiveSecretScanningDiffSuccess', {
diff: values[0].data,
enrichData: values[1].data,
});
})
.catch(() => {
dispatch('receiveSecretScanningDiffError');
});
};
export const updateSecretScanningIssue = ({ commit }, issue) =>
commit(types.UPDATE_SECRET_SCANNING_ISSUE, issue);
export const openModal = ({ dispatch }, payload) => {
dispatch('setModalData', payload);
......
......@@ -11,6 +11,14 @@ export const groupedContainerScanningText = ({ containerScanning }) =>
messages.CONTAINER_SCANNING_IS_LOADING,
);
export const groupedSecretScanningText = ({ secretScanning }) =>
groupedReportText(
secretScanning,
messages.SECRET_SCANNING,
messages.SECRET_SCANNING_HAS_ERROR,
messages.SECRET_SCANNING_IS_LOADING,
);
export const groupedDastText = ({ dast }) =>
groupedReportText(dast, messages.DAST, messages.DAST_HAS_ERROR, messages.DAST_IS_LOADING);
......@@ -23,7 +31,13 @@ export const groupedDependencyText = ({ dependencyScanning }) =>
);
export const summaryCounts = state =>
[state.sast, state.containerScanning, state.dast, state.dependencyScanning].reduce(
[
state.sast,
state.containerScanning,
state.dast,
state.dependencyScanning,
state.secretScanning,
].reduce(
(acc, report) => {
const curr = countIssues(report);
acc.added += curr.added;
......@@ -98,47 +112,57 @@ export const dependencyScanningStatusIcon = ({ dependencyScanning }) =>
dependencyScanning.newIssues.length,
);
export const secretScanningStatusIcon = ({ secretScanning }) =>
statusIcon(secretScanning.isLoading, secretScanning.hasError, secretScanning.newIssues.length);
export const areReportsLoading = state =>
state.sast.isLoading ||
state.dast.isLoading ||
state.containerScanning.isLoading ||
state.dependencyScanning.isLoading;
state.dependencyScanning.isLoading ||
state.secretScanning.isLoading;
export const areAllReportsLoading = state =>
state.sast.isLoading &&
state.dast.isLoading &&
state.containerScanning.isLoading &&
state.dependencyScanning.isLoading;
state.dependencyScanning.isLoading &&
state.secretScanning.isLoading;
export const allReportsHaveError = state =>
state.sast.hasError &&
state.dast.hasError &&
state.containerScanning.hasError &&
state.dependencyScanning.hasError;
state.dependencyScanning.hasError &&
state.secretScanning.hasError;
export const anyReportHasError = state =>
state.sast.hasError ||
state.dast.hasError ||
state.containerScanning.hasError ||
state.dependencyScanning.hasError;
state.dependencyScanning.hasError ||
state.secretScanning.hasError;
export const noBaseInAllReports = state =>
!state.sast.hasBaseReport &&
!state.dast.hasBaseReport &&
!state.containerScanning.hasBaseReport &&
!state.dependencyScanning.hasBaseReport;
!state.dependencyScanning.hasBaseReport &&
!state.secretScanning.hasBaseReport;
export const anyReportHasIssues = state =>
state.sast.newIssues.length > 0 ||
state.dast.newIssues.length > 0 ||
state.containerScanning.newIssues.length > 0 ||
state.dependencyScanning.newIssues.length > 0;
state.dependencyScanning.newIssues.length > 0 ||
state.secretScanning.newIssues.length > 0;
export const isBaseSecurityReportOutOfDate = state =>
state.sast.baseReportOutofDate ||
state.dast.baseReportOutofDate ||
state.containerScanning.baseReportOutofDate ||
state.dependencyScanning.baseReportOutofDate;
state.dependencyScanning.baseReportOutofDate ||
state.secretScanning.baseReportOutofDate;
export const canCreateIssue = state => Boolean(state.createVulnerabilityFeedbackIssuePath);
......
......@@ -5,6 +5,7 @@ const updateIssueActionsMap = {
dependency_scanning: 'updateDependencyScanningIssue',
container_scanning: 'updateContainerScanningIssue',
dast: 'updateDastIssue',
secret_scanning: 'updateSecretScanningIssue',
};
export default function configureMediator(store) {
......
......@@ -7,12 +7,14 @@ const SAST = s__('ciReport|SAST');
const DAST = s__('ciReport|DAST');
const CONTAINER_SCANNING = s__('ciReport|Container scanning');
const DEPENDENCY_SCANNING = s__('ciReport|Dependency scanning');
const SECRET_SCANNING = s__('ciReport|Secret scanning');
export default {
SAST,
DAST,
CONTAINER_SCANNING,
DEPENDENCY_SCANNING,
SECRET_SCANNING,
TRANSLATION_IS_LOADING,
TRANSLATION_HAS_ERROR,
SAST_IS_LOADING: sprintf(TRANSLATION_IS_LOADING, { reportType: SAST }),
......@@ -29,4 +31,8 @@ export default {
DEPENDENCY_SCANNING_HAS_ERROR: sprintf(TRANSLATION_HAS_ERROR, {
reportType: DEPENDENCY_SCANNING,
}),
SECRET_SCANNING_IS_LOADING: sprintf(TRANSLATION_IS_LOADING, {
reportType: SECRET_SCANNING,
}),
SECRET_SCANNING_HAS_ERROR: sprintf(TRANSLATION_HAS_ERROR, { reportType: SECRET_SCANNING }),
};
......@@ -29,6 +29,12 @@ export const REQUEST_DEPENDENCY_SCANNING_DIFF = 'REQUEST_DEPENDENCY_SCANNING_DIF
export const RECEIVE_DEPENDENCY_SCANNING_DIFF_SUCCESS = 'RECEIVE_DEPENDENCY_SCANNING_DIFF_SUCCESS';
export const RECEIVE_DEPENDENCY_SCANNING_DIFF_ERROR = 'RECEIVE_DEPENDENCY_SCANNING_DIFF_ERROR';
// SECRET SCANNING
export const SET_SECRET_SCANNING_DIFF_ENDPOINT = 'SET_SECRET_SCANNING_DIFF_ENDPOINT';
export const REQUEST_SECRET_SCANNING_DIFF = 'REQUEST_SECRET_SCANNING_DIFF';
export const RECEIVE_SECRET_SCANNING_DIFF_SUCCESS = 'RECEIVE_SECRET_SCANNING_DIFF_SUCCESS';
export const RECEIVE_SECRET_SCANNING_DIFF_ERROR = 'RECEIVE_SECRET_SCANNING_DIFF_ERROR';
// Dismiss security issue
export const SET_ISSUE_MODAL_DATA = 'SET_ISSUE_MODAL_DATA';
export const REQUEST_DISMISS_VULNERABILITY = 'REQUEST_DISMISS_VULNERABILITY';
......@@ -56,6 +62,7 @@ export const HIDE_DISMISSAL_DELETE_BUTTONS = 'HIDE_DISMISSAL_DELETE_BUTTONS';
export const UPDATE_DEPENDENCY_SCANNING_ISSUE = 'UPDATE_DEPENDENCY_SCANNING_ISSUE';
export const UPDATE_CONTAINER_SCANNING_ISSUE = 'UPDATE_CONTAINER_SCANNING_ISSUE';
export const UPDATE_DAST_ISSUE = 'UPDATE_DAST_ISSUE';
export const UPDATE_SECRET_SCANNING_ISSUE = 'UPDATE_SECRET_SCANNING_ISSUE';
export const OPEN_DISMISSAL_COMMENT_BOX = 'OPEN_DISMISSAL_COMMENT_BOX ';
export const CLOSE_DISMISSAL_COMMENT_BOX = 'CLOSE_DISMISSAL_COMMENT_BOX';
......@@ -125,6 +125,33 @@ export default {
Vue.set(state.dependencyScanning, 'hasError', true);
},
// SECRET SCANNING
[types.SET_SECRET_SCANNING_DIFF_ENDPOINT](state, path) {
Vue.set(state.secretScanning.paths, 'diffEndpoint', path);
},
[types.REQUEST_SECRET_SCANNING_DIFF](state) {
Vue.set(state.secretScanning, 'isLoading', true);
},
[types.RECEIVE_SECRET_SCANNING_DIFF_SUCCESS](state, { diff, enrichData }) {
const { added, fixed, existing } = parseDiff(diff, enrichData);
const baseReportOutofDate = diff.base_report_out_of_date || false;
const hasBaseReport = Boolean(diff.base_report_created_at);
Vue.set(state.secretScanning, 'isLoading', false);
Vue.set(state.secretScanning, 'newIssues', added);
Vue.set(state.secretScanning, 'resolvedIssues', fixed);
Vue.set(state.secretScanning, 'allIssues', existing);
Vue.set(state.secretScanning, 'baseReportOutofDate', baseReportOutofDate);
Vue.set(state.secretScanning, 'hasBaseReport', hasBaseReport);
},
[types.RECEIVE_SECRET_SCANNING_DIFF_ERROR](state) {
Vue.set(state.secretScanning, 'isLoading', false);
Vue.set(state.secretScanning, 'hasError', true);
},
[types.SET_ISSUE_MODAL_DATA](state, payload) {
const { issue, status } = payload;
......@@ -236,6 +263,26 @@ export default {
}
},
[types.UPDATE_SECRET_SCANNING_ISSUE](state, issue) {
// Find issue in the correct list and update it
const newIssuesIndex = findIssueIndex(state.secretScanning.newIssues, issue);
if (newIssuesIndex !== -1) {
state.secretScanning.newIssues.splice(newIssuesIndex, 1, issue);
return;
}
const resolvedIssuesIndex = findIssueIndex(state.secretScanning.resolvedIssues, issue);
if (resolvedIssuesIndex !== -1) {
state.secretScanning.resolvedIssues.splice(resolvedIssuesIndex, 1, issue);
}
const allIssuesIndex = findIssueIndex(state.secretScanning.allIssues, issue);
if (allIssuesIndex !== -1) {
state.secretScanning.allIssues.splice(allIssuesIndex, 1, issue);
}
},
[types.REQUEST_CREATE_ISSUE](state) {
state.isCreatingIssue = true;
// reset error in case previous state was error
......
......@@ -60,6 +60,22 @@ export default () => ({
baseReportOutofDate: false,
hasBaseReport: false,
},
secretScanning: {
paths: {
head: null,
base: null,
diffEndpoint: null,
},
isLoading: false,
hasError: false,
newIssues: [],
resolvedIssues: [],
allIssues: [],
baseReportOutofDate: false,
hasBaseReport: false,
},
modal: {
title: null,
......
......@@ -13,6 +13,7 @@
window.gl.mrWidgetData.container_scanning_help_path = '#{help_page_path("user/application_security/container_scanning/index")}';
window.gl.mrWidgetData.dast_help_path = '#{help_page_path("user/application_security/dast/index")}';
window.gl.mrWidgetData.dependency_scanning_help_path = '#{help_page_path("user/application_security/dependency_scanning/index")}';
window.gl.mrWidgetData.secret_scanning_help_path = '#{help_page_path('user/application_security/sast/index', anchor: 'secret-detection')}';
window.gl.mrWidgetData.vulnerability_feedback_help_path = '#{help_page_path("user/application_security/index")}';
window.gl.mrWidgetData.approvals_help_path = '#{help_page_path("user/project/merge_requests/merge_request_approvals")}';
window.gl.mrWidgetData.codequality_help_path = '#{help_page_path("user/project/merge_requests/code_quality", anchor: "code-quality-reports")}';
......
......@@ -16,6 +16,7 @@ export default Object.assign({}, mockData, {
dast: false,
dependency_scanning: false,
license_management: false,
secret_scanning: false,
},
});
......
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Secret Scanning Issue Body matches snapshot 1`] = `
<div
class="report-block-list-issue-description prepend-top-5 append-bottom-5"
>
<div
class="report-block-list-issue-description-text"
>
<severity-badge-stub
class="d-inline-block"
severity="Critical"
/>
<modal-open-name-stub
issue="[object Object]"
status="Failed"
/>
</div>
</div>
`;
import { shallowMount } from '@vue/test-utils';
import SecretScanningIssueBody from 'ee/vue_shared/security_reports/components/secret_scanning_issue_body.vue';
import SeverityBadge from 'ee/vue_shared/security_reports/components/severity_badge.vue';
describe('Secret Scanning Issue Body', () => {
let wrapper;
const createComponent = (severity = undefined) => {
wrapper = shallowMount(SecretScanningIssueBody, {
propsData: {
issue: {
title: 'AWS SecretKey Found',
severity,
},
status: 'Failed',
},
});
};
afterEach(() => {
wrapper.destroy();
wrapper = null;
});
it('matches snapshot', () => {
createComponent('Critical');
expect(wrapper.element).toMatchSnapshot();
});
it('does show SeverityBadge if severity is present', () => {
createComponent('Critical');
expect(wrapper.find(SeverityBadge).props('severity')).toBe('Critical');
});
it('does not show SeverityBadge if severity is not present', () => {
createComponent();
expect(wrapper.contains(SeverityBadge)).toBe(false);
});
});
......@@ -17,6 +17,7 @@ import {
dastDiffSuccessMock,
containerScanningDiffSuccessMock,
dependencyScanningDiffSuccessMock,
secretScanningDiffSuccessMock,
mockFindings,
} from './mock_data';
......@@ -24,6 +25,7 @@ const CONTAINER_SCANNING_DIFF_ENDPOINT = 'container_scanning.json';
const DEPENDENCY_SCANNING_DIFF_ENDPOINT = 'dependency_scanning.json';
const DAST_DIFF_ENDPOINT = 'dast.json';
const SAST_DIFF_ENDPOINT = 'sast.json';
const SECRET_SCANNING_DIFF_ENDPOINT = 'secret_scanning.json';
describe('Grouped security reports app', () => {
let wrapper;
......@@ -36,6 +38,7 @@ describe('Grouped security reports app', () => {
containerScanningHelpPath: 'path',
dastHelpPath: 'path',
dependencyScanningHelpPath: 'path',
secretScanningHelpPath: 'path',
vulnerabilityFeedbackPath: 'vulnerability_feedback_path.json',
vulnerabilityFeedbackHelpPath: 'path',
pipelineId: 123,
......@@ -70,6 +73,7 @@ describe('Grouped security reports app', () => {
dast: true,
containerScanning: true,
dependencyScanning: true,
secretScanning: true,
},
};
......@@ -79,6 +83,7 @@ describe('Grouped security reports app', () => {
gl.mrWidgetData.dependency_scanning_comparison_path = DEPENDENCY_SCANNING_DIFF_ENDPOINT;
gl.mrWidgetData.dast_comparison_path = DAST_DIFF_ENDPOINT;
gl.mrWidgetData.sast_comparison_path = SAST_DIFF_ENDPOINT;
gl.mrWidgetData.secret_scanning_comparison_path = SECRET_SCANNING_DIFF_ENDPOINT;
});
describe('with error', () => {
......@@ -87,6 +92,7 @@ describe('Grouped security reports app', () => {
mock.onGet(DEPENDENCY_SCANNING_DIFF_ENDPOINT).reply(500);
mock.onGet(DAST_DIFF_ENDPOINT).reply(500);
mock.onGet(SAST_DIFF_ENDPOINT).reply(500);
mock.onGet(SECRET_SCANNING_DIFF_ENDPOINT).reply(500);
createWrapper(allReportProps);
......@@ -95,6 +101,7 @@ describe('Grouped security reports app', () => {
waitForMutation(wrapper.vm.$store, types.RECEIVE_CONTAINER_SCANNING_DIFF_ERROR),
waitForMutation(wrapper.vm.$store, types.RECEIVE_DAST_DIFF_ERROR),
waitForMutation(wrapper.vm.$store, types.RECEIVE_DEPENDENCY_SCANNING_DIFF_ERROR),
waitForMutation(wrapper.vm.$store, types.RECEIVE_SECRET_SCANNING_DIFF_ERROR),
]);
});
......@@ -121,6 +128,8 @@ describe('Grouped security reports app', () => {
);
expect(wrapper.vm.$el.textContent).toContain('DAST: Loading resulted in an error');
expect(wrapper.text()).toContain('Secret scanning: Loading resulted in an error');
});
});
......@@ -130,6 +139,7 @@ describe('Grouped security reports app', () => {
mock.onGet(DEPENDENCY_SCANNING_DIFF_ENDPOINT).reply(200, {});
mock.onGet(DAST_DIFF_ENDPOINT).reply(200, {});
mock.onGet(SAST_DIFF_ENDPOINT).reply(200, {});
mock.onGet(SECRET_SCANNING_DIFF_ENDPOINT).reply(200, {});
createWrapper(allReportProps);
});
......@@ -157,6 +167,7 @@ describe('Grouped security reports app', () => {
mock.onGet(DEPENDENCY_SCANNING_DIFF_ENDPOINT).reply(200, dependencyScanningDiffSuccessMock);
mock.onGet(DAST_DIFF_ENDPOINT).reply(200, dastDiffSuccessMock);
mock.onGet(SAST_DIFF_ENDPOINT).reply(200, sastDiffSuccessMock);
mock.onGet(SECRET_SCANNING_DIFF_ENDPOINT).reply(200, secretScanningDiffSuccessMock);
createWrapper(allReportProps);
......@@ -165,6 +176,7 @@ describe('Grouped security reports app', () => {
waitForMutation(wrapper.vm.$store, types.RECEIVE_DAST_DIFF_SUCCESS),
waitForMutation(wrapper.vm.$store, types.RECEIVE_CONTAINER_SCANNING_DIFF_SUCCESS),
waitForMutation(wrapper.vm.$store, types.RECEIVE_DEPENDENCY_SCANNING_DIFF_SUCCESS),
waitForMutation(wrapper.vm.$store, types.RECEIVE_SECRET_SCANNING_DIFF_SUCCESS),
]);
});
......@@ -174,7 +186,7 @@ describe('Grouped security reports app', () => {
// Renders the summary text
expect(wrapper.vm.$el.querySelector('.js-code-text').textContent.trim()).toEqual(
'Security scanning detected 6 new, and 6 fixed vulnerabilities',
'Security scanning detected 8 new, and 7 fixed vulnerabilities',
);
// Renders the expand button
......@@ -266,7 +278,7 @@ describe('Grouped security reports app', () => {
});
it('should display the correct numbers of vulnerabilities', () => {
expect(wrapper.vm.$el.textContent).toContain(
expect(wrapper.text()).toContain(
'Container scanning detected 2 new, and 1 fixed vulnerabilities',
);
});
......@@ -373,6 +385,54 @@ describe('Grouped security reports app', () => {
});
});
describe('secret scanning reports', () => {
const initSecretScan = (isEnabled = true) => {
gl.mrWidgetData = gl.mrWidgetData || {};
gl.mrWidgetData.secret_scanning_comparison_path = SECRET_SCANNING_DIFF_ENDPOINT;
mock.onGet(SECRET_SCANNING_DIFF_ENDPOINT).reply(200, secretScanningDiffSuccessMock);
createWrapper({
...props,
enabledReports: {
secretScanning: isEnabled,
},
});
return waitForMutation(wrapper.vm.$store, types.RECEIVE_SECRET_SCANNING_DIFF_SUCCESS);
};
describe('enabled', () => {
beforeEach(() => {
return initSecretScan();
});
it('should render the component', () => {
expect(wrapper.contains('[data-qa-selector="secret_scan_report"]')).toBe(true);
});
it('should set setSecretScanningDiffEndpoint', () => {
expect(wrapper.vm.secretScanning.paths.diffEndpoint).toEqual(SECRET_SCANNING_DIFF_ENDPOINT);
});
it('should display the correct numbers of vulnerabilities', () => {
expect(wrapper.text()).toContain(
'Secret scanning detected 2 new, and 1 fixed vulnerabilities',
);
});
});
describe('disabled', () => {
beforeEach(() => {
initSecretScan(false);
});
it('should not render the component', () => {
expect(wrapper.contains('[data-qa-selector="secret_scan_report"]')).toBe(false);
});
});
});
describe('sast reports', () => {
beforeEach(() => {
gl.mrWidgetData = gl.mrWidgetData || {};
......
......@@ -175,6 +175,16 @@ export const parsedDast = [
},
];
export const secretScanningParsedIssues = [
{
title: 'AWS SecretKey detected',
path: 'Gemfile.lock',
line: 12,
severity: 'Critical',
urlPath: 'foo/Gemfile.lock',
},
];
export const dependencyScanningFeedbacks = [
{
id: 3,
......@@ -250,6 +260,31 @@ export const containerScanningFeedbacks = [
},
];
export const secretScanningFeedbacks = [
{
id: 3,
project_id: 17,
author_id: 1,
issue_iid: null,
pipeline_id: 132,
category: 'secret_scanning',
feedback_type: 'dismissal',
branch: 'try_new_secret_scanning',
project_fingerprint: libTiffCveFingerprint2,
},
{
id: 4,
project_id: 17,
author_id: 1,
issue_iid: 123,
pipeline_id: 132,
category: 'secret_scanning',
feedback_type: 'issue',
branch: 'try_new_secret_scanning',
project_fingerprint: libTiffCveFingerprint2,
},
];
export const mockFindings = [
{
id: null,
......@@ -579,3 +614,11 @@ export const dependencyScanningDiffSuccessMock = {
base_report_out_of_date: false,
head_report_created_at: '2020-01-10T10:00:00.000Z',
};
export const secretScanningDiffSuccessMock = {
added: [mockFindings[0], mockFindings[1]],
fixed: [mockFindings[2]],
base_report_created_at: '2020-01-01T10:00:00.000Z',
base_report_out_of_date: false,
head_report_created_at: '2020-01-10T10:00:00.000Z',
};
......@@ -27,6 +27,7 @@ import {
updateDependencyScanningIssue,
updateContainerScanningIssue,
updateDastIssue,
updateSecretScanningIssue,
addDismissalComment,
receiveAddDismissalCommentError,
receiveAddDismissalCommentSuccess,
......@@ -49,6 +50,10 @@ import {
receiveDastDiffSuccess,
receiveDastDiffError,
fetchDastDiff,
setSecretScanningDiffEndpoint,
receiveSecretScanningDiffSuccess,
receiveSecretScanningDiffError,
fetchSecretScanningDiff,
} 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';
......@@ -58,6 +63,7 @@ import {
dastFeedbacks,
containerScanningFeedbacks,
dependencyScanningFeedbacks,
secretScanningFeedbacks,
} from '../mock_data';
import toasted from '~/vue_shared/plugins/global_toast';
......@@ -1029,6 +1035,26 @@ describe('security reports actions', () => {
});
});
describe('updateSecretScanningIssue', () => {
it('commits update secret scanning issue', done => {
const payload = { foo: 'bar' };
testAction(
updateSecretScanningIssue,
payload,
mockedState,
[
{
type: types.UPDATE_SECRET_SCANNING_ISSUE,
payload,
},
],
[],
done,
);
});
});
describe('updateDastIssue', () => {
it('commits update dast issue', done => {
const payload = { foo: 'bar' };
......@@ -1520,4 +1546,162 @@ describe('security reports actions', () => {
});
});
});
describe('setSecretScanningDiffEndpoint', () => {
it('should pass down the endpoint to the mutation', done => {
const payload = '/secret_scanning_endpoint.json';
testAction(
setSecretScanningDiffEndpoint,
payload,
mockedState,
[
{
type: types.SET_SECRET_SCANNING_DIFF_ENDPOINT,
payload,
},
],
[],
done,
);
});
});
describe('receiveSecretScanningDiffSuccess', () => {
it('should pass down the response to the mutation', done => {
const payload = { data: 'Effort yields its own rewards.' };
testAction(
receiveSecretScanningDiffSuccess,
payload,
mockedState,
[
{
type: types.RECEIVE_SECRET_SCANNING_DIFF_SUCCESS,
payload,
},
],
[],
done,
);
});
});
describe('receiveSecretScanningDiffError', () => {
it('should commit secret diff error mutation', done => {
testAction(
receiveSecretScanningDiffError,
undefined,
mockedState,
[
{
type: types.RECEIVE_SECRET_SCANNING_DIFF_ERROR,
},
],
[],
done,
);
});
});
describe('fetchSecretScanningDiff', () => {
const diff = { vulnerabilities: [] };
const endpoint = 'secret_scanning_diff.json';
beforeEach(() => {
mockedState.vulnerabilityFeedbackPath = 'vulnerabilities_feedback';
mockedState.secretScanning.paths.diffEndpoint = endpoint;
});
describe('on success', () => {
it('should dispatch `receiveSecretScanningDiffSuccess`', done => {
mock.onGet(endpoint).reply(200, diff);
mock
.onGet('vulnerabilities_feedback', {
params: {
category: 'secret_scanning',
},
})
.reply(200, secretScanningFeedbacks);
testAction(
fetchSecretScanningDiff,
null,
mockedState,
[],
[
{
type: 'requestSecretScanningDiff',
},
{
type: 'receiveSecretScanningDiffSuccess',
payload: {
diff,
enrichData: secretScanningFeedbacks,
},
},
],
done,
);
});
});
describe('when vulnerabilities path errors', () => {
it('should dispatch `receiveSecretScanningError`', done => {
mock.onGet(endpoint).reply(500);
mock
.onGet('vulnerabilities_feedback', {
params: {
category: 'secret_scanning',
},
})
.reply(200, secretScanningFeedbacks);
testAction(
fetchSecretScanningDiff,
null,
mockedState,
[],
[
{
type: 'requestSecretScanningDiff',
},
{
type: 'receiveSecretScanningDiffError',
},
],
done,
);
});
});
describe('when feedback path errors', () => {
it('should dispatch `receiveSecretScanningError`', done => {
mock.onGet(endpoint).reply(200, diff);
mock
.onGet('vulnerabilities_feedback', {
params: {
category: 'secret_scanning',
},
})
.reply(500);
testAction(
fetchSecretScanningDiff,
null,
mockedState,
[],
[
{
type: 'requestSecretScanningDiff',
},
{
type: 'receiveSecretScanningDiffError',
},
],
done,
);
});
});
});
});
......@@ -4,6 +4,7 @@ import {
groupedContainerScanningText,
groupedDastText,
groupedDependencyText,
groupedSecretScanningText,
groupedSummaryText,
allReportsHaveError,
noBaseInAllReports,
......@@ -253,6 +254,81 @@ describe('Security reports getters', () => {
});
});
describe('groupedSecretScanningText', () => {
describe('with no issues', () => {
it('returns no issues text', () => {
state.secretScanning.paths.head = HEAD_PATH;
state.secretScanning.paths.base = BASE_PATH;
expect(groupedSecretScanningText(state)).toEqual(
'Secret scanning detected no vulnerabilities',
);
});
});
describe('with new issues and without base', () => {
it('returns unable to compare text', () => {
state.secretScanning.paths.head = HEAD_PATH;
state.secretScanning.newIssues = [{}];
expect(groupedSecretScanningText(state)).toEqual(
'Secret scanning detected 1 vulnerability for the source branch only',
);
});
});
describe('with base and head', () => {
describe('with only new issues', () => {
it('returns new issues text', () => {
state.secretScanning.paths.head = HEAD_PATH;
state.secretScanning.paths.base = BASE_PATH;
state.secretScanning.newIssues = [{}];
expect(groupedSecretScanningText(state)).toEqual(
'Secret scanning detected 1 new vulnerability',
);
});
});
describe('with only dismissed issues', () => {
it('returns dismissed issues text', () => {
state.secretScanning.paths.head = HEAD_PATH;
state.secretScanning.paths.base = BASE_PATH;
state.secretScanning.newIssues = [{ isDismissed: true }];
expect(groupedSecretScanningText(state)).toEqual(
'Secret scanning detected 1 dismissed vulnerability',
);
});
});
describe('with new and resolved issues', () => {
it('returns new and fixed issues text', () => {
state.secretScanning.paths.head = HEAD_PATH;
state.secretScanning.paths.base = BASE_PATH;
state.secretScanning.newIssues = [{}];
state.secretScanning.resolvedIssues = [{}];
expect(removeBreakLine(groupedSecretScanningText(state))).toEqual(
'Secret scanning detected 1 new, and 1 fixed vulnerabilities',
);
});
});
describe('with only resolved issues', () => {
it('returns fixed issues text', () => {
state.secretScanning.paths.head = HEAD_PATH;
state.secretScanning.paths.base = BASE_PATH;
state.secretScanning.resolvedIssues = [{}];
expect(groupedSecretScanningText(state)).toEqual(
'Secret scanning detected 1 fixed vulnerability',
);
});
});
});
});
describe('summaryCounts', () => {
it('returns 0 count for empty state', () => {
expect(summaryCounts(state)).toEqual({
......@@ -495,6 +571,7 @@ describe('Security reports getters', () => {
state.dast.hasError = true;
state.containerScanning.hasError = true;
state.dependencyScanning.hasError = true;
state.secretScanning.hasError = true;
expect(allReportsHaveError(state)).toEqual(true);
});
......@@ -507,6 +584,7 @@ describe('Security reports getters', () => {
state.dast.hasError = false;
state.containerScanning.hasError = true;
state.dependencyScanning.hasError = true;
state.secretScanning.hasError = true;
expect(allReportsHaveError(state)).toEqual(false);
});
......
......@@ -79,6 +79,14 @@ describe('security reports mutations', () => {
});
});
describe('REQUEST_SECRET_SCANNING_DIFF', () => {
it('should set secret scanning loading flag to true', () => {
mutations[types.REQUEST_SECRET_SCANNING_DIFF](stateCopy);
expect(stateCopy.secretScanning.isLoading).toEqual(true);
});
});
describe('SET_ISSUE_MODAL_DATA', () => {
it('has default data', () => {
expect(stateCopy.modal.vulnerability.isDismissed).toEqual(false);
......@@ -468,6 +476,34 @@ describe('security reports mutations', () => {
});
});
describe('UPDATE_SECRET_SCANNING_ISSUE', () => {
it('updates issue in the new issues list', () => {
stateCopy.secretScanning.newIssues = mockFindings;
stateCopy.secretScanning.resolvedIssues = [];
const updatedIssue = {
...mockFindings[0],
foo: 'bar',
};
mutations[types.UPDATE_SECRET_SCANNING_ISSUE](stateCopy, updatedIssue);
expect(stateCopy.secretScanning.newIssues[0]).toEqual(updatedIssue);
});
it('updates issue in the resolved issues list', () => {
stateCopy.secretScanning.newIssues = [];
stateCopy.secretScanning.resolvedIssues = mockFindings;
const updatedIssue = {
...mockFindings[0],
foo: 'bar',
};
mutations[types.UPDATE_SECRET_SCANNING_ISSUE](stateCopy, updatedIssue);
expect(stateCopy.secretScanning.resolvedIssues[0]).toEqual(updatedIssue);
});
});
describe('SET_CONTAINER_SCANNING_DIFF_ENDPOINT', () => {
const endpoint = 'container_scanning_diff_endpoint.json';
......@@ -696,4 +732,70 @@ describe('security reports mutations', () => {
expect(stateCopy.dast.hasError).toEqual(true);
});
});
describe('SET_SECRET_SCANNING_DIFF_ENDPOINT', () => {
const endpoint = 'secret_scanning_diff_endpoint.json';
beforeEach(() => {
mutations[types.SET_SECRET_SCANNING_DIFF_ENDPOINT](stateCopy, endpoint);
});
it('should set the correct endpoint', () => {
expect(stateCopy.secretScanning.paths.diffEndpoint).toEqual(endpoint);
});
});
describe('RECEIVE_SECRET_SCANNING_DIFF_SUCCESS', () => {
const reports = {
diff: {
added: [
{ name: 'added vuln 1', report_type: 'secret_scanning' },
{ name: 'added vuln 2', report_type: 'secret_scanning' },
],
fixed: [{ name: 'fixed vuln 1', report_type: 'secret_scanning' }],
base_report_out_of_date: true,
},
};
beforeEach(() => {
mutations[types.RECEIVE_SECRET_SCANNING_DIFF_SUCCESS](stateCopy, reports);
});
it('should set isLoading to false', () => {
expect(stateCopy.secretScanning.isLoading).toBe(false);
});
it('should set baseReportOutofDate to true', () => {
expect(stateCopy.secretScanning.baseReportOutofDate).toBe(true);
});
it('should parse and set the added vulnerabilities', () => {
reports.diff.added.forEach((vuln, i) => {
expect(stateCopy.secretScanning.newIssues[i]).toMatchObject({
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.secretScanning.resolvedIssues[i]).toMatchObject({
name: vuln.name,
title: vuln.name,
category: vuln.report_type,
});
});
});
});
describe('RECEIVE_SECRET_SCANNING_DIFF_ERROR', () => {
it('should set secret scanning loading flag to false and error flag to true', () => {
mutations[types.RECEIVE_SECRET_SCANNING_DIFF_ERROR](stateCopy);
expect(stateCopy.secretScanning.isLoading).toEqual(false);
expect(stateCopy.secretScanning.hasError).toEqual(true);
});
});
});
......@@ -24,12 +24,14 @@ import {
dastDiffSuccessMock,
containerScanningDiffSuccessMock,
dependencyScanningDiffSuccessMock,
secretScanningDiffSuccessMock,
} from 'ee_spec/vue_shared/security_reports/mock_data';
const SAST_SELECTOR = '.js-sast-widget';
const DAST_SELECTOR = '.js-dast-widget';
const DEPENDENCY_SCANNING_SELECTOR = '.js-dependency-scanning-widget';
const CONTAINER_SCANNING_SELECTOR = '.js-container-scanning';
const SECRET_SCANNING_SELECTOR = '.js-secret-scanning';
describe('ee merge request widget options', () => {
let vm;
......@@ -780,6 +782,80 @@ describe('ee merge request widget options', () => {
});
});
describe('Secret Scanning', () => {
const SECRET_SCANNING_ENDPOINT = 'secret_scanning';
beforeEach(() => {
gl.mrWidgetData = {
...mockData,
enabled_reports: {
secret_scanning: true,
// The below property needs to exist until
// secret scanning is implemented in backend
// Or for some other reason I'm yet to find
dast: true,
},
secret_scanning_comparison_path: SECRET_SCANNING_ENDPOINT,
vulnerability_feedback_path: VULNERABILITY_FEEDBACK_ENDPOINT,
};
});
describe('when it is loading', () => {
it('should render loading indicator', () => {
mock.onGet(SECRET_SCANNING_ENDPOINT).reply(200, secretScanningDiffSuccessMock);
mock.onGet(VULNERABILITY_FEEDBACK_ENDPOINT).reply(200, []);
vm = mountComponent(Component, { mrData: gl.mrWidgetData });
expect(
removeBreakLine(findSecurityWidget().querySelector(SECRET_SCANNING_SELECTOR).textContent),
).toContain('Secret scanning is loading');
});
});
describe('with successful request', () => {
beforeEach(() => {
mock.onGet(SECRET_SCANNING_ENDPOINT).reply(200, secretScanningDiffSuccessMock);
mock.onGet(VULNERABILITY_FEEDBACK_ENDPOINT).reply(200, []);
vm = mountComponent(Component, { mrData: gl.mrWidgetData });
});
it('should render provided data', done => {
setTimeout(() => {
expect(
removeBreakLine(
findSecurityWidget().querySelector(
`${SECRET_SCANNING_SELECTOR} .report-block-list-issue-description`,
).textContent,
),
).toEqual('Secret scanning detected 2 new, and 1 fixed vulnerabilities');
done();
}, 0);
});
});
describe('with failed request', () => {
beforeEach(() => {
mock.onGet(SECRET_SCANNING_ENDPOINT).reply(500, {});
mock.onGet(VULNERABILITY_FEEDBACK_ENDPOINT).reply(500, []);
vm = mountComponent(Component, { mrData: gl.mrWidgetData });
});
it('should render error indicator', done => {
setTimeout(() => {
expect(
findSecurityWidget()
.querySelector(SECRET_SCANNING_SELECTOR)
.textContent.trim(),
).toContain('Secret scanning: Loading resulted in an error');
done();
}, 0);
});
});
});
describe('license scanning report', () => {
const licenseManagementApiUrl = `${TEST_HOST}/manage_license_api`;
......@@ -1102,6 +1178,7 @@ describe('ee merge request widget options', () => {
sast: false,
container_scanning: false,
dependency_scanning: false,
secret_scanning: false,
},
];
......
......@@ -7,6 +7,7 @@ import {
sastParsedIssues,
dockerReportParsed,
parsedDast,
secretScanningParsedIssues,
} from 'ee_spec/vue_shared/security_reports/mock_data';
import { STATUS_FAILED, STATUS_SUCCESS } from '~/reports/constants';
import reportIssues from '~/reports/components/report_item.vue';
......@@ -123,4 +124,24 @@ describe('Report issues', () => {
expect(vm.$el.textContent).toContain(`${parsedDast[0].severity}`);
});
});
describe('for secret scanning issues', () => {
beforeEach(() => {
vm = mountComponent(ReportIssues, {
issue: secretScanningParsedIssues[0],
component: componentNames.SecretScanningIssueBody,
status: STATUS_FAILED,
});
});
it('renders severity', () => {
expect(vm.$el.textContent.trim()).toContain(secretScanningParsedIssues[0].severity);
});
it('renders CVE name', () => {
expect(vm.$el.querySelector('.report-block-list-issue button').textContent.trim()).toEqual(
secretScanningParsedIssues[0].title,
);
});
});
});
......@@ -7,6 +7,7 @@ import {
sastParsedIssues,
dockerReportParsed,
parsedDast,
secretScanningParsedIssues,
} from 'ee_spec/vue_shared/security_reports/mock_data';
import { STATUS_FAILED, STATUS_SUCCESS } from '~/reports/constants';
import reportIssue from '~/reports/components/report_item.vue';
......@@ -124,6 +125,26 @@ describe('Report issue', () => {
});
});
describe('for secret scanning issue', () => {
beforeEach(() => {
vm = mountComponent(ReportIssue, {
issue: secretScanningParsedIssues[0],
component: componentNames.SecretScanningIssueBody,
status: STATUS_FAILED,
});
});
it('renders severity', () => {
expect(vm.$el.textContent.trim()).toContain(secretScanningParsedIssues[0].severity);
});
it('renders CVE name', () => {
expect(vm.$el.querySelector('button').textContent.trim()).toEqual(
secretScanningParsedIssues[0].title,
);
});
});
describe('showReportSectionStatusIcon', () => {
it('does not render CI Status Icon when showReportSectionStatusIcon is false', () => {
vm = mountComponentWithStore(ReportIssue, {
......
......@@ -7,8 +7,10 @@ export const {
dockerReportParsed,
parsedDast,
sastParsedIssues,
secretScanningParsedIssues,
sastDiffSuccessMock,
dastDiffSuccessMock,
containerScanningDiffSuccessMock,
dependencyScanningDiffSuccessMock,
secretScanningDiffSuccessMock,
} = mockData;
......@@ -24191,6 +24191,9 @@ msgstr ""
msgid "ciReport|%{linkStartTag}Learn more about SAST %{linkEndTag}"
msgstr ""
msgid "ciReport|%{linkStartTag}Learn more about Secret Scanning %{linkEndTag}"
msgstr ""
msgid "ciReport|%{linkStartTag}Learn more about codequality reports %{linkEndTag}"
msgstr ""
......@@ -24354,6 +24357,12 @@ msgstr ""
msgid "ciReport|SAST"
msgstr ""
msgid "ciReport|Secret scanning"
msgstr ""
msgid "ciReport|Secret scanning detects secrets and credentials vulnerabilities in your source code."
msgstr ""
msgid "ciReport|Security scanning"
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