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 {
'dependencyScanning',
'containerScanning',
'coverageFuzzing',
'apiFuzzing',
'secretDetection',
],
};
......@@ -327,6 +328,7 @@ export default {
:enabled-reports="mr.enabledReports"
:sast-help-path="mr.sastHelp"
:dast-help-path="mr.dastHelp"
:api-fuzzing-help-path="mr.apiFuzzingHelp"
:coverage-fuzzing-help-path="mr.coverageFuzzingHelp"
:container-scanning-help-path="mr.containerScanningHelp"
:dependency-scanning-help-path="mr.dependencyScanningHelp"
......@@ -348,6 +350,7 @@ export default {
:target-branch-tree-path="mr.targetBranchTreePath"
:new-pipeline-path="mr.newPipelinePath"
:container-scanning-comparison-path="mr.containerScanningComparisonPath"
:api-fuzzing-comparison-path="mr.apiFuzzingComparisonPath"
:coverage-fuzzing-comparison-path="mr.coverageFuzzingComparisonPath"
:dast-comparison-path="mr.dastComparisonPath"
:dependency-scanning-comparison-path="mr.dependencyScanningComparisonPath"
......
......@@ -10,6 +10,7 @@ export default class MergeRequestStore extends CEMergeRequestStore {
this.sastHelp = data.sast_help_path;
this.containerScanningHelp = data.container_scanning_help_path;
this.dastHelp = data.dast_help_path;
this.apiFuzzingHelp = data.api_fuzzing_help_path;
this.coverageFuzzingHelp = data.coverage_fuzzing_help_path;
this.secretScanningHelp = data.secret_scanning_help_path;
this.dependencyScanningHelp = data.dependency_scanning_help_path;
......@@ -56,6 +57,7 @@ export default class MergeRequestStore extends CEMergeRequestStore {
// Security scan diff paths
this.containerScanningComparisonPath = data.container_scanning_comparison_path;
this.coverageFuzzingComparisonPath = data.coverage_fuzzing_comparison_path;
this.apiFuzzingComparisonPath = data.api_fuzzing_comparison_path;
this.dastComparisonPath = data.dast_comparison_path;
this.dependencyScanningComparisonPath = data.dependency_scanning_comparison_path;
}
......
<script>
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 CodeBlock from '~/vue_shared/components/code_block.vue';
import SeverityBadge from './severity_badge.vue';
......@@ -60,11 +61,24 @@ export default {
links() {
return this.asNonEmptyListOrNull(this.vulnerability.links);
},
requestHeaders() {
return this.headersToFormattedString(this.vulnerability.request?.headers);
assertion() {
return this.vulnerability.evidenceSource?.name;
},
responseHeaders() {
return this.headersToFormattedString(this.vulnerability.response?.headers);
recordedMessage() {
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() {
return this.vulnerability.response?.status_code;
......@@ -93,8 +107,22 @@ export default {
stacktraceSnippet() {
return this.vulnLocation?.stacktrace_snippet;
},
hasRequest() {
return Boolean(this.constructedRequest);
},
hasResponse() {
return Boolean(this.constructedResponse);
},
hasRecordedResponse() {
return Boolean(this.constructedRecordedResponse);
},
},
methods: {
getHeadersAsCodeBlockLines(headers) {
return Array.isArray(headers)
? headers.map(({ name, value }) => `${name}: ${value}`).join('\n')
: '';
},
hasMoreValues(index, values) {
return index < values.length - 1;
},
......@@ -104,6 +132,22 @@ export default {
headersToFormattedString(headers = []) {
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>
......@@ -131,14 +175,17 @@ export default {
<gl-friendly-wrap :text="url" />
</gl-link>
</vulnerability-detail>
<vulnerability-detail v-if="requestHeaders" :label="__('Request Headers')">
<code-block ref="requestHeaders" :code="requestHeaders" max-height="225px" />
<vulnerability-detail v-if="hasRequest" :label="s__('Vulnerability|Request')">
<code-block ref="request" :code="constructedRequest" max-height="225px" />
</vulnerability-detail>
<vulnerability-detail v-if="responseStatusCode" :label="__('Response Status')">
<gl-friendly-wrap ref="responseStatusCode" :text="responseStatusCode" />
<vulnerability-detail
v-if="hasRecordedResponse"
:label="s__('Vulnerability|Unmodified Response')"
>
<code-block ref="recordedResponse" :code="constructedRecordedResponse" max-height="225px" />
</vulnerability-detail>
<vulnerability-detail v-if="responseHeaders" :label="__('Response Headers')">
<code-block ref="responseHeaders" :code="responseHeaders" max-height="225px" />
<vulnerability-detail v-if="hasResponse" :label="s__('Vulnerability|Actual Response')">
<code-block ref="unmodifiedResponse" :code="constructedResponse" max-height="225px" />
</vulnerability-detail>
<vulnerability-detail v-if="file" :label="s__('Vulnerability|File')">
<gl-link
......
......@@ -19,6 +19,7 @@ import { fetchPolicies } from '~/lib/graphql';
import securityReportSummaryQuery from './graphql/mr_security_report_summary.graphql';
import {
MODULE_CONTAINER_SCANNING,
MODULE_API_FUZZING,
MODULE_COVERAGE_FUZZING,
MODULE_DAST,
MODULE_DEPENDENCY_SCANNING,
......@@ -101,6 +102,11 @@ export default {
required: false,
default: '',
},
apiFuzzingHelpPath: {
type: String,
required: false,
default: '',
},
coverageFuzzingHelpPath: {
type: String,
required: false,
......@@ -185,6 +191,11 @@ export default {
type: String,
required: true,
},
apiFuzzingComparisonPath: {
type: String,
required: false,
default: '',
},
containerScanningComparisonPath: {
type: String,
required: false,
......@@ -222,6 +233,7 @@ export default {
MODULE_SAST,
MODULE_CONTAINER_SCANNING,
MODULE_DAST,
MODULE_API_FUZZING,
MODULE_COVERAGE_FUZZING,
MODULE_DEPENDENCY_SCANNING,
MODULE_SECRET_DETECTION,
......@@ -252,6 +264,7 @@ export default {
'groupedSecretDetectionText',
'secretDetectionStatusIcon',
]),
...mapGetters(MODULE_API_FUZZING, ['groupedApiFuzzingText', 'apiFuzzingStatusIcon']),
...mapGetters('pipelineJobs', ['hasFuzzingArtifacts', 'fuzzingJobsWithArtifact']),
securityTab() {
return `${this.pipelinePath}/security`;
......@@ -265,6 +278,9 @@ export default {
hasDastReports() {
return this.enabledReports.dast;
},
hasApiFuzzingReports() {
return this.enabledReports.apiFuzzing;
},
hasCoverageFuzzingReports() {
// TODO: Remove feature flag in https://gitlab.com/gitlab-org/gitlab/-/issues/257839
return this.enabledReports.coverageFuzzing && this.glFeatures.coverageFuzzingMrWidget;
......@@ -293,6 +309,9 @@ export default {
dastDownloadLink() {
return this.dastSummary?.scannedResourcesCsvPath || '';
},
hasApiFuzzingIssues() {
return this.hasIssuesForReportType(MODULE_API_FUZZING);
},
hasCoverageFuzzingIssues() {
return this.hasIssuesForReportType(MODULE_COVERAGE_FUZZING);
},
......@@ -359,6 +378,11 @@ export default {
this.fetchCoverageFuzzingDiff();
this.fetchPipelineJobs();
}
if (this.apiFuzzingComparisonPath && this.hasApiFuzzingReports) {
this.setApiFuzzingDiffEndpoint(this.apiFuzzingComparisonPath);
this.fetchApiFuzzingDiff();
}
},
methods: {
...mapActions([
......@@ -400,6 +424,10 @@ export default {
setSecretDetectionDiffEndpoint: 'setDiffEndpoint',
fetchSecretDetectionDiff: 'fetchDiff',
}),
...mapActions(MODULE_API_FUZZING, {
setApiFuzzingDiffEndpoint: 'setDiffEndpoint',
fetchApiFuzzingDiff: 'fetchDiff',
}),
...mapActions('pipelineJobs', ['fetchPipelineJobs', 'setPipelineJobsPath', 'setProjectId']),
...mapActions('pipelineJobs', {
setPipelineJobsId: 'setPipelineId',
......@@ -616,6 +644,28 @@ export default {
/>
</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
:modal="modal"
:can-create-issue="canCreateIssue"
......
......@@ -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';
* namespaces in the store state, as if they were modules.
*/
export const MODULE_CONTAINER_SCANNING = 'containerScanning';
export const MODULE_API_FUZZING = 'apiFuzzing';
export const MODULE_COVERAGE_FUZZING = 'coverageFuzzing';
export const MODULE_DAST = 'dast';
export const MODULE_DEPENDENCY_SCANNING = 'dependencyScanning';
......
......@@ -6,10 +6,11 @@ import * as actions from './actions';
import * as getters from './getters';
import mutations from './mutations';
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 secretDetection from './modules/secret_detection';
import apiFuzzing from './modules/api_fuzzing';
Vue.use(Vuex);
......@@ -18,6 +19,7 @@ export default () =>
modules: {
[MODULE_SAST]: sast,
[MODULE_SECRET_DETECTION]: secretDetection,
[MODULE_API_FUZZING]: apiFuzzing,
pipelineJobs,
},
actions,
......
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 = {
sast: `${MODULE_SAST}/updateVulnerability`,
......@@ -8,6 +8,7 @@ export const updateIssueActionsMap = {
dast: 'updateDastIssue',
secret_detection: `${MODULE_SECRET_DETECTION}/updateVulnerability`,
coverage_fuzzing: 'updateCoverageFuzzingIssue',
api_fuzzing: `${MODULE_API_FUZZING}/updateVulnerability`,
};
export default function configureMediator(store) {
......
......@@ -10,6 +10,7 @@ const CONTAINER_SCANNING = s__('ciReport|Container scanning');
const DEPENDENCY_SCANNING = s__('ciReport|Dependency scanning');
const SECRET_SCANNING = s__('ciReport|Secret scanning');
const COVERAGE_FUZZING = s__('ciReport|Coverage fuzzing');
const API_FUZZING = s__('ciReport|API fuzzing');
export default {
SAST,
......@@ -18,6 +19,7 @@ export default {
DEPENDENCY_SCANNING,
SECRET_SCANNING,
COVERAGE_FUZZING,
API_FUZZING,
TRANSLATION_IS_LOADING,
TRANSLATION_HAS_ERROR,
SAST_IS_LOADING: sprintf(TRANSLATION_IS_LOADING, { reportType: SAST }),
......@@ -42,4 +44,8 @@ export default {
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
export const UPDATE_CONTAINER_SCANNING_ISSUE = 'UPDATE_CONTAINER_SCANNING_ISSUE';
export const UPDATE_DAST_ISSUE = 'UPDATE_DAST_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 CLOSE_DISMISSAL_COMMENT_BOX = 'CLOSE_DISMISSAL_COMMENT_BOX';
import {
MODULE_API_FUZZING,
MODULE_CONTAINER_SCANNING,
MODULE_COVERAGE_FUZZING,
MODULE_DAST,
......@@ -23,6 +24,7 @@ export default () => ({
reportTypes: [
MODULE_CONTAINER_SCANNING,
MODULE_API_FUZZING,
MODULE_COVERAGE_FUZZING,
MODULE_DAST,
MODULE_DEPENDENCY_SCANNING,
......
......@@ -11,6 +11,7 @@
window.gl.mrWidgetData.container_scanning_help_path = '#{help_page_path("user/application_security/container_scanning/index")}';
window.gl.mrWidgetData.dast_help_path = '#{help_page_path("user/application_security/dast/index")}';
window.gl.mrWidgetData.dependency_scanning_help_path = '#{help_page_path("user/application_security/dependency_scanning/index")}';
window.gl.mrWidgetData.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.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)}'
......@@ -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.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.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 {
dependencyScanningDiffSuccessMock,
secretScanningDiffSuccessMock,
coverageFuzzingDiffSuccessMock,
apiFuzzingDiffSuccessMock,
} from 'ee_jest/vue_shared/security_reports/mock_data';
import { TEST_HOST } from 'helpers/test_constants';
import { trimText } from 'helpers/text_helper';
......@@ -37,6 +38,7 @@ const DEPENDENCY_SCANNING_SELECTOR = '.js-dependency-scanning-widget';
const CONTAINER_SCANNING_SELECTOR = '.js-container-scanning';
const SECRET_SCANNING_SELECTOR = '.js-secret-scanning';
const COVERAGE_FUZZING_SELECTOR = '.js-coverage-fuzzing-widget';
const API_FUZZING_SELECTOR = '.js-api-fuzzing-widget';
describe('ee merge request widget options', () => {
let vm;
......@@ -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', () => {
const licenseManagementApiUrl = `${TEST_HOST}/manage_license_api`;
......
......@@ -15,6 +15,7 @@ export default {
dependency_scanning_comparison_path: '/dependency_scanning_comparison_path',
dast_comparison_path: '/dast_comparison_path',
coverage_fuzzing_comparison_path: '/coverage_fuzzing_comparison_path',
api_fuzzing_comparison_path: '/api_fuzzing_comparison_path',
};
// Browser Performance Testing
......
......@@ -75,6 +75,7 @@ describe('MergeRequestStore', () => {
'sast_comparison_path',
'dast_comparison_path',
'secret_scanning_comparison_path',
'api_fuzzing_comparison_path',
'coverage_fuzzing_comparison_path',
])('should set %s path', property => {
// Ensure something is set in the mock data
......
......@@ -55,34 +55,11 @@ exports[`VulnerabilityDetails component pin test renders correctly 1`] = `
</gl-link-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
label="File"
......
import { GlLink, GlBadge } from '@gitlab/ui';
import { mount, shallowMount } from '@vue/test-utils';
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 VulnerabilityDetails from 'ee/vue_shared/security_reports/components/vulnerability_details.vue';
import { TEST_HOST } from 'helpers/test_constants';
import CodeBlock from '~/vue_shared/components/code_block.vue';
import { mockFindings } from '../mock_data';
function makeVulnerability(changes = {}) {
......@@ -30,9 +32,9 @@ describe('VulnerabilityDetails component', () => {
};
const findLink = name => wrapper.find({ ref: `${name}Link` });
const findRequestHeaders = () => wrapper.find({ ref: 'requestHeaders' });
const findResponseHeaders = () => wrapper.find({ ref: 'responseHeaders' });
const findResponseStatusCode = () => wrapper.find({ ref: 'responseStatusCode' });
const findRequest = () => wrapper.find({ ref: 'request' });
const findRecordedResponse = () => wrapper.find({ ref: 'recordedResponse' });
const findUnmodifiedResponse = () => wrapper.find({ ref: 'unmodifiedResponse' });
const findCrashAddress = () => wrapper.find({ ref: 'crashAddress' });
const findCrashState = () => wrapper.find({ ref: 'crashState' });
const findCrashType = () => wrapper.find({ ref: 'crashType' });
......@@ -164,9 +166,12 @@ describe('VulnerabilityDetails component', () => {
});
describe('with request information', () => {
let vulnerability;
beforeEach(() => {
const vulnerability = makeVulnerability({
vulnerability = makeVulnerability({
request: {
method: 'GET',
url: 'http://foo.bar/path',
headers: [{ name: 'key1', value: 'value1' }, { name: 'key2', value: 'value2' }],
},
......@@ -178,14 +183,17 @@ describe('VulnerabilityDetails component', () => {
expect(findLink('url').attributes('href')).toBe('http://foo.bar/path');
});
it('renders a code-block containing the http headers', () => {
expect(findRequestHeaders().is(CodeBlock)).toBe(true);
expect(findRequestHeaders().text()).toBe('key1: value1\nkey2: value2');
it('renders a code-block containing the http request', () => {
const { method, url } = vulnerability.request;
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', () => {
expect(findRequestHeaders().props('maxHeight')).not.toBeFalsy();
expect(findRequestHeaders().props('maxHeight')).toEqual(expect.any(String));
expect(findRequest().props('maxHeight')).not.toBeFalsy();
expect(findRequest().props('maxHeight')).toEqual(expect.any(String));
});
});
......@@ -204,16 +212,19 @@ describe('VulnerabilityDetails component', () => {
expect(findLink('url').text()).toBe('http://foo.com/bar');
});
it('does not render a code block containing the request-headers', () => {
expect(findRequestHeaders().exists()).toBe(false);
it('does not render a code block containing the request', () => {
expect(findRequest().exists()).toBe(false);
});
});
describe('with response information', () => {
let vulnerability;
beforeEach(() => {
const vulnerability = makeVulnerability({
vulnerability = makeVulnerability({
response: {
status_code: '200',
reason_phrase: 'INTERNAL SERVER ERROR',
headers: [{ name: 'key1', value: 'value1' }, { name: 'key2', value: 'value2' }],
},
});
......@@ -221,29 +232,71 @@ describe('VulnerabilityDetails component', () => {
});
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', () => {
const responseHeaders = findResponseHeaders();
it('renders a code block containing the response', () => {
const { reason_phrase } = vulnerability.response;
const response = findUnmodifiedResponse();
expect(responseHeaders.is(CodeBlock)).toBe(true);
expect(responseHeaders.text()).toBe('key1: value1\nkey2: value2');
expect(response.is(CodeBlock)).toBe(true);
expect(response.text()).toContain(reason_phrase);
expect(response.text()).toContain('key1: value1\nkey2: value2');
});
});
describe('without response information', () => {
describe('without unmodified response information', () => {
beforeEach(() => {
const vulnerability = makeVulnerability();
componentFactory(vulnerability);
});
it('does not render the status code', () => {
expect(findResponseStatusCode().exists()).toBe(false);
it('does not render the response', () => {
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', () => {
expect(findResponseHeaders().exists()).toBe(false);
it('does not render the response', () => {
expect(findRecordedResponse().exists()).toBe(false);
});
});
......
......@@ -6,6 +6,7 @@ import appStore from 'ee/vue_shared/security_reports/store';
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 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 { trimText } from 'helpers/text_helper';
import { mockTracking, unmockTracking } from 'helpers/tracking_helper';
......@@ -22,6 +23,7 @@ import {
dependencyScanningDiffSuccessMock,
secretScanningDiffSuccessMock,
coverageFuzzingDiffSuccessMock,
apiFuzzingDiffSuccessMock,
mockFindings,
} from './mock_data';
......@@ -31,6 +33,7 @@ const DAST_DIFF_ENDPOINT = 'dast.json';
const SAST_DIFF_ENDPOINT = 'sast.json';
const PIPELINE_JOBS_ENDPOINT = 'jobs.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';
describe('Grouped security reports app', () => {
......@@ -50,9 +53,11 @@ describe('Grouped security reports app', () => {
canReadVulnerabilityFeedbackPath: true,
vulnerabilityFeedbackPath: 'vulnerability_feedback_path.json',
coverageFuzzingHelpPath: 'path',
apiFuzzingHelpPath: 'path',
pipelineId: 123,
projectId: 321,
projectFullPath: 'path',
apiFuzzingComparisonPath: API_FUZZING_DIFF_ENDPOINT,
containerScanningComparisonPath: CONTAINER_SCANNING_DIFF_ENDPOINT,
coverageFuzzingComparisonPath: COVERAGE_FUZZING_DIFF_ENDPOINT,
dastComparisonPath: DAST_DIFF_ENDPOINT,
......@@ -114,6 +119,7 @@ describe('Grouped security reports app', () => {
dependencyScanning: true,
secretDetection: true,
coverageFuzzing: true,
apiFuzzing: true,
},
};
......@@ -125,6 +131,7 @@ describe('Grouped security reports app', () => {
mock.onGet(SAST_DIFF_ENDPOINT).reply(500);
mock.onGet(SECRET_DETECTION_DIFF_ENDPOINT).reply(500);
mock.onGet(COVERAGE_FUZZING_DIFF_ENDPOINT).reply(500);
mock.onGet(API_FUZZING_DIFF_ENDPOINT).reply(500);
createWrapper(allReportProps);
......@@ -138,6 +145,7 @@ describe('Grouped security reports app', () => {
`secretDetection/${secretDetectionTypes.RECEIVE_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', () => {
mock.onGet(SAST_DIFF_ENDPOINT).reply(200, {});
mock.onGet(SECRET_DETECTION_DIFF_ENDPOINT).reply(200, {});
mock.onGet(COVERAGE_FUZZING_DIFF_ENDPOINT).reply(200, {});
mock.onGet(API_FUZZING_DIFF_ENDPOINT).reply(200, {});
createWrapper(allReportProps);
});
......@@ -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('DAST 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', () => {
mock.onGet(SAST_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(API_FUZZING_DIFF_ENDPOINT).reply(200, emptyResponse);
createWrapper(allReportProps);
......@@ -226,6 +237,7 @@ describe('Grouped security reports app', () => {
`secretDetection/${secretDetectionTypes.RECEIVE_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', () => {
// Renders DAST result
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', () => {
mock.onGet(SAST_DIFF_ENDPOINT).reply(200, sastDiffSuccessMock);
mock.onGet(SECRET_DETECTION_DIFF_ENDPOINT).reply(200, secretScanningDiffSuccessMock);
mock.onGet(COVERAGE_FUZZING_DIFF_ENDPOINT).reply(200, coverageFuzzingDiffSuccessMock);
mock.onGet(API_FUZZING_DIFF_ENDPOINT).reply(200, apiFuzzingDiffSuccessMock);
createWrapper(allReportProps);
......@@ -279,6 +300,7 @@ describe('Grouped security reports app', () => {
`secretDetection/${secretDetectionTypes.RECEIVE_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', () => {
wrapper.vm.$el.querySelector('[data-testid="report-section-code-text"]').textContent,
),
).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
......@@ -320,10 +342,15 @@ describe('Grouped security reports app', () => {
'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(
'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', () => {
......@@ -348,6 +375,7 @@ describe('Grouped security reports app', () => {
${'dast'} | ${dastDiffSuccessMock.fixed} | ${dastDiffSuccessMock.added}
${'secret-scanning'} | ${secretScanningDiffSuccessMock.fixed} | ${secretScanningDiffSuccessMock.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',
({ reportType, resolvedIssues, unresolvedIssues }) => {
......@@ -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', () => {
beforeEach(() => {
mock.onGet(CONTAINER_SCANNING_DIFF_ENDPOINT).reply(200, containerScanningDiffSuccessMock);
......
......@@ -337,3 +337,11 @@ export const coverageFuzzingDiffSuccessMock = {
base_report_out_of_date: false,
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 {
} from 'ee/vue_shared/security_reports/store/getters';
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 createApiFuzzingState from 'ee/vue_shared/security_reports/store/modules/api_fuzzing/state';
import createState from 'ee/vue_shared/security_reports/store/state';
import { groupedTextBuilder } from 'ee/vue_shared/security_reports/store/utils';
......@@ -40,6 +41,7 @@ describe('Security reports getters', () => {
state = createState();
state.sast = createSastState();
state.secretDetection = createSecretScanningState();
state.apiFuzzing = createApiFuzzingState();
});
describe.each`
......@@ -223,6 +225,7 @@ describe('Security reports getters', () => {
state.dependencyScanning.isLoading = true;
state.secretDetection.isLoading = true;
state.coverageFuzzing.isLoading = true;
state.apiFuzzing.isLoading = true;
expect(areAllReportsLoading(state)).toEqual(true);
});
......@@ -246,6 +249,7 @@ describe('Security reports getters', () => {
state.dependencyScanning.hasError = true;
state.secretDetection.hasError = true;
state.coverageFuzzing.hasError = true;
state.apiFuzzing.hasError = 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 ""
msgid "Request Access"
msgstr ""
msgid "Request Headers"
msgstr ""
msgid "Request details"
msgstr ""
......@@ -23592,12 +23589,6 @@ msgstr ""
msgid "Response"
msgstr ""
msgid "Response Headers"
msgstr ""
msgid "Response Status"
msgstr ""
msgid "Response didn't include `service_desk_address`"
msgstr ""
......@@ -30671,6 +30662,9 @@ msgstr ""
msgid "Vulnerability|Activity"
msgstr ""
msgid "Vulnerability|Actual Response"
msgstr ""
msgid "Vulnerability|Actual received response is the one received when this fault was detected"
msgstr ""
......@@ -30725,6 +30719,9 @@ msgstr ""
msgid "Vulnerability|Project"
msgstr ""
msgid "Vulnerability|Request"
msgstr ""
msgid "Vulnerability|Request/Response"
msgstr ""
......@@ -30743,6 +30740,9 @@ msgstr ""
msgid "Vulnerability|The unmodified response is the original response that had no mutations done to the request"
msgstr ""
msgid "Vulnerability|Unmodified Response"
msgstr ""
msgid "Wait for the file to load to copy its contents"
msgstr ""
......@@ -32262,6 +32262,9 @@ msgstr ""
msgid "ciReport|%{improvedNum} improved"
msgstr ""
msgid "ciReport|%{linkStartTag}Learn more about API Fuzzing%{linkEndTag}"
msgstr ""
msgid "ciReport|%{linkStartTag}Learn more about Container Scanning %{linkEndTag}"
msgstr ""
......@@ -32301,6 +32304,9 @@ msgstr ""
msgid "ciReport|API Fuzzing"
msgstr ""
msgid "ciReport|API fuzzing"
msgstr ""
msgid "ciReport|All projects"
msgstr ""
......
......@@ -30,6 +30,8 @@ module QA
element :dependency_scan_report
element :container_scan_report
element :dast_scan_report
element :coverage_fuzzing_report
element :api_fuzzing_report
end
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