Commit 35baa114 authored by Kushal Pandya's avatar Kushal Pandya

Merge branch...

Merge branch '215110-dast-vulnerabilities-show-more-information-about-the-request-frontend' into 'master'

Add request information to vulnerability-detail modal

See merge request gitlab-org/gitlab!31422
parents 9086b62b 4e4e3994
...@@ -6,11 +6,26 @@ export default { ...@@ -6,11 +6,26 @@ export default {
type: String, type: String,
required: true, required: true,
}, },
maxHeight: {
type: String,
required: false,
default: 'initial',
},
},
computed: {
styleObject() {
const { maxHeight } = this;
const isScrollable = maxHeight !== 'initial';
const scrollableStyles = {
maxHeight,
overflowY: 'auto',
};
return isScrollable ? scrollableStyles : null;
},
}, },
}; };
</script> </script>
<template> <template>
<pre class="code-block rounded"> <pre class="code-block rounded" :style="styleObject"><code class="d-block">{{ code }}</code></pre>
<code class="d-block">{{ code }}</code>
</pre>
</template> </template>
...@@ -38,7 +38,7 @@ Running the pipeline on any other commit has no effect on the merge request. ...@@ -38,7 +38,7 @@ Running the pipeline on any other commit has no effect on the merge request.
By clicking on one of the detected linked vulnerabilities, you can By clicking on one of the detected linked vulnerabilities, you can
see the details and the URL(s) affected. see the details and the URL(s) affected.
![DAST Widget Clicked](img/dast_single_v12_9.png) ![DAST Widget Clicked](img/dast_single_v13_0.png)
[Dynamic Application Security Testing (DAST)](https://en.wikipedia.org/wiki/Dynamic_Application_Security_Testing) [Dynamic Application Security Testing (DAST)](https://en.wikipedia.org/wiki/Dynamic_Application_Security_Testing)
uses the popular open source tool [OWASP ZAProxy](https://github.com/zaproxy/zaproxy) uses the popular open source tool [OWASP ZAProxy](https://github.com/zaproxy/zaproxy)
......
...@@ -12,8 +12,8 @@ export default { ...@@ -12,8 +12,8 @@ export default {
<template functional> <template functional>
<div class="d-sm-flex my-sm-2 my-4"> <div class="d-sm-flex my-sm-2 my-4">
<label class="col-sm-3 text-sm-right font-weight-bold pl-0">{{ props.label }}:</label> <label class="col-sm-4 text-sm-right font-weight-bold pl-0">{{ props.label }}:</label>
<div class="col-sm-9 pl-0 text-secondary"> <div class="col-sm-8 pl-0 text-secondary">
<slot></slot> <slot></slot>
</div> </div>
</div> </div>
......
<script> <script>
import { GlFriendlyWrap } from '@gitlab/ui'; import { GlFriendlyWrap } from '@gitlab/ui';
import CodeBlock from '~/vue_shared/components/code_block.vue';
import SafeLink from 'ee/vue_shared/components/safe_link.vue'; import SafeLink from 'ee/vue_shared/components/safe_link.vue';
import Icon from '~/vue_shared/components/icon.vue'; import Icon from '~/vue_shared/components/icon.vue';
import ExpandButton from '~/vue_shared/components/expand_button.vue'; import ExpandButton from '~/vue_shared/components/expand_button.vue';
...@@ -12,6 +13,7 @@ import { REPORT_TYPES } from 'ee/security_dashboard/store/constants'; ...@@ -12,6 +13,7 @@ import { REPORT_TYPES } from 'ee/security_dashboard/store/constants';
export default { export default {
name: 'VulnerabilityDetails', name: 'VulnerabilityDetails',
components: { components: {
CodeBlock,
ExpandButton, ExpandButton,
GlFriendlyWrap, GlFriendlyWrap,
Icon, Icon,
...@@ -27,7 +29,7 @@ export default { ...@@ -27,7 +29,7 @@ export default {
}, },
computed: { computed: {
url() { url() {
return getFileLocation(this.vulnerability.location); return this.vulnerability.request?.url || getFileLocation(this.vulnerability.location);
}, },
file() { file() {
const file = this.vulnerability?.location?.file; const file = this.vulnerability?.location?.file;
...@@ -69,6 +71,15 @@ export default { ...@@ -69,6 +71,15 @@ export default {
instances() { instances() {
return this.asNonEmptyListOrNull(this.vulnerability.instances); return this.asNonEmptyListOrNull(this.vulnerability.instances);
}, },
requestHeaders() {
return this.headersToFormattedString(this.vulnerability.request?.headers);
},
responseHeaders() {
return this.headersToFormattedString(this.vulnerability.response?.headers);
},
responseStatusCode() {
return this.vulnerability.response?.status_code;
},
scannerType() { scannerType() {
return REPORT_TYPES[this.vulnerability.report_type]; return REPORT_TYPES[this.vulnerability.report_type];
}, },
...@@ -99,6 +110,9 @@ export default { ...@@ -99,6 +110,9 @@ export default {
asNonEmptyListOrNull(list) { asNonEmptyListOrNull(list) {
return list?.length > 0 ? list : null; return list?.length > 0 ? list : null;
}, },
headersToFormattedString(headers = []) {
return headers.map(({ name, value }) => `${name}: ${value}`).join('\n');
},
}, },
}; };
</script> </script>
...@@ -118,12 +132,28 @@ export default { ...@@ -118,12 +132,28 @@ export default {
</safe-link> </safe-link>
</vulnerability-detail> </vulnerability-detail>
<vulnerability-detail v-if="methodName" :label="s__('Vulnerability|Method')">
<gl-friendly-wrap :text="methodName" />
</vulnerability-detail>
<vulnerability-detail v-if="url" :label="__('URL')"> <vulnerability-detail v-if="url" :label="__('URL')">
<safe-link ref="urlLink" :href="url" target="_blank"> <safe-link ref="urlLink" :href="url" target="_blank">
<gl-friendly-wrap :text="url" /> <gl-friendly-wrap :text="url" />
</safe-link> </safe-link>
</vulnerability-detail> </vulnerability-detail>
<vulnerability-detail v-if="requestHeaders" :label="__('Request Headers')">
<code-block ref="requestHeaders" :code="requestHeaders" max-height="225px" />
</vulnerability-detail>
<vulnerability-detail v-if="responseStatusCode" :label="__('Response Status')">
<gl-friendly-wrap ref="responseStatusCode" :text="responseStatusCode" />
</vulnerability-detail>
<vulnerability-detail v-if="responseHeaders" :label="__('Response Headers')">
<code-block ref="responseHeaders" :code="responseHeaders" max-height="225px" />
</vulnerability-detail>
<vulnerability-detail v-if="file" :label="s__('Vulnerability|File')"> <vulnerability-detail v-if="file" :label="s__('Vulnerability|File')">
<safe-link <safe-link
v-if="vulnerability.blob_path" v-if="vulnerability.blob_path"
...@@ -177,10 +207,6 @@ export default { ...@@ -177,10 +207,6 @@ export default {
<gl-friendly-wrap :text="className" /> <gl-friendly-wrap :text="className" />
</vulnerability-detail> </vulnerability-detail>
<vulnerability-detail v-if="methodName" :label="s__('Vulnerability|Method')">
<gl-friendly-wrap :text="methodName" />
</vulnerability-detail>
<vulnerability-detail v-if="image" :label="s__('Vulnerability|Image')"> <vulnerability-detail v-if="image" :label="s__('Vulnerability|Image')">
<gl-friendly-wrap :text="image" /> <gl-friendly-wrap :text="image" />
</vulnerability-detail> </vulnerability-detail>
......
---
title: Add request information to vulnerability-detail modal
merge_request: 31422
author:
type: changed
...@@ -5,13 +5,13 @@ exports[`VulnerabilityDetail component renders the label prop and default slot 1 ...@@ -5,13 +5,13 @@ exports[`VulnerabilityDetail component renders the label prop and default slot 1
class="d-sm-flex my-sm-2 my-4" class="d-sm-flex my-sm-2 my-4"
> >
<label <label
class="col-sm-3 text-sm-right font-weight-bold pl-0" class="col-sm-4 text-sm-right font-weight-bold pl-0"
> >
foo: foo:
</label> </label>
<div <div
class="col-sm-9 pl-0 text-secondary" class="col-sm-8 pl-0 text-secondary"
> >
<p> <p>
bar bar
......
...@@ -29,6 +29,49 @@ exports[`VulnerabilityDetails component pin test renders correctly 1`] = ` ...@@ -29,6 +29,49 @@ exports[`VulnerabilityDetails component pin test renders correctly 1`] = `
<!----> <!---->
<vulnerability-detail-stub
label="URL"
>
<safe-link-stub
href="http://foo.bar/path"
target="_blank"
>
<gl-friendly-wrap-stub
symbols="/"
text="http://foo.bar/path"
/>
</safe-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 <vulnerability-detail-stub
label="File" label="File"
> >
...@@ -124,8 +167,6 @@ exports[`VulnerabilityDetails component pin test renders correctly 1`] = ` ...@@ -124,8 +167,6 @@ exports[`VulnerabilityDetails component pin test renders correctly 1`] = `
<!----> <!---->
<!---->
<vulnerability-detail-stub <vulnerability-detail-stub
label="Links" label="Links"
> >
......
import { mount, shallowMount } from '@vue/test-utils'; import { mount, shallowMount } from '@vue/test-utils';
import CodeBlock from '~/vue_shared/components/code_block.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 SeverityBadge from 'ee/vue_shared/security_reports/components/severity_badge.vue'; import SeverityBadge from 'ee/vue_shared/security_reports/components/severity_badge.vue';
import SafeLink from 'ee/vue_shared/components/safe_link.vue'; import SafeLink from 'ee/vue_shared/components/safe_link.vue';
...@@ -29,6 +30,9 @@ describe('VulnerabilityDetails component', () => { ...@@ -29,6 +30,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 findResponseHeaders = () => wrapper.find({ ref: 'responseHeaders' });
const findResponseStatusCode = () => wrapper.find({ ref: 'responseStatusCode' });
afterEach(() => { afterEach(() => {
wrapper.destroy(); wrapper.destroy();
...@@ -151,6 +155,90 @@ describe('VulnerabilityDetails component', () => { ...@@ -151,6 +155,90 @@ describe('VulnerabilityDetails component', () => {
}); });
}); });
describe('with request information', () => {
beforeEach(() => {
const vulnerability = makeVulnerability({
request: {
url: 'http://foo.bar/path',
headers: [{ name: 'key1', value: 'value1' }, { name: 'key2', value: 'value2' }],
},
});
componentFactory(vulnerability);
});
it('renders the request-url', () => {
expect(findLink('url').props('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('limits the code-blocks maximum height', () => {
expect(findRequestHeaders().props('maxHeight')).not.toBeFalsy();
expect(findRequestHeaders().props('maxHeight')).toEqual(expect.any(String));
});
});
describe('without request information', () => {
beforeEach(() => {
const vulnerability = makeVulnerability({
location: {
hostname: 'http://foo.com',
path: '/bar',
},
});
componentFactory(vulnerability);
});
it('renders the location-url', () => {
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);
});
});
describe('with response information', () => {
beforeEach(() => {
const vulnerability = makeVulnerability({
response: {
status_code: '200',
headers: [{ name: 'key1', value: 'value1' }, { name: 'key2', value: 'value2' }],
},
});
componentFactory(vulnerability);
});
it('renders the response status code', () => {
expect(findResponseStatusCode().text()).toBe('200');
});
it('renders a code block containing the request-headers', () => {
const responseHeaders = findResponseHeaders();
expect(responseHeaders.is(CodeBlock)).toBe(true);
expect(responseHeaders.text()).toBe('key1: value1\nkey2: value2');
});
});
describe('without 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 http-headers', () => {
expect(findResponseHeaders().exists()).toBe(false);
});
});
describe('scanner details', () => { describe('scanner details', () => {
describe('with additional information', () => { describe('with additional information', () => {
beforeEach(() => { beforeEach(() => {
...@@ -211,6 +299,14 @@ describe('VulnerabilityDetails component', () => { ...@@ -211,6 +299,14 @@ describe('VulnerabilityDetails component', () => {
uri: '/bar', uri: '/bar',
}, },
], ],
request: {
url: 'http://foo.bar/path',
headers: [{ name: 'key1', value: 'value1' }, { name: 'key2', value: 'value2' }],
},
response: {
status_code: '200',
headers: [{ name: 'key1', value: 'value1' }, { name: 'key2', value: 'value2' }],
},
}), }),
); );
......
...@@ -17863,6 +17863,9 @@ msgstr "" ...@@ -17863,6 +17863,9 @@ msgstr ""
msgid "Request Access" msgid "Request Access"
msgstr "" msgstr ""
msgid "Request Headers"
msgstr ""
msgid "Request parameter %{param} is missing." msgid "Request parameter %{param} is missing."
msgstr "" msgstr ""
...@@ -18002,6 +18005,12 @@ msgstr "" ...@@ -18002,6 +18005,12 @@ 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 ""
......
// Jest Snapshot v1, https://goo.gl/fbAQLP // Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Code Block matches snapshot 1`] = ` exports[`Code Block with default props renders correctly 1`] = `
<pre <pre
class="code-block rounded" class="code-block rounded"
> >
<code <code
class="d-block" class="d-block"
> >
test-code test-code
</code> </code>
</pre>
`;
exports[`Code Block with maxHeight set to "200px" renders correctly 1`] = `
<pre
class="code-block rounded"
style="max-height: 200px; overflow-y: auto;"
>
<code
class="d-block"
>
test-code
</code>
</pre> </pre>
`; `;
...@@ -4,10 +4,15 @@ import CodeBlock from '~/vue_shared/components/code_block.vue'; ...@@ -4,10 +4,15 @@ import CodeBlock from '~/vue_shared/components/code_block.vue';
describe('Code Block', () => { describe('Code Block', () => {
let wrapper; let wrapper;
const createComponent = () => { const defaultProps = {
code: 'test-code',
};
const createComponent = (props = {}) => {
wrapper = shallowMount(CodeBlock, { wrapper = shallowMount(CodeBlock, {
propsData: { propsData: {
code: 'test-code', ...defaultProps,
...props,
}, },
}); });
}; };
...@@ -17,9 +22,23 @@ describe('Code Block', () => { ...@@ -17,9 +22,23 @@ describe('Code Block', () => {
wrapper = null; wrapper = null;
}); });
it('matches snapshot', () => { describe('with default props', () => {
createComponent(); beforeEach(() => {
createComponent();
});
expect(wrapper.element).toMatchSnapshot(); it('renders correctly', () => {
expect(wrapper.element).toMatchSnapshot();
});
});
describe('with maxHeight set to "200px"', () => {
beforeEach(() => {
createComponent({ maxHeight: '200px' });
});
it('renders correctly', () => {
expect(wrapper.element).toMatchSnapshot();
});
}); });
}); });
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