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 = { ...@@ -18,6 +18,7 @@ export const REPORT_TYPES = {
sast: s__('ciReport|SAST'), sast: s__('ciReport|SAST'),
secret_detection: s__('ciReport|Secret Detection'), secret_detection: s__('ciReport|Secret Detection'),
coverage_fuzzing: s__('ciReport|Coverage Fuzzing'), coverage_fuzzing: s__('ciReport|Coverage Fuzzing'),
api_fuzzing: s__('ciReport|API Fuzzing'),
}; };
export const DASHBOARD_TYPES = { export const DASHBOARD_TYPES = {
......
<script> <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 SeverityBadge from 'ee/vue_shared/security_reports/components/severity_badge.vue';
import CodeBlock from '~/vue_shared/components/code_block.vue'; import CodeBlock from '~/vue_shared/components/code_block.vue';
import { __ } from '~/locale'; import { __ } from '~/locale';
...@@ -7,7 +8,10 @@ import DetailItem from './detail_item.vue'; ...@@ -7,7 +8,10 @@ import DetailItem from './detail_item.vue';
export default { export default {
name: 'VulnerabilityDetails', name: 'VulnerabilityDetails',
components: { CodeBlock, GlLink, SeverityBadge, DetailItem, GlSprintf }, components: { CodeBlock, GlLink, SeverityBadge, DetailItem, GlSprintf, GlIcon },
directives: {
GlTooltip: GlTooltipDirective,
},
props: { props: {
vulnerability: { vulnerability: {
type: Object, type: Object,
...@@ -53,25 +57,32 @@ export default { ...@@ -53,25 +57,32 @@ export default {
properties: {}, 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() { requestData() {
if (!this.vulnerability.request) { if (!this.vulnerability.request) {
return []; return [];
} }
const { method, url, headers = [] } = this.vulnerability.request;
return [ return [
{ {
label: __('%{labelStart}Method:%{labelEnd} %{method}'), label: __('%{labelStart}Sent request:%{labelEnd} %{headers}'),
content: method, content: this.constructedRequest,
},
{
label: __('%{labelStart}URL:%{labelEnd} %{url}'),
content: url,
},
{
label: __('%{labelStart}Headers:%{labelEnd} %{headers}'),
content: this.getHeadersAsCodeBlockLines(headers),
isCode: true, isCode: true,
}, },
].filter(x => x.content); ].filter(x => x.content);
...@@ -81,20 +92,23 @@ export default { ...@@ -81,20 +92,23 @@ export default {
return []; return [];
} }
const {
status_code: statusCode,
reason_phrase: reasonPhrase,
headers = [],
} = this.vulnerability.response;
return [ return [
{ {
label: __('%{labelStart}Status:%{labelEnd} %{status}'), label: __('%{labelStart}Actual response:%{labelEnd} %{headers}'),
content: statusCode && reasonPhrase ? `${statusCode} ${reasonPhrase}` : '', content: this.constructedResponse,
isCode: true,
}, },
].filter(x => x.content);
},
recordedResponseData() {
if (!this.recordedMessage) {
return [];
}
return [
{ {
label: __('%{labelStart}Headers:%{labelEnd} %{headers}'), label: __('%{labelStart}Unmodified response:%{labelEnd} %{headers}'),
content: this.getHeadersAsCodeBlockLines(headers), content: this.constructedRecordedResponse,
isCode: true, isCode: true,
}, },
].filter(x => x.content); ].filter(x => x.content);
...@@ -109,6 +123,18 @@ export default { ...@@ -109,6 +123,18 @@ export default {
this.location.operating_system 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: { methods: {
getHeadersAsCodeBlockLines(headers) { getHeadersAsCodeBlockLines(headers) {
...@@ -116,6 +142,22 @@ export default { ...@@ -116,6 +142,22 @@ export default {
? headers.map(({ name, value }) => `${name}: ${value}`).join('\n') ? 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>
...@@ -247,15 +289,45 @@ export default { ...@@ -247,15 +289,45 @@ export default {
</ul> </ul>
</template> </template>
<section v-if="requestData.length" data-testid="request"> <section v-if="hasRequest" data-testid="request">
<h3>{{ s__('Vulnerability|Request') }}</h3> <h3>{{ s__('Vulnerability|Request/Response') }}</h3>
<ul> <ul>
<detail-item <detail-item
v-for="({ label, isCode, content }, index) in requestData" v-for="({ label, isCode, content }, index) in requestData"
:key="`${index}:${label}`" :key="`${index}:${label}`"
:sprintf-message="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>
</detail-item>
</ul>
</section>
<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> <template v-else>
{{ content }} {{ content }}
</template> </template>
...@@ -263,15 +335,28 @@ export default { ...@@ -263,15 +335,28 @@ export default {
</ul> </ul>
</section> </section>
<section v-if="responseData.length" data-testid="response"> <section
<h3>{{ s__('Vulnerability|Response') }}</h3> v-if="hasResponse"
:class="hasRecordedResponse ? 'col-6' : 'col'"
data-testid="response"
>
<ul> <ul>
<detail-item <detail-item
v-for="({ label, isCode, content }, index) in responseData" v-for="({ label, isCode, content }, index) in responseData"
:key="`${index}:${label}`" :key="`${index}:${label}`"
:sprintf-message="label" :sprintf-message="label"
> >
<code-block v-if="isCode" class="mt-1" :code="content" max-height="225px" /> <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> <template v-else>
{{ content }} {{ content }}
</template> </template>
...@@ -279,4 +364,14 @@ export default { ...@@ -279,4 +364,14 @@ export default {
</ul> </ul>
</section> </section>
</div> </div>
<template v-if="assertion">
<h3>{{ s__('Vulnerability|Additional Info') }}</h3>
<ul>
<detail-item :sprintf-message="__('%{labelStart}Assert:%{labelEnd} %{assertion}')">
{{ assertion }}
</detail-item>
</ul>
</template>
</div>
</template> </template>
...@@ -69,3 +69,8 @@ export const REGEXES = { ...@@ -69,3 +69,8 @@ export const REGEXES = {
ISSUE_FORMAT: /^#?(\d+)$/, // Matches '123' and '#123'. ISSUE_FORMAT: /^#?(\d+)$/, // Matches '123' and '#123'.
LINK_FORMAT: /\/(.+\/.+)\/-\/issues\/(\d+)/, // Matches '/username/project/-/issues/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 { mount } from '@vue/test-utils';
import { getAllByRole, getByTestId } from '@testing-library/dom'; import { getAllByRole, getByTestId } from '@testing-library/dom';
import { GlLink } from '@gitlab/ui'; 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 SeverityBadge from 'ee/vue_shared/security_reports/components/severity_badge.vue';
import VulnerabilityDetails from 'ee/vulnerabilities/components/details.vue'; import VulnerabilityDetails from 'ee/vulnerabilities/components/details.vue';
...@@ -173,10 +174,21 @@ describe('Vulnerability Details', () => { ...@@ -173,10 +174,21 @@ describe('Vulnerability Details', () => {
describe('http data', () => { describe('http data', () => {
const TEST_HEADERS = [{ name: 'Name1', value: 'Value1' }, { name: 'Name2', value: 'Value2' }]; const TEST_HEADERS = [{ name: 'Name1', value: 'Value1' }, { name: 'Name2', value: 'Value2' }];
const TEST_URL = 'http://foo.bar/test'; const EXPECT_REQUEST = {
const EXPECT_HEADERS = { label: 'Sent request:',
label: 'Headers:', content: 'GET http://www.gitlab.com\nName1: Value1\nName2: Value2\n\n[{"user_id":1,}]',
content: 'Name1: Value1\nName2: Value2', 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, isCode: true,
}; };
...@@ -201,11 +213,11 @@ describe('Vulnerability Details', () => { ...@@ -201,11 +213,11 @@ describe('Vulnerability Details', () => {
request | expectedData request | expectedData
${null} | ${null} ${null} | ${null}
${{}} | ${null} ${{}} | ${null}
${{ headers: TEST_HEADERS }} | ${[EXPECT_HEADERS]} ${{ headers: TEST_HEADERS }} | ${null}
${{ headers: TEST_HEADERS, method: 'GET' }} | ${[{ label: 'Method:', content: 'GET' }, EXPECT_HEADERS]} ${{ method: 'GET' }} | ${null}
${{ headers: TEST_HEADERS, method: 'GET', url: TEST_URL }} | ${[{ label: 'Method:', content: 'GET' }, { label: 'URL:', content: TEST_URL }, EXPECT_HEADERS]} ${{ method: 'GET', url: 'http://www.gitlab.com' }} | ${null}
${{ url: TEST_URL }} | ${[{ label: 'URL:', content: TEST_URL }]} ${{ method: 'GET', url: 'http://www.gitlab.com', body: '[{"user_id":1,}]' }} | ${null}
${{ method: 'GET' }} | ${[{ label: 'Method:', content: 'GET' }]} ${{ headers: TEST_HEADERS, method: 'GET', url: 'http://www.gitlab.com', body: '[{"user_id":1,}]' }} | ${[EXPECT_REQUEST]}
`('shows request data for $request', ({ request, expectedData }) => { `('shows request data for $request', ({ request, expectedData }) => {
createWrapper({ request }); createWrapper({ request });
expect(getSectionData('request')).toEqual(expectedData); expect(getSectionData('request')).toEqual(expectedData);
...@@ -215,13 +227,29 @@ describe('Vulnerability Details', () => { ...@@ -215,13 +227,29 @@ describe('Vulnerability Details', () => {
response | expectedData response | expectedData
${null} | ${null} ${null} | ${null}
${{}} | ${null} ${{}} | ${null}
${{ headers: TEST_HEADERS }} | ${[EXPECT_HEADERS]} ${{ headers: TEST_HEADERS }} | ${null}
${{ headers: TEST_HEADERS, status_code: 200 }} | ${[EXPECT_HEADERS]} ${{ headers: TEST_HEADERS, body: '[{"user_id":1,}]' }} | ${null}
${{ headers: TEST_HEADERS, status_code: 200, reason_phrase: 'OK' }} | ${[{ label: 'Status:', content: '200 OK' }, EXPECT_HEADERS]} ${{ headers: TEST_HEADERS, body: '[{"user_id":1,}]', status_code: '500' }} | ${null}
${{ status_code: 400, reason_phrase: 'Something bad' }} | ${[{ label: 'Status:', content: '400 Something bad' }]} ${{ headers: TEST_HEADERS, body: '[{"user_id":1,}]', status_code: '500', reason_phrase: 'INTERNAL SERVER ERROR' }} | ${[EXPECT_RESPONSE]}
`('shows response data for $response', ({ response, expectedData }) => { `('shows response data for $response', ({ response, expectedData }) => {
createWrapper({ response }); createWrapper({ response });
expect(getSectionData('response')).toEqual(expectedData); 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 "" ...@@ -543,6 +543,12 @@ msgstr ""
msgid "%{issuesSize} with a limit of %{maxIssueCount}" msgid "%{issuesSize} with a limit of %{maxIssueCount}"
msgstr "" msgstr ""
msgid "%{labelStart}Actual response:%{labelEnd} %{headers}"
msgstr ""
msgid "%{labelStart}Assert:%{labelEnd} %{assertion}"
msgstr ""
msgid "%{labelStart}Class:%{labelEnd} %{class}" msgid "%{labelStart}Class:%{labelEnd} %{class}"
msgstr "" msgstr ""
...@@ -558,9 +564,6 @@ msgstr "" ...@@ -558,9 +564,6 @@ msgstr ""
msgid "%{labelStart}File:%{labelEnd} %{file}" msgid "%{labelStart}File:%{labelEnd} %{file}"
msgstr "" msgstr ""
msgid "%{labelStart}Headers:%{labelEnd} %{headers}"
msgstr ""
msgid "%{labelStart}Image:%{labelEnd} %{image}" msgid "%{labelStart}Image:%{labelEnd} %{image}"
msgstr "" msgstr ""
...@@ -576,13 +579,13 @@ msgstr "" ...@@ -576,13 +579,13 @@ msgstr ""
msgid "%{labelStart}Scanner:%{labelEnd} %{scanner}" msgid "%{labelStart}Scanner:%{labelEnd} %{scanner}"
msgstr "" msgstr ""
msgid "%{labelStart}Severity:%{labelEnd} %{severity}" msgid "%{labelStart}Sent request:%{labelEnd} %{headers}"
msgstr "" msgstr ""
msgid "%{labelStart}Status:%{labelEnd} %{status}" msgid "%{labelStart}Severity:%{labelEnd} %{severity}"
msgstr "" msgstr ""
msgid "%{labelStart}URL:%{labelEnd} %{url}" msgid "%{labelStart}Unmodified response:%{labelEnd} %{headers}"
msgstr "" msgstr ""
msgid "%{label_for_message} unavailable" msgid "%{label_for_message} unavailable"
...@@ -30075,6 +30078,12 @@ msgstr "" ...@@ -30075,6 +30078,12 @@ msgstr ""
msgid "Vulnerability|Activity" msgid "Vulnerability|Activity"
msgstr "" msgstr ""
msgid "Vulnerability|Actual received response is the one received when this fault was detected"
msgstr ""
msgid "Vulnerability|Additional Info"
msgstr ""
msgid "Vulnerability|Class" msgid "Vulnerability|Class"
msgstr "" msgstr ""
...@@ -30123,10 +30132,7 @@ msgstr "" ...@@ -30123,10 +30132,7 @@ msgstr ""
msgid "Vulnerability|Project" msgid "Vulnerability|Project"
msgstr "" msgstr ""
msgid "Vulnerability|Request" msgid "Vulnerability|Request/Response"
msgstr ""
msgid "Vulnerability|Response"
msgstr "" msgstr ""
msgid "Vulnerability|Scanner" msgid "Vulnerability|Scanner"
...@@ -30141,6 +30147,9 @@ msgstr "" ...@@ -30141,6 +30147,9 @@ msgstr ""
msgid "Vulnerability|Status" msgid "Vulnerability|Status"
msgstr "" 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" msgid "Wait for the file to load to copy its contents"
msgstr "" msgstr ""
...@@ -31651,6 +31660,9 @@ msgstr "" ...@@ -31651,6 +31660,9 @@ msgstr ""
msgid "ciReport|: Loading resulted in an error" msgid "ciReport|: Loading resulted in an error"
msgstr "" msgstr ""
msgid "ciReport|API Fuzzing"
msgstr ""
msgid "ciReport|All projects" msgid "ciReport|All projects"
msgstr "" 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