Commit f57a874c authored by Mark Florian's avatar Mark Florian

Merge branch '271529-add-api-fuzzing-category' into 'master'

Add API Fuzzing to security dashboard and vulnerabilities details page

See merge request gitlab-org/gitlab!46854
parents 6a4151e4 2b20fe6d
......@@ -18,6 +18,7 @@ export const REPORT_TYPES = {
sast: s__('ciReport|SAST'),
secret_detection: s__('ciReport|Secret Detection'),
coverage_fuzzing: s__('ciReport|Coverage Fuzzing'),
api_fuzzing: s__('ciReport|API Fuzzing'),
};
export const DASHBOARD_TYPES = {
......
<script>
import { GlLink, GlSprintf } from '@gitlab/ui';
import { GlLink, GlSprintf, GlTooltipDirective, GlIcon } from '@gitlab/ui';
import { SUPPORTING_MESSAGE_TYPES } from 'ee/vulnerabilities/constants';
import SeverityBadge from 'ee/vue_shared/security_reports/components/severity_badge.vue';
import CodeBlock from '~/vue_shared/components/code_block.vue';
import { __ } from '~/locale';
......@@ -7,7 +8,10 @@ import DetailItem from './detail_item.vue';
export default {
name: 'VulnerabilityDetails',
components: { CodeBlock, GlLink, SeverityBadge, DetailItem, GlSprintf },
components: { CodeBlock, GlLink, SeverityBadge, DetailItem, GlSprintf, GlIcon },
directives: {
GlTooltip: GlTooltipDirective,
},
props: {
vulnerability: {
type: Object,
......@@ -53,25 +57,32 @@ export default {
properties: {},
};
},
assertion() {
return this.vulnerability.evidence_source?.name;
},
recordedMessage() {
return this.vulnerability?.supporting_messages?.find(
msg => msg.name === SUPPORTING_MESSAGE_TYPES.RECORDED,
)?.response;
},
constructedRequest() {
return this.constructRequest(this.vulnerability.request);
},
constructedResponse() {
return this.constructResponse(this.vulnerability.response);
},
constructedRecordedResponse() {
return this.constructResponse(this.recordedMessage);
},
requestData() {
if (!this.vulnerability.request) {
return [];
}
const { method, url, headers = [] } = this.vulnerability.request;
return [
{
label: __('%{labelStart}Method:%{labelEnd} %{method}'),
content: method,
},
{
label: __('%{labelStart}URL:%{labelEnd} %{url}'),
content: url,
},
{
label: __('%{labelStart}Headers:%{labelEnd} %{headers}'),
content: this.getHeadersAsCodeBlockLines(headers),
label: __('%{labelStart}Sent request:%{labelEnd} %{headers}'),
content: this.constructedRequest,
isCode: true,
},
].filter(x => x.content);
......@@ -81,20 +92,23 @@ export default {
return [];
}
const {
status_code: statusCode,
reason_phrase: reasonPhrase,
headers = [],
} = this.vulnerability.response;
return [
{
label: __('%{labelStart}Status:%{labelEnd} %{status}'),
content: statusCode && reasonPhrase ? `${statusCode} ${reasonPhrase}` : '',
label: __('%{labelStart}Actual response:%{labelEnd} %{headers}'),
content: this.constructedResponse,
isCode: true,
},
].filter(x => x.content);
},
recordedResponseData() {
if (!this.recordedMessage) {
return [];
}
return [
{
label: __('%{labelStart}Headers:%{labelEnd} %{headers}'),
content: this.getHeadersAsCodeBlockLines(headers),
label: __('%{labelStart}Unmodified response:%{labelEnd} %{headers}'),
content: this.constructedRecordedResponse,
isCode: true,
},
].filter(x => x.content);
......@@ -109,6 +123,18 @@ export default {
this.location.operating_system
);
},
hasRequest() {
return Boolean(this.requestData.length);
},
hasResponse() {
return Boolean(this.responseData.length);
},
hasRecordedResponse() {
return Boolean(this.recordedResponseData.length);
},
hasResponses() {
return Boolean(this.hasResponse || this.hasRecordedResponse);
},
},
methods: {
getHeadersAsCodeBlockLines(headers) {
......@@ -116,6 +142,22 @@ export default {
? 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>
......@@ -247,15 +289,15 @@ export default {
</ul>
</template>
<section v-if="requestData.length" data-testid="request">
<h3>{{ s__('Vulnerability|Request') }}</h3>
<section v-if="hasRequest" data-testid="request">
<h3>{{ s__('Vulnerability|Request/Response') }}</h3>
<ul>
<detail-item
v-for="({ label, isCode, content }, index) in requestData"
:key="`${index}:${label}`"
:sprintf-message="label"
>
<code-block v-if="isCode" class="mt-1" :code="content" max-height="225px" />
<code-block v-if="isCode" class="gl-mt-2" :code="content" max-height="225px" />
<template v-else>
{{ content }}
</template>
......@@ -263,20 +305,73 @@ export default {
</ul>
</section>
<section v-if="responseData.length" data-testid="response">
<h3>{{ s__('Vulnerability|Response') }}</h3>
<div v-if="hasResponses" class="row">
<section
v-if="hasRecordedResponse"
:class="hasResponse ? 'col-6' : 'col'"
data-testid="recorded-response"
>
<ul>
<detail-item
v-for="({ label, isCode, content }, index) in recordedResponseData"
:key="`${index}:${label}`"
:sprintf-message="label"
>
<gl-icon
v-gl-tooltip
name="information-o"
class="gl-hover-cursor-pointer gl-mr-3"
:title="
s__(
'Vulnerability|The unmodified response is the original response that had no mutations done to the request',
)
"
/>
<code-block v-if="isCode" class="gl-mt-2" :code="content" max-height="225px" />
<template v-else>
{{ content }}
</template>
</detail-item>
</ul>
</section>
<section
v-if="hasResponse"
:class="hasRecordedResponse ? 'col-6' : 'col'"
data-testid="response"
>
<ul>
<detail-item
v-for="({ label, isCode, content }, index) in responseData"
:key="`${index}:${label}`"
:sprintf-message="label"
>
<gl-icon
v-gl-tooltip
name="information-o"
class="gl-hover-cursor-pointer gl-mr-3"
:title="
s__(
'Vulnerability|Actual received response is the one received when this fault was detected',
)
"
/>
<code-block v-if="isCode" class="gl-mt-2" :code="content" max-height="225px" />
<template v-else>
{{ content }}
</template>
</detail-item>
</ul>
</section>
</div>
<template v-if="assertion">
<h3>{{ s__('Vulnerability|Additional Info') }}</h3>
<ul>
<detail-item
v-for="({ label, isCode, content }, index) in responseData"
:key="`${index}:${label}`"
:sprintf-message="label"
>
<code-block v-if="isCode" class="mt-1" :code="content" max-height="225px" />
<template v-else>
{{ content }}
</template>
<detail-item :sprintf-message="__('%{labelStart}Assert:%{labelEnd} %{assertion}')">
{{ assertion }}
</detail-item>
</ul>
</section>
</template>
</div>
</template>
......@@ -69,3 +69,8 @@ export const REGEXES = {
ISSUE_FORMAT: /^#?(\d+)$/, // Matches '123' and '#123'.
LINK_FORMAT: /\/(.+\/.+)\/-\/issues\/(\d+)/, // Matches '/username/project/-/issues/123'.
};
export const SUPPORTING_MESSAGE_TYPES = {
// eslint-disable-next-line @gitlab/require-i18n-strings
RECORDED: 'Recorded',
};
---
title: Add API Fuzzing to security dashboard and vulnerabilities details page
merge_request: 46854
author:
type: added
import { mount } from '@vue/test-utils';
import { getAllByRole, getByTestId } from '@testing-library/dom';
import { GlLink } from '@gitlab/ui';
import { SUPPORTING_MESSAGE_TYPES } from 'ee/vulnerabilities/constants';
import SeverityBadge from 'ee/vue_shared/security_reports/components/severity_badge.vue';
import VulnerabilityDetails from 'ee/vulnerabilities/components/details.vue';
......@@ -173,10 +174,21 @@ describe('Vulnerability Details', () => {
describe('http data', () => {
const TEST_HEADERS = [{ name: 'Name1', value: 'Value1' }, { name: 'Name2', value: 'Value2' }];
const TEST_URL = 'http://foo.bar/test';
const EXPECT_HEADERS = {
label: 'Headers:',
content: 'Name1: Value1\nName2: Value2',
const EXPECT_REQUEST = {
label: 'Sent request:',
content: 'GET http://www.gitlab.com\nName1: Value1\nName2: Value2\n\n[{"user_id":1,}]',
isCode: true,
};
const EXPECT_RESPONSE = {
label: 'Actual response:',
content: '500 INTERNAL SERVER ERROR\nName1: Value1\nName2: Value2\n\n[{"user_id":1,}]',
isCode: true,
};
const EXPECT_RECORDED_RESPONSE = {
label: 'Unmodified response:',
content: '200 OK\nName1: Value1\nName2: Value2\n\n[{"user_id":1,}]',
isCode: true,
};
......@@ -198,30 +210,46 @@ describe('Vulnerability Details', () => {
};
it.each`
request | expectedData
${null} | ${null}
${{}} | ${null}
${{ headers: TEST_HEADERS }} | ${[EXPECT_HEADERS]}
${{ headers: TEST_HEADERS, method: 'GET' }} | ${[{ label: 'Method:', content: 'GET' }, EXPECT_HEADERS]}
${{ headers: TEST_HEADERS, method: 'GET', url: TEST_URL }} | ${[{ label: 'Method:', content: 'GET' }, { label: 'URL:', content: TEST_URL }, EXPECT_HEADERS]}
${{ url: TEST_URL }} | ${[{ label: 'URL:', content: TEST_URL }]}
${{ method: 'GET' }} | ${[{ label: 'Method:', content: 'GET' }]}
request | expectedData
${null} | ${null}
${{}} | ${null}
${{ headers: TEST_HEADERS }} | ${null}
${{ method: 'GET' }} | ${null}
${{ method: 'GET', url: 'http://www.gitlab.com' }} | ${null}
${{ method: 'GET', url: 'http://www.gitlab.com', body: '[{"user_id":1,}]' }} | ${null}
${{ headers: TEST_HEADERS, method: 'GET', url: 'http://www.gitlab.com', body: '[{"user_id":1,}]' }} | ${[EXPECT_REQUEST]}
`('shows request data for $request', ({ request, expectedData }) => {
createWrapper({ request });
expect(getSectionData('request')).toEqual(expectedData);
});
it.each`
response | expectedData
${null} | ${null}
${{}} | ${null}
${{ headers: TEST_HEADERS }} | ${[EXPECT_HEADERS]}
${{ headers: TEST_HEADERS, status_code: 200 }} | ${[EXPECT_HEADERS]}
${{ headers: TEST_HEADERS, status_code: 200, reason_phrase: 'OK' }} | ${[{ label: 'Status:', content: '200 OK' }, EXPECT_HEADERS]}
${{ status_code: 400, reason_phrase: 'Something bad' }} | ${[{ label: 'Status:', content: '400 Something bad' }]}
response | expectedData
${null} | ${null}
${{}} | ${null}
${{ headers: TEST_HEADERS }} | ${null}
${{ headers: TEST_HEADERS, body: '[{"user_id":1,}]' }} | ${null}
${{ headers: TEST_HEADERS, body: '[{"user_id":1,}]', status_code: '500' }} | ${null}
${{ headers: TEST_HEADERS, body: '[{"user_id":1,}]', status_code: '500', reason_phrase: 'INTERNAL SERVER ERROR' }} | ${[EXPECT_RESPONSE]}
`('shows response data for $response', ({ response, expectedData }) => {
createWrapper({ response });
expect(getSectionData('response')).toEqual(expectedData);
});
it.each`
supporting_messages | expectedData
${null} | ${null}
${[]} | ${null}
${[{}]} | ${null}
${[{}, { response: {} }]} | ${null}
${[{}, { response: { headers: TEST_HEADERS } }]} | ${null}
${[{}, { response: { headers: TEST_HEADERS, body: '[{"user_id":1,}]' } }]} | ${null}
${[{}, { response: { headers: TEST_HEADERS, body: '[{"user_id":1,}]', status_code: '200' } }]} | ${null}
${[{}, { response: { headers: TEST_HEADERS, body: '[{"user_id":1,}]', status_code: '200', reason_phrase: 'OK' } }]} | ${null}
${[{}, { name: SUPPORTING_MESSAGE_TYPES.RECORDED, response: { headers: TEST_HEADERS, body: '[{"user_id":1,}]', status_code: '200', reason_phrase: 'OK' } }]} | ${[EXPECT_RECORDED_RESPONSE]}
`('shows response data for $supporting_messages', ({ supporting_messages, expectedData }) => {
createWrapper({ supporting_messages });
expect(getSectionData('recorded-response')).toEqual(expectedData);
});
});
});
......@@ -543,6 +543,12 @@ msgstr ""
msgid "%{issuesSize} with a limit of %{maxIssueCount}"
msgstr ""
msgid "%{labelStart}Actual response:%{labelEnd} %{headers}"
msgstr ""
msgid "%{labelStart}Assert:%{labelEnd} %{assertion}"
msgstr ""
msgid "%{labelStart}Class:%{labelEnd} %{class}"
msgstr ""
......@@ -558,9 +564,6 @@ msgstr ""
msgid "%{labelStart}File:%{labelEnd} %{file}"
msgstr ""
msgid "%{labelStart}Headers:%{labelEnd} %{headers}"
msgstr ""
msgid "%{labelStart}Image:%{labelEnd} %{image}"
msgstr ""
......@@ -576,13 +579,13 @@ msgstr ""
msgid "%{labelStart}Scanner:%{labelEnd} %{scanner}"
msgstr ""
msgid "%{labelStart}Severity:%{labelEnd} %{severity}"
msgid "%{labelStart}Sent request:%{labelEnd} %{headers}"
msgstr ""
msgid "%{labelStart}Status:%{labelEnd} %{status}"
msgid "%{labelStart}Severity:%{labelEnd} %{severity}"
msgstr ""
msgid "%{labelStart}URL:%{labelEnd} %{url}"
msgid "%{labelStart}Unmodified response:%{labelEnd} %{headers}"
msgstr ""
msgid "%{label_for_message} unavailable"
......@@ -30075,6 +30078,12 @@ msgstr ""
msgid "Vulnerability|Activity"
msgstr ""
msgid "Vulnerability|Actual received response is the one received when this fault was detected"
msgstr ""
msgid "Vulnerability|Additional Info"
msgstr ""
msgid "Vulnerability|Class"
msgstr ""
......@@ -30123,10 +30132,7 @@ msgstr ""
msgid "Vulnerability|Project"
msgstr ""
msgid "Vulnerability|Request"
msgstr ""
msgid "Vulnerability|Response"
msgid "Vulnerability|Request/Response"
msgstr ""
msgid "Vulnerability|Scanner"
......@@ -30141,6 +30147,9 @@ msgstr ""
msgid "Vulnerability|Status"
msgstr ""
msgid "Vulnerability|The unmodified response is the original response that had no mutations done to the request"
msgstr ""
msgid "Wait for the file to load to copy its contents"
msgstr ""
......@@ -31651,6 +31660,9 @@ msgstr ""
msgid "ciReport|: Loading resulted in an error"
msgstr ""
msgid "ciReport|API Fuzzing"
msgstr ""
msgid "ciReport|All projects"
msgstr ""
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment