Commit d278b76d authored by Phil Hughes's avatar Phil Hughes

Merge branch '4991-security-reports-css' into 'master'

Resolve "SAST report rendering is broken with long filenames"

Closes #4991

See merge request gitlab-org/gitlab-ee!4671
parents de0e6349 a15af9a1
...@@ -39,7 +39,7 @@ ...@@ -39,7 +39,7 @@
@click="onClick"> @click="onClick">
... ...
</button> </button>
<span v-show="!isCollapsed"> <span v-if="!isCollapsed">
<slot name="expanded"></slot> <slot name="expanded"></slot>
</span> </span>
</span> </span>
......
<script>
import { s__ } from '~/locale';
import Icon from '~/vue_shared/components/icon.vue';
import ExpandButton from '~/vue_shared/components/expand_button.vue';
export default {
name: 'ModalDast',
components: {
ExpandButton,
Icon,
},
props: {
title: {
type: String,
required: true,
default: '',
},
targetId: {
type: String,
required: false,
default: '',
},
description: {
type: String,
required: true,
default: '',
},
instances: {
type: Array,
required: false,
default: () => ([]),
},
},
computed: {
instancesLabel() {
return s__('ciReport|Instances');
},
},
mounted() {
$(this.$el).on('hidden.bs.modal', () => {
this.$emit('clearData');
});
},
};
</script>
<template>
<div
:id="targetId"
class="modal fade"
tabindex="-1"
role="dialog"
>
<div
class="modal-dialog modal-lg"
role="document"
>
<div class="modal-content">
<div class="modal-header">
<button
type="button"
class="close"
data-dismiss="modal"
aria-label="Close"
>
<span aria-hidden="true">&times;</span>
</button>
<h4 class="modal-title">
{{ title }}
</h4>
</div>
<div class="modal-body">
{{ description }}
<h5 class="prepend-top-20">{{ instancesLabel }}</h5>
<ul
v-if="instances"
class="report-block-list"
>
<li
v-for="(instance, i) in instances"
:key="i"
class="report-block-list-item-modal failed"
>
<icon
class="report-block-icon"
name="status_failed_borderless"
:size="32"
/>
{{ instance.method }}
<a
:href="instance.uri"
target="_blank"
rel="noopener noreferrer nofollow"
class="prepend-left-5"
>
{{ instance.uri }}
</a>
<expand-button v-if="instance.evidence">
<pre
slot="expanded"
class="block report-block-dast-code prepend-top-10">{{ instance.evidence }}</pre>
</expand-button>
</li>
</ul>
</div>
</div>
</div>
</div>
</template>
<script> <script>
import { s__ } from '~/locale'; import { s__ } from '~/locale';
import Icon from '~/vue_shared/components/icon.vue'; import Icon from '~/vue_shared/components/icon.vue';
import Modal from './dast_modal.vue'; import Modal from '~/vue_shared/components/gl_modal.vue';
import ExpandButton from '~/vue_shared/components/expand_button.vue';
const modalDefaultData = { const modalDefaultData = {
modalId: 'modal-mrwidget-issue', modalId: 'modal-mrwidget-issue',
...@@ -16,6 +17,7 @@ ...@@ -16,6 +17,7 @@
components: { components: {
Modal, Modal,
Icon, Icon,
ExpandButton,
}, },
props: { props: {
issues: { issues: {
...@@ -79,6 +81,11 @@ ...@@ -79,6 +81,11 @@
return this.type === 'dast'; return this.type === 'dast';
}, },
}, },
mounted() {
$(this.$refs.modal).on('hidden.bs.modal', () => {
this.clearModalData();
});
},
methods: { methods: {
shouldRenderPriority(issue) { shouldRenderPriority(issue) {
return this.hasPriority && issue.priority; return this.hasPriority && issue.priority;
...@@ -117,85 +124,140 @@ ...@@ -117,85 +124,140 @@
}; };
</script> </script>
<template> <template>
<ul class="report-block-list"> <div>
<li <ul class="report-block-list">
:class="{ <li
failed: isStatusFailed, class="report-block-list-issue"
success: isStatusSuccess, v-for="(issue, index) in issues"
neutral: isStatusNeutral :key="index"
}" >
class="report-block-list-item" <div
v-for="(issue, index) in issues" class="report-block-list-icon append-right-5"
:key="index" :class="{
> failed: isStatusFailed,
<icon success: isStatusSuccess,
class="report-block-icon" neutral: isStatusNeutral,
:name="iconName" }"
:size="32"
/>
<template v-if="isStatusSuccess && isTypeQuality">{{ fixedLabel }}</template>
<template v-if="shouldRenderPriority(issue)">{{ issue.priority }}:</template>
<template v-if="isTypeDocker">
<a
v-if="issue.nameLink"
:href="issue.nameLink"
target="_blank"
rel="noopener noreferrer nofollow"
class="prepend-left-5"
>
{{ issue.name }}
</a>
<template v-else>
{{ issue.name }}
</template>
</template>
<template v-else-if="isTypeDast">
<button
type="button"
@click="openDastModal(issue, index)"
data-toggle="modal"
class="btn-link btn-blank btn-open-modal"
:data-target="modalTargetId"
> >
{{ issue.name }} <icon
</button> :name="iconName"
</template> :size="32"
<template v-else> />
{{ issue.name }}<template v-if="issue.score">: </div>
<strong>{{ formatScore(issue.score) }}</strong></template> <div class="report-block-list-issue-description prepend-top-5 append-bottom-5">
</template> <div class="report-block-list-issue-description-text append-right-5">
<template v-if="isStatusSuccess && isTypeQuality">{{ fixedLabel }}</template>
<template v-if="shouldRenderPriority(issue)">{{ issue.priority }}:</template>
<template v-if="isTypePerformance && issue.delta != null"> <template v-if="isTypeDocker">
({{ issue.delta >= 0 ? '+' : '' }}{{ formatScore(issue.delta) }}) <a
</template> v-if="issue.nameLink"
:href="issue.nameLink"
target="_blank"
rel="noopener noreferrer nofollow"
>{{ issue.name }}</a>
<template v-else>
{{ issue.name }}
</template>
</template>
<template v-else-if="isTypeDast">
<button
type="button"
@click="openDastModal(issue, index)"
data-toggle="modal"
class="js-modal-dast btn-link btn-blank text-left break-link"
:data-target="modalTargetId"
>
{{ issue.name }}
</button>
</template>
<template v-else>
{{ issue.name }}<template v-if="issue.score">:
<strong>{{ formatScore(issue.score) }}</strong></template>
</template>
<template v-if="issue.path"> <template v-if="isTypePerformance && issue.delta != null">
in ({{ issue.delta >= 0 ? '+' : '' }}{{ formatScore(issue.delta) }})
</template>
</div>
<div class="report-block-list-issue-description-link">
<template v-if="issue.path">
in
<a <a
v-if="issue.urlPath" v-if="issue.urlPath"
:href="issue.urlPath" :href="issue.urlPath"
target="_blank" target="_blank"
rel="noopener noreferrer nofollow" rel="noopener noreferrer nofollow"
class="prepend-left-5" class="break-link"
> >
{{ issue.path }}<template v-if="issue.line">:{{ issue.line }}</template> {{ issue.path }}<template v-if="issue.line">:{{ issue.line }}</template>
</a> </a>
<template v-else> <template v-else>
{{ issue.path }}<template v-if="issue.line">:{{ issue.line }}</template> {{ issue.path }}<template v-if="issue.line">:{{ issue.line }}</template>
</template> </template>
</template> </template>
</li> </div>
</div>
</li>
</ul>
<modal <modal
:target-id="modalId" v-if="isTypeDast"
:title="modalTitle" :id="modalId"
:hide-footer="true" :header-title-text="modalTitle"
:description="modalDesc" ref="modal"
:instances="modalInstances" class="modal-security-report-dast"
@clearData="clearModalData()" >
/>
</ul> <slot>
{{ modalDesc }}
<h5 class="prepend-top-20">
{{ s__('ciReport|Instances') }}
</h5>
<ul
v-if="modalInstances"
class="report-block-list"
>
<li
v-for="(instance, i) in modalInstances"
:key="i"
class="report-block-list-issue"
>
<div class="report-block-list-icon append-right-5 failed">
<icon
name="status_failed_borderless"
:size="32"
/>
</div>
<div class="report-block-list-issue-description prepend-top-5 append-bottom-5">
<div class="report-block-list-issue-description-text append-right-5">
{{ instance.method }}
</div>
<div class="report-block-list-issue-description-link">
<a
:href="instance.uri"
target="_blank"
rel="noopener noreferrer nofollow"
class="break-link"
>
{{ instance.uri }}
</a>
</div>
<expand-button v-if="instance.evidence">
<pre
slot="expanded"
class="block report-block-dast-code prepend-top-10 report-block-issue-code"
>{{ instance.evidence }}</pre>
</expand-button>
</div>
</li>
</ul>
</slot>
<div slot="footer">
</div>
</modal>
</div>
</template> </template>
...@@ -96,7 +96,9 @@ ...@@ -96,7 +96,9 @@
return this.status === 'success'; return this.status === 'success';
}, },
statusIconName() { statusIconName() {
if (this.loadingFailed || this.unresolvedIssues.length) { if (this.loadingFailed ||
this.unresolvedIssues.length ||
this.neutralIssues.length) {
return 'warning'; return 'warning';
} }
return 'success'; return 'success';
...@@ -221,7 +223,7 @@ ...@@ -221,7 +223,7 @@
<button <button
v-if="allIssues.length && !isFullReportVisible" v-if="allIssues.length && !isFullReportVisible"
type="button" type="button"
class="btn-link btn-blank prepend-left-10 js-expand-full-list" class="btn-link btn-blank prepend-left-10 js-expand-full-list break-link"
@click="openFullReport" @click="openFullReport"
> >
{{ s__("ciReport|Show complete code vulnerabilities report") }} {{ s__("ciReport|Show complete code vulnerabilities report") }}
......
.pipeline-tab-content {
.space-children,
.space-children > span {
display: flex;
}
.media {
align-items: center;
}
}
.report-block-container { .report-block-container {
border-top: 1px solid $gray-darker; border-top: 1px solid $gray-darker;
padding: $gl-padding-top; padding: $gl-padding-top;
background-color: $gray-light; background-color: $gray-light;
margin: $gl-padding #{-$gl-padding} #{-$gl-padding}; margin: $gl-padding #{-$gl-padding} #{-$gl-padding};
}
.report-block-dast-code { // Clean MR widget CSS
margin-left: 26px; line-height: 20px;
} }
.report-block-list { .report-block-list {
list-style: none; list-style: none;
padding: 0 1px; padding: 0 1px;
margin: 0; margin: 0;
line-height: $code_line_height; }
.btn-open-modal {
padding: 0 5px 4px;
}
.report-block-list-item { .report-block-list-icon {
display: flex; display: flex;
}
.report-block-list-item-modal { &.failed {
display: flex;
flex-wrap: wrap;
}
.failed .report-block-icon {
color: $red-500; color: $red-500;
} }
.success .report-block-icon { &.success {
color: $green-500; color: $green-500;
} }
.neutral .report-block-icon { &.neutral {
color: $theme-gray-700; color: $theme-gray-700;
} }
}
.report-block-icon { .report-block-list-issue {
margin: -5px 4px 0 0; display: flex;
fill: currentColor; align-items: flex-start;
} align-content: flex-start;
} }
.pipeline-tab-content { .report-block-list-issue-description {
.space-children, align-content: space-around;
.space-children > * { align-items: flex-start;
display: flex; flex-wrap: wrap;
display: flex;
align-self: center;
}
.report-block {
.break-link {
word-wrap: break-word;
word-break: break-all;
} }
}
.media { .report-block-issue-code {
align-items: center; width: $modal-lg - 70px;
}
.modal-security-report-dast {
.modal-dialog {
width: $modal-lg;
} }
} // TODO remove this when gl_modal support not rendering the footer
\ No newline at end of file .modal-footer {
display: none;
}
}
---
title: Improve security reports to handle big links and to work on mobile devices
merge_request: 4671
author:
type: fixed
import Vue from 'vue';
import modal from 'ee/vue_shared/security_reports/components/dast_modal.vue';
import mountComponent from 'spec/helpers/vue_mount_component_helper';
describe('mr widget modal', () => {
let vm;
let Modal;
beforeEach(() => {
Modal = Vue.extend(modal);
vm = mountComponent(Modal, {
title: 'Title',
targetId: 'targetId',
instances: [{
uri: 'uri',
method: 'GET',
evidence: 'evidence',
}],
description: 'Description!',
});
});
afterEach(() => {
vm.$destroy();
});
it('renders a title', () => {
expect(vm.$el.querySelector('.modal-title').textContent.trim()).toEqual('Title');
});
it('renders the target id', () => {
expect(vm.$el.getAttribute('id')).toEqual('targetId');
});
it('renders the description', () => {
expect(vm.$el.querySelector('.modal-body').textContent).toContain('Description!');
});
it('renders list of instances', () => {
const instance = vm.$el.querySelector('.modal-body li').textContent;
expect(instance).toContain('uri');
expect(instance).toContain('GET');
expect(instance).toContain('evidence');
});
});
...@@ -159,5 +159,20 @@ describe('Report issues', () => { ...@@ -159,5 +159,20 @@ describe('Report issues', () => {
expect(vm.$el.textContent).toContain(parsedDast[0].name); expect(vm.$el.textContent).toContain(parsedDast[0].name);
expect(vm.$el.textContent).toContain(parsedDast[0].priority); expect(vm.$el.textContent).toContain(parsedDast[0].priority);
}); });
it('opens modal with more information and list of instances', (done) => {
vm.$el.querySelector('.js-modal-dast').click();
Vue.nextTick(() => {
expect(vm.$el.querySelector('.modal-title').textContent.trim()).toEqual('Low (Medium): Absence of Anti-CSRF Tokens');
expect(vm.$el.querySelector('.modal-body').textContent).toContain('No Anti-CSRF tokens were found in a HTML submission form.');
const instance = vm.$el.querySelector('.modal-body li').textContent;
expect(instance).toContain('http://192.168.32.236:3001/explore?sort=latest_activity_desc');
expect(instance).toContain('GET');
done();
});
});
}); });
}); });
...@@ -192,7 +192,7 @@ describe('Report section', () => { ...@@ -192,7 +192,7 @@ describe('Report section', () => {
it('should show the report by default', () => { it('should show the report by default', () => {
expect( expect(
vm.$el.querySelectorAll('.report-block-list .report-block-list-item').length, vm.$el.querySelectorAll('.report-block-list .report-block-list-issue').length,
).toEqual(codequalityParsedIssues.length); ).toEqual(codequalityParsedIssues.length);
}); });
}); });
......
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