Commit c4eb1b3f authored by David O'Regan's avatar David O'Regan

Merge branch '273423-implement-ce-vuex-store' into 'master'

Implement CE security_reports Vuex store

See merge request gitlab-org/gitlab!47556
parents c39c1546 4c4f0962
...@@ -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