Commit 4c4f0962 authored by Mark Florian's avatar Mark Florian Committed by David O'Regan

Create CE security_reports Vuex store

This creates a CE security_reports store that is a subset of the EE
version. This subset is roughly the minimum necessary to render counts
on the CE security MR widget. See this [epic][1] for more details about
the overall direction.

The majority of this change is just existing code that's been moved
and/or refactored. The only _genuinely_ new piece of code is the new
store's entry point:
`app/assets/javascripts/vue_shared/security_reports/store/index.js`.

Since the CE version only supports SAST and Secret Detection, this MR
also refactored the getters to generalise over an array of report types,
rather than hard code them all into every getter. This means that these
getters can be shared as-is between CE and EE, and behave correctly due
to the `reportTypes` field added to the state of both stores. These
getters are tested in both CE and EE, to verify the different behaviour
based on the available report types.

The `noBaseInAllReports` getter turned out not to be used anywhere, so
it was deleted.

Two existing getters, `anyReportHasIssues` and `areAllReportsLoading`
didn't have tests, so tests were added for them in both CE and EE.

Since the getters now more explicitly depend on the report types
installed in the store, constants were extracted to make their
connection explicit, and reduce the chance that typos would lead to
runtime errors.

The vulnerability severity constants were moved to a common location in
CE.

The definitions of the `LOADING`, `SUCCESS` and `ERROR` constants were
deleted from the `security_reports` directory, since their definitions
already exist in the `~/reports/constants.js` file. These constants are
intended to be used with/by the `ReportSection` component, so it makes
sense not to redefine them elsewhere.

Part of https://gitlab.com/gitlab-org/gitlab/-/issues/273423.

[1]: https://gitlab.com/groups/gitlab-org/-/epics/4394
parent b7c276c0
...@@ -18,9 +18,9 @@ export const ICON_SUCCESS = 'success'; ...@@ -18,9 +18,9 @@ export const ICON_SUCCESS = 'success';
export const ICON_NOTFOUND = 'notfound'; export const ICON_NOTFOUND = 'notfound';
export const status = { export const status = {
LOADING: 'LOADING', LOADING,
ERROR: 'ERROR', ERROR,
SUCCESS: 'SUCCESS', SUCCESS,
}; };
export const ACCESSIBILITY_ISSUE_ERROR = 'error'; export const ACCESSIBILITY_ISSUE_ERROR = 'error';
......
/**
* Vuex module names corresponding to security scan types. These are similar to
* the snake_case report types from the backend, but should not be considered
* to be equivalent.
*/
export const MODULE_SAST = 'sast';
export const MODULE_SECRET_DETECTION = 'secretDetection';
import { s__, sprintf } from '~/locale';
import { countVulnerabilities, groupedTextBuilder } from './utils';
import { LOADING, ERROR, SUCCESS } from '~/reports/constants';
import { TRANSLATION_IS_LOADING } from './messages';
export const summaryCounts = state =>
countVulnerabilities(
state.reportTypes.reduce((acc, reportType) => {
acc.push(...state[reportType].newIssues);
return acc;
}, []),
);
export const groupedSummaryText = (state, getters) => {
const reportType = s__('ciReport|Security scanning');
let status = '';
// All reports are loading
if (getters.areAllReportsLoading) {
return { message: sprintf(TRANSLATION_IS_LOADING, { reportType }) };
}
// All reports returned error
if (getters.allReportsHaveError) {
return { message: s__('ciReport|Security scanning failed loading any results') };
}
if (getters.areReportsLoading && getters.anyReportHasError) {
status = s__('ciReport|is loading, errors when loading results');
} else if (getters.areReportsLoading && !getters.anyReportHasError) {
status = s__('ciReport|is loading');
} else if (!getters.areReportsLoading && getters.anyReportHasError) {
status = s__('ciReport|: Loading resulted in an error');
}
const { critical, high, other } = getters.summaryCounts;
return groupedTextBuilder({ reportType, status, critical, high, other });
};
export const summaryStatus = (state, getters) => {
if (getters.areReportsLoading) {
return LOADING;
}
if (getters.anyReportHasError || getters.anyReportHasIssues) {
return ERROR;
}
return SUCCESS;
};
export const areReportsLoading = state =>
state.reportTypes.some(reportType => state[reportType].isLoading);
export const areAllReportsLoading = state =>
state.reportTypes.every(reportType => state[reportType].isLoading);
export const allReportsHaveError = state =>
state.reportTypes.every(reportType => state[reportType].hasError);
export const anyReportHasError = state =>
state.reportTypes.some(reportType => state[reportType].hasError);
export const anyReportHasIssues = state =>
state.reportTypes.some(reportType => state[reportType].newIssues.length > 0);
import Vuex from 'vuex';
import * as getters from './getters';
import state from './state';
import { MODULE_SAST, MODULE_SECRET_DETECTION } from './constants';
import sast from './modules/sast';
import secretDetection from './modules/secret_detection';
export default () =>
new Vuex.Store({
modules: {
[MODULE_SAST]: sast,
[MODULE_SECRET_DETECTION]: secretDetection,
},
getters,
state,
});
import { s__ } from '~/locale';
export const TRANSLATION_IS_LOADING = s__('ciReport|%{reportType} is loading');
export const TRANSLATION_HAS_ERROR = s__('ciReport|%{reportType}: Loading resulted in an error');
import { MODULE_SAST, MODULE_SECRET_DETECTION } from './constants';
export default () => ({
reportTypes: [MODULE_SAST, MODULE_SECRET_DETECTION],
});
import pollUntilComplete from '~/lib/utils/poll_until_complete'; import pollUntilComplete from '~/lib/utils/poll_until_complete';
import axios from '~/lib/utils/axios_utils'; import axios from '~/lib/utils/axios_utils';
import { __, n__, sprintf } from '~/locale';
import { CRITICAL, HIGH } from '~/vulnerabilities/constants';
import { import {
FEEDBACK_TYPE_DISMISSAL, FEEDBACK_TYPE_DISMISSAL,
FEEDBACK_TYPE_ISSUE, FEEDBACK_TYPE_ISSUE,
...@@ -73,3 +75,79 @@ export const parseDiff = (diff, enrichData) => { ...@@ -73,3 +75,79 @@ export const parseDiff = (diff, enrichData) => {
existing: diff.existing ? diff.existing.map(enrichVulnerability) : [], existing: diff.existing ? diff.existing.map(enrichVulnerability) : [],
}; };
}; };
const createCountMessage = ({ critical, high, other, total }) => {
const otherMessage = n__('%d Other', '%d Others', other);
const countMessage = __(
'%{criticalStart}%{critical} Critical%{criticalEnd} %{highStart}%{high} High%{highEnd} and %{otherStart}%{otherMessage}%{otherEnd}',
);
return total ? sprintf(countMessage, { critical, high, otherMessage }) : '';
};
const createStatusMessage = ({ reportType, status, total }) => {
const vulnMessage = n__('vulnerability', 'vulnerabilities', total);
let message;
if (status) {
message = __('%{reportType} %{status}');
} else if (!total) {
message = __('%{reportType} detected %{totalStart}no%{totalEnd} vulnerabilities.');
} else {
message = __(
'%{reportType} detected %{totalStart}%{total}%{totalEnd} potential %{vulnMessage}',
);
}
return sprintf(message, { reportType, status, total, vulnMessage });
};
/**
* Counts vulnerabilities.
* Returns the amount of critical, high, and other vulnerabilities.
*
* @param {Array} vulnerabilities The raw vulnerabilities to parse
* @returns {{critical: number, high: number, other: number}}
*/
export const countVulnerabilities = (vulnerabilities = []) =>
vulnerabilities.reduce(
(acc, { severity }) => {
if (severity === CRITICAL) {
acc.critical += 1;
} else if (severity === HIGH) {
acc.high += 1;
} else {
acc.other += 1;
}
return acc;
},
{ critical: 0, high: 0, other: 0 },
);
/**
* Takes an object of options and returns the object with an externalized string representing
* the critical, high, and other severity vulnerabilities for a given report.
*
* The resulting string _may_ still contain sprintf-style placeholders. These
* are left in place so they can be replaced with markup, via the
* SecuritySummary component.
* @param {{reportType: string, status: string, critical: number, high: number, other: number}} options
* @returns {Object} the parameters with an externalized string
*/
export const groupedTextBuilder = ({
reportType = '',
status = '',
critical = 0,
high = 0,
other = 0,
} = {}) => {
const total = critical + high + other;
return {
countMessage: createCountMessage({ critical, high, other, total }),
message: createStatusMessage({ reportType, status, total }),
critical,
high,
other,
status,
total,
};
};
/**
* Vulnerability severities as provided by the backend on vulnerability
* objects.
*/
export const CRITICAL = 'critical';
export const HIGH = 'high';
export const MEDIUM = 'medium';
export const LOW = 'low';
export const INFO = 'info';
export const UNKNOWN = 'unknown';
/**
* All vulnerability severities in decreasing order.
*/
export const SEVERITIES = [CRITICAL, HIGH, MEDIUM, LOW, INFO, UNKNOWN];
import httpStatusCodes from '~/lib/utils/http_status'; import httpStatusCodes from '~/lib/utils/http_status';
export const CRITICAL = 'critical'; export {
export const HIGH = 'high'; CRITICAL,
export const MEDIUM = 'medium'; HIGH,
export const LOW = 'low'; MEDIUM,
export const INFO = 'info'; LOW,
export const UNKNOWN = 'unknown'; INFO,
export const SEVERITIES = [CRITICAL, HIGH, MEDIUM, LOW, INFO, UNKNOWN]; UNKNOWN,
SEVERITIES,
} from '~/vulnerabilities/constants';
export const DAYS = { export const DAYS = {
THIRTY: 30, THIRTY: 30,
......
...@@ -234,6 +234,8 @@ export default { ...@@ -234,6 +234,8 @@ export default {
}; };
}, },
}, },
// TODO: Use the snake_case report types rather than the camelCased versions
// of them. See https://gitlab.com/gitlab-org/gitlab/-/issues/282430
securityReportTypes: [ securityReportTypes: [
'dast', 'dast',
'sast', 'sast',
......
...@@ -3,7 +3,6 @@ import { mapActions, mapState, mapGetters } from 'vuex'; ...@@ -3,7 +3,6 @@ import { mapActions, mapState, mapGetters } from 'vuex';
import { once } from 'lodash'; import { once } from 'lodash';
import { componentNames } from 'ee/reports/components/issue_body'; import { componentNames } from 'ee/reports/components/issue_body';
import { GlButton, GlSprintf, GlLink, GlModalDirective } from '@gitlab/ui'; import { GlButton, GlSprintf, GlLink, GlModalDirective } from '@gitlab/ui';
import { trackMrSecurityReportDetails } from 'ee/vue_shared/security_reports/store/constants';
import FuzzingArtifactsDownload from 'ee/security_dashboard/components/fuzzing_artifacts_download.vue'; import FuzzingArtifactsDownload from 'ee/security_dashboard/components/fuzzing_artifacts_download.vue';
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin'; import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import ReportSection from '~/reports/components/report_section.vue'; import ReportSection from '~/reports/components/report_section.vue';
...@@ -18,6 +17,15 @@ import { mrStates } from '~/mr_popover/constants'; ...@@ -18,6 +17,15 @@ import { mrStates } from '~/mr_popover/constants';
import { fetchPolicies } from '~/lib/graphql'; import { fetchPolicies } from '~/lib/graphql';
import securityReportSummaryQuery from './graphql/mr_security_report_summary.graphql'; import securityReportSummaryQuery from './graphql/mr_security_report_summary.graphql';
import SecuritySummary from './components/security_summary.vue'; import SecuritySummary from './components/security_summary.vue';
import {
MODULE_CONTAINER_SCANNING,
MODULE_COVERAGE_FUZZING,
MODULE_DAST,
MODULE_DEPENDENCY_SCANNING,
MODULE_SAST,
MODULE_SECRET_DETECTION,
trackMrSecurityReportDetails,
} from './store/constants';
export default { export default {
store: createStore(), store: createStore(),
...@@ -186,12 +194,12 @@ export default { ...@@ -186,12 +194,12 @@ export default {
componentNames, componentNames,
computed: { computed: {
...mapState([ ...mapState([
'sast', MODULE_SAST,
'containerScanning', MODULE_CONTAINER_SCANNING,
'dast', MODULE_DAST,
'coverageFuzzing', MODULE_COVERAGE_FUZZING,
'dependencyScanning', MODULE_DEPENDENCY_SCANNING,
'secretDetection', MODULE_SECRET_DETECTION,
'summaryCounts', 'summaryCounts',
'modal', 'modal',
'isCreatingIssue', 'isCreatingIssue',
...@@ -214,8 +222,11 @@ export default { ...@@ -214,8 +222,11 @@ export default {
'canCreateMergeRequest', 'canCreateMergeRequest',
'canDismissVulnerability', 'canDismissVulnerability',
]), ]),
...mapGetters('sast', ['groupedSastText', 'sastStatusIcon']), ...mapGetters(MODULE_SAST, ['groupedSastText', 'sastStatusIcon']),
...mapGetters('secretDetection', ['groupedSecretDetectionText', 'secretDetectionStatusIcon']), ...mapGetters(MODULE_SECRET_DETECTION, [
'groupedSecretDetectionText',
'secretDetectionStatusIcon',
]),
...mapGetters('pipelineJobs', ['hasFuzzingArtifacts', 'fuzzingJobsWithArtifact']), ...mapGetters('pipelineJobs', ['hasFuzzingArtifacts', 'fuzzingJobsWithArtifact']),
securityTab() { securityTab() {
return `${this.pipelinePath}/security`; return `${this.pipelinePath}/security`;
...@@ -258,22 +269,22 @@ export default { ...@@ -258,22 +269,22 @@ export default {
return this.dastSummary?.scannedResourcesCsvPath || ''; return this.dastSummary?.scannedResourcesCsvPath || '';
}, },
hasCoverageFuzzingIssues() { hasCoverageFuzzingIssues() {
return this.hasIssuesForReportType('coverageFuzzing'); return this.hasIssuesForReportType(MODULE_COVERAGE_FUZZING);
}, },
hasSastIssues() { hasSastIssues() {
return this.hasIssuesForReportType('sast'); return this.hasIssuesForReportType(MODULE_SAST);
}, },
hasDependencyScanningIssues() { hasDependencyScanningIssues() {
return this.hasIssuesForReportType('dependencyScanning'); return this.hasIssuesForReportType(MODULE_DEPENDENCY_SCANNING);
}, },
hasContainerScanningIssues() { hasContainerScanningIssues() {
return this.hasIssuesForReportType('containerScanning'); return this.hasIssuesForReportType(MODULE_CONTAINER_SCANNING);
}, },
hasDastIssues() { hasDastIssues() {
return this.hasIssuesForReportType('dast'); return this.hasIssuesForReportType(MODULE_DAST);
}, },
hasSecretDetectionIssues() { hasSecretDetectionIssues() {
return this.hasIssuesForReportType('secretDetection'); return this.hasIssuesForReportType(MODULE_SECRET_DETECTION);
}, },
}, },
...@@ -369,11 +380,11 @@ export default { ...@@ -369,11 +380,11 @@ export default {
'fetchCoverageFuzzingDiff', 'fetchCoverageFuzzingDiff',
'setCoverageFuzzingDiffEndpoint', 'setCoverageFuzzingDiffEndpoint',
]), ]),
...mapActions('sast', { ...mapActions(MODULE_SAST, {
setSastDiffEndpoint: 'setDiffEndpoint', setSastDiffEndpoint: 'setDiffEndpoint',
fetchSastDiff: 'fetchDiff', fetchSastDiff: 'fetchDiff',
}), }),
...mapActions('secretDetection', { ...mapActions(MODULE_SECRET_DETECTION, {
setSecretDetectionDiffEndpoint: 'setDiffEndpoint', setSecretDetectionDiffEndpoint: 'setDiffEndpoint',
fetchSecretDetectionDiff: 'fetchDiff', fetchSecretDetectionDiff: 'fetchDiff',
}), }),
......
import { LOADING, ERROR, SUCCESS } from '../store/constants'; import { LOADING, ERROR, SUCCESS } from '~/reports/constants';
export default { export default {
methods: { methods: {
......
export const LOADING = 'LOADING'; export * from '~/vue_shared/security_reports/store/constants';
export const ERROR = 'ERROR';
export const SUCCESS = 'SUCCESS'; /**
* Vuex module names corresponding to security scan types. These are similar to
* the snake_case report types from the backend, but should not be considered
* to be equivalent.
*
* These aren't technically Vuex modules yet, but they do correspond to
* namespaces in the store state, as if they were modules.
*/
export const MODULE_CONTAINER_SCANNING = 'containerScanning';
export const MODULE_COVERAGE_FUZZING = 'coverageFuzzing';
export const MODULE_DAST = 'dast';
export const MODULE_DEPENDENCY_SCANNING = 'dependencyScanning';
/** /**
* Tracks snowplow event when user views report details * Tracks snowplow event when user views report details
......
import { s__, sprintf } from '~/locale'; import { statusIcon, groupedReportText } from './utils';
import { countVulnerabilities, groupedTextBuilder, statusIcon, groupedReportText } from './utils';
import { LOADING, ERROR, SUCCESS } from './constants';
import messages from './messages'; import messages from './messages';
export {
allReportsHaveError,
anyReportHasError,
anyReportHasIssues,
areAllReportsLoading,
areReportsLoading,
groupedSummaryText,
summaryCounts,
summaryStatus,
} from '~/vue_shared/security_reports/store/getters';
export const groupedContainerScanningText = ({ containerScanning }) => export const groupedContainerScanningText = ({ containerScanning }) =>
groupedReportText( groupedReportText(
containerScanning, containerScanning,
...@@ -30,65 +39,6 @@ export const groupedCoverageFuzzingText = ({ coverageFuzzing }) => ...@@ -30,65 +39,6 @@ export const groupedCoverageFuzzingText = ({ coverageFuzzing }) =>
messages.COVERAGE_FUZZING_IS_LOADING, messages.COVERAGE_FUZZING_IS_LOADING,
); );
export const summaryCounts = ({
containerScanning,
dast,
dependencyScanning,
sast,
secretDetection,
coverageFuzzing,
} = {}) => {
const allNewVulns = [
...containerScanning.newIssues,
...dast.newIssues,
...dependencyScanning.newIssues,
...sast.newIssues,
...secretDetection.newIssues,
...coverageFuzzing.newIssues,
];
return countVulnerabilities(allNewVulns);
};
export const groupedSummaryText = (state, getters) => {
const reportType = s__('ciReport|Security scanning');
let status = '';
// All reports are loading
if (getters.areAllReportsLoading) {
return { message: sprintf(messages.TRANSLATION_IS_LOADING, { reportType }) };
}
// All reports returned error
if (getters.allReportsHaveError) {
return { message: s__('ciReport|Security scanning failed loading any results') };
}
if (getters.areReportsLoading && getters.anyReportHasError) {
status = s__('ciReport|is loading, errors when loading results');
} else if (getters.areReportsLoading && !getters.anyReportHasError) {
status = s__('ciReport|is loading');
} else if (!getters.areReportsLoading && getters.anyReportHasError) {
status = s__('ciReport|: Loading resulted in an error');
}
const { critical, high, other } = getters.summaryCounts;
return groupedTextBuilder({ reportType, status, critical, high, other });
};
export const summaryStatus = (state, getters) => {
if (getters.areReportsLoading) {
return LOADING;
}
if (getters.anyReportHasError || getters.anyReportHasIssues) {
return ERROR;
}
return SUCCESS;
};
export const containerScanningStatusIcon = ({ containerScanning }) => export const containerScanningStatusIcon = ({ containerScanning }) =>
statusIcon( statusIcon(
containerScanning.isLoading, containerScanning.isLoading,
...@@ -109,61 +59,8 @@ export const dependencyScanningStatusIcon = ({ dependencyScanning }) => ...@@ -109,61 +59,8 @@ export const dependencyScanningStatusIcon = ({ dependencyScanning }) =>
export const coverageFuzzingStatusIcon = ({ coverageFuzzing }) => export const coverageFuzzingStatusIcon = ({ coverageFuzzing }) =>
statusIcon(coverageFuzzing.isLoading, coverageFuzzing.hasError, coverageFuzzing.newIssues.length); statusIcon(coverageFuzzing.isLoading, coverageFuzzing.hasError, coverageFuzzing.newIssues.length);
export const areReportsLoading = state =>
state.sast.isLoading ||
state.dast.isLoading ||
state.containerScanning.isLoading ||
state.dependencyScanning.isLoading ||
state.secretDetection.isLoading ||
state.coverageFuzzing.isLoading;
export const areAllReportsLoading = state =>
state.sast.isLoading &&
state.dast.isLoading &&
state.containerScanning.isLoading &&
state.dependencyScanning.isLoading &&
state.secretDetection.isLoading &&
state.coverageFuzzing.isLoading;
export const allReportsHaveError = state =>
state.sast.hasError &&
state.dast.hasError &&
state.containerScanning.hasError &&
state.dependencyScanning.hasError &&
state.secretDetection.hasError &&
state.coverageFuzzing.hasError;
export const anyReportHasError = state =>
state.sast.hasError ||
state.dast.hasError ||
state.containerScanning.hasError ||
state.dependencyScanning.hasError ||
state.secretDetection.hasError ||
state.coverageFuzzing.hasError;
export const noBaseInAllReports = state =>
!state.sast.hasBaseReport &&
!state.dast.hasBaseReport &&
!state.containerScanning.hasBaseReport &&
!state.dependencyScanning.hasBaseReport &&
!state.secretDetection.hasBaseReport &&
!state.coverageFuzzing.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.secretDetection.newIssues.length > 0 ||
state.coverageFuzzing.newIssues.length > 0;
export const isBaseSecurityReportOutOfDate = state => export const isBaseSecurityReportOutOfDate = state =>
state.sast.baseReportOutofDate || state.reportTypes.some(reportType => state[reportType].baseReportOutofDate);
state.dast.baseReportOutofDate ||
state.containerScanning.baseReportOutofDate ||
state.dependencyScanning.baseReportOutofDate ||
state.secretDetection.baseReportOutofDate ||
state.coverageFuzzing.baseReportOutofDate;
export const canCreateIssue = state => Boolean(state.createVulnerabilityFeedbackIssuePath); export const canCreateIssue = state => Boolean(state.createVulnerabilityFeedbackIssuePath);
......
...@@ -6,6 +6,7 @@ import * as actions from './actions'; ...@@ -6,6 +6,7 @@ import * as actions from './actions';
import * as getters from './getters'; import * as getters from './getters';
import mutations from './mutations'; import mutations from './mutations';
import state from './state'; import state from './state';
import { MODULE_SAST, MODULE_SECRET_DETECTION } from './constants';
import sast from './modules/sast'; import sast from './modules/sast';
import secretDetection from './modules/secret_detection'; import secretDetection from './modules/secret_detection';
...@@ -15,8 +16,8 @@ Vue.use(Vuex); ...@@ -15,8 +16,8 @@ Vue.use(Vuex);
export default () => export default () =>
new Vuex.Store({ new Vuex.Store({
modules: { modules: {
sast, [MODULE_SAST]: sast,
secretDetection, [MODULE_SECRET_DETECTION]: secretDetection,
pipelineJobs, pipelineJobs,
}, },
actions, actions,
......
import * as types from './mutation_types'; import * as types from './mutation_types';
import { MODULE_SAST, MODULE_SECRET_DETECTION } from './constants';
export const updateIssueActionsMap = { export const updateIssueActionsMap = {
sast: 'sast/updateVulnerability', sast: `${MODULE_SAST}/updateVulnerability`,
dependency_scanning: 'updateDependencyScanningIssue', dependency_scanning: 'updateDependencyScanningIssue',
container_scanning: 'updateContainerScanningIssue', container_scanning: 'updateContainerScanningIssue',
dast: 'updateDastIssue', dast: 'updateDastIssue',
secret_detection: 'secretDetection/updateVulnerability', secret_detection: `${MODULE_SECRET_DETECTION}/updateVulnerability`,
coverage_fuzzing: 'updateCoverageFuzzingIssue', coverage_fuzzing: 'updateCoverageFuzzingIssue',
}; };
......
import { s__, sprintf } from '~/locale'; import { s__, sprintf } from '~/locale';
import {
const TRANSLATION_IS_LOADING = s__('ciReport|%{reportType} is loading'); TRANSLATION_IS_LOADING,
const TRANSLATION_HAS_ERROR = s__('ciReport|%{reportType}: Loading resulted in an error'); TRANSLATION_HAS_ERROR,
} from '~/vue_shared/security_reports/store/messages';
const SAST = s__('ciReport|SAST'); const SAST = s__('ciReport|SAST');
const DAST = s__('ciReport|DAST'); const DAST = s__('ciReport|DAST');
......
import {
MODULE_CONTAINER_SCANNING,
MODULE_COVERAGE_FUZZING,
MODULE_DAST,
MODULE_DEPENDENCY_SCANNING,
MODULE_SAST,
MODULE_SECRET_DETECTION,
} from './constants';
export default () => ({ export default () => ({
blobPath: { blobPath: {
head: null, head: null,
...@@ -13,7 +22,16 @@ export default () => ({ ...@@ -13,7 +22,16 @@ export default () => ({
createVulnerabilityFeedbackDismissalPath: null, createVulnerabilityFeedbackDismissalPath: null,
pipelineId: null, pipelineId: null,
containerScanning: { reportTypes: [
MODULE_CONTAINER_SCANNING,
MODULE_COVERAGE_FUZZING,
MODULE_DAST,
MODULE_DEPENDENCY_SCANNING,
MODULE_SAST,
MODULE_SECRET_DETECTION,
],
[MODULE_CONTAINER_SCANNING]: {
paths: { paths: {
head: null, head: null,
base: null, base: null,
...@@ -28,7 +46,7 @@ export default () => ({ ...@@ -28,7 +46,7 @@ export default () => ({
baseReportOutofDate: false, baseReportOutofDate: false,
hasBaseReport: false, hasBaseReport: false,
}, },
dast: { [MODULE_DAST]: {
paths: { paths: {
head: null, head: null,
base: null, base: null,
...@@ -44,7 +62,7 @@ export default () => ({ ...@@ -44,7 +62,7 @@ export default () => ({
hasBaseReport: false, hasBaseReport: false,
scans: [], scans: [],
}, },
coverageFuzzing: { [MODULE_COVERAGE_FUZZING]: {
paths: { paths: {
head: null, head: null,
base: null, base: null,
...@@ -60,7 +78,7 @@ export default () => ({ ...@@ -60,7 +78,7 @@ export default () => ({
baseReportOutofDate: false, baseReportOutofDate: false,
hasBaseReport: false, hasBaseReport: false,
}, },
dependencyScanning: { [MODULE_DEPENDENCY_SCANNING]: {
paths: { paths: {
head: null, head: null,
base: null, base: null,
......
import { CRITICAL, HIGH } from 'ee/security_dashboard/store/modules/vulnerabilities/constants'; import {
import { __, n__, sprintf } from '~/locale'; groupedTextBuilder,
countVulnerabilities,
} from '~/vue_shared/security_reports/store/utils';
export { groupedTextBuilder, countVulnerabilities };
/** /**
* Returns the index of an issue in given list * Returns the index of an issue in given list
...@@ -9,59 +13,6 @@ import { __, n__, sprintf } from '~/locale'; ...@@ -9,59 +13,6 @@ import { __, n__, sprintf } from '~/locale';
export const findIssueIndex = (issues, issue) => export const findIssueIndex = (issues, issue) =>
issues.findIndex(el => el.project_fingerprint === issue.project_fingerprint); issues.findIndex(el => el.project_fingerprint === issue.project_fingerprint);
const createCountMessage = ({ critical, high, other, total }) => {
const otherMessage = n__('%d Other', '%d Others', other);
const countMessage = __(
'%{criticalStart}%{critical} Critical%{criticalEnd} %{highStart}%{high} High%{highEnd} and %{otherStart}%{otherMessage}%{otherEnd}',
);
return total ? sprintf(countMessage, { critical, high, otherMessage }) : '';
};
const createStatusMessage = ({ reportType, status, total }) => {
const vulnMessage = n__('vulnerability', 'vulnerabilities', total);
let message;
if (status) {
message = __('%{reportType} %{status}');
} else if (!total) {
message = __('%{reportType} detected %{totalStart}no%{totalEnd} vulnerabilities.');
} else {
message = __(
'%{reportType} detected %{totalStart}%{total}%{totalEnd} potential %{vulnMessage}',
);
}
return sprintf(message, { reportType, status, total, vulnMessage });
};
/**
* Takes an object of options and returns the object with an externalized string representing
* the critical, high, and other severity vulnerabilities for a given report.
*
* The resulting string _may_ still contain sprintf-style placeholders. These
* are left in place so they can be replaced with markup, via the
* SecuritySummary component.
* @param {{reportType: string, status: string, critical: number, high: number, other: number}} options
* @returns {Object} the parameters with an externalized string
*/
export const groupedTextBuilder = ({
reportType = '',
status = '',
critical = 0,
high = 0,
other = 0,
} = {}) => {
const total = critical + high + other;
return {
countMessage: createCountMessage({ critical, high, other, total }),
message: createStatusMessage({ reportType, status, total }),
critical,
high,
other,
status,
total,
};
};
export const statusIcon = (loading = false, failed = false, newIssues = 0, neutralIssues = 0) => { export const statusIcon = (loading = false, failed = false, newIssues = 0, neutralIssues = 0) => {
if (loading) { if (loading) {
return 'loading'; return 'loading';
...@@ -74,25 +25,6 @@ export const statusIcon = (loading = false, failed = false, newIssues = 0, neutr ...@@ -74,25 +25,6 @@ export const statusIcon = (loading = false, failed = false, newIssues = 0, neutr
return 'success'; return 'success';
}; };
/**
* Counts vulnerabilities.
* Returns the amount of critical, high, and other vulnerabilities.
*
* @param {Array} vulnerabilities The raw vulnerabilities to parse
* @returns {{critical: number, high: number, other: number}}
*/
export const countVulnerabilities = (vulnerabilities = []) => {
const critical = vulnerabilities.filter(vuln => vuln.severity === CRITICAL).length;
const high = vulnerabilities.filter(vuln => vuln.severity === HIGH).length;
const other = vulnerabilities.length - critical - high;
return {
critical,
high,
other,
};
};
/** /**
* Generates a report message based on some of the report parameters and supplied messages. * Generates a report message based on some of the report parameters and supplied messages.
* *
......
import { shallowMount, createLocalVue } from '@vue/test-utils'; import { shallowMount, createLocalVue } from '@vue/test-utils';
import Vuex from 'vuex'; import Vuex from 'vuex';
import LicenseManagement from 'ee/vue_shared/license_compliance/mr_widget_license_report.vue'; import LicenseManagement from 'ee/vue_shared/license_compliance/mr_widget_license_report.vue';
import { LOADING, ERROR, SUCCESS } from 'ee/vue_shared/security_reports/store/constants';
import { TEST_HOST } from 'spec/test_constants'; import { TEST_HOST } from 'spec/test_constants';
import ReportItem from '~/reports/components/report_item.vue'; import ReportItem from '~/reports/components/report_item.vue';
import ReportSection from '~/reports/components/report_section.vue'; import ReportSection from '~/reports/components/report_section.vue';
import { LOADING, ERROR, SUCCESS } from '~/reports/constants';
import { import {
approvedLicense, approvedLicense,
blacklistedLicense, blacklistedLicense,
......
...@@ -11,12 +11,13 @@ import { ...@@ -11,12 +11,13 @@ import {
groupedCoverageFuzzingText, groupedCoverageFuzzingText,
groupedSummaryText, groupedSummaryText,
allReportsHaveError, allReportsHaveError,
noBaseInAllReports,
areReportsLoading, areReportsLoading,
areAllReportsLoading,
containerScanningStatusIcon, containerScanningStatusIcon,
dastStatusIcon, dastStatusIcon,
dependencyScanningStatusIcon, dependencyScanningStatusIcon,
anyReportHasError, anyReportHasError,
anyReportHasIssues,
summaryCounts, summaryCounts,
isBaseSecurityReportOutOfDate, isBaseSecurityReportOutOfDate,
canCreateIssue, canCreateIssue,
...@@ -214,6 +215,29 @@ describe('Security reports getters', () => { ...@@ -214,6 +215,29 @@ describe('Security reports getters', () => {
}); });
}); });
describe('areAllReportsLoading', () => {
it('returns true when all reports are loading', () => {
state.sast.isLoading = true;
state.dast.isLoading = true;
state.containerScanning.isLoading = true;
state.dependencyScanning.isLoading = true;
state.secretDetection.isLoading = true;
state.coverageFuzzing.isLoading = true;
expect(areAllReportsLoading(state)).toEqual(true);
});
it('returns false when some of the reports are loading', () => {
state.sast.isLoading = true;
expect(areAllReportsLoading(state)).toEqual(false);
});
it('returns false when none of the reports are loading', () => {
expect(areAllReportsLoading(state)).toEqual(false);
});
});
describe('allReportsHaveError', () => { describe('allReportsHaveError', () => {
it('returns true when all reports have error', () => { it('returns true when all reports have error', () => {
state.sast.hasError = true; state.sast.hasError = true;
...@@ -252,15 +276,15 @@ describe('Security reports getters', () => { ...@@ -252,15 +276,15 @@ describe('Security reports getters', () => {
}); });
}); });
describe('noBaseInAllReports', () => { describe('anyReportHasIssues', () => {
it('returns true when none reports have base', () => { it('returns true when any of the reports has new issues', () => {
expect(noBaseInAllReports(state)).toEqual(true); state.dast.newIssues.push(generateVuln(LOW));
});
it('returns false when any of the reports has a base', () => { expect(anyReportHasIssues(state)).toEqual(true);
state.dast.hasBaseReport = true; });
expect(noBaseInAllReports(state)).toEqual(false); it('returns false when none of the reports has error', () => {
expect(anyReportHasIssues(state)).toEqual(false);
}); });
}); });
......
import createState from '~/vue_shared/security_reports/store/state';
import createSastState from '~/vue_shared/security_reports/store/modules/sast/state';
import createSecretScanningState from '~/vue_shared/security_reports/store/modules/secret_detection/state';
import { groupedTextBuilder } from '~/vue_shared/security_reports/store/utils';
import {
groupedSummaryText,
allReportsHaveError,
areReportsLoading,
anyReportHasError,
areAllReportsLoading,
anyReportHasIssues,
summaryCounts,
} from '~/vue_shared/security_reports/store/getters';
import { CRITICAL, HIGH, LOW } from '~/vulnerabilities/constants';
const generateVuln = severity => ({ severity });
describe('Security reports getters', () => {
let state;
beforeEach(() => {
state = createState();
state.sast = createSastState();
state.secretDetection = createSecretScanningState();
});
describe('summaryCounts', () => {
it('returns 0 count for empty state', () => {
expect(summaryCounts(state)).toEqual({
critical: 0,
high: 0,
other: 0,
});
});
describe('combines all reports', () => {
it('of the same severity', () => {
state.sast.newIssues = [generateVuln(CRITICAL)];
state.secretDetection.newIssues = [generateVuln(CRITICAL)];
expect(summaryCounts(state)).toEqual({
critical: 2,
high: 0,
other: 0,
});
});
it('of different severities', () => {
state.sast.newIssues = [generateVuln(CRITICAL)];
state.secretDetection.newIssues = [generateVuln(HIGH), generateVuln(LOW)];
expect(summaryCounts(state)).toEqual({
critical: 1,
high: 1,
other: 1,
});
});
});
});
describe('groupedSummaryText', () => {
it('returns failed text', () => {
expect(
groupedSummaryText(state, {
allReportsHaveError: true,
areReportsLoading: false,
summaryCounts: {},
}),
).toEqual({ message: 'Security scanning failed loading any results' });
});
it('returns `is loading` as status text', () => {
expect(
groupedSummaryText(state, {
allReportsHaveError: false,
areReportsLoading: true,
summaryCounts: {},
}),
).toEqual(
groupedTextBuilder({
reportType: 'Security scanning',
critical: 0,
high: 0,
other: 0,
status: 'is loading',
}),
);
});
it('returns no new status text if there are existing ones', () => {
expect(
groupedSummaryText(state, {
allReportsHaveError: false,
areReportsLoading: false,
summaryCounts: {},
}),
).toEqual(
groupedTextBuilder({
reportType: 'Security scanning',
critical: 0,
high: 0,
other: 0,
status: '',
}),
);
});
});
describe('areReportsLoading', () => {
it('returns true when any report is loading', () => {
state.sast.isLoading = true;
expect(areReportsLoading(state)).toEqual(true);
});
it('returns false when none of the reports are loading', () => {
expect(areReportsLoading(state)).toEqual(false);
});
});
describe('areAllReportsLoading', () => {
it('returns true when all reports are loading', () => {
state.sast.isLoading = true;
state.secretDetection.isLoading = true;
expect(areAllReportsLoading(state)).toEqual(true);
});
it('returns false when some of the reports are loading', () => {
state.sast.isLoading = true;
expect(areAllReportsLoading(state)).toEqual(false);
});
it('returns false when none of the reports are loading', () => {
expect(areAllReportsLoading(state)).toEqual(false);
});
});
describe('allReportsHaveError', () => {
it('returns true when all reports have error', () => {
state.sast.hasError = true;
state.secretDetection.hasError = true;
expect(allReportsHaveError(state)).toEqual(true);
});
it('returns false when none of the reports have error', () => {
expect(allReportsHaveError(state)).toEqual(false);
});
it('returns false when one of the reports does not have error', () => {
state.secretDetection.hasError = true;
expect(allReportsHaveError(state)).toEqual(false);
});
});
describe('anyReportHasError', () => {
it('returns true when any of the reports has error', () => {
state.sast.hasError = true;
expect(anyReportHasError(state)).toEqual(true);
});
it('returns false when none of the reports has error', () => {
expect(anyReportHasError(state)).toEqual(false);
});
});
describe('anyReportHasIssues', () => {
it('returns true when any of the reports has new issues', () => {
state.sast.newIssues.push(generateVuln(LOW));
expect(anyReportHasIssues(state)).toEqual(true);
});
it('returns false when none of the reports has error', () => {
expect(anyReportHasIssues(state)).toEqual(false);
});
});
});
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