Commit 89870ebb authored by Mark Florian's avatar Mark Florian

Merge branch 'djadmin-secret-scanning' into 'master'

Secret detection for MR Widget

See merge request gitlab-org/gitlab!28672
parents 29775fbd e681c186
......@@ -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;
......@@ -24239,6 +24239,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 ""
......@@ -24402,6 +24405,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