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 ...@@ -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 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 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 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 MetricsReportsIssueBody from 'ee/vue_shared/metrics_reports/components/metrics_reports_issue_body.vue';
import { import {
components as componentsCE, components as componentsCE,
...@@ -19,6 +20,7 @@ export const components = { ...@@ -19,6 +20,7 @@ export const components = {
ContainerScanningIssueBody, ContainerScanningIssueBody,
SastIssueBody, SastIssueBody,
DastIssueBody, DastIssueBody,
SecretScanningIssueBody,
MetricsReportsIssueBody, MetricsReportsIssueBody,
BlockingMergeRequestsBody, BlockingMergeRequestsBody,
}; };
...@@ -31,6 +33,7 @@ export const componentNames = { ...@@ -31,6 +33,7 @@ export const componentNames = {
ContainerScanningIssueBody: ContainerScanningIssueBody.name, ContainerScanningIssueBody: ContainerScanningIssueBody.name,
SastIssueBody: SastIssueBody.name, SastIssueBody: SastIssueBody.name,
DastIssueBody: DastIssueBody.name, DastIssueBody: DastIssueBody.name,
SecretScanningIssueBody: SecretScanningIssueBody.name,
MetricsReportsIssueBody: MetricsReportsIssueBody.name, MetricsReportsIssueBody: MetricsReportsIssueBody.name,
BlockingMergeRequestsBody: BlockingMergeRequestsBody.name, BlockingMergeRequestsBody: BlockingMergeRequestsBody.name,
}; };
...@@ -314,6 +314,7 @@ export default { ...@@ -314,6 +314,7 @@ export default {
:dast-help-path="mr.dastHelp" :dast-help-path="mr.dastHelp"
:container-scanning-help-path="mr.containerScanningHelp" :container-scanning-help-path="mr.containerScanningHelp"
:dependency-scanning-help-path="mr.dependencyScanningHelp" :dependency-scanning-help-path="mr.dependencyScanningHelp"
:secret-scanning-help-path="mr.secretScanningHelp"
:vulnerability-feedback-path="mr.vulnerabilityFeedbackPath" :vulnerability-feedback-path="mr.vulnerabilityFeedbackPath"
:vulnerability-feedback-help-path="mr.vulnerabilityFeedbackHelpPath" :vulnerability-feedback-help-path="mr.vulnerabilityFeedbackHelpPath"
:create-vulnerability-feedback-issue-path="mr.createVulnerabilityFeedbackIssuePath" :create-vulnerability-feedback-issue-path="mr.createVulnerabilityFeedbackIssuePath"
......
...@@ -13,6 +13,7 @@ export default class MergeRequestStore extends CEMergeRequestStore { ...@@ -13,6 +13,7 @@ export default class MergeRequestStore extends CEMergeRequestStore {
this.sastHelp = data.sast_help_path; this.sastHelp = data.sast_help_path;
this.containerScanningHelp = data.container_scanning_help_path; this.containerScanningHelp = data.container_scanning_help_path;
this.dastHelp = data.dast_help_path; this.dastHelp = data.dast_help_path;
this.secretScanningHelp = data.secret_scanning_help_path;
this.dependencyScanningHelp = data.dependency_scanning_help_path; this.dependencyScanningHelp = data.dependency_scanning_help_path;
this.vulnerabilityFeedbackPath = data.vulnerability_feedback_path; this.vulnerabilityFeedbackPath = data.vulnerability_feedback_path;
this.vulnerabilityFeedbackHelpPath = data.vulnerability_feedback_help_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 { ...@@ -69,6 +69,11 @@ export default {
required: false, required: false,
default: '', default: '',
}, },
secretScanningHelpPath: {
type: String,
required: false,
default: '',
},
vulnerabilityFeedbackPath: { vulnerabilityFeedbackPath: {
type: String, type: String,
required: false, required: false,
...@@ -132,6 +137,7 @@ export default { ...@@ -132,6 +137,7 @@ export default {
'containerScanning', 'containerScanning',
'dast', 'dast',
'dependencyScanning', 'dependencyScanning',
'secretScanning',
'summaryCounts', 'summaryCounts',
'modal', 'modal',
'isCreatingIssue', 'isCreatingIssue',
...@@ -144,9 +150,11 @@ export default { ...@@ -144,9 +150,11 @@ export default {
'groupedContainerScanningText', 'groupedContainerScanningText',
'groupedDastText', 'groupedDastText',
'groupedDependencyText', 'groupedDependencyText',
'groupedSecretScanningText',
'containerScanningStatusIcon', 'containerScanningStatusIcon',
'dastStatusIcon', 'dastStatusIcon',
'dependencyScanningStatusIcon', 'dependencyScanningStatusIcon',
'secretScanningStatusIcon',
'isBaseSecurityReportOutOfDate', 'isBaseSecurityReportOutOfDate',
'canCreateIssue', 'canCreateIssue',
'canCreateMergeRequest', 'canCreateMergeRequest',
...@@ -168,6 +176,9 @@ export default { ...@@ -168,6 +176,9 @@ export default {
hasSastReports() { hasSastReports() {
return this.enabledReports.sast; return this.enabledReports.sast;
}, },
hasSecretScanningReports() {
return this.enabledReports.secretScanning;
},
isMRActive() { isMRActive() {
return this.mrState !== mrStates.merged && this.mrState !== mrStates.closed; return this.mrState !== mrStates.merged && this.mrState !== mrStates.closed;
}, },
...@@ -220,6 +231,12 @@ export default { ...@@ -220,6 +231,12 @@ export default {
this.setDependencyScanningDiffEndpoint(dependencyScanningDiffEndpoint); this.setDependencyScanningDiffEndpoint(dependencyScanningDiffEndpoint);
this.fetchDependencyScanningDiff(); this.fetchDependencyScanningDiff();
} }
const secretScanningDiffEndpoint = gl?.mrWidgetData?.secret_scanning_comparison_path;
if (secretScanningDiffEndpoint && this.hasSecretScanningReports) {
this.setSecretScanningDiffEndpoint(secretScanningDiffEndpoint);
this.fetchSecretScanningDiff();
}
}, },
methods: { methods: {
...mapActions([ ...mapActions([
...@@ -250,6 +267,8 @@ export default { ...@@ -250,6 +267,8 @@ export default {
'setDependencyScanningDiffEndpoint', 'setDependencyScanningDiffEndpoint',
'fetchDastDiff', 'fetchDastDiff',
'setDastDiffEndpoint', 'setDastDiffEndpoint',
'fetchSecretScanningDiff',
'setSecretScanningDiffEndpoint',
]), ]),
...mapActions('sast', { ...mapActions('sast', {
setSastDiffEndpoint: 'setDiffEndpoint', setSastDiffEndpoint: 'setDiffEndpoint',
...@@ -404,6 +423,24 @@ export default { ...@@ -404,6 +423,24 @@ export default {
/> />
</template> </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 <issue-modal
:modal="modal" :modal="modal"
:vulnerability-feedback-help-path="vulnerabilityFeedbackHelpPath" :vulnerability-feedback-help-path="vulnerabilityFeedbackHelpPath"
......
...@@ -70,5 +70,20 @@ export default { ...@@ -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 }) => { ...@@ -159,6 +159,46 @@ export const fetchDependencyScanningDiff = ({ state, dispatch }) => {
export const updateDependencyScanningIssue = ({ commit }, issue) => export const updateDependencyScanningIssue = ({ commit }, issue) =>
commit(types.UPDATE_DEPENDENCY_SCANNING_ISSUE, 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) => { export const openModal = ({ dispatch }, payload) => {
dispatch('setModalData', payload); dispatch('setModalData', payload);
......
...@@ -11,6 +11,14 @@ export const groupedContainerScanningText = ({ containerScanning }) => ...@@ -11,6 +11,14 @@ export const groupedContainerScanningText = ({ containerScanning }) =>
messages.CONTAINER_SCANNING_IS_LOADING, 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 }) => export const groupedDastText = ({ dast }) =>
groupedReportText(dast, messages.DAST, messages.DAST_HAS_ERROR, messages.DAST_IS_LOADING); groupedReportText(dast, messages.DAST, messages.DAST_HAS_ERROR, messages.DAST_IS_LOADING);
...@@ -23,7 +31,13 @@ export const groupedDependencyText = ({ dependencyScanning }) => ...@@ -23,7 +31,13 @@ export const groupedDependencyText = ({ dependencyScanning }) =>
); );
export const summaryCounts = state => 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) => { (acc, report) => {
const curr = countIssues(report); const curr = countIssues(report);
acc.added += curr.added; acc.added += curr.added;
...@@ -98,47 +112,57 @@ export const dependencyScanningStatusIcon = ({ dependencyScanning }) => ...@@ -98,47 +112,57 @@ export const dependencyScanningStatusIcon = ({ dependencyScanning }) =>
dependencyScanning.newIssues.length, dependencyScanning.newIssues.length,
); );
export const secretScanningStatusIcon = ({ secretScanning }) =>
statusIcon(secretScanning.isLoading, secretScanning.hasError, secretScanning.newIssues.length);
export const areReportsLoading = state => export const areReportsLoading = state =>
state.sast.isLoading || state.sast.isLoading ||
state.dast.isLoading || state.dast.isLoading ||
state.containerScanning.isLoading || state.containerScanning.isLoading ||
state.dependencyScanning.isLoading; state.dependencyScanning.isLoading ||
state.secretScanning.isLoading;
export const areAllReportsLoading = state => export const areAllReportsLoading = state =>
state.sast.isLoading && state.sast.isLoading &&
state.dast.isLoading && state.dast.isLoading &&
state.containerScanning.isLoading && state.containerScanning.isLoading &&
state.dependencyScanning.isLoading; state.dependencyScanning.isLoading &&
state.secretScanning.isLoading;
export const allReportsHaveError = state => export const allReportsHaveError = state =>
state.sast.hasError && state.sast.hasError &&
state.dast.hasError && state.dast.hasError &&
state.containerScanning.hasError && state.containerScanning.hasError &&
state.dependencyScanning.hasError; state.dependencyScanning.hasError &&
state.secretScanning.hasError;
export const anyReportHasError = state => export const anyReportHasError = state =>
state.sast.hasError || state.sast.hasError ||
state.dast.hasError || state.dast.hasError ||
state.containerScanning.hasError || state.containerScanning.hasError ||
state.dependencyScanning.hasError; state.dependencyScanning.hasError ||
state.secretScanning.hasError;
export const noBaseInAllReports = state => export const noBaseInAllReports = state =>
!state.sast.hasBaseReport && !state.sast.hasBaseReport &&
!state.dast.hasBaseReport && !state.dast.hasBaseReport &&
!state.containerScanning.hasBaseReport && !state.containerScanning.hasBaseReport &&
!state.dependencyScanning.hasBaseReport; !state.dependencyScanning.hasBaseReport &&
!state.secretScanning.hasBaseReport;
export const anyReportHasIssues = state => export const anyReportHasIssues = state =>
state.sast.newIssues.length > 0 || state.sast.newIssues.length > 0 ||
state.dast.newIssues.length > 0 || state.dast.newIssues.length > 0 ||
state.containerScanning.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 => export const isBaseSecurityReportOutOfDate = state =>
state.sast.baseReportOutofDate || state.sast.baseReportOutofDate ||
state.dast.baseReportOutofDate || state.dast.baseReportOutofDate ||
state.containerScanning.baseReportOutofDate || state.containerScanning.baseReportOutofDate ||
state.dependencyScanning.baseReportOutofDate; state.dependencyScanning.baseReportOutofDate ||
state.secretScanning.baseReportOutofDate;
export const canCreateIssue = state => Boolean(state.createVulnerabilityFeedbackIssuePath); export const canCreateIssue = state => Boolean(state.createVulnerabilityFeedbackIssuePath);
......
...@@ -5,6 +5,7 @@ const updateIssueActionsMap = { ...@@ -5,6 +5,7 @@ const updateIssueActionsMap = {
dependency_scanning: 'updateDependencyScanningIssue', dependency_scanning: 'updateDependencyScanningIssue',
container_scanning: 'updateContainerScanningIssue', container_scanning: 'updateContainerScanningIssue',
dast: 'updateDastIssue', dast: 'updateDastIssue',
secret_scanning: 'updateSecretScanningIssue',
}; };
export default function configureMediator(store) { export default function configureMediator(store) {
......
...@@ -7,12 +7,14 @@ const SAST = s__('ciReport|SAST'); ...@@ -7,12 +7,14 @@ const SAST = s__('ciReport|SAST');
const DAST = s__('ciReport|DAST'); const DAST = s__('ciReport|DAST');
const CONTAINER_SCANNING = s__('ciReport|Container scanning'); const CONTAINER_SCANNING = s__('ciReport|Container scanning');
const DEPENDENCY_SCANNING = s__('ciReport|Dependency scanning'); const DEPENDENCY_SCANNING = s__('ciReport|Dependency scanning');
const SECRET_SCANNING = s__('ciReport|Secret scanning');
export default { export default {
SAST, SAST,
DAST, DAST,
CONTAINER_SCANNING, CONTAINER_SCANNING,
DEPENDENCY_SCANNING, DEPENDENCY_SCANNING,
SECRET_SCANNING,
TRANSLATION_IS_LOADING, TRANSLATION_IS_LOADING,
TRANSLATION_HAS_ERROR, TRANSLATION_HAS_ERROR,
SAST_IS_LOADING: sprintf(TRANSLATION_IS_LOADING, { reportType: SAST }), SAST_IS_LOADING: sprintf(TRANSLATION_IS_LOADING, { reportType: SAST }),
...@@ -29,4 +31,8 @@ export default { ...@@ -29,4 +31,8 @@ export default {
DEPENDENCY_SCANNING_HAS_ERROR: sprintf(TRANSLATION_HAS_ERROR, { DEPENDENCY_SCANNING_HAS_ERROR: sprintf(TRANSLATION_HAS_ERROR, {
reportType: DEPENDENCY_SCANNING, 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 ...@@ -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_SUCCESS = 'RECEIVE_DEPENDENCY_SCANNING_DIFF_SUCCESS';
export const RECEIVE_DEPENDENCY_SCANNING_DIFF_ERROR = 'RECEIVE_DEPENDENCY_SCANNING_DIFF_ERROR'; 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 // Dismiss security issue
export const SET_ISSUE_MODAL_DATA = 'SET_ISSUE_MODAL_DATA'; export const SET_ISSUE_MODAL_DATA = 'SET_ISSUE_MODAL_DATA';
export const REQUEST_DISMISS_VULNERABILITY = 'REQUEST_DISMISS_VULNERABILITY'; export const REQUEST_DISMISS_VULNERABILITY = 'REQUEST_DISMISS_VULNERABILITY';
...@@ -56,6 +62,7 @@ export const HIDE_DISMISSAL_DELETE_BUTTONS = 'HIDE_DISMISSAL_DELETE_BUTTONS'; ...@@ -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_DEPENDENCY_SCANNING_ISSUE = 'UPDATE_DEPENDENCY_SCANNING_ISSUE';
export const UPDATE_CONTAINER_SCANNING_ISSUE = 'UPDATE_CONTAINER_SCANNING_ISSUE'; export const UPDATE_CONTAINER_SCANNING_ISSUE = 'UPDATE_CONTAINER_SCANNING_ISSUE';
export const UPDATE_DAST_ISSUE = 'UPDATE_DAST_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 OPEN_DISMISSAL_COMMENT_BOX = 'OPEN_DISMISSAL_COMMENT_BOX ';
export const CLOSE_DISMISSAL_COMMENT_BOX = 'CLOSE_DISMISSAL_COMMENT_BOX'; export const CLOSE_DISMISSAL_COMMENT_BOX = 'CLOSE_DISMISSAL_COMMENT_BOX';
...@@ -125,6 +125,33 @@ export default { ...@@ -125,6 +125,33 @@ export default {
Vue.set(state.dependencyScanning, 'hasError', true); 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) { [types.SET_ISSUE_MODAL_DATA](state, payload) {
const { issue, status } = payload; const { issue, status } = payload;
...@@ -236,6 +263,26 @@ export default { ...@@ -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) { [types.REQUEST_CREATE_ISSUE](state) {
state.isCreatingIssue = true; state.isCreatingIssue = true;
// reset error in case previous state was error // reset error in case previous state was error
......
...@@ -60,6 +60,22 @@ export default () => ({ ...@@ -60,6 +60,22 @@ export default () => ({
baseReportOutofDate: false, baseReportOutofDate: false,
hasBaseReport: false, hasBaseReport: false,
}, },
secretScanning: {
paths: {
head: null,
base: null,
diffEndpoint: null,
},
isLoading: false,
hasError: false,
newIssues: [],
resolvedIssues: [],
allIssues: [],
baseReportOutofDate: false,
hasBaseReport: false,
},
modal: { modal: {
title: null, title: null,
......
...@@ -13,6 +13,7 @@ ...@@ -13,6 +13,7 @@
window.gl.mrWidgetData.container_scanning_help_path = '#{help_page_path("user/application_security/container_scanning/index")}'; 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.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.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.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.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")}'; 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, { ...@@ -16,6 +16,7 @@ export default Object.assign({}, mockData, {
dast: false, dast: false,
dependency_scanning: false, dependency_scanning: false,
license_management: 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 { ...@@ -17,6 +17,7 @@ import {
dastDiffSuccessMock, dastDiffSuccessMock,
containerScanningDiffSuccessMock, containerScanningDiffSuccessMock,
dependencyScanningDiffSuccessMock, dependencyScanningDiffSuccessMock,
secretScanningDiffSuccessMock,
mockFindings, mockFindings,
} from './mock_data'; } from './mock_data';
...@@ -24,6 +25,7 @@ const CONTAINER_SCANNING_DIFF_ENDPOINT = 'container_scanning.json'; ...@@ -24,6 +25,7 @@ const CONTAINER_SCANNING_DIFF_ENDPOINT = 'container_scanning.json';
const DEPENDENCY_SCANNING_DIFF_ENDPOINT = 'dependency_scanning.json'; const DEPENDENCY_SCANNING_DIFF_ENDPOINT = 'dependency_scanning.json';
const DAST_DIFF_ENDPOINT = 'dast.json'; const DAST_DIFF_ENDPOINT = 'dast.json';
const SAST_DIFF_ENDPOINT = 'sast.json'; const SAST_DIFF_ENDPOINT = 'sast.json';
const SECRET_SCANNING_DIFF_ENDPOINT = 'secret_scanning.json';
describe('Grouped security reports app', () => { describe('Grouped security reports app', () => {
let wrapper; let wrapper;
...@@ -36,6 +38,7 @@ describe('Grouped security reports app', () => { ...@@ -36,6 +38,7 @@ describe('Grouped security reports app', () => {
containerScanningHelpPath: 'path', containerScanningHelpPath: 'path',
dastHelpPath: 'path', dastHelpPath: 'path',
dependencyScanningHelpPath: 'path', dependencyScanningHelpPath: 'path',
secretScanningHelpPath: 'path',
vulnerabilityFeedbackPath: 'vulnerability_feedback_path.json', vulnerabilityFeedbackPath: 'vulnerability_feedback_path.json',
vulnerabilityFeedbackHelpPath: 'path', vulnerabilityFeedbackHelpPath: 'path',
pipelineId: 123, pipelineId: 123,
...@@ -70,6 +73,7 @@ describe('Grouped security reports app', () => { ...@@ -70,6 +73,7 @@ describe('Grouped security reports app', () => {
dast: true, dast: true,
containerScanning: true, containerScanning: true,
dependencyScanning: true, dependencyScanning: true,
secretScanning: true,
}, },
}; };
...@@ -79,6 +83,7 @@ describe('Grouped security reports app', () => { ...@@ -79,6 +83,7 @@ describe('Grouped security reports app', () => {
gl.mrWidgetData.dependency_scanning_comparison_path = DEPENDENCY_SCANNING_DIFF_ENDPOINT; gl.mrWidgetData.dependency_scanning_comparison_path = DEPENDENCY_SCANNING_DIFF_ENDPOINT;
gl.mrWidgetData.dast_comparison_path = DAST_DIFF_ENDPOINT; gl.mrWidgetData.dast_comparison_path = DAST_DIFF_ENDPOINT;
gl.mrWidgetData.sast_comparison_path = SAST_DIFF_ENDPOINT; gl.mrWidgetData.sast_comparison_path = SAST_DIFF_ENDPOINT;
gl.mrWidgetData.secret_scanning_comparison_path = SECRET_SCANNING_DIFF_ENDPOINT;
}); });
describe('with error', () => { describe('with error', () => {
...@@ -87,6 +92,7 @@ describe('Grouped security reports app', () => { ...@@ -87,6 +92,7 @@ describe('Grouped security reports app', () => {
mock.onGet(DEPENDENCY_SCANNING_DIFF_ENDPOINT).reply(500); mock.onGet(DEPENDENCY_SCANNING_DIFF_ENDPOINT).reply(500);
mock.onGet(DAST_DIFF_ENDPOINT).reply(500); mock.onGet(DAST_DIFF_ENDPOINT).reply(500);
mock.onGet(SAST_DIFF_ENDPOINT).reply(500); mock.onGet(SAST_DIFF_ENDPOINT).reply(500);
mock.onGet(SECRET_SCANNING_DIFF_ENDPOINT).reply(500);
createWrapper(allReportProps); createWrapper(allReportProps);
...@@ -95,6 +101,7 @@ describe('Grouped security reports app', () => { ...@@ -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_CONTAINER_SCANNING_DIFF_ERROR),
waitForMutation(wrapper.vm.$store, types.RECEIVE_DAST_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_DEPENDENCY_SCANNING_DIFF_ERROR),
waitForMutation(wrapper.vm.$store, types.RECEIVE_SECRET_SCANNING_DIFF_ERROR),
]); ]);
}); });
...@@ -121,6 +128,8 @@ describe('Grouped security reports app', () => { ...@@ -121,6 +128,8 @@ describe('Grouped security reports app', () => {
); );
expect(wrapper.vm.$el.textContent).toContain('DAST: Loading resulted in an error'); 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', () => { ...@@ -130,6 +139,7 @@ describe('Grouped security reports app', () => {
mock.onGet(DEPENDENCY_SCANNING_DIFF_ENDPOINT).reply(200, {}); mock.onGet(DEPENDENCY_SCANNING_DIFF_ENDPOINT).reply(200, {});
mock.onGet(DAST_DIFF_ENDPOINT).reply(200, {}); mock.onGet(DAST_DIFF_ENDPOINT).reply(200, {});
mock.onGet(SAST_DIFF_ENDPOINT).reply(200, {}); mock.onGet(SAST_DIFF_ENDPOINT).reply(200, {});
mock.onGet(SECRET_SCANNING_DIFF_ENDPOINT).reply(200, {});
createWrapper(allReportProps); createWrapper(allReportProps);
}); });
...@@ -157,6 +167,7 @@ describe('Grouped security reports app', () => { ...@@ -157,6 +167,7 @@ describe('Grouped security reports app', () => {
mock.onGet(DEPENDENCY_SCANNING_DIFF_ENDPOINT).reply(200, dependencyScanningDiffSuccessMock); mock.onGet(DEPENDENCY_SCANNING_DIFF_ENDPOINT).reply(200, dependencyScanningDiffSuccessMock);
mock.onGet(DAST_DIFF_ENDPOINT).reply(200, dastDiffSuccessMock); mock.onGet(DAST_DIFF_ENDPOINT).reply(200, dastDiffSuccessMock);
mock.onGet(SAST_DIFF_ENDPOINT).reply(200, sastDiffSuccessMock); mock.onGet(SAST_DIFF_ENDPOINT).reply(200, sastDiffSuccessMock);
mock.onGet(SECRET_SCANNING_DIFF_ENDPOINT).reply(200, secretScanningDiffSuccessMock);
createWrapper(allReportProps); createWrapper(allReportProps);
...@@ -165,6 +176,7 @@ describe('Grouped security reports app', () => { ...@@ -165,6 +176,7 @@ describe('Grouped security reports app', () => {
waitForMutation(wrapper.vm.$store, types.RECEIVE_DAST_DIFF_SUCCESS), 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_CONTAINER_SCANNING_DIFF_SUCCESS),
waitForMutation(wrapper.vm.$store, types.RECEIVE_DEPENDENCY_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', () => { ...@@ -174,7 +186,7 @@ describe('Grouped security reports app', () => {
// Renders the summary text // Renders the summary text
expect(wrapper.vm.$el.querySelector('.js-code-text').textContent.trim()).toEqual( 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 // Renders the expand button
...@@ -266,7 +278,7 @@ describe('Grouped security reports app', () => { ...@@ -266,7 +278,7 @@ describe('Grouped security reports app', () => {
}); });
it('should display the correct numbers of vulnerabilities', () => { 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', 'Container scanning detected 2 new, and 1 fixed vulnerabilities',
); );
}); });
...@@ -373,6 +385,54 @@ describe('Grouped security reports app', () => { ...@@ -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', () => { describe('sast reports', () => {
beforeEach(() => { beforeEach(() => {
gl.mrWidgetData = gl.mrWidgetData || {}; gl.mrWidgetData = gl.mrWidgetData || {};
......
...@@ -175,6 +175,16 @@ export const parsedDast = [ ...@@ -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 = [ export const dependencyScanningFeedbacks = [
{ {
id: 3, id: 3,
...@@ -250,6 +260,31 @@ export const containerScanningFeedbacks = [ ...@@ -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 = [ export const mockFindings = [
{ {
id: null, id: null,
...@@ -579,3 +614,11 @@ export const dependencyScanningDiffSuccessMock = { ...@@ -579,3 +614,11 @@ export const dependencyScanningDiffSuccessMock = {
base_report_out_of_date: false, base_report_out_of_date: false,
head_report_created_at: '2020-01-10T10:00:00.000Z', 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 { ...@@ -27,6 +27,7 @@ import {
updateDependencyScanningIssue, updateDependencyScanningIssue,
updateContainerScanningIssue, updateContainerScanningIssue,
updateDastIssue, updateDastIssue,
updateSecretScanningIssue,
addDismissalComment, addDismissalComment,
receiveAddDismissalCommentError, receiveAddDismissalCommentError,
receiveAddDismissalCommentSuccess, receiveAddDismissalCommentSuccess,
...@@ -49,6 +50,10 @@ import { ...@@ -49,6 +50,10 @@ import {
receiveDastDiffSuccess, receiveDastDiffSuccess,
receiveDastDiffError, receiveDastDiffError,
fetchDastDiff, fetchDastDiff,
setSecretScanningDiffEndpoint,
receiveSecretScanningDiffSuccess,
receiveSecretScanningDiffError,
fetchSecretScanningDiff,
} from 'ee/vue_shared/security_reports/store/actions'; } from 'ee/vue_shared/security_reports/store/actions';
import * as types from 'ee/vue_shared/security_reports/store/mutation_types'; import * as types from 'ee/vue_shared/security_reports/store/mutation_types';
import state from 'ee/vue_shared/security_reports/store/state'; import state from 'ee/vue_shared/security_reports/store/state';
...@@ -58,6 +63,7 @@ import { ...@@ -58,6 +63,7 @@ import {
dastFeedbacks, dastFeedbacks,
containerScanningFeedbacks, containerScanningFeedbacks,
dependencyScanningFeedbacks, dependencyScanningFeedbacks,
secretScanningFeedbacks,
} from '../mock_data'; } from '../mock_data';
import toasted from '~/vue_shared/plugins/global_toast'; import toasted from '~/vue_shared/plugins/global_toast';
...@@ -1029,6 +1035,26 @@ describe('security reports actions', () => { ...@@ -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', () => { describe('updateDastIssue', () => {
it('commits update dast issue', done => { it('commits update dast issue', done => {
const payload = { foo: 'bar' }; const payload = { foo: 'bar' };
...@@ -1520,4 +1546,162 @@ describe('security reports actions', () => { ...@@ -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 { ...@@ -4,6 +4,7 @@ import {
groupedContainerScanningText, groupedContainerScanningText,
groupedDastText, groupedDastText,
groupedDependencyText, groupedDependencyText,
groupedSecretScanningText,
groupedSummaryText, groupedSummaryText,
allReportsHaveError, allReportsHaveError,
noBaseInAllReports, noBaseInAllReports,
...@@ -253,6 +254,81 @@ describe('Security reports getters', () => { ...@@ -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', () => { describe('summaryCounts', () => {
it('returns 0 count for empty state', () => { it('returns 0 count for empty state', () => {
expect(summaryCounts(state)).toEqual({ expect(summaryCounts(state)).toEqual({
...@@ -495,6 +571,7 @@ describe('Security reports getters', () => { ...@@ -495,6 +571,7 @@ describe('Security reports getters', () => {
state.dast.hasError = true; state.dast.hasError = true;
state.containerScanning.hasError = true; state.containerScanning.hasError = true;
state.dependencyScanning.hasError = true; state.dependencyScanning.hasError = true;
state.secretScanning.hasError = true;
expect(allReportsHaveError(state)).toEqual(true); expect(allReportsHaveError(state)).toEqual(true);
}); });
...@@ -507,6 +584,7 @@ describe('Security reports getters', () => { ...@@ -507,6 +584,7 @@ describe('Security reports getters', () => {
state.dast.hasError = false; state.dast.hasError = false;
state.containerScanning.hasError = true; state.containerScanning.hasError = true;
state.dependencyScanning.hasError = true; state.dependencyScanning.hasError = true;
state.secretScanning.hasError = true;
expect(allReportsHaveError(state)).toEqual(false); expect(allReportsHaveError(state)).toEqual(false);
}); });
......
...@@ -79,6 +79,14 @@ describe('security reports mutations', () => { ...@@ -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', () => { describe('SET_ISSUE_MODAL_DATA', () => {
it('has default data', () => { it('has default data', () => {
expect(stateCopy.modal.vulnerability.isDismissed).toEqual(false); expect(stateCopy.modal.vulnerability.isDismissed).toEqual(false);
...@@ -468,6 +476,34 @@ describe('security reports mutations', () => { ...@@ -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', () => { describe('SET_CONTAINER_SCANNING_DIFF_ENDPOINT', () => {
const endpoint = 'container_scanning_diff_endpoint.json'; const endpoint = 'container_scanning_diff_endpoint.json';
...@@ -696,4 +732,70 @@ describe('security reports mutations', () => { ...@@ -696,4 +732,70 @@ describe('security reports mutations', () => {
expect(stateCopy.dast.hasError).toEqual(true); 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 { ...@@ -24,12 +24,14 @@ import {
dastDiffSuccessMock, dastDiffSuccessMock,
containerScanningDiffSuccessMock, containerScanningDiffSuccessMock,
dependencyScanningDiffSuccessMock, dependencyScanningDiffSuccessMock,
secretScanningDiffSuccessMock,
} from 'ee_spec/vue_shared/security_reports/mock_data'; } from 'ee_spec/vue_shared/security_reports/mock_data';
const SAST_SELECTOR = '.js-sast-widget'; const SAST_SELECTOR = '.js-sast-widget';
const DAST_SELECTOR = '.js-dast-widget'; const DAST_SELECTOR = '.js-dast-widget';
const DEPENDENCY_SCANNING_SELECTOR = '.js-dependency-scanning-widget'; const DEPENDENCY_SCANNING_SELECTOR = '.js-dependency-scanning-widget';
const CONTAINER_SCANNING_SELECTOR = '.js-container-scanning'; const CONTAINER_SCANNING_SELECTOR = '.js-container-scanning';
const SECRET_SCANNING_SELECTOR = '.js-secret-scanning';
describe('ee merge request widget options', () => { describe('ee merge request widget options', () => {
let vm; let vm;
...@@ -780,6 +782,80 @@ describe('ee merge request widget options', () => { ...@@ -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', () => { describe('license scanning report', () => {
const licenseManagementApiUrl = `${TEST_HOST}/manage_license_api`; const licenseManagementApiUrl = `${TEST_HOST}/manage_license_api`;
...@@ -1102,6 +1178,7 @@ describe('ee merge request widget options', () => { ...@@ -1102,6 +1178,7 @@ describe('ee merge request widget options', () => {
sast: false, sast: false,
container_scanning: false, container_scanning: false,
dependency_scanning: false, dependency_scanning: false,
secret_scanning: false,
}, },
]; ];
......
...@@ -7,6 +7,7 @@ import { ...@@ -7,6 +7,7 @@ import {
sastParsedIssues, sastParsedIssues,
dockerReportParsed, dockerReportParsed,
parsedDast, parsedDast,
secretScanningParsedIssues,
} from 'ee_spec/vue_shared/security_reports/mock_data'; } from 'ee_spec/vue_shared/security_reports/mock_data';
import { STATUS_FAILED, STATUS_SUCCESS } from '~/reports/constants'; import { STATUS_FAILED, STATUS_SUCCESS } from '~/reports/constants';
import reportIssues from '~/reports/components/report_item.vue'; import reportIssues from '~/reports/components/report_item.vue';
...@@ -123,4 +124,24 @@ describe('Report issues', () => { ...@@ -123,4 +124,24 @@ describe('Report issues', () => {
expect(vm.$el.textContent).toContain(`${parsedDast[0].severity}`); 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 { ...@@ -7,6 +7,7 @@ import {
sastParsedIssues, sastParsedIssues,
dockerReportParsed, dockerReportParsed,
parsedDast, parsedDast,
secretScanningParsedIssues,
} from 'ee_spec/vue_shared/security_reports/mock_data'; } from 'ee_spec/vue_shared/security_reports/mock_data';
import { STATUS_FAILED, STATUS_SUCCESS } from '~/reports/constants'; import { STATUS_FAILED, STATUS_SUCCESS } from '~/reports/constants';
import reportIssue from '~/reports/components/report_item.vue'; import reportIssue from '~/reports/components/report_item.vue';
...@@ -124,6 +125,26 @@ describe('Report issue', () => { ...@@ -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', () => { describe('showReportSectionStatusIcon', () => {
it('does not render CI Status Icon when showReportSectionStatusIcon is false', () => { it('does not render CI Status Icon when showReportSectionStatusIcon is false', () => {
vm = mountComponentWithStore(ReportIssue, { vm = mountComponentWithStore(ReportIssue, {
......
...@@ -7,8 +7,10 @@ export const { ...@@ -7,8 +7,10 @@ export const {
dockerReportParsed, dockerReportParsed,
parsedDast, parsedDast,
sastParsedIssues, sastParsedIssues,
secretScanningParsedIssues,
sastDiffSuccessMock, sastDiffSuccessMock,
dastDiffSuccessMock, dastDiffSuccessMock,
containerScanningDiffSuccessMock, containerScanningDiffSuccessMock,
dependencyScanningDiffSuccessMock, dependencyScanningDiffSuccessMock,
secretScanningDiffSuccessMock,
} = mockData; } = mockData;
...@@ -24239,6 +24239,9 @@ msgstr "" ...@@ -24239,6 +24239,9 @@ msgstr ""
msgid "ciReport|%{linkStartTag}Learn more about SAST %{linkEndTag}" msgid "ciReport|%{linkStartTag}Learn more about SAST %{linkEndTag}"
msgstr "" msgstr ""
msgid "ciReport|%{linkStartTag}Learn more about Secret Scanning %{linkEndTag}"
msgstr ""
msgid "ciReport|%{linkStartTag}Learn more about codequality reports %{linkEndTag}" msgid "ciReport|%{linkStartTag}Learn more about codequality reports %{linkEndTag}"
msgstr "" msgstr ""
...@@ -24402,6 +24405,12 @@ msgstr "" ...@@ -24402,6 +24405,12 @@ msgstr ""
msgid "ciReport|SAST" msgid "ciReport|SAST"
msgstr "" msgstr ""
msgid "ciReport|Secret scanning"
msgstr ""
msgid "ciReport|Secret scanning detects secrets and credentials vulnerabilities in your source code."
msgstr ""
msgid "ciReport|Security scanning" msgid "ciReport|Security scanning"
msgstr "" 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