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 @@
@click="onClick">
...
</button>
<span v-show="!isCollapsed">
<span v-if="!isCollapsed">
<slot name="expanded"></slot>
</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>
import { s__ } from '~/locale';
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 = {
modalId: 'modal-mrwidget-issue',
......@@ -16,6 +17,7 @@
components: {
Modal,
Icon,
ExpandButton,
},
props: {
issues: {
......@@ -79,6 +81,11 @@
return this.type === 'dast';
},
},
mounted() {
$(this.$refs.modal).on('hidden.bs.modal', () => {
this.clearModalData();
});
},
methods: {
shouldRenderPriority(issue) {
return this.hasPriority && issue.priority;
......@@ -117,85 +124,140 @@
};
</script>
<template>
<ul class="report-block-list">
<li
:class="{
failed: isStatusFailed,
success: isStatusSuccess,
neutral: isStatusNeutral
}"
class="report-block-list-item"
v-for="(issue, index) in issues"
:key="index"
>
<icon
class="report-block-icon"
: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"
<div>
<ul class="report-block-list">
<li
class="report-block-list-issue"
v-for="(issue, index) in issues"
:key="index"
>
<div
class="report-block-list-icon append-right-5"
:class="{
failed: isStatusFailed,
success: isStatusSuccess,
neutral: isStatusNeutral,
}"
>
{{ issue.name }}
</button>
</template>
<template v-else>
{{ issue.name }}<template v-if="issue.score">:
<strong>{{ formatScore(issue.score) }}</strong></template>
</template>
<icon
:name="iconName"
: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">
<template v-if="isStatusSuccess && isTypeQuality">{{ fixedLabel }}</template>
<template v-if="shouldRenderPriority(issue)">{{ issue.priority }}:</template>
<template v-if="isTypePerformance && issue.delta != null">
({{ issue.delta >= 0 ? '+' : '' }}{{ formatScore(issue.delta) }})
</template>
<template v-if="isTypeDocker">
<a
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">
in
<template v-if="isTypePerformance && issue.delta != null">
({{ issue.delta >= 0 ? '+' : '' }}{{ formatScore(issue.delta) }})
</template>
</div>
<div class="report-block-list-issue-description-link">
<template v-if="issue.path">
in
<a
v-if="issue.urlPath"
:href="issue.urlPath"
target="_blank"
rel="noopener noreferrer nofollow"
class="prepend-left-5"
>
{{ issue.path }}<template v-if="issue.line">:{{ issue.line }}</template>
</a>
<template v-else>
{{ issue.path }}<template v-if="issue.line">:{{ issue.line }}</template>
</template>
</template>
</li>
<a
v-if="issue.urlPath"
:href="issue.urlPath"
target="_blank"
rel="noopener noreferrer nofollow"
class="break-link"
>
{{ issue.path }}<template v-if="issue.line">:{{ issue.line }}</template>
</a>
<template v-else>
{{ issue.path }}<template v-if="issue.line">:{{ issue.line }}</template>
</template>
</template>
</div>
</div>
</li>
</ul>
<modal
:target-id="modalId"
:title="modalTitle"
:hide-footer="true"
:description="modalDesc"
:instances="modalInstances"
@clearData="clearModalData()"
/>
</ul>
v-if="isTypeDast"
:id="modalId"
:header-title-text="modalTitle"
ref="modal"
class="modal-security-report-dast"
>
<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>
......@@ -96,7 +96,9 @@
return this.status === 'success';
},
statusIconName() {
if (this.loadingFailed || this.unresolvedIssues.length) {
if (this.loadingFailed ||
this.unresolvedIssues.length ||
this.neutralIssues.length) {
return 'warning';
}
return 'success';
......@@ -221,7 +223,7 @@
<button
v-if="allIssues.length && !isFullReportVisible"
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"
>
{{ s__("ciReport|Show complete code vulnerabilities report") }}
......
.pipeline-tab-content {
.space-children,
.space-children > span {
display: flex;
}
.media {
align-items: center;
}
}
.report-block-container {
border-top: 1px solid $gray-darker;
padding: $gl-padding-top;
background-color: $gray-light;
margin: $gl-padding #{-$gl-padding} #{-$gl-padding};
}
.report-block-dast-code {
margin-left: 26px;
// Clean MR widget CSS
line-height: 20px;
}
.report-block-list {
list-style: none;
padding: 0 1px;
margin: 0;
line-height: $code_line_height;
.btn-open-modal {
padding: 0 5px 4px;
}
}
.report-block-list-item {
display: flex;
}
.report-block-list-icon {
display: flex;
.report-block-list-item-modal {
display: flex;
flex-wrap: wrap;
}
.failed .report-block-icon {
&.failed {
color: $red-500;
}
.success .report-block-icon {
&.success {
color: $green-500;
}
.neutral .report-block-icon {
&.neutral {
color: $theme-gray-700;
}
}
.report-block-icon {
margin: -5px 4px 0 0;
fill: currentColor;
}
.report-block-list-issue {
display: flex;
align-items: flex-start;
align-content: flex-start;
}
.pipeline-tab-content {
.space-children,
.space-children > * {
display: flex;
.report-block-list-issue-description {
align-content: space-around;
align-items: flex-start;
flex-wrap: wrap;
display: flex;
align-self: center;
}
.report-block {
.break-link {
word-wrap: break-word;
word-break: break-all;
}
}
.media {
align-items: center;
.report-block-issue-code {
width: $modal-lg - 70px;
}
.modal-security-report-dast {
.modal-dialog {
width: $modal-lg;
}
}
\ No newline at end of file
// TODO remove this when gl_modal support not rendering the footer
.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', () => {
expect(vm.$el.textContent).toContain(parsedDast[0].name);
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', () => {
it('should show the report by default', () => {
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);
});
});
......
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