Commit 523df990 authored by Mark Florian's avatar Mark Florian

Merge branch '277341-api-fuzzing-mr-widget' into 'master'

Implement API Fuzzing MR Widget and Vuln Modal

See merge request gitlab-org/gitlab!48123
parents c0b25778 445301cd
...@@ -238,6 +238,7 @@ export default { ...@@ -238,6 +238,7 @@ export default {
'dependencyScanning', 'dependencyScanning',
'containerScanning', 'containerScanning',
'coverageFuzzing', 'coverageFuzzing',
'apiFuzzing',
'secretDetection', 'secretDetection',
], ],
}; };
...@@ -327,6 +328,7 @@ export default { ...@@ -327,6 +328,7 @@ export default {
:enabled-reports="mr.enabledReports" :enabled-reports="mr.enabledReports"
:sast-help-path="mr.sastHelp" :sast-help-path="mr.sastHelp"
:dast-help-path="mr.dastHelp" :dast-help-path="mr.dastHelp"
:api-fuzzing-help-path="mr.apiFuzzingHelp"
:coverage-fuzzing-help-path="mr.coverageFuzzingHelp" :coverage-fuzzing-help-path="mr.coverageFuzzingHelp"
:container-scanning-help-path="mr.containerScanningHelp" :container-scanning-help-path="mr.containerScanningHelp"
:dependency-scanning-help-path="mr.dependencyScanningHelp" :dependency-scanning-help-path="mr.dependencyScanningHelp"
...@@ -348,6 +350,7 @@ export default { ...@@ -348,6 +350,7 @@ export default {
:target-branch-tree-path="mr.targetBranchTreePath" :target-branch-tree-path="mr.targetBranchTreePath"
:new-pipeline-path="mr.newPipelinePath" :new-pipeline-path="mr.newPipelinePath"
:container-scanning-comparison-path="mr.containerScanningComparisonPath" :container-scanning-comparison-path="mr.containerScanningComparisonPath"
:api-fuzzing-comparison-path="mr.apiFuzzingComparisonPath"
:coverage-fuzzing-comparison-path="mr.coverageFuzzingComparisonPath" :coverage-fuzzing-comparison-path="mr.coverageFuzzingComparisonPath"
:dast-comparison-path="mr.dastComparisonPath" :dast-comparison-path="mr.dastComparisonPath"
:dependency-scanning-comparison-path="mr.dependencyScanningComparisonPath" :dependency-scanning-comparison-path="mr.dependencyScanningComparisonPath"
......
...@@ -10,6 +10,7 @@ export default class MergeRequestStore extends CEMergeRequestStore { ...@@ -10,6 +10,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.apiFuzzingHelp = data.api_fuzzing_help_path;
this.coverageFuzzingHelp = data.coverage_fuzzing_help_path; this.coverageFuzzingHelp = data.coverage_fuzzing_help_path;
this.secretScanningHelp = data.secret_scanning_help_path; this.secretScanningHelp = data.secret_scanning_help_path;
this.dependencyScanningHelp = data.dependency_scanning_help_path; this.dependencyScanningHelp = data.dependency_scanning_help_path;
...@@ -56,6 +57,7 @@ export default class MergeRequestStore extends CEMergeRequestStore { ...@@ -56,6 +57,7 @@ export default class MergeRequestStore extends CEMergeRequestStore {
// Security scan diff paths // Security scan diff paths
this.containerScanningComparisonPath = data.container_scanning_comparison_path; this.containerScanningComparisonPath = data.container_scanning_comparison_path;
this.coverageFuzzingComparisonPath = data.coverage_fuzzing_comparison_path; this.coverageFuzzingComparisonPath = data.coverage_fuzzing_comparison_path;
this.apiFuzzingComparisonPath = data.api_fuzzing_comparison_path;
this.dastComparisonPath = data.dast_comparison_path; this.dastComparisonPath = data.dast_comparison_path;
this.dependencyScanningComparisonPath = data.dependency_scanning_comparison_path; this.dependencyScanningComparisonPath = data.dependency_scanning_comparison_path;
} }
......
<script> <script>
import { GlFriendlyWrap, GlLink, GlBadge } from '@gitlab/ui'; import { GlFriendlyWrap, GlLink, GlBadge } from '@gitlab/ui';
import { SUPPORTING_MESSAGE_TYPES } from 'ee/vulnerabilities/constants';
import { REPORT_TYPES } from 'ee/security_dashboard/store/constants'; import { REPORT_TYPES } from 'ee/security_dashboard/store/constants';
import CodeBlock from '~/vue_shared/components/code_block.vue'; import CodeBlock from '~/vue_shared/components/code_block.vue';
import SeverityBadge from './severity_badge.vue'; import SeverityBadge from './severity_badge.vue';
...@@ -60,11 +61,24 @@ export default { ...@@ -60,11 +61,24 @@ export default {
links() { links() {
return this.asNonEmptyListOrNull(this.vulnerability.links); return this.asNonEmptyListOrNull(this.vulnerability.links);
}, },
requestHeaders() { assertion() {
return this.headersToFormattedString(this.vulnerability.request?.headers); return this.vulnerability.evidenceSource?.name;
}, },
responseHeaders() { recordedMessage() {
return this.headersToFormattedString(this.vulnerability.response?.headers); return this.vulnerability.supporting_messages?.find(
msg => msg.name === SUPPORTING_MESSAGE_TYPES.RECORDED,
)?.response;
},
constructedRequest() {
const { request } = this.vulnerability;
return request ? this.constructRequest(request) : '';
},
constructedResponse() {
const { response } = this.vulnerability;
return response ? this.constructResponse(response) : '';
},
constructedRecordedResponse() {
return this.recordedMessage ? this.constructResponse(this.recordedMessage) : '';
}, },
responseStatusCode() { responseStatusCode() {
return this.vulnerability.response?.status_code; return this.vulnerability.response?.status_code;
...@@ -93,8 +107,22 @@ export default { ...@@ -93,8 +107,22 @@ export default {
stacktraceSnippet() { stacktraceSnippet() {
return this.vulnLocation?.stacktrace_snippet; return this.vulnLocation?.stacktrace_snippet;
}, },
hasRequest() {
return Boolean(this.constructedRequest);
},
hasResponse() {
return Boolean(this.constructedResponse);
},
hasRecordedResponse() {
return Boolean(this.constructedRecordedResponse);
},
}, },
methods: { methods: {
getHeadersAsCodeBlockLines(headers) {
return Array.isArray(headers)
? headers.map(({ name, value }) => `${name}: ${value}`).join('\n')
: '';
},
hasMoreValues(index, values) { hasMoreValues(index, values) {
return index < values.length - 1; return index < values.length - 1;
}, },
...@@ -104,6 +132,22 @@ export default { ...@@ -104,6 +132,22 @@ export default {
headersToFormattedString(headers = []) { headersToFormattedString(headers = []) {
return headers.map(({ name, value }) => `${name}: ${value}`).join('\n'); return headers.map(({ name, value }) => `${name}: ${value}`).join('\n');
}, },
constructResponse(response) {
const { body, status_code: statusCode, reason_phrase: reasonPhrase, headers = [] } = response;
const headerLines = this.getHeadersAsCodeBlockLines(headers);
return statusCode && reasonPhrase && headerLines
? [`${statusCode} ${reasonPhrase}\n`, headerLines, '\n\n', body].join('')
: '';
},
constructRequest(request) {
const { body, method, url, headers = [] } = request;
const headerLines = this.getHeadersAsCodeBlockLines(headers);
return method && url && headerLines
? [`${method} ${url}\n`, headerLines, '\n\n', body].join('')
: '';
},
}, },
}; };
</script> </script>
...@@ -131,14 +175,17 @@ export default { ...@@ -131,14 +175,17 @@ export default {
<gl-friendly-wrap :text="url" /> <gl-friendly-wrap :text="url" />
</gl-link> </gl-link>
</vulnerability-detail> </vulnerability-detail>
<vulnerability-detail v-if="requestHeaders" :label="__('Request Headers')"> <vulnerability-detail v-if="hasRequest" :label="s__('Vulnerability|Request')">
<code-block ref="requestHeaders" :code="requestHeaders" max-height="225px" /> <code-block ref="request" :code="constructedRequest" max-height="225px" />
</vulnerability-detail> </vulnerability-detail>
<vulnerability-detail v-if="responseStatusCode" :label="__('Response Status')"> <vulnerability-detail
<gl-friendly-wrap ref="responseStatusCode" :text="responseStatusCode" /> v-if="hasRecordedResponse"
:label="s__('Vulnerability|Unmodified Response')"
>
<code-block ref="recordedResponse" :code="constructedRecordedResponse" max-height="225px" />
</vulnerability-detail> </vulnerability-detail>
<vulnerability-detail v-if="responseHeaders" :label="__('Response Headers')"> <vulnerability-detail v-if="hasResponse" :label="s__('Vulnerability|Actual Response')">
<code-block ref="responseHeaders" :code="responseHeaders" max-height="225px" /> <code-block ref="unmodifiedResponse" :code="constructedResponse" max-height="225px" />
</vulnerability-detail> </vulnerability-detail>
<vulnerability-detail v-if="file" :label="s__('Vulnerability|File')"> <vulnerability-detail v-if="file" :label="s__('Vulnerability|File')">
<gl-link <gl-link
......
...@@ -19,6 +19,7 @@ import { fetchPolicies } from '~/lib/graphql'; ...@@ -19,6 +19,7 @@ import { fetchPolicies } from '~/lib/graphql';
import securityReportSummaryQuery from './graphql/mr_security_report_summary.graphql'; import securityReportSummaryQuery from './graphql/mr_security_report_summary.graphql';
import { import {
MODULE_CONTAINER_SCANNING, MODULE_CONTAINER_SCANNING,
MODULE_API_FUZZING,
MODULE_COVERAGE_FUZZING, MODULE_COVERAGE_FUZZING,
MODULE_DAST, MODULE_DAST,
MODULE_DEPENDENCY_SCANNING, MODULE_DEPENDENCY_SCANNING,
...@@ -101,6 +102,11 @@ export default { ...@@ -101,6 +102,11 @@ export default {
required: false, required: false,
default: '', default: '',
}, },
apiFuzzingHelpPath: {
type: String,
required: false,
default: '',
},
coverageFuzzingHelpPath: { coverageFuzzingHelpPath: {
type: String, type: String,
required: false, required: false,
...@@ -185,6 +191,11 @@ export default { ...@@ -185,6 +191,11 @@ export default {
type: String, type: String,
required: true, required: true,
}, },
apiFuzzingComparisonPath: {
type: String,
required: false,
default: '',
},
containerScanningComparisonPath: { containerScanningComparisonPath: {
type: String, type: String,
required: false, required: false,
...@@ -222,6 +233,7 @@ export default { ...@@ -222,6 +233,7 @@ export default {
MODULE_SAST, MODULE_SAST,
MODULE_CONTAINER_SCANNING, MODULE_CONTAINER_SCANNING,
MODULE_DAST, MODULE_DAST,
MODULE_API_FUZZING,
MODULE_COVERAGE_FUZZING, MODULE_COVERAGE_FUZZING,
MODULE_DEPENDENCY_SCANNING, MODULE_DEPENDENCY_SCANNING,
MODULE_SECRET_DETECTION, MODULE_SECRET_DETECTION,
...@@ -252,6 +264,7 @@ export default { ...@@ -252,6 +264,7 @@ export default {
'groupedSecretDetectionText', 'groupedSecretDetectionText',
'secretDetectionStatusIcon', 'secretDetectionStatusIcon',
]), ]),
...mapGetters(MODULE_API_FUZZING, ['groupedApiFuzzingText', 'apiFuzzingStatusIcon']),
...mapGetters('pipelineJobs', ['hasFuzzingArtifacts', 'fuzzingJobsWithArtifact']), ...mapGetters('pipelineJobs', ['hasFuzzingArtifacts', 'fuzzingJobsWithArtifact']),
securityTab() { securityTab() {
return `${this.pipelinePath}/security`; return `${this.pipelinePath}/security`;
...@@ -265,6 +278,9 @@ export default { ...@@ -265,6 +278,9 @@ export default {
hasDastReports() { hasDastReports() {
return this.enabledReports.dast; return this.enabledReports.dast;
}, },
hasApiFuzzingReports() {
return this.enabledReports.apiFuzzing;
},
hasCoverageFuzzingReports() { hasCoverageFuzzingReports() {
// TODO: Remove feature flag in https://gitlab.com/gitlab-org/gitlab/-/issues/257839 // TODO: Remove feature flag in https://gitlab.com/gitlab-org/gitlab/-/issues/257839
return this.enabledReports.coverageFuzzing && this.glFeatures.coverageFuzzingMrWidget; return this.enabledReports.coverageFuzzing && this.glFeatures.coverageFuzzingMrWidget;
...@@ -293,6 +309,9 @@ export default { ...@@ -293,6 +309,9 @@ export default {
dastDownloadLink() { dastDownloadLink() {
return this.dastSummary?.scannedResourcesCsvPath || ''; return this.dastSummary?.scannedResourcesCsvPath || '';
}, },
hasApiFuzzingIssues() {
return this.hasIssuesForReportType(MODULE_API_FUZZING);
},
hasCoverageFuzzingIssues() { hasCoverageFuzzingIssues() {
return this.hasIssuesForReportType(MODULE_COVERAGE_FUZZING); return this.hasIssuesForReportType(MODULE_COVERAGE_FUZZING);
}, },
...@@ -359,6 +378,11 @@ export default { ...@@ -359,6 +378,11 @@ export default {
this.fetchCoverageFuzzingDiff(); this.fetchCoverageFuzzingDiff();
this.fetchPipelineJobs(); this.fetchPipelineJobs();
} }
if (this.apiFuzzingComparisonPath && this.hasApiFuzzingReports) {
this.setApiFuzzingDiffEndpoint(this.apiFuzzingComparisonPath);
this.fetchApiFuzzingDiff();
}
}, },
methods: { methods: {
...mapActions([ ...mapActions([
...@@ -400,6 +424,10 @@ export default { ...@@ -400,6 +424,10 @@ export default {
setSecretDetectionDiffEndpoint: 'setDiffEndpoint', setSecretDetectionDiffEndpoint: 'setDiffEndpoint',
fetchSecretDetectionDiff: 'fetchDiff', fetchSecretDetectionDiff: 'fetchDiff',
}), }),
...mapActions(MODULE_API_FUZZING, {
setApiFuzzingDiffEndpoint: 'setDiffEndpoint',
fetchApiFuzzingDiff: 'fetchDiff',
}),
...mapActions('pipelineJobs', ['fetchPipelineJobs', 'setPipelineJobsPath', 'setProjectId']), ...mapActions('pipelineJobs', ['fetchPipelineJobs', 'setPipelineJobsPath', 'setProjectId']),
...mapActions('pipelineJobs', { ...mapActions('pipelineJobs', {
setPipelineJobsId: 'setPipelineId', setPipelineJobsId: 'setPipelineId',
...@@ -616,6 +644,28 @@ export default { ...@@ -616,6 +644,28 @@ export default {
/> />
</template> </template>
<template v-if="hasApiFuzzingReports">
<summary-row
:status-icon="apiFuzzingStatusIcon"
:popover-options="apiFuzzingPopover"
class="js-api-fuzzing-widget"
data-qa-selector="api_fuzzing_report"
>
<template #summary>
<security-summary :message="groupedApiFuzzingText" />
</template>
</summary-row>
<grouped-issues-list
v-if="hasApiFuzzingIssues"
:unresolved-issues="apiFuzzing.newIssues"
:resolved-issues="apiFuzzing.resolvedIssues"
:component="$options.componentNames.SecurityIssueBody"
class="report-block-group-list"
data-testid="api-fuzzing-issues-list"
/>
</template>
<issue-modal <issue-modal
:modal="modal" :modal="modal"
:can-create-issue="canCreateIssue" :can-create-issue="canCreateIssue"
......
...@@ -99,5 +99,18 @@ export default { ...@@ -99,5 +99,18 @@ export default {
), ),
}; };
}, },
apiFuzzingPopover() {
return {
title: s__('ciReport|API Fuzzing'),
content: sprintf(
s__('ciReport|%{linkStartTag}Learn more about API Fuzzing%{linkEndTag}'),
{
linkStartTag: getLinkStartTag(this.apiFuzzingHelpPath),
linkEndTag,
},
false,
),
};
},
}, },
}; };
...@@ -9,6 +9,7 @@ export * from '~/vue_shared/security_reports/store/constants'; ...@@ -9,6 +9,7 @@ export * from '~/vue_shared/security_reports/store/constants';
* namespaces in the store state, as if they were modules. * namespaces in the store state, as if they were modules.
*/ */
export const MODULE_CONTAINER_SCANNING = 'containerScanning'; export const MODULE_CONTAINER_SCANNING = 'containerScanning';
export const MODULE_API_FUZZING = 'apiFuzzing';
export const MODULE_COVERAGE_FUZZING = 'coverageFuzzing'; export const MODULE_COVERAGE_FUZZING = 'coverageFuzzing';
export const MODULE_DAST = 'dast'; export const MODULE_DAST = 'dast';
export const MODULE_DEPENDENCY_SCANNING = 'dependencyScanning'; export const MODULE_DEPENDENCY_SCANNING = 'dependencyScanning';
......
...@@ -6,10 +6,11 @@ import * as actions from './actions'; ...@@ -6,10 +6,11 @@ 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 { MODULE_API_FUZZING, 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';
import apiFuzzing from './modules/api_fuzzing';
Vue.use(Vuex); Vue.use(Vuex);
...@@ -18,6 +19,7 @@ export default () => ...@@ -18,6 +19,7 @@ export default () =>
modules: { modules: {
[MODULE_SAST]: sast, [MODULE_SAST]: sast,
[MODULE_SECRET_DETECTION]: secretDetection, [MODULE_SECRET_DETECTION]: secretDetection,
[MODULE_API_FUZZING]: apiFuzzing,
pipelineJobs, pipelineJobs,
}, },
actions, actions,
......
import * as types from './mutation_types'; import * as types from './mutation_types';
import { MODULE_SAST, MODULE_SECRET_DETECTION } from './constants'; import { MODULE_API_FUZZING, MODULE_SAST, MODULE_SECRET_DETECTION } from './constants';
export const updateIssueActionsMap = { export const updateIssueActionsMap = {
sast: `${MODULE_SAST}/updateVulnerability`, sast: `${MODULE_SAST}/updateVulnerability`,
...@@ -8,6 +8,7 @@ export const updateIssueActionsMap = { ...@@ -8,6 +8,7 @@ export const updateIssueActionsMap = {
dast: 'updateDastIssue', dast: 'updateDastIssue',
secret_detection: `${MODULE_SECRET_DETECTION}/updateVulnerability`, secret_detection: `${MODULE_SECRET_DETECTION}/updateVulnerability`,
coverage_fuzzing: 'updateCoverageFuzzingIssue', coverage_fuzzing: 'updateCoverageFuzzingIssue',
api_fuzzing: `${MODULE_API_FUZZING}/updateVulnerability`,
}; };
export default function configureMediator(store) { export default function configureMediator(store) {
......
...@@ -10,6 +10,7 @@ const CONTAINER_SCANNING = s__('ciReport|Container scanning'); ...@@ -10,6 +10,7 @@ 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'); const SECRET_SCANNING = s__('ciReport|Secret scanning');
const COVERAGE_FUZZING = s__('ciReport|Coverage fuzzing'); const COVERAGE_FUZZING = s__('ciReport|Coverage fuzzing');
const API_FUZZING = s__('ciReport|API fuzzing');
export default { export default {
SAST, SAST,
...@@ -18,6 +19,7 @@ export default { ...@@ -18,6 +19,7 @@ export default {
DEPENDENCY_SCANNING, DEPENDENCY_SCANNING,
SECRET_SCANNING, SECRET_SCANNING,
COVERAGE_FUZZING, COVERAGE_FUZZING,
API_FUZZING,
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 }),
...@@ -42,4 +44,8 @@ export default { ...@@ -42,4 +44,8 @@ export default {
reportType: COVERAGE_FUZZING, reportType: COVERAGE_FUZZING,
}), }),
COVERAGE_FUZZING_HAS_ERROR: sprintf(TRANSLATION_HAS_ERROR, { reportType: COVERAGE_FUZZING }), COVERAGE_FUZZING_HAS_ERROR: sprintf(TRANSLATION_HAS_ERROR, { reportType: COVERAGE_FUZZING }),
API_FUZZING_IS_LOADING: sprintf(TRANSLATION_IS_LOADING, {
reportType: API_FUZZING,
}),
API_FUZZING_HAS_ERROR: sprintf(TRANSLATION_HAS_ERROR, { reportType: API_FUZZING }),
}; };
import { fetchDiffData } from '~/vue_shared/security_reports/store/utils';
import * as types from './mutation_types';
export const setDiffEndpoint = ({ commit }, path) => commit(types.SET_DIFF_ENDPOINT, path);
export const requestDiff = ({ commit }) => commit(types.REQUEST_DIFF);
export const receiveDiffSuccess = ({ commit }, response) =>
commit(types.RECEIVE_DIFF_SUCCESS, response);
export const receiveDiffError = ({ commit }, response) =>
commit(types.RECEIVE_DIFF_ERROR, response);
export const fetchDiff = ({ state, rootState, dispatch }) => {
dispatch('requestDiff');
return fetchDiffData(rootState, state.paths.diffEndpoint, 'api_fuzzing')
.then(data => {
dispatch('receiveDiffSuccess', data);
})
.catch(() => {
dispatch('receiveDiffError');
});
};
export const updateVulnerability = ({ commit }, vulnerability) =>
commit(types.UPDATE_VULNERABILITY, vulnerability);
import { statusIcon, groupedReportText } from '../../utils';
import messages from '../../messages';
export const groupedApiFuzzingText = state =>
groupedReportText(
state,
messages.API_FUZZING,
messages.API_FUZZING_HAS_ERROR,
messages.API_FUZZING_IS_LOADING,
);
export const apiFuzzingStatusIcon = ({ isLoading, hasError, newIssues }) =>
statusIcon(isLoading, hasError, newIssues.length);
import state from './state';
import mutations from './mutations';
import * as getters from './getters';
import * as actions from './actions';
export default {
namespaced: true,
state,
mutations,
getters,
actions,
};
export const SET_DIFF_ENDPOINT = 'SET_DIFF_ENDPOINT';
export const REQUEST_DIFF = 'REQUEST_DIFF';
export const RECEIVE_DIFF_SUCCESS = 'RECEIVE_DIFF_SUCCESS';
export const RECEIVE_DIFF_ERROR = 'RECEIVE_DIFF_ERROR';
export const UPDATE_VULNERABILITY = 'UPDATE_VULNERABILITY';
import Vue from 'vue';
import { parseDiff } from '~/vue_shared/security_reports/store/utils';
import { findIssueIndex } from '../../utils';
import * as types from './mutation_types';
export default {
[types.SET_DIFF_ENDPOINT](state, path) {
state.paths.diffEndpoint = path;
},
[types.REQUEST_DIFF](state) {
state.isLoading = true;
},
[types.RECEIVE_DIFF_SUCCESS](state, { diff, enrichData }) {
const { added, fixed, existing } = parseDiff(diff, enrichData);
const baseReportOutofDate = diff.base_report_out_of_date || false;
const scans = diff.scans || [];
const hasBaseReport = Boolean(diff.base_report_created_at);
state.isLoading = false;
state.newIssues = added;
state.resolvedIssues = fixed;
state.allIssues = existing;
state.baseReportOutofDate = baseReportOutofDate;
state.hasBaseReport = hasBaseReport;
state.scans = scans;
},
[types.RECEIVE_DIFF_ERROR](state) {
Vue.set(state, 'isLoading', false);
Vue.set(state, 'hasError', true);
},
[types.UPDATE_VULNERABILITY](state, issue) {
const newIssuesIndex = findIssueIndex(state.newIssues, issue);
if (newIssuesIndex !== -1) {
state.newIssues.splice(newIssuesIndex, 1, issue);
return;
}
const resolvedIssuesIndex = findIssueIndex(state.resolvedIssues, issue);
if (resolvedIssuesIndex !== -1) {
state.resolvedIssues.splice(resolvedIssuesIndex, 1, issue);
return;
}
const allIssuesIndex = findIssueIndex(state.allIssues, issue);
if (allIssuesIndex !== -1) {
state.allIssues.splice(allIssuesIndex, 1, issue);
}
},
};
export default () => ({
paths: {
head: null,
base: null,
diffEndpoint: null,
},
isLoading: false,
hasError: false,
newIssues: [],
resolvedIssues: [],
allIssues: [],
baseReportOutofDate: false,
hasBaseReport: false,
scans: [],
});
...@@ -63,6 +63,7 @@ export const UPDATE_DEPENDENCY_SCANNING_ISSUE = 'UPDATE_DEPENDENCY_SCANNING_ISSU ...@@ -63,6 +63,7 @@ export const UPDATE_DEPENDENCY_SCANNING_ISSUE = 'UPDATE_DEPENDENCY_SCANNING_ISSU
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_COVERAGE_FUZZING_ISSUE = 'UPDATE_COVERAGE_FUZZING_ISSUE'; export const UPDATE_COVERAGE_FUZZING_ISSUE = 'UPDATE_COVERAGE_FUZZING_ISSUE';
export const UPDATE_API_FUZZING_ISSUE = 'UPDATE_API_FUZZING_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';
import { import {
MODULE_API_FUZZING,
MODULE_CONTAINER_SCANNING, MODULE_CONTAINER_SCANNING,
MODULE_COVERAGE_FUZZING, MODULE_COVERAGE_FUZZING,
MODULE_DAST, MODULE_DAST,
...@@ -23,6 +24,7 @@ export default () => ({ ...@@ -23,6 +24,7 @@ export default () => ({
reportTypes: [ reportTypes: [
MODULE_CONTAINER_SCANNING, MODULE_CONTAINER_SCANNING,
MODULE_API_FUZZING,
MODULE_COVERAGE_FUZZING, MODULE_COVERAGE_FUZZING,
MODULE_DAST, MODULE_DAST,
MODULE_DEPENDENCY_SCANNING, MODULE_DEPENDENCY_SCANNING,
......
...@@ -11,6 +11,7 @@ ...@@ -11,6 +11,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.api_fuzzing_help_path = '#{help_page_path("user/application_security/api_fuzzing/index")}';
window.gl.mrWidgetData.coverage_fuzzing_help_path = '#{help_page_path("user/application_security/coverage_fuzzing/index")}'; window.gl.mrWidgetData.coverage_fuzzing_help_path = '#{help_page_path("user/application_security/coverage_fuzzing/index")}';
window.gl.mrWidgetData.visual_review_app_available = '#{@project.feature_available?(:visual_review_app)}' === 'true'; window.gl.mrWidgetData.visual_review_app_available = '#{@project.feature_available?(:visual_review_app)}' === 'true';
window.gl.mrWidgetData.license_scanning_comparison_path = '#{license_scanning_reports_project_merge_request_path(@project, @merge_request) if @project.feature_available?(:license_scanning)}' window.gl.mrWidgetData.license_scanning_comparison_path = '#{license_scanning_reports_project_merge_request_path(@project, @merge_request) if @project.feature_available?(:license_scanning)}'
...@@ -20,3 +21,5 @@ ...@@ -20,3 +21,5 @@
window.gl.mrWidgetData.dast_comparison_path = '#{dast_reports_project_merge_request_path(@project, @merge_request) if @project.feature_available?(:dast)}' window.gl.mrWidgetData.dast_comparison_path = '#{dast_reports_project_merge_request_path(@project, @merge_request) if @project.feature_available?(:dast)}'
window.gl.mrWidgetData.secret_scanning_comparison_path = '#{secret_detection_reports_project_merge_request_path(@project, @merge_request) if @project.feature_available?(:secret_detection)}' window.gl.mrWidgetData.secret_scanning_comparison_path = '#{secret_detection_reports_project_merge_request_path(@project, @merge_request) if @project.feature_available?(:secret_detection)}'
window.gl.mrWidgetData.coverage_fuzzing_comparison_path = '#{coverage_fuzzing_reports_project_merge_request_path(@project, @merge_request) if @project.feature_available?(:coverage_fuzzing) && Feature.enabled?(:coverage_fuzzing_mr_widget, @project, default_enabled: true)}' window.gl.mrWidgetData.coverage_fuzzing_comparison_path = '#{coverage_fuzzing_reports_project_merge_request_path(@project, @merge_request) if @project.feature_available?(:coverage_fuzzing) && Feature.enabled?(:coverage_fuzzing_mr_widget, @project, default_enabled: true)}'
window.gl.mrWidgetData.api_fuzzing_comparison_path = '#{api_fuzzing_reports_project_merge_request_path(@project, @merge_request) if @project.feature_available?(:api_fuzzing)}'
...@@ -8,6 +8,7 @@ import { ...@@ -8,6 +8,7 @@ import {
dependencyScanningDiffSuccessMock, dependencyScanningDiffSuccessMock,
secretScanningDiffSuccessMock, secretScanningDiffSuccessMock,
coverageFuzzingDiffSuccessMock, coverageFuzzingDiffSuccessMock,
apiFuzzingDiffSuccessMock,
} from 'ee_jest/vue_shared/security_reports/mock_data'; } from 'ee_jest/vue_shared/security_reports/mock_data';
import { TEST_HOST } from 'helpers/test_constants'; import { TEST_HOST } from 'helpers/test_constants';
import { trimText } from 'helpers/text_helper'; import { trimText } from 'helpers/text_helper';
...@@ -37,6 +38,7 @@ const DEPENDENCY_SCANNING_SELECTOR = '.js-dependency-scanning-widget'; ...@@ -37,6 +38,7 @@ 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'; const SECRET_SCANNING_SELECTOR = '.js-secret-scanning';
const COVERAGE_FUZZING_SELECTOR = '.js-coverage-fuzzing-widget'; const COVERAGE_FUZZING_SELECTOR = '.js-coverage-fuzzing-widget';
const API_FUZZING_SELECTOR = '.js-api-fuzzing-widget';
describe('ee merge request widget options', () => { describe('ee merge request widget options', () => {
let vm; let vm;
...@@ -930,6 +932,78 @@ describe('ee merge request widget options', () => { ...@@ -930,6 +932,78 @@ describe('ee merge request widget options', () => {
}); });
}); });
describe('API Fuzzing', () => {
const API_FUZZING_ENDPOINT = 'api_fuzzing_report';
beforeEach(() => {
gl.mrWidgetData = {
...mockData,
enabled_reports: {
api_fuzzing: true,
},
api_fuzzing_comparison_path: API_FUZZING_ENDPOINT,
vulnerability_feedback_path: VULNERABILITY_FEEDBACK_ENDPOINT,
};
});
describe('when it is loading', () => {
it('should render loading indicator', () => {
mock.onGet(API_FUZZING_ENDPOINT).reply(200, apiFuzzingDiffSuccessMock);
mock.onGet(VULNERABILITY_FEEDBACK_ENDPOINT).reply(200, []);
vm = mountComponent(Component, { mrData: gl.mrWidgetData });
expect(
trimText(findExtendedSecurityWidget().querySelector(API_FUZZING_SELECTOR).textContent),
).toContain('API fuzzing is loading');
});
});
describe('with successful request', () => {
beforeEach(() => {
mock.onGet(API_FUZZING_ENDPOINT).reply(200, apiFuzzingDiffSuccessMock);
mock.onGet(VULNERABILITY_FEEDBACK_ENDPOINT).reply(200, []);
vm = mountComponent(Component, { mrData: gl.mrWidgetData });
});
it('should render provided data', done => {
setImmediate(() => {
expect(
trimText(
findExtendedSecurityWidget().querySelector(
`${API_FUZZING_SELECTOR} .report-block-list-issue-description`,
).textContent,
),
).toEqual(
'API fuzzing detected 2 potential vulnerabilities 1 Critical 1 High and 0 Others',
);
done();
});
});
});
describe('with failed request', () => {
beforeEach(() => {
mock.onGet(API_FUZZING_ENDPOINT).reply(500, {});
mock.onGet(VULNERABILITY_FEEDBACK_ENDPOINT).reply(500, []);
vm = mountComponent(Component, { mrData: gl.mrWidgetData });
});
it('should render error indicator', done => {
setImmediate(() => {
expect(
findExtendedSecurityWidget()
.querySelector(API_FUZZING_SELECTOR)
.textContent.trim(),
).toContain('API fuzzing: Loading resulted in an error');
done();
});
});
});
});
describe('license scanning report', () => { describe('license scanning report', () => {
const licenseManagementApiUrl = `${TEST_HOST}/manage_license_api`; const licenseManagementApiUrl = `${TEST_HOST}/manage_license_api`;
......
...@@ -15,6 +15,7 @@ export default { ...@@ -15,6 +15,7 @@ export default {
dependency_scanning_comparison_path: '/dependency_scanning_comparison_path', dependency_scanning_comparison_path: '/dependency_scanning_comparison_path',
dast_comparison_path: '/dast_comparison_path', dast_comparison_path: '/dast_comparison_path',
coverage_fuzzing_comparison_path: '/coverage_fuzzing_comparison_path', coverage_fuzzing_comparison_path: '/coverage_fuzzing_comparison_path',
api_fuzzing_comparison_path: '/api_fuzzing_comparison_path',
}; };
// Browser Performance Testing // Browser Performance Testing
......
...@@ -75,6 +75,7 @@ describe('MergeRequestStore', () => { ...@@ -75,6 +75,7 @@ describe('MergeRequestStore', () => {
'sast_comparison_path', 'sast_comparison_path',
'dast_comparison_path', 'dast_comparison_path',
'secret_scanning_comparison_path', 'secret_scanning_comparison_path',
'api_fuzzing_comparison_path',
'coverage_fuzzing_comparison_path', 'coverage_fuzzing_comparison_path',
])('should set %s path', property => { ])('should set %s path', property => {
// Ensure something is set in the mock data // Ensure something is set in the mock data
......
...@@ -55,34 +55,11 @@ exports[`VulnerabilityDetails component pin test renders correctly 1`] = ` ...@@ -55,34 +55,11 @@ exports[`VulnerabilityDetails component pin test renders correctly 1`] = `
</gl-link-stub> </gl-link-stub>
</vulnerability-detail-stub> </vulnerability-detail-stub>
<vulnerability-detail-stub <!---->
label="Request Headers"
>
<code-block-stub
code="key1: value1
key2: value2"
maxheight="225px"
/>
</vulnerability-detail-stub>
<vulnerability-detail-stub <!---->
label="Response Status"
>
<gl-friendly-wrap-stub
symbols="/"
text="200"
/>
</vulnerability-detail-stub>
<vulnerability-detail-stub <!---->
label="Response Headers"
>
<code-block-stub
code="key1: value1
key2: value2"
maxheight="225px"
/>
</vulnerability-detail-stub>
<vulnerability-detail-stub <vulnerability-detail-stub
label="File" label="File"
......
import { GlLink, GlBadge } from '@gitlab/ui'; import { GlLink, GlBadge } from '@gitlab/ui';
import { mount, shallowMount } from '@vue/test-utils'; import { mount, shallowMount } from '@vue/test-utils';
import { cloneDeep } from 'lodash'; import { cloneDeep } from 'lodash';
import { SUPPORTING_MESSAGE_TYPES } from 'ee/vulnerabilities/constants';
import SeverityBadge from 'ee/vue_shared/security_reports/components/severity_badge.vue'; import SeverityBadge from 'ee/vue_shared/security_reports/components/severity_badge.vue';
import VulnerabilityDetails from 'ee/vue_shared/security_reports/components/vulnerability_details.vue'; import VulnerabilityDetails from 'ee/vue_shared/security_reports/components/vulnerability_details.vue';
import { TEST_HOST } from 'helpers/test_constants'; import { TEST_HOST } from 'helpers/test_constants';
import CodeBlock from '~/vue_shared/components/code_block.vue'; import CodeBlock from '~/vue_shared/components/code_block.vue';
import { mockFindings } from '../mock_data'; import { mockFindings } from '../mock_data';
function makeVulnerability(changes = {}) { function makeVulnerability(changes = {}) {
...@@ -30,9 +32,9 @@ describe('VulnerabilityDetails component', () => { ...@@ -30,9 +32,9 @@ describe('VulnerabilityDetails component', () => {
}; };
const findLink = name => wrapper.find({ ref: `${name}Link` }); const findLink = name => wrapper.find({ ref: `${name}Link` });
const findRequestHeaders = () => wrapper.find({ ref: 'requestHeaders' }); const findRequest = () => wrapper.find({ ref: 'request' });
const findResponseHeaders = () => wrapper.find({ ref: 'responseHeaders' }); const findRecordedResponse = () => wrapper.find({ ref: 'recordedResponse' });
const findResponseStatusCode = () => wrapper.find({ ref: 'responseStatusCode' }); const findUnmodifiedResponse = () => wrapper.find({ ref: 'unmodifiedResponse' });
const findCrashAddress = () => wrapper.find({ ref: 'crashAddress' }); const findCrashAddress = () => wrapper.find({ ref: 'crashAddress' });
const findCrashState = () => wrapper.find({ ref: 'crashState' }); const findCrashState = () => wrapper.find({ ref: 'crashState' });
const findCrashType = () => wrapper.find({ ref: 'crashType' }); const findCrashType = () => wrapper.find({ ref: 'crashType' });
...@@ -164,9 +166,12 @@ describe('VulnerabilityDetails component', () => { ...@@ -164,9 +166,12 @@ describe('VulnerabilityDetails component', () => {
}); });
describe('with request information', () => { describe('with request information', () => {
let vulnerability;
beforeEach(() => { beforeEach(() => {
const vulnerability = makeVulnerability({ vulnerability = makeVulnerability({
request: { request: {
method: 'GET',
url: 'http://foo.bar/path', url: 'http://foo.bar/path',
headers: [{ name: 'key1', value: 'value1' }, { name: 'key2', value: 'value2' }], headers: [{ name: 'key1', value: 'value1' }, { name: 'key2', value: 'value2' }],
}, },
...@@ -178,14 +183,17 @@ describe('VulnerabilityDetails component', () => { ...@@ -178,14 +183,17 @@ describe('VulnerabilityDetails component', () => {
expect(findLink('url').attributes('href')).toBe('http://foo.bar/path'); expect(findLink('url').attributes('href')).toBe('http://foo.bar/path');
}); });
it('renders a code-block containing the http headers', () => { it('renders a code-block containing the http request', () => {
expect(findRequestHeaders().is(CodeBlock)).toBe(true); const { method, url } = vulnerability.request;
expect(findRequestHeaders().text()).toBe('key1: value1\nkey2: value2'); expect(findRequest().is(CodeBlock)).toBe(true);
expect(findRequest().text()).toContain(method);
expect(findRequest().text()).toContain(url);
expect(findRequest().text()).toContain('key1: value1\nkey2: value2');
}); });
it('limits the code-blocks maximum height', () => { it('limits the code-blocks maximum height', () => {
expect(findRequestHeaders().props('maxHeight')).not.toBeFalsy(); expect(findRequest().props('maxHeight')).not.toBeFalsy();
expect(findRequestHeaders().props('maxHeight')).toEqual(expect.any(String)); expect(findRequest().props('maxHeight')).toEqual(expect.any(String));
}); });
}); });
...@@ -204,16 +212,19 @@ describe('VulnerabilityDetails component', () => { ...@@ -204,16 +212,19 @@ describe('VulnerabilityDetails component', () => {
expect(findLink('url').text()).toBe('http://foo.com/bar'); expect(findLink('url').text()).toBe('http://foo.com/bar');
}); });
it('does not render a code block containing the request-headers', () => { it('does not render a code block containing the request', () => {
expect(findRequestHeaders().exists()).toBe(false); expect(findRequest().exists()).toBe(false);
}); });
}); });
describe('with response information', () => { describe('with response information', () => {
let vulnerability;
beforeEach(() => { beforeEach(() => {
const vulnerability = makeVulnerability({ vulnerability = makeVulnerability({
response: { response: {
status_code: '200', status_code: '200',
reason_phrase: 'INTERNAL SERVER ERROR',
headers: [{ name: 'key1', value: 'value1' }, { name: 'key2', value: 'value2' }], headers: [{ name: 'key1', value: 'value1' }, { name: 'key2', value: 'value2' }],
}, },
}); });
...@@ -221,29 +232,71 @@ describe('VulnerabilityDetails component', () => { ...@@ -221,29 +232,71 @@ describe('VulnerabilityDetails component', () => {
}); });
it('renders the response status code', () => { it('renders the response status code', () => {
expect(findResponseStatusCode().text()).toBe('200'); expect(findUnmodifiedResponse().text()).toContain('200');
}); });
it('renders a code block containing the request-headers', () => { it('renders a code block containing the response', () => {
const responseHeaders = findResponseHeaders(); const { reason_phrase } = vulnerability.response;
const response = findUnmodifiedResponse();
expect(responseHeaders.is(CodeBlock)).toBe(true); expect(response.is(CodeBlock)).toBe(true);
expect(responseHeaders.text()).toBe('key1: value1\nkey2: value2'); expect(response.text()).toContain(reason_phrase);
expect(response.text()).toContain('key1: value1\nkey2: value2');
}); });
}); });
describe('without response information', () => { describe('without unmodified response information', () => {
beforeEach(() => { beforeEach(() => {
const vulnerability = makeVulnerability(); const vulnerability = makeVulnerability();
componentFactory(vulnerability); componentFactory(vulnerability);
}); });
it('does not render the status code', () => { it('does not render the response', () => {
expect(findResponseStatusCode().exists()).toBe(false); expect(findUnmodifiedResponse().exists()).toBe(false);
});
});
describe('with recorded response information', () => {
let vulnerability;
beforeEach(() => {
vulnerability = makeVulnerability({
supporting_messages: [
{
name: SUPPORTING_MESSAGE_TYPES.RECORDED,
response: {
status_code: '200',
reason_phrase: 'INTERNAL SERVER ERROR',
headers: [{ name: 'key1', value: 'value1' }, { name: 'key2', value: 'value2' }],
},
},
],
});
componentFactory(vulnerability);
});
it('renders the response status code', () => {
expect(findRecordedResponse().text()).toContain('200');
});
it('renders a code block containing the response', () => {
const { reason_phrase } = vulnerability.supporting_messages[0].response;
const response = findRecordedResponse();
expect(response.is(CodeBlock)).toBe(true);
expect(response.text()).toContain(reason_phrase);
expect(response.text()).toContain('key1: value1\nkey2: value2');
});
});
describe('without response information', () => {
beforeEach(() => {
const vulnerability = makeVulnerability();
componentFactory(vulnerability);
}); });
it('does not render the http-headers', () => { it('does not render the response', () => {
expect(findResponseHeaders().exists()).toBe(false); expect(findRecordedResponse().exists()).toBe(false);
}); });
}); });
......
...@@ -6,6 +6,7 @@ import appStore from 'ee/vue_shared/security_reports/store'; ...@@ -6,6 +6,7 @@ import appStore from 'ee/vue_shared/security_reports/store';
import { trackMrSecurityReportDetails } from 'ee/vue_shared/security_reports/store/constants'; import { trackMrSecurityReportDetails } from 'ee/vue_shared/security_reports/store/constants';
import * as sastTypes from 'ee/vue_shared/security_reports/store/modules/sast/mutation_types'; import * as sastTypes from 'ee/vue_shared/security_reports/store/modules/sast/mutation_types';
import * as secretDetectionTypes from 'ee/vue_shared/security_reports/store/modules/secret_detection/mutation_types'; import * as secretDetectionTypes from 'ee/vue_shared/security_reports/store/modules/secret_detection/mutation_types';
import * as apiFuzzingTypes from 'ee/vue_shared/security_reports/store/modules/api_fuzzing/mutation_types';
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 { trimText } from 'helpers/text_helper'; import { trimText } from 'helpers/text_helper';
import { mockTracking, unmockTracking } from 'helpers/tracking_helper'; import { mockTracking, unmockTracking } from 'helpers/tracking_helper';
...@@ -22,6 +23,7 @@ import { ...@@ -22,6 +23,7 @@ import {
dependencyScanningDiffSuccessMock, dependencyScanningDiffSuccessMock,
secretScanningDiffSuccessMock, secretScanningDiffSuccessMock,
coverageFuzzingDiffSuccessMock, coverageFuzzingDiffSuccessMock,
apiFuzzingDiffSuccessMock,
mockFindings, mockFindings,
} from './mock_data'; } from './mock_data';
...@@ -31,6 +33,7 @@ const DAST_DIFF_ENDPOINT = 'dast.json'; ...@@ -31,6 +33,7 @@ const DAST_DIFF_ENDPOINT = 'dast.json';
const SAST_DIFF_ENDPOINT = 'sast.json'; const SAST_DIFF_ENDPOINT = 'sast.json';
const PIPELINE_JOBS_ENDPOINT = 'jobs.json'; const PIPELINE_JOBS_ENDPOINT = 'jobs.json';
const SECRET_DETECTION_DIFF_ENDPOINT = 'secret_detection.json'; const SECRET_DETECTION_DIFF_ENDPOINT = 'secret_detection.json';
const API_FUZZING_DIFF_ENDPOINT = 'api_fuzzing.json';
const COVERAGE_FUZZING_DIFF_ENDPOINT = 'coverage_fuzzing.json'; const COVERAGE_FUZZING_DIFF_ENDPOINT = 'coverage_fuzzing.json';
describe('Grouped security reports app', () => { describe('Grouped security reports app', () => {
...@@ -50,9 +53,11 @@ describe('Grouped security reports app', () => { ...@@ -50,9 +53,11 @@ describe('Grouped security reports app', () => {
canReadVulnerabilityFeedbackPath: true, canReadVulnerabilityFeedbackPath: true,
vulnerabilityFeedbackPath: 'vulnerability_feedback_path.json', vulnerabilityFeedbackPath: 'vulnerability_feedback_path.json',
coverageFuzzingHelpPath: 'path', coverageFuzzingHelpPath: 'path',
apiFuzzingHelpPath: 'path',
pipelineId: 123, pipelineId: 123,
projectId: 321, projectId: 321,
projectFullPath: 'path', projectFullPath: 'path',
apiFuzzingComparisonPath: API_FUZZING_DIFF_ENDPOINT,
containerScanningComparisonPath: CONTAINER_SCANNING_DIFF_ENDPOINT, containerScanningComparisonPath: CONTAINER_SCANNING_DIFF_ENDPOINT,
coverageFuzzingComparisonPath: COVERAGE_FUZZING_DIFF_ENDPOINT, coverageFuzzingComparisonPath: COVERAGE_FUZZING_DIFF_ENDPOINT,
dastComparisonPath: DAST_DIFF_ENDPOINT, dastComparisonPath: DAST_DIFF_ENDPOINT,
...@@ -114,6 +119,7 @@ describe('Grouped security reports app', () => { ...@@ -114,6 +119,7 @@ describe('Grouped security reports app', () => {
dependencyScanning: true, dependencyScanning: true,
secretDetection: true, secretDetection: true,
coverageFuzzing: true, coverageFuzzing: true,
apiFuzzing: true,
}, },
}; };
...@@ -125,6 +131,7 @@ describe('Grouped security reports app', () => { ...@@ -125,6 +131,7 @@ describe('Grouped security reports app', () => {
mock.onGet(SAST_DIFF_ENDPOINT).reply(500); mock.onGet(SAST_DIFF_ENDPOINT).reply(500);
mock.onGet(SECRET_DETECTION_DIFF_ENDPOINT).reply(500); mock.onGet(SECRET_DETECTION_DIFF_ENDPOINT).reply(500);
mock.onGet(COVERAGE_FUZZING_DIFF_ENDPOINT).reply(500); mock.onGet(COVERAGE_FUZZING_DIFF_ENDPOINT).reply(500);
mock.onGet(API_FUZZING_DIFF_ENDPOINT).reply(500);
createWrapper(allReportProps); createWrapper(allReportProps);
...@@ -138,6 +145,7 @@ describe('Grouped security reports app', () => { ...@@ -138,6 +145,7 @@ describe('Grouped security reports app', () => {
`secretDetection/${secretDetectionTypes.RECEIVE_DIFF_ERROR}`, `secretDetection/${secretDetectionTypes.RECEIVE_DIFF_ERROR}`,
), ),
waitForMutation(wrapper.vm.$store, types.RECEIVE_COVERAGE_FUZZING_DIFF_ERROR), waitForMutation(wrapper.vm.$store, types.RECEIVE_COVERAGE_FUZZING_DIFF_ERROR),
waitForMutation(wrapper.vm.$store, `apiFuzzing/${apiFuzzingTypes.RECEIVE_DIFF_ERROR}`),
]); ]);
}); });
...@@ -180,6 +188,7 @@ describe('Grouped security reports app', () => { ...@@ -180,6 +188,7 @@ describe('Grouped security reports app', () => {
mock.onGet(SAST_DIFF_ENDPOINT).reply(200, {}); mock.onGet(SAST_DIFF_ENDPOINT).reply(200, {});
mock.onGet(SECRET_DETECTION_DIFF_ENDPOINT).reply(200, {}); mock.onGet(SECRET_DETECTION_DIFF_ENDPOINT).reply(200, {});
mock.onGet(COVERAGE_FUZZING_DIFF_ENDPOINT).reply(200, {}); mock.onGet(COVERAGE_FUZZING_DIFF_ENDPOINT).reply(200, {});
mock.onGet(API_FUZZING_DIFF_ENDPOINT).reply(200, {});
createWrapper(allReportProps); createWrapper(allReportProps);
}); });
...@@ -201,6 +210,7 @@ describe('Grouped security reports app', () => { ...@@ -201,6 +210,7 @@ describe('Grouped security reports app', () => {
expect(wrapper.vm.$el.textContent).toContain('Container scanning is loading'); expect(wrapper.vm.$el.textContent).toContain('Container scanning is loading');
expect(wrapper.vm.$el.textContent).toContain('DAST is loading'); expect(wrapper.vm.$el.textContent).toContain('DAST is loading');
expect(wrapper.vm.$el.textContent).toContain('Coverage fuzzing is loading'); expect(wrapper.vm.$el.textContent).toContain('Coverage fuzzing is loading');
expect(wrapper.vm.$el.textContent).toContain('API fuzzing is loading');
}); });
}); });
...@@ -213,6 +223,7 @@ describe('Grouped security reports app', () => { ...@@ -213,6 +223,7 @@ describe('Grouped security reports app', () => {
mock.onGet(SAST_DIFF_ENDPOINT).reply(200, emptyResponse); mock.onGet(SAST_DIFF_ENDPOINT).reply(200, emptyResponse);
mock.onGet(SECRET_DETECTION_DIFF_ENDPOINT).reply(200, emptyResponse); mock.onGet(SECRET_DETECTION_DIFF_ENDPOINT).reply(200, emptyResponse);
mock.onGet(COVERAGE_FUZZING_DIFF_ENDPOINT).reply(200, emptyResponse); mock.onGet(COVERAGE_FUZZING_DIFF_ENDPOINT).reply(200, emptyResponse);
mock.onGet(API_FUZZING_DIFF_ENDPOINT).reply(200, emptyResponse);
createWrapper(allReportProps); createWrapper(allReportProps);
...@@ -226,6 +237,7 @@ describe('Grouped security reports app', () => { ...@@ -226,6 +237,7 @@ describe('Grouped security reports app', () => {
`secretDetection/${secretDetectionTypes.RECEIVE_DIFF_SUCCESS}`, `secretDetection/${secretDetectionTypes.RECEIVE_DIFF_SUCCESS}`,
), ),
waitForMutation(wrapper.vm.$store, types.RECEIVE_COVERAGE_FUZZING_DIFF_SUCCESS), waitForMutation(wrapper.vm.$store, types.RECEIVE_COVERAGE_FUZZING_DIFF_SUCCESS),
waitForMutation(wrapper.vm.$store, `apiFuzzing/${apiFuzzingTypes.RECEIVE_DIFF_SUCCESS}`),
]); ]);
}); });
...@@ -255,6 +267,14 @@ describe('Grouped security reports app', () => { ...@@ -255,6 +267,14 @@ describe('Grouped security reports app', () => {
// Renders DAST result // Renders DAST result
expect(wrapper.vm.$el.textContent).toContain('DAST detected no vulnerabilities.'); expect(wrapper.vm.$el.textContent).toContain('DAST detected no vulnerabilities.');
// Renders Coverage Fuzzing result
expect(wrapper.vm.$el.textContent).toContain(
'Coverage fuzzing detected no vulnerabilities.',
);
// Renders API Fuzzing result
expect(wrapper.vm.$el.textContent).toContain('API fuzzing detected no vulnerabilities.');
}); });
}); });
...@@ -266,6 +286,7 @@ describe('Grouped security reports app', () => { ...@@ -266,6 +286,7 @@ describe('Grouped security reports app', () => {
mock.onGet(SAST_DIFF_ENDPOINT).reply(200, sastDiffSuccessMock); mock.onGet(SAST_DIFF_ENDPOINT).reply(200, sastDiffSuccessMock);
mock.onGet(SECRET_DETECTION_DIFF_ENDPOINT).reply(200, secretScanningDiffSuccessMock); mock.onGet(SECRET_DETECTION_DIFF_ENDPOINT).reply(200, secretScanningDiffSuccessMock);
mock.onGet(COVERAGE_FUZZING_DIFF_ENDPOINT).reply(200, coverageFuzzingDiffSuccessMock); mock.onGet(COVERAGE_FUZZING_DIFF_ENDPOINT).reply(200, coverageFuzzingDiffSuccessMock);
mock.onGet(API_FUZZING_DIFF_ENDPOINT).reply(200, apiFuzzingDiffSuccessMock);
createWrapper(allReportProps); createWrapper(allReportProps);
...@@ -279,6 +300,7 @@ describe('Grouped security reports app', () => { ...@@ -279,6 +300,7 @@ describe('Grouped security reports app', () => {
`secretDetection/${secretDetectionTypes.RECEIVE_DIFF_SUCCESS}`, `secretDetection/${secretDetectionTypes.RECEIVE_DIFF_SUCCESS}`,
), ),
waitForMutation(wrapper.vm.$store, types.RECEIVE_COVERAGE_FUZZING_DIFF_SUCCESS), waitForMutation(wrapper.vm.$store, types.RECEIVE_COVERAGE_FUZZING_DIFF_SUCCESS),
waitForMutation(wrapper.vm.$store, `apiFuzzing/${apiFuzzingTypes.RECEIVE_DIFF_SUCCESS}`),
]); ]);
}); });
...@@ -292,7 +314,7 @@ describe('Grouped security reports app', () => { ...@@ -292,7 +314,7 @@ describe('Grouped security reports app', () => {
wrapper.vm.$el.querySelector('[data-testid="report-section-code-text"]').textContent, wrapper.vm.$el.querySelector('[data-testid="report-section-code-text"]').textContent,
), ),
).toEqual( ).toEqual(
'Security scanning detected 10 potential vulnerabilities 6 Critical 4 High and 0 Others', 'Security scanning detected 12 potential vulnerabilities 7 Critical 5 High and 0 Others',
); );
// Renders the expand button // Renders the expand button
...@@ -320,10 +342,15 @@ describe('Grouped security reports app', () => { ...@@ -320,10 +342,15 @@ describe('Grouped security reports app', () => {
'DAST detected 1 potential vulnerability 1 Critical 0 High and 0 Others', 'DAST detected 1 potential vulnerability 1 Critical 0 High and 0 Others',
); );
// Renders container scanning result // Renders coverage fuzzing scanning result
expect(trimText(wrapper.vm.$el.textContent)).toContain( expect(trimText(wrapper.vm.$el.textContent)).toContain(
'Coverage fuzzing detected 2 potential vulnerabilities 1 Critical 1 High and 0 Others', 'Coverage fuzzing detected 2 potential vulnerabilities 1 Critical 1 High and 0 Others',
); );
// Renders api fuzzing scanning result
expect(trimText(wrapper.vm.$el.textContent)).toContain(
'API fuzzing detected 2 potential vulnerabilities 1 Critical 1 High and 0 Others',
);
}); });
it('opens modal with more information', () => { it('opens modal with more information', () => {
...@@ -348,6 +375,7 @@ describe('Grouped security reports app', () => { ...@@ -348,6 +375,7 @@ describe('Grouped security reports app', () => {
${'dast'} | ${dastDiffSuccessMock.fixed} | ${dastDiffSuccessMock.added} ${'dast'} | ${dastDiffSuccessMock.fixed} | ${dastDiffSuccessMock.added}
${'secret-scanning'} | ${secretScanningDiffSuccessMock.fixed} | ${secretScanningDiffSuccessMock.added} ${'secret-scanning'} | ${secretScanningDiffSuccessMock.fixed} | ${secretScanningDiffSuccessMock.added}
${'coverage-fuzzing'} | ${coverageFuzzingDiffSuccessMock.fixed} | ${coverageFuzzingDiffSuccessMock.added} ${'coverage-fuzzing'} | ${coverageFuzzingDiffSuccessMock.fixed} | ${coverageFuzzingDiffSuccessMock.added}
${'api-fuzzing'} | ${apiFuzzingDiffSuccessMock.fixed} | ${apiFuzzingDiffSuccessMock.added}
`( `(
'renders a grouped-issues-list with the correct props for "$reportType" issues', 'renders a grouped-issues-list with the correct props for "$reportType" issues',
({ reportType, resolvedIssues, unresolvedIssues }) => { ({ reportType, resolvedIssues, unresolvedIssues }) => {
...@@ -413,6 +441,34 @@ describe('Grouped security reports app', () => { ...@@ -413,6 +441,34 @@ describe('Grouped security reports app', () => {
}); });
}); });
describe('api fuzzing reports', () => {
beforeEach(() => {
mock.onGet(API_FUZZING_DIFF_ENDPOINT).reply(200, apiFuzzingDiffSuccessMock);
createWrapper({
...props,
enabledReports: {
apiFuzzing: true,
},
});
return waitForMutation(
wrapper.vm.$store,
`apiFuzzing/${apiFuzzingTypes.RECEIVE_DIFF_SUCCESS}`,
);
});
it('should set setApiFuzzingDiffEndpoint', () => {
expect(wrapper.vm.apiFuzzing.paths.diffEndpoint).toEqual(API_FUZZING_DIFF_ENDPOINT);
});
it('should display the correct numbers of vulnerabilities', () => {
expect(trimText(wrapper.text())).toContain(
'API fuzzing detected 2 potential vulnerabilities 1 Critical 1 High and 0 Others',
);
});
});
describe('container scanning reports', () => { describe('container scanning reports', () => {
beforeEach(() => { beforeEach(() => {
mock.onGet(CONTAINER_SCANNING_DIFF_ENDPOINT).reply(200, containerScanningDiffSuccessMock); mock.onGet(CONTAINER_SCANNING_DIFF_ENDPOINT).reply(200, containerScanningDiffSuccessMock);
......
...@@ -337,3 +337,11 @@ export const coverageFuzzingDiffSuccessMock = { ...@@ -337,3 +337,11 @@ export const coverageFuzzingDiffSuccessMock = {
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 apiFuzzingDiffSuccessMock = {
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',
};
...@@ -26,6 +26,7 @@ import { ...@@ -26,6 +26,7 @@ import {
} from 'ee/vue_shared/security_reports/store/getters'; } from 'ee/vue_shared/security_reports/store/getters';
import createSastState from 'ee/vue_shared/security_reports/store/modules/sast/state'; import createSastState from 'ee/vue_shared/security_reports/store/modules/sast/state';
import createSecretScanningState from 'ee/vue_shared/security_reports/store/modules/secret_detection/state'; import createSecretScanningState from 'ee/vue_shared/security_reports/store/modules/secret_detection/state';
import createApiFuzzingState from 'ee/vue_shared/security_reports/store/modules/api_fuzzing/state';
import createState from 'ee/vue_shared/security_reports/store/state'; import createState from 'ee/vue_shared/security_reports/store/state';
import { groupedTextBuilder } from 'ee/vue_shared/security_reports/store/utils'; import { groupedTextBuilder } from 'ee/vue_shared/security_reports/store/utils';
...@@ -40,6 +41,7 @@ describe('Security reports getters', () => { ...@@ -40,6 +41,7 @@ describe('Security reports getters', () => {
state = createState(); state = createState();
state.sast = createSastState(); state.sast = createSastState();
state.secretDetection = createSecretScanningState(); state.secretDetection = createSecretScanningState();
state.apiFuzzing = createApiFuzzingState();
}); });
describe.each` describe.each`
...@@ -223,6 +225,7 @@ describe('Security reports getters', () => { ...@@ -223,6 +225,7 @@ describe('Security reports getters', () => {
state.dependencyScanning.isLoading = true; state.dependencyScanning.isLoading = true;
state.secretDetection.isLoading = true; state.secretDetection.isLoading = true;
state.coverageFuzzing.isLoading = true; state.coverageFuzzing.isLoading = true;
state.apiFuzzing.isLoading = true;
expect(areAllReportsLoading(state)).toEqual(true); expect(areAllReportsLoading(state)).toEqual(true);
}); });
...@@ -246,6 +249,7 @@ describe('Security reports getters', () => { ...@@ -246,6 +249,7 @@ describe('Security reports getters', () => {
state.dependencyScanning.hasError = true; state.dependencyScanning.hasError = true;
state.secretDetection.hasError = true; state.secretDetection.hasError = true;
state.coverageFuzzing.hasError = true; state.coverageFuzzing.hasError = true;
state.apiFuzzing.hasError = true;
expect(allReportsHaveError(state)).toEqual(true); expect(allReportsHaveError(state)).toEqual(true);
}); });
......
import MockAdapter from 'axios-mock-adapter';
import * as actions from 'ee/vue_shared/security_reports/store/modules/api_fuzzing/actions';
import * as types from 'ee/vue_shared/security_reports/store/modules/api_fuzzing/mutation_types';
import createState from 'ee/vue_shared/security_reports/store/modules/api_fuzzing/state';
import testAction from 'helpers/vuex_action_helper';
import axios from '~/lib/utils/axios_utils';
const diffEndpoint = 'diff-endpoint.json';
const blobPath = 'blob-path.json';
const reports = {
base: 'base',
head: 'head',
enrichData: 'enrichData',
diff: 'diff',
};
const error = 'Something went wrong';
const vulnerabilityFeedbackPath = 'vulnerability-feedback-path';
const rootState = { vulnerabilityFeedbackPath, blobPath };
const issue = {};
let state;
describe('EE api fuzzing report actions', () => {
beforeEach(() => {
state = createState();
});
describe('updateVulnerability', () => {
it(`should commit ${types.UPDATE_VULNERABILITY} with the correct response`, done => {
testAction(
actions.updateVulnerability,
issue,
state,
[
{
type: types.UPDATE_VULNERABILITY,
payload: issue,
},
],
[],
done,
);
});
});
describe('setDiffEndpoint', () => {
it(`should commit ${types.SET_DIFF_ENDPOINT} with the correct path`, done => {
testAction(
actions.setDiffEndpoint,
diffEndpoint,
state,
[
{
type: types.SET_DIFF_ENDPOINT,
payload: diffEndpoint,
},
],
[],
done,
);
});
});
describe('requestDiff', () => {
it(`should commit ${types.REQUEST_DIFF}`, done => {
testAction(actions.requestDiff, {}, state, [{ type: types.REQUEST_DIFF }], [], done);
});
});
describe('receiveDiffSuccess', () => {
it(`should commit ${types.RECEIVE_DIFF_SUCCESS} with the correct response`, done => {
testAction(
actions.receiveDiffSuccess,
reports,
state,
[
{
type: types.RECEIVE_DIFF_SUCCESS,
payload: reports,
},
],
[],
done,
);
});
});
describe('receiveDiffError', () => {
it(`should commit ${types.RECEIVE_DIFF_ERROR} with the correct response`, done => {
testAction(
actions.receiveDiffError,
error,
state,
[
{
type: types.RECEIVE_DIFF_ERROR,
payload: error,
},
],
[],
done,
);
});
});
describe('fetchDiff', () => {
let mock;
beforeEach(() => {
mock = new MockAdapter(axios);
state.paths.diffEndpoint = diffEndpoint;
rootState.canReadVulnerabilityFeedback = true;
});
afterEach(() => {
mock.restore();
});
describe('when diff and vulnerability feedback endpoints respond successfully', () => {
beforeEach(() => {
mock
.onGet(diffEndpoint)
.replyOnce(200, reports.diff)
.onGet(vulnerabilityFeedbackPath)
.replyOnce(200, reports.enrichData);
});
it('should dispatch the `receiveDiffSuccess` action', done => {
const { diff, enrichData } = reports;
testAction(
actions.fetchDiff,
{},
{ ...rootState, ...state },
[],
[
{ type: 'requestDiff' },
{
type: 'receiveDiffSuccess',
payload: {
diff,
enrichData,
},
},
],
done,
);
});
});
describe('when diff endpoint responds successfully and fetching vulnerability feedback is not authorized', () => {
beforeEach(() => {
rootState.canReadVulnerabilityFeedback = false;
mock.onGet(diffEndpoint).replyOnce(200, reports.diff);
});
it('should dispatch the `receiveDiffSuccess` action with empty enrich data', done => {
const { diff } = reports;
const enrichData = [];
testAction(
actions.fetchDiff,
{},
{ ...rootState, ...state },
[],
[
{ type: 'requestDiff' },
{
type: 'receiveDiffSuccess',
payload: {
diff,
enrichData,
},
},
],
done,
);
});
});
describe('when the vulnerability feedback endpoint fails', () => {
beforeEach(() => {
mock
.onGet(diffEndpoint)
.replyOnce(200, reports.diff)
.onGet(vulnerabilityFeedbackPath)
.replyOnce(404);
});
it('should dispatch the `receiveError` action', done => {
testAction(
actions.fetchDiff,
{},
{ ...rootState, ...state },
[],
[{ type: 'requestDiff' }, { type: 'receiveDiffError' }],
done,
);
});
});
describe('when the diff endpoint fails', () => {
beforeEach(() => {
mock
.onGet(diffEndpoint)
.replyOnce(404)
.onGet(vulnerabilityFeedbackPath)
.replyOnce(200, reports.enrichData);
});
it('should dispatch the `receiveDiffError` action', done => {
testAction(
actions.fetchDiff,
{},
{ ...rootState, ...state },
[],
[{ type: 'requestDiff' }, { type: 'receiveDiffError' }],
done,
);
});
});
});
});
import messages from 'ee/vue_shared/security_reports/store/messages';
import * as getters from 'ee/vue_shared/security_reports/store/modules/api_fuzzing/getters';
const createReport = (config = {}) => ({
paths: [],
newIssues: [],
...config,
});
describe('groupedApiFuzzingText', () => {
it("should return the error message if there's an error", () => {
const apiFuzzing = createReport({ hasError: true });
const result = getters.groupedApiFuzzingText(apiFuzzing);
expect(result).toStrictEqual({ message: messages.API_FUZZING_HAS_ERROR });
});
it("should return the loading message if it's still loading", () => {
const apiFuzzing = createReport({ isLoading: true });
const result = getters.groupedApiFuzzingText(apiFuzzing);
expect(result).toStrictEqual({ message: messages.API_FUZZING_IS_LOADING });
});
it('should call groupedTextBuilder if everything is fine', () => {
const apiFuzzing = createReport();
const result = getters.groupedApiFuzzingText(apiFuzzing);
expect(result).toStrictEqual({
countMessage: '',
critical: 0,
high: 0,
message: 'API fuzzing detected %{totalStart}no%{totalEnd} vulnerabilities.',
other: 0,
status: '',
total: 0,
});
});
});
describe('apiFuzzingStatusIcon', () => {
it("should return `loading` when we're still loading", () => {
const apiFuzzing = createReport({ isLoading: true });
const result = getters.apiFuzzingStatusIcon(apiFuzzing);
expect(result).toBe('loading');
});
it("should return `warning` when there's an issue", () => {
const apiFuzzing = createReport({ hasError: true });
const result = getters.apiFuzzingStatusIcon(apiFuzzing);
expect(result).toBe('warning');
});
it('should return `success` when nothing is wrong', () => {
const apiFuzzing = createReport();
const result = getters.apiFuzzingStatusIcon(apiFuzzing);
expect(result).toBe('success');
});
});
import * as types from 'ee/vue_shared/security_reports/store/modules/api_fuzzing/mutation_types';
import mutations from 'ee/vue_shared/security_reports/store/modules/api_fuzzing/mutations';
import createState from 'ee/vue_shared/security_reports/store/modules/api_fuzzing/state';
const createIssue = ({ ...config }) => ({ changed: false, ...config });
describe('EE api fuzzing module mutations', () => {
const path = 'path';
let state;
beforeEach(() => {
state = createState();
});
describe(types.UPDATE_VULNERABILITY, () => {
let newIssue;
let resolvedIssue;
let allIssue;
beforeEach(() => {
newIssue = createIssue({ project_fingerprint: 'new' });
resolvedIssue = createIssue({ project_fingerprint: 'resolved' });
allIssue = createIssue({ project_fingerprint: 'all' });
state.newIssues.push(newIssue);
state.resolvedIssues.push(resolvedIssue);
state.allIssues.push(allIssue);
});
describe('with a `new` issue', () => {
beforeEach(() => {
mutations[types.UPDATE_VULNERABILITY](state, { ...newIssue, changed: true });
});
it('should update the correct issue', () => {
expect(state.newIssues[0].changed).toBe(true);
});
});
describe('with a `resolved` issue', () => {
beforeEach(() => {
mutations[types.UPDATE_VULNERABILITY](state, { ...resolvedIssue, changed: true });
});
it('should update the correct issue', () => {
expect(state.resolvedIssues[0].changed).toBe(true);
});
});
describe('with an `all` issue', () => {
beforeEach(() => {
mutations[types.UPDATE_VULNERABILITY](state, { ...allIssue, changed: true });
});
it('should update the correct issue', () => {
expect(state.allIssues[0].changed).toBe(true);
});
});
describe('with an invalid issue', () => {
beforeEach(() => {
mutations[types.UPDATE_VULNERABILITY](
state,
createIssue({ project_fingerprint: 'invalid', changed: true }),
);
});
it('should ignore the issue', () => {
expect(state.newIssues[0].changed).toBe(false);
expect(state.resolvedIssues[0].changed).toBe(false);
expect(state.allIssues[0].changed).toBe(false);
});
});
});
describe(types.SET_DIFF_ENDPOINT, () => {
it('should set the API Fuzzing diff endpoint', () => {
mutations[types.SET_DIFF_ENDPOINT](state, path);
expect(state.paths.diffEndpoint).toBe(path);
});
});
describe(types.REQUEST_DIFF, () => {
it('should set the `isLoading` status to `true`', () => {
mutations[types.REQUEST_DIFF](state);
expect(state.isLoading).toBe(true);
});
});
describe(types.RECEIVE_DIFF_SUCCESS, () => {
const scans = [
{
scanned_resources_count: 123,
job_path: '/group/project/-/jobs/123546789',
},
{
scanned_resources_count: 321,
job_path: '/group/project/-/jobs/987654321',
},
];
beforeEach(() => {
const reports = {
diff: {
added: [
createIssue({ cve: 'CVE-1' }),
createIssue({ cve: 'CVE-2' }),
createIssue({ cve: 'CVE-3' }),
],
fixed: [createIssue({ cve: 'CVE-4' }), createIssue({ cve: 'CVE-5' })],
existing: [createIssue({ cve: 'CVE-6' })],
base_report_out_of_date: true,
scans,
},
};
state.isLoading = true;
mutations[types.RECEIVE_DIFF_SUCCESS](state, reports);
});
it('should set the `isLoading` status to `false`', () => {
expect(state.isLoading).toBe(false);
});
it('should set the `baseReportOutofDate` status to `false`', () => {
expect(state.baseReportOutofDate).toBe(true);
});
it('should have the relevant `new` issues', () => {
expect(state.newIssues).toHaveLength(3);
});
it('should have the relevant `resolved` issues', () => {
expect(state.resolvedIssues).toHaveLength(2);
});
it('should have the relevant `all` issues', () => {
expect(state.allIssues).toHaveLength(1);
});
it('should set scans', () => {
expect(state.scans).toEqual(scans);
});
});
describe(types.RECEIVE_DIFF_ERROR, () => {
beforeEach(() => {
state.isLoading = true;
mutations[types.RECEIVE_DIFF_ERROR](state);
});
it('should set the `isLoading` status to `false`', () => {
expect(state.isLoading).toBe(false);
});
it('should set the `hasError` status to `true`', () => {
expect(state.hasError).toBe(true);
});
});
});
...@@ -23411,9 +23411,6 @@ msgstr "" ...@@ -23411,9 +23411,6 @@ msgstr ""
msgid "Request Access" msgid "Request Access"
msgstr "" msgstr ""
msgid "Request Headers"
msgstr ""
msgid "Request details" msgid "Request details"
msgstr "" msgstr ""
...@@ -23592,12 +23589,6 @@ msgstr "" ...@@ -23592,12 +23589,6 @@ msgstr ""
msgid "Response" msgid "Response"
msgstr "" msgstr ""
msgid "Response Headers"
msgstr ""
msgid "Response Status"
msgstr ""
msgid "Response didn't include `service_desk_address`" msgid "Response didn't include `service_desk_address`"
msgstr "" msgstr ""
...@@ -30671,6 +30662,9 @@ msgstr "" ...@@ -30671,6 +30662,9 @@ msgstr ""
msgid "Vulnerability|Activity" msgid "Vulnerability|Activity"
msgstr "" msgstr ""
msgid "Vulnerability|Actual Response"
msgstr ""
msgid "Vulnerability|Actual received response is the one received when this fault was detected" msgid "Vulnerability|Actual received response is the one received when this fault was detected"
msgstr "" msgstr ""
...@@ -30725,6 +30719,9 @@ msgstr "" ...@@ -30725,6 +30719,9 @@ msgstr ""
msgid "Vulnerability|Project" msgid "Vulnerability|Project"
msgstr "" msgstr ""
msgid "Vulnerability|Request"
msgstr ""
msgid "Vulnerability|Request/Response" msgid "Vulnerability|Request/Response"
msgstr "" msgstr ""
...@@ -30743,6 +30740,9 @@ msgstr "" ...@@ -30743,6 +30740,9 @@ msgstr ""
msgid "Vulnerability|The unmodified response is the original response that had no mutations done to the request" msgid "Vulnerability|The unmodified response is the original response that had no mutations done to the request"
msgstr "" msgstr ""
msgid "Vulnerability|Unmodified Response"
msgstr ""
msgid "Wait for the file to load to copy its contents" msgid "Wait for the file to load to copy its contents"
msgstr "" msgstr ""
...@@ -32262,6 +32262,9 @@ msgstr "" ...@@ -32262,6 +32262,9 @@ msgstr ""
msgid "ciReport|%{improvedNum} improved" msgid "ciReport|%{improvedNum} improved"
msgstr "" msgstr ""
msgid "ciReport|%{linkStartTag}Learn more about API Fuzzing%{linkEndTag}"
msgstr ""
msgid "ciReport|%{linkStartTag}Learn more about Container Scanning %{linkEndTag}" msgid "ciReport|%{linkStartTag}Learn more about Container Scanning %{linkEndTag}"
msgstr "" msgstr ""
...@@ -32301,6 +32304,9 @@ msgstr "" ...@@ -32301,6 +32304,9 @@ msgstr ""
msgid "ciReport|API Fuzzing" msgid "ciReport|API Fuzzing"
msgstr "" msgstr ""
msgid "ciReport|API fuzzing"
msgstr ""
msgid "ciReport|All projects" msgid "ciReport|All projects"
msgstr "" msgstr ""
......
...@@ -30,6 +30,8 @@ module QA ...@@ -30,6 +30,8 @@ module QA
element :dependency_scan_report element :dependency_scan_report
element :container_scan_report element :container_scan_report
element :dast_scan_report element :dast_scan_report
element :coverage_fuzzing_report
element :api_fuzzing_report
end end
view 'app/assets/javascripts/reports/components/report_section.vue' do view 'app/assets/javascripts/reports/components/report_section.vue' do
......
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