Commit 8b585bde authored by Dmitriy Zaporozhets's avatar Dmitriy Zaporozhets Committed by Tim Zallmann

Resolve "Show DAST results in the MR widget"

parent 752657df
......@@ -64,3 +64,12 @@ export const truncate = (string, maxLength) => `${string.substr(0, (maxLength -
export function capitalizeFirstCharacter(text) {
return `${text[0].toUpperCase()}${text.slice(1)}`;
}
/**
* Replaces all html tags from a string with the given replacement.
*
* @param {String} string
* @param {*} replace
* @returns {String}
*/
export const stripeHtml = (string, replace = '') => string.replace(/<[^>]*>/g, replace);
<script>
import { __ } from '~/locale';
/**
* Port of detail_behavior expand button.
*
* @example
* <expand-button>
* <template slot="expanded">
* Text goes here.
* </template>
* </expand-button>
*/
export default {
name: 'expandButton',
data() {
return {
isCollapsed: true,
};
},
computed: {
ariaLabel() {
return __('Click to expand text');
},
},
methods: {
onClick() {
this.isCollapsed = !this.isCollapsed;
},
},
};
</script>
<template>
<span>
<button
type="button"
v-show="isCollapsed"
class="text-expander btn-blank"
:aria-label="ariaLabel"
@click="onClick">
...
</button>
<span v-show="!isCollapsed">
<slot name="expanded"></slot>
</span>
</span>
</template>
......@@ -799,6 +799,10 @@
padding-left: 12px;
}
.mr-widget-dast-code {
margin-left: 26px;
}
.mr-widget-code-quality-list {
list-style: none;
padding: 0 12px;
......@@ -826,6 +830,10 @@
.neutral {
color: $gl-gray-light;
}
.modal-body {
color: $gl-text-color;
}
}
}
}
---
title: Show results from DAST scan in the merge request widget
merge_request: 3885
author:
type: added
......@@ -58,6 +58,10 @@ Apart from those, here is an collection of tutorials and guides on setting up yo
- [Scan your code for vulnerabilities](sast.md)
### Dynamic Application Security Testing (DAST)
- [Scan your app for vulnerabilities](dast.md)
### Browser Performance Testing with Sitespeed.io
- [Analyze browser performance with Sitespeed.io](browser_performance.md)
......
# Dynamic application security testing with GitLab CI/CD
NOTE: **Note:**
In order to use this tool, a [GitLab Enterprise Edition Ultimate][ee] license
is needed.
This example shows how to run
[Dynamic Application Security Testing (DAST)](https://en.wikipedia.org/wiki/Dynamic_program_analysis)
on your project's source code by using GitLab CI/CD.
All you need is a GitLab Runner with the Docker executor (the shared Runners on
GitLab.com will work fine). You can then add a new job to `.gitlab-ci.yml`,
called `dast`:
```yaml
dast:
image: owasp/zap2docker-stable
script:
- mkdir /zap/wrk/
- /zap/zap-baseline.py -J gl-dast-report.json -t http://dzaporozhets.me/ || true
- cp /zap/wrk/gl-dast-report.json .
artifacts:
paths: [gl-dast-report.json]
```
DAST is using a popular open source tool
[OWASP ZAProxy](https://github.com/zaproxy/zaproxy) to perform an analysis.
The above example will create a `dast` job in your CI pipeline and will allow
you to download and analyze the report artifact in JSON format.
TIP: **Tip:**
Starting with GitLab Enterprise Edition Ultimate 10.4, this information will
be automatically extracted and shown right in the merge request widget. To do
so, the CI job must be named `dast` and the artifact path must be
`gl-dast-report.json`.
[Learn more on application security testing results shown in merge requests](../../user/project/merge_requests/sast.md).
[ee]: https://about.gitlab.com/gitlab-ee/
# Dynamic Application Security Testing (SAST)
> [Introduced][ee-4348] in [GitLab Enterprise Edition Ultimate][ee] 10.4.
## Overview
If you are using [GitLab CI/CD][ci], you can analyze your web application for known
vulnerabilities using Dynamic Application Security Testing (DAST), either by
including the CI job in your [existing `.gitlab-ci.yml` file][cc-docs] or
by implicitly using [Auto DAST](../../../topics/autodevops/index.md#auto-dast)
that is provided by [Auto DevOps](../../../topics/autodevops/index.md).
Going a step further, GitLab can show the vulnerability list right in the merge
request widget area:
![DAST Widget](img/dast-all.png)
By clicking on vlunerability you will be able to see details and url affected:
![DAST Widget Clicked](img/dast-single.png)
## Use cases
It helps you automatically find security vulnerabilities in your web applications
while you are developing and testing your applications
## How it works
In order for the report to show in the merge request, you need to specify a
`dast` job (exact name) that will analyze the running application and upload the resulting
`gl-dast-report.json` file as an artifact. GitLab will then check this file and
show the information inside the merge request.
This JSON file needs to be the only artifact file for the job. If you try
to also include other files, it will break the vulnerability display in the
merge request.
For more information on how the `dast` job should look like, check the
[example on analyzing a project's code for vulnerabilities][cc-docs].
[ee-4348]: https://gitlab.com/gitlab-org/gitlab-ee/issues/4348
[ee]: https://about.gitlab.com/gitlab-ee/
[ci]: ../../../ci/README.md
[cc-docs]: ../../../ci/examples/dast.md
<script>
import { s__ } from '~/locale';
import { spriteIcon } from '~/lib/utils/common_utils';
import expandButton from '~/vue_shared/components/expand_button.vue';
export default {
name: 'modal',
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: [],
},
},
components: {
expandButton,
},
computed: {
cutIcon() {
return spriteIcon('cut');
},
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"
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="mr-widget-code-quality-list">
<li
v-for="(instance, i) in instances"
:key="i"
class="failed">
<span
class="mr-widget-code-quality-icon"
v-html="cutIcon"
>
</span>
{{instance.method}}
<a
:href="instance.uri"
target="_blank"
rel="noopener noreferrer nofollow"
>
{{instance.uri}}
</a>
<expand-button v-if="instance.evidence">
<pre
slot="expanded"
class="block mr-widget-dast-code prepend-top-10">{{instance.evidence}}</pre>
</expand-button>
</li>
</ul>
</div>
</div>
</div>
</div>
</template>
......@@ -48,6 +48,11 @@ export default {
type: String,
required: false,
},
hasPriority: {
type: Boolean,
required: false,
default: false,
},
},
components: {
......@@ -145,6 +150,7 @@ export default {
:type="type"
status="failed"
:issues="unresolvedIssues"
:has-priority="hasPriority"
/>
<issues-block
......@@ -153,6 +159,7 @@ export default {
:type="type"
status="neutral"
:issues="neutralIssues"
:has-priority="hasPriority"
/>
<issues-block
......@@ -161,6 +168,7 @@ export default {
:type="type"
status="success"
:issues="resolvedIssues"
:has-priority="hasPriority"
/>
</div>
<div
......
<script>
import { s__ } from '~/locale';
import { spriteIcon } from '~/lib/utils/common_utils';
import modal from './mr_widget_dast_modal.vue';
const modalDefaultData = {
modalId: 'modal-mrwidget-issue',
modalDesc: '',
modalTitle: '',
modalInstances: [],
modalTargetId: '#modal-mrwidget-issue',
};
export default {
name: 'mrWidgetReportIssues',
......@@ -8,7 +18,7 @@
type: Array,
required: true,
},
// security || codequality || performance || docker
// security || codequality || performance || docker || dast
type: {
type: String,
required: true,
......@@ -18,10 +28,27 @@
type: String,
required: true,
},
hasPriority: {
type: Boolean,
required: false,
default: false,
},
},
data() {
return modalDefaultData;
},
components: {
modal,
},
computed: {
icon() {
return this.isStatusSuccess ? spriteIcon('plus') : spriteIcon('cut');
return this.isStatusSuccess ? spriteIcon('plus') : this.cutIcon;
},
cutIcon() {
return spriteIcon('cut');
},
fixedLabel() {
return s__('ciReport|Fixed:');
},
isStatusFailed() {
return this.status === 'failed';
......@@ -44,10 +71,37 @@
isTypeDocker() {
return this.type === 'docker';
},
isTypeDast() {
return this.type === 'dast';
},
},
methods: {
shouldRenderPriority(issue) {
return (this.isTypeSecurity || this.isTypeDocker) && issue.priority;
return this.hasPriority && issue.priority;
},
getmodalId(index) {
return `modal-mrwidget-issue-${index}`;
},
modalIdTarget(index) {
return `#${this.getmodalId(index)}`;
},
openDastModal(issue, index) {
this.modalId = this.getmodalId(index);
this.modalTitle = `${issue.priority}: ${issue.name}`;
this.modalTargetId = `#${this.getmodalId(index)}`;
this.modalInstances = issue.instances;
this.modalDesc = issue.parsedDescription;
},
/**
* Because of https://vuejs.org/v2/guide/list.html#Caveats
* we need to clear the instances to make sure everything is properly reset.
*/
clearModalData() {
this.modalId = modalDefaultData.modalId;
this.modalDesc = modalDefaultData.modalDesc;
this.modalTitle = modalDefaultData.modalTitle;
this.modalInstances = modalDefaultData.modalInstances;
this.modalTargetId = modalDefaultData.modalTargetId;
},
},
};
......@@ -60,14 +114,17 @@
success: isStatusSuccess,
neutral: isStatusNeutral
}
"v-for="issue in issues">
"v-for="(issue, index) in issues"
:key="index"
>
<span
class="mr-widget-code-quality-icon"
v-html="icon">
v-html="icon"
>
</span>
<template v-if="isStatusSuccess && isTypeQuality">Fixed:</template>
<template v-if="isStatusSuccess && isTypeQuality">{{ fixedLabel }}</template>
<template v-if="shouldRenderPriority(issue)">{{issue.priority}}:</template>
<template v-if="isTypeDocker">
......@@ -82,6 +139,17 @@
{{issue.name}}
</template>
</template>
<template v-else-if="isTypeDast">
<button
type="button"
@click="openDastModal(issue, index)"
data-toggle="modal"
class="btn-link btn-blank"
:data-target="modalTargetId"
>
{{issue.name}}
</button>
</template>
<template v-else>
{{issue.name}}<template v-if="issue.score">: <strong>{{issue.score}}</strong></template>
</template>
......@@ -104,7 +172,16 @@
{{issue.path}}<template v-if="issue.line">:{{issue.line}}</template>
</template>
</template>
</li>
<modal
:target-id="modalId"
:title="modalTitle"
:hide-footer="true"
:description="modalDesc"
:instances="modalInstances"
@clearData="clearModalData()"
/>
</modal>
</ul>
</template>
......@@ -19,10 +19,12 @@ export default {
isLoadingPerformance: false,
isLoadingSecurity: false,
isLoadingDocker: false,
isLoadingDast: false,
loadingCodequalityFailed: false,
loadingPerformanceFailed: false,
loadingSecurityFailed: false,
loadingDockerFailed: false,
loadingDastFailed: false,
};
},
computed: {
......@@ -43,6 +45,9 @@ export default {
shouldRenderDockerReport() {
return this.mr.sastContainer;
},
shouldRenderDastReport() {
return this.mr.dast;
},
codequalityText() {
const { newIssues, resolvedIssues } = this.mr.codeclimateMetrics;
const text = [];
......@@ -153,6 +158,18 @@ export default {
)}`;
},
dastText() {
if (this.mr.dastReport.length) {
return n__(
'%d DAST alert detected by analyzing the review app',
'%d DAST alerts detected by analyzing the review app',
this.mr.dastReport.length,
);
}
return s__('ciReport|No DAST alerts detected by analyzing the review app');
},
codequalityStatus() {
return this.checkReportStatus(this.isLoadingCodequality, this.loadingCodequalityFailed);
},
......@@ -169,6 +186,10 @@ export default {
return this.checkReportStatus(this.isLoadingDocker, this.loadingDockerFailed);
},
dastStatus() {
return this.checkReportStatus(this.isLoadingDast, this.loadingDastFailed);
},
dockerInformationText() {
return sprintf(
s__('ciReport|Unapproved vulnerabilities (red) can be marked as approved. %{helpLink}'), {
......@@ -259,6 +280,20 @@ export default {
});
},
fetchDastReport() {
this.isLoadingDast = true;
this.service.fetchReport(this.mr.dast.path)
.then((data) => {
this.mr.setDastReport(data);
this.isLoadingDast = false;
})
.catch(() => {
this.isLoadingDast = false;
this.loadingDastFailed = true;
});
},
translateText(type) {
return {
error: s__(`ciReport|Failed to load ${type} report`),
......@@ -282,6 +317,10 @@ export default {
if (this.shouldRenderDockerReport) {
this.fetchDockerReport();
}
if (this.shouldRenderDastReport) {
this.fetchDastReport();
}
},
template: `
<div class="mr-state-widget prepend-top-default">
......@@ -334,6 +373,7 @@ export default {
:error-text="translateText('security').error"
:success-text="securityText"
:unresolved-issues="mr.securityReport"
:has-priority="true"
/>
<collapsible-section
class="js-docker-widget"
......@@ -346,6 +386,18 @@ export default {
:unresolved-issues="mr.dockerReport.unapproved"
:neutral-issues="mr.dockerReport.approved"
:info-text="dockerInformationText"
:has-priority="true"
/>
<collapsible-section
class="js-dast-widget"
v-if="shouldRenderDastReport"
type="dast"
:status="dastStatus"
:loading-text="translateText('DAST').loading"
:error-text="translateText('DAST').error"
:success-text="dastText"
:unresolved-issues="mr.dastReport"
:has-priority="true"
/>
<div class="mr-widget-section">
<component
......
import CEMergeRequestStore from '~/vue_merge_request_widget/stores/mr_widget_store';
import { stripeHtml } from '~/lib/utils/text_utility';
export default class MergeRequestStore extends CEMergeRequestStore {
constructor(data) {
......@@ -7,6 +8,7 @@ export default class MergeRequestStore extends CEMergeRequestStore {
this.initPerformanceReport(data);
this.initSecurityReport(data);
this.initDockerReport(data);
this.initDastReport(data);
}
setData(data) {
......@@ -81,6 +83,11 @@ export default class MergeRequestStore extends CEMergeRequestStore {
};
}
initDastReport(data) {
this.dast = data.dast;
this.dastReport = [];
}
setSecurityReport(issues, path) {
this.securityReport = MergeRequestStore.parseIssues(issues, path);
}
......@@ -103,6 +110,21 @@ export default class MergeRequestStore extends CEMergeRequestStore {
this.dockerReport.unapproved = parsedVulnerabilities
.filter(item => unapproved.find(el => el === item.vulnerability)) || [];
}
/**
* Dast Report sends some keys in HTML, we need to stripe the `<p>` tags.
* This should be moved to the backend.
*
* @param {Array} data
* @returns {Array}
*/
setDastReport(data) {
this.dastReport = data.site.alerts.map(alert => ({
name: alert.name,
parsedDescription: stripeHtml(alert.desc, ' '),
priority: alert.riskdesc,
...alert,
}));
}
static parseDockerVulnerabilities(data) {
return data.map(el => ({
......
......@@ -11,16 +11,24 @@ module EE
SAST_FILE = 'gl-sast-report.json'.freeze
PERFORMANCE_FILE = 'performance.json'.freeze
SAST_CONTAINER_FILE = 'gl-sast-container-report.json'.freeze
DAST_FILE = 'gl-dast-report.json'.freeze
included do
scope :codequality, -> { where(name: %w[codequality codeclimate]) }
scope :performance, -> { where(name: %w[performance deploy]) }
scope :sast, -> { where(name: 'sast') }
scope :sast_container, -> { where(name: 'sast:container') }
scope :dast, -> { where(name: 'dast') }
after_save :stick_build_if_status_changed
end
class_methods do
def find_dast
dast.find(&:has_dast_json?)
end
end
def shared_runners_minutes_limit_enabled?
runner && runner.shared? && project.shared_runners_minutes_limit_enabled?
end
......@@ -48,6 +56,10 @@ module EE
has_artifact?(SAST_CONTAINER_FILE)
end
def has_dast_json?
has_artifact?(DAST_FILE)
end
private
def has_artifact?(name)
......
......@@ -28,6 +28,10 @@ module EE
def sast_container_artifact
artifacts.sast_container.find(&:has_sast_container_json?)
end
def dast_artifact
artifacts.dast.find(&:has_dast_json?)
end
end
end
end
......@@ -15,6 +15,7 @@ module EE
delegate :performance_artifact, to: :base_pipeline, prefix: :base, allow_nil: true
delegate :sast_artifact, to: :head_pipeline, allow_nil: true
delegate :sast_container_artifact, to: :head_pipeline, allow_nil: true
delegate :dast_artifact, to: :head_pipeline, allow_nil: true
delegate :sha, to: :head_pipeline, prefix: :head_pipeline, allow_nil: true
delegate :sha, to: :base_pipeline, prefix: :base_pipeline, allow_nil: true
end
......@@ -59,5 +60,9 @@ module EE
def has_sast_container_data?
sast_container_artifact&.success?
end
def has_dast_data?
dast_artifact&.success?
end
end
end
......@@ -56,6 +56,7 @@ class License < ActiveRecord::Base
EEU_FEATURES = EEP_FEATURES + %i[
sast
sast_container
dast
epics
].freeze
......
......@@ -62,6 +62,14 @@ module EE
project_blob_path(merge_request.project, merge_request.head_pipeline_sha)
end
end
expose :dast, if: -> (mr, _) { expose_dast_data?(mr, current_user) } do
expose :path do |merge_request|
raw_project_build_artifacts_url(merge_request.source_project,
merge_request.dast_artifact,
path: Ci::Build::DAST_FILE)
end
end
end
private
......@@ -82,5 +90,11 @@ module EE
mr.has_sast_container_data? &&
can?(current_user, :read_build, mr.sast_container_artifact)
end
def expose_dast_data?(mr, current_user)
mr.project.feature_available?(:dast) &&
mr.has_dast_data? &&
can?(current_user, :read_build, mr.dast_artifact)
end
end
end
......@@ -132,7 +132,8 @@ describe Ci::Build do
has_codeclimate_json?: Ci::Build::CODEQUALITY_FILE,
has_performance_json?: Ci::Build::PERFORMANCE_FILE,
has_sast_json?: Ci::Build::SAST_FILE,
has_sast_container_json?: Ci::Build::SAST_CONTAINER_FILE
has_sast_container_json?: Ci::Build::SAST_CONTAINER_FILE,
has_dast_json?: Ci::Build::DAST_FILE
}.freeze
ARTIFACTS_METHODS.each do |method, filename|
......
......@@ -19,7 +19,8 @@ describe Ci::Pipeline do
codeclimate_artifact: [Ci::Build::CODEQUALITY_FILE, 'codequality'],
performance_artifact: [Ci::Build::PERFORMANCE_FILE, 'performance'],
sast_artifact: [Ci::Build::SAST_FILE, 'sast'],
sast_container_artifact: [Ci::Build::SAST_CONTAINER_FILE, 'sast:container']
sast_container_artifact: [Ci::Build::SAST_CONTAINER_FILE, 'sast:container'],
dast_artifact: [Ci::Build::DAST_FILE, 'dast']
}.freeze
ARTIFACTS_METHODS.each do |method, options|
......
......@@ -250,4 +250,18 @@ describe MergeRequest do
describe '#sast_container_artifact' do
it { is_expected.to delegate_method(:sast_container_artifact).to(:head_pipeline) }
end
describe '#has_dast_data?' do
let(:artifact) { double(success?: true) }
before do
allow(merge_request).to receive(:dast_artifact).and_return(artifact)
end
it { expect(merge_request.has_dast_data?).to be_truthy }
end
describe '#dast_artifact' do
it { is_expected.to delegate_method(:dast_artifact).to(:head_pipeline) }
end
end
......@@ -60,4 +60,14 @@ describe('text_utility', () => {
expect(textUtils.capitalizeFirstCharacter('gitlab')).toEqual('Gitlab');
});
});
describe('stripeHtml', () => {
it('replaces html tag with the default replacement', () => {
expect(textUtils.stripeHtml('This is a text with <p>html</p>.')).toEqual('This is a text with html.');
});
it('replaces html tags with the provided replacement', () => {
expect(textUtils.stripeHtml('This is a text with <p>html</p>.', ' ')).toEqual('This is a text with html .');
});
});
});
import Vue from 'vue';
import modal from 'ee/vue_merge_request_widget/components/mr_widget_dast_modal.vue';
import mountComponent from '../../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');
});
});
......@@ -5,6 +5,7 @@ import {
securityParsedIssues,
codequalityParsedIssues,
dockerReportParsed,
parsedDast,
} from '../mock_data';
describe('merge request report issues', () => {
......@@ -66,6 +67,7 @@ describe('merge request report issues', () => {
issues: securityParsedIssues,
type: 'security',
status: 'failed',
hasPriority: true,
});
});
......@@ -112,6 +114,7 @@ describe('merge request report issues', () => {
issues: dockerReportParsed.unapproved,
type: 'docker',
status: 'failed',
hasPriority: true,
});
});
......@@ -139,4 +142,20 @@ describe('merge request report issues', () => {
).toContain('in');
});
});
describe('for dast issues', () => {
beforeEach(() => {
vm = mountComponent(MRWidgetCodeQualityIssues, {
issues: parsedDast,
type: 'dast',
status: 'failed',
hasPriority: true,
});
});
it('renders priority and name', () => {
expect(vm.$el.textContent).toContain(parsedDast[0].name);
expect(vm.$el.textContent).toContain(parsedDast[0].priority);
});
});
});
......@@ -12,6 +12,8 @@ import mockData, {
securityIssues,
dockerReport,
dockerReportParsed,
dast,
parsedDast,
} from './mock_data';
import mountComponent from '../helpers/vue_mount_component_helper';
......@@ -452,6 +454,84 @@ describe('ee merge request widget options', () => {
});
});
describe('dast report', () => {
beforeEach(() => {
gl.mrWidgetData = {
...mockData,
dast: {
path: 'dast.json',
},
};
Component.mr = new MRWidgetStore(gl.mrWidgetData);
Component.service = new MRWidgetService({});
});
describe('when it is loading', () => {
it('should render loading indicator', () => {
vm = mountComponent(Component);
expect(
vm.$el.querySelector('.js-dast-widget').textContent.trim(),
).toContain('Loading DAST report');
});
});
describe('with successful request', () => {
let mock;
beforeEach(() => {
mock = mock = new MockAdapter(axios);
mock.onGet('dast.json').reply(200, dast);
vm = mountComponent(Component);
});
afterEach(() => {
mock.reset();
});
it('should render provided data', (done) => {
setTimeout(() => {
expect(
vm.$el.querySelector('.js-dast-widget .js-code-text').textContent.trim(),
).toEqual('2 DAST alerts detected by analyzing the review app');
vm.$el.querySelector('.js-dast-widget button').click();
Vue.nextTick(() => {
const firstVulnerability = vm.$el.querySelector('.js-dast-widget .mr-widget-code-quality-list').textContent.trim();
expect(firstVulnerability).toContain(parsedDast[0].name);
expect(firstVulnerability).toContain(parsedDast[0].priority);
done();
});
}, 0);
});
});
describe('with failed request', () => {
let mock;
beforeEach(() => {
mock = mock = new MockAdapter(axios);
mock.onGet('dast.json').reply(500, {});
vm = mountComponent(Component);
});
afterEach(() => {
mock.reset();
});
it('should render error indicator', (done) => {
setTimeout(() => {
expect(
vm.$el.querySelector('.js-dast-widget').textContent.trim(),
).toContain('Failed to load DAST report');
done();
}, 0);
});
});
});
describe('computed', () => {
describe('shouldRenderApprovals', () => {
it('should return false when no approvals', () => {
......
......@@ -516,3 +516,63 @@ export const dockerReportParsed = {
}
]
};
export const dast = {
site: {
alerts: [{
name: 'Absence of Anti-CSRF Tokens',
riskcode: '1',
riskdesc: 'Low (Medium)',
desc: '<p>No Anti-CSRF tokens were found in a HTML submission form.<\/p>',
instances: [{
uri: 'http://192.168.32.236:3001/explore?sort=latest_activity_desc',
method: 'GET',
evidence: '<form class=\'navbar-form\' action=\'/search\' accept-charset=\'UTF-8\' method=\'get\'>'
}, {
uri: 'http://192.168.32.236:3001/help/user/group/subgroups/index.md',
method: 'GET',
evidence: '<form class=\'navbar-form\' action=\'/search\' accept-charset=\'UTF-8\' method=\'get\'>'
}]
}, {
alert: 'X-Content-Type-Options Header Missing',
name: 'X-Content-Type-Options Header Missing',
riskdesc: 'Low (Medium)',
desc: '<p>The Anti-MIME-Sniffing header X-Content-Type-Options was not set to "nosniff".<\/p>',
instances: [{
uri: 'http://192.168.32.236:3001/assets/webpack/main.bundle.js',
method: 'GET',
param: 'X-Content-Type-Options'
}]
}]
}
};
export const parsedDast = [{
name: 'Absence of Anti-CSRF Tokens',
riskcode: '1',
riskdesc: 'Low (Medium)',
priority: 'Low (Medium)',
desc: '<p>No Anti-CSRF tokens were found in a HTML submission form.<\/p>',
parsedDescription: ' No Anti-CSRF tokens were found in a HTML submission form. ',
instances: [{
uri: 'http://192.168.32.236:3001/explore?sort=latest_activity_desc',
method: 'GET',
evidence: '<form class=\'navbar-form\' action=\'/search\' accept-charset=\'UTF-8\' method=\'get\'>'
}, {
uri: 'http://192.168.32.236:3001/help/user/group/subgroups/index.md',
method: 'GET',
evidence: '<form class=\'navbar-form\' action=\'/search\' accept-charset=\'UTF-8\' method=\'get\'>'
}]
}, {
alert: 'X-Content-Type-Options Header Missing',
name: 'X-Content-Type-Options Header Missing',
riskdesc: 'Low (Medium)',
priority: 'Low (Medium)',
desc: '<p>The Anti-MIME-Sniffing header X-Content-Type-Options was not set to "nosniff".<\/p>',
parsedDescription: ' The Anti-MIME-Sniffing header X-Content-Type-Options was not set to "nosniff". ',
instances: [{
uri: 'http://192.168.32.236:3001/assets/webpack/main.bundle.js',
method: 'GET',
param: 'X-Content-Type-Options'
}]
}];
\ No newline at end of file
......@@ -9,6 +9,8 @@ import mockData, {
parsedSecurityIssuesStore,
dockerReport,
dockerReportParsed,
dast,
parsedDast,
} from '../mock_data';
describe('MergeRequestStore', () => {
......@@ -169,4 +171,21 @@ describe('MergeRequestStore', () => {
);
});
});
describe('initDastReport', () => {
it('sets the defaults', () => {
store.initDastReport({ dast: { path: 'dast.json' } });
expect(store.dast).toEqual({ path: 'dast.json' });
expect(store.dastReport).toEqual([]);
});
});
describe('setDastReport', () => {
it('parsed data and sets the report', () => {
store.setDastReport(dast);
expect(store.dastReport).toEqual(parsedDast);
});
});
});
import Vue from 'vue';
import expandButton from '~/vue_shared/components/expand_button.vue';
import mountComponent from '../../helpers/vue_mount_component_helper';
describe('expand button', () => {
let vm;
beforeEach(() => {
const Component = Vue.extend(expandButton);
vm = mountComponent(Component, {
slots: {
expanded: '<p>Expanded!</p>',
},
});
});
afterEach(() => {
vm.$destroy();
});
it('renders a collpased button', () => {
expect(vm.$el.textContent.trim()).toEqual('...');
});
it('hides expander on click', (done) => {
vm.$el.querySelector('button').click();
vm.$nextTick(() => {
expect(vm.$el.querySelector('button').getAttribute('style')).toEqual('display: none;');
done();
});
});
});
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