Commit 61c45e10 authored by Phil Hughes's avatar Phil Hughes

Merge branch '6169-add-security-reports-pipeline-view' into 'master'

Resolve "Add DAST reports at pipeline level"

Closes #6169 and #6168

See merge request gitlab-org/gitlab-ee!6040
parents d8f4994f 18544247
......@@ -115,6 +115,10 @@ export default () => {
const dependencyScanningHelpPath = datasetOptions.dependencyScanningHelpPath;
const vulnerabilityFeedbackPath = datasetOptions.vulnerabilityFeedbackPath;
const vulnerabilityFeedbackHelpPath = datasetOptions.vulnerabilityFeedbackHelpPath;
const dastEndpoint = datasetOptions.dastEndpoint;
const sastContainerEndpoint = datasetOptions.sastContainerEndpoint;
const dastHelpPath = datasetOptions.dastHelpPath;
const sastContainerHelpPath = datasetOptions.sastContainerHelpPath;
const pipelineId = parseInt(datasetOptions.pipelineId, 10);
const store = createStore();
......@@ -164,6 +168,10 @@ export default () => {
vulnerabilityFeedbackPath,
vulnerabilityFeedbackHelpPath,
pipelineId,
dastHeadPath: dastEndpoint,
sastContainerHeadPath: sastContainerEndpoint,
dastHelpPath,
sastContainerHelpPath,
},
on: {
updateBadgeCount: this.updateBadge,
......
#js-pipeline-header-vue.pipeline-header-container
- sast_artifact = @pipeline.sast_artifact
- dependency_artifact = @pipeline.dependency_scanning_artifact
- dast_artifact = @pipeline.dast_artifact
- sast_container_artifact = @pipeline.sast_container_artifact
- if @commit.present?
.commit-box
......@@ -36,5 +38,5 @@
= link_to @pipeline.sha, project_commit_path(@project, @pipeline.sha), class: "commit-sha commit-hash-full"
= clipboard_button(text: @pipeline.sha, title: "Copy commit SHA to clipboard")
- if sast_artifact || dependency_artifact
- if sast_artifact || dependency_artifact || dast_artifact || sast_container_artifact
.js-sast-summary
- expose_sast_data = @pipeline.expose_sast_data?
- expose_dependency_data = @pipeline.expose_dependency_scanning_data?
- expose_dast_data = @pipeline.expose_dast_data?
- expose_sast_container_data = @pipeline.expose_sast_container_data?
- expose_container_scanning_data = @pipeline.expose_container_scanning_data?
- blob_path = project_blob_path(@project, @pipeline.sha)
.tabs-holder
......@@ -84,13 +87,17 @@
%pre.build-trace.build-trace-rounded
%code.bash.js-build-output
= build_summary(build)
- if expose_sast_data || expose_dependency_data
- if expose_sast_data || expose_dependency_data || expose_dast_data || expose_sast_container_data || expose_container_scanning_data
#js-tab-security.build-security.tab-pane
#js-security-report-app{ data: { endpoint: expose_sast_data ? sast_artifact_url(@pipeline) : nil,
blob_path: blob_path,
dependency_scanning_endpoint: expose_dependency_data ? dependency_scanning_artifact_url(@pipeline) : nil,
dast_endpoint: expose_dast_data ? dast_artifact_url(@pipeline) : nil,
sast_container_endpoint: expose_sast_container_data ? sast_container_artifact_url(@pipeline) : (expose_container_scanning_data ? container_scanning_artifact_url(@pipeline) : nil),
pipeline_id: @pipeline.id,
vulnerability_feedback_path: project_vulnerability_feedback_index_path(@project),
vulnerability_feedback_help_path: help_page_path("user/project/merge_requests/index", anchor: "interacting-with-security-reports-ultimate"),
sast_help_path: help_page_path('user/project/merge_requests/sast'),
dependency_scanning_help_path: help_page_path('user/project/merge_requests/dependency_scanning')} }
dependency_scanning_help_path: help_page_path('user/project/merge_requests/dependency_scanning'),
dast_help_path: help_page_path('user/project/merge_requests/dast'),
sast_container_help_path: help_page_path('user/project/merge_requests/sast_container')} }
......@@ -12,13 +12,19 @@ export default {
LoadingIcon,
},
computed: {
...mapState(['sast', 'dependencyScanning']),
...mapState(['sast', 'dependencyScanning', 'dast', 'sastContainer']),
sastLink() {
return this.link(this.sast.newIssues.length);
},
dependencyScanningLink() {
return this.link(this.dependencyScanning.newIssues.length);
},
dastLink() {
return this.link(this.dast.newIssues.length);
},
sastContainerLink() {
return this.link(this.sastContainer.newIssues.length);
},
sastIcon() {
return this.statusIcon(this.hasSastError, this.sast.newIssues.length);
},
......@@ -28,24 +34,48 @@ export default {
this.dependencyScanning.newIssues.length,
);
},
dastIcon() {
return this.statusIcon(this.hasDastError, this.dast.newIssues.length);
},
sastContainerIcon() {
return this.statusIcon(this.hasSastContainerError, this.sastContainer.newIssues.length);
},
hasSast() {
return this.sast.paths.head !== null;
},
hasDependencyScanning() {
return this.dependencyScanning.paths.head !== null;
},
hasDast() {
return this.dast.paths.head !== null;
},
hasSastContainer() {
return this.sastContainer.paths.head !== null;
},
isLoadingSast() {
return this.sast.isLoading;
},
isLoadingDependencyScanning() {
return this.dependencyScanning.isLoading;
},
isLoadingDast() {
return this.dast.isLoading;
},
isLoadingSastContainer() {
return this.sastContainer.isLoading;
},
hasSastError() {
return this.sast.hasError;
},
hasDependencyScanningError() {
return this.dependencyScanning.hasError;
},
hasDastError() {
return this.dast.hasError;
},
hasSastContainerError() {
return this.sastContainer.hasError;
},
},
methods: {
openTab() {
......@@ -117,7 +147,7 @@ export default {
v-if="hasDependencyScanning"
>
<loading-icon
v-if="dependencyScanning.isLoading"
v-if="isLoadingDependencyScanning"
/>
<ci-icon
v-else
......@@ -146,5 +176,73 @@ export default {
</template>
</span>
</div>
<div
class="well-segment flex js-sast-container-summary"
v-if="hasSastContainer"
>
<loading-icon
v-if="isLoadingSastContainer"
/>
<ci-icon
v-else
:status="sastContainerIcon"
class="flex flex-align-self-center"
/>
<span
class="prepend-left-10 flex flex-align-self-center"
>
<template v-if="hasSastContainerError">
{{ s__('ciReport|Container scanning resulted in error while loading results') }}
</template>
<template v-else-if="isLoadingSastContainer">
{{ s__('ciReport|Container scanning is loading') }}
</template>
<template v-else>
{{ s__('ciReport|Container scanning detected') }}
<button
type="button"
class="btn-link btn-blank prepend-left-5"
@click="openTab"
>
{{ sastContainerLink }}
</button>
</template>
</span>
</div>
<div
class="well-segment flex js-dast-summary"
v-if="hasDast"
>
<loading-icon
v-if="isLoadingDast"
/>
<ci-icon
v-else
:status="dastIcon"
class="flex flex-align-self-center"
/>
<span
class="prepend-left-10 flex flex-align-self-center"
>
<template v-if="hasDastError">
{{ s__('ciReport|DAST resulted in error while loading results') }}
</template>
<template v-else-if="isLoadingDast">
{{ s__('ciReport|DAST is loading') }}
</template>
<template v-else>
{{ s__('ciReport|DAST detected') }}
<button
type="button"
class="btn-link btn-blank prepend-left-5"
@click="openTab"
>
{{ dastLink }}
</button>
</template>
</span>
</div>
</div>
</template>
......@@ -2,7 +2,7 @@
import { mapActions, mapState } from 'vuex';
import { s__, sprintf, n__ } from '~/locale';
import createFlash from '~/flash';
import { SAST } from './store/constants';
import { SAST, DAST, SAST_CONTAINER } from './store/constants';
import ReportSection from './components/report_section.vue';
import IssueModal from './components/modal.vue';
import mixin from './mixins/security_report_mixin';
......@@ -24,6 +24,16 @@ export default {
required: false,
default: null,
},
dastHeadPath: {
type: String,
required: false,
default: null,
},
sastContainerHeadPath: {
type: String,
required: false,
default: null,
},
dependencyScanningHeadPath: {
type: String,
required: false,
......@@ -34,6 +44,16 @@ export default {
required: false,
default: null,
},
sastContainerHelpPath: {
type: String,
required: false,
default: '',
},
dastHelpPath: {
type: String,
required: false,
default: '',
},
dependencyScanningHelpPath: {
type: String,
required: false,
......@@ -56,8 +76,10 @@ export default {
},
},
sast: SAST,
dast: DAST,
sastContainer: SAST_CONTAINER,
computed: {
...mapState(['sast', 'dependencyScanning']),
...mapState(['sast', 'dependencyScanning', 'sastContainer', 'dast']),
sastText() {
return this.summaryTextBuilder('SAST', this.sast.newIssues.length);
......@@ -69,6 +91,14 @@ export default {
this.dependencyScanning.newIssues.length,
);
},
sastContainerText() {
return this.summaryTextBuilder('Container scanning', this.sastContainer.newIssues.length);
},
dastText() {
return this.summaryTextBuilder('DAST', this.dast.newIssues.length);
},
},
created() {
// update the store with the received props
......@@ -98,6 +128,30 @@ export default {
createFlash(s__('ciReport|There was an error loading dependency scanning report')),
);
}
if (this.sastContainerHeadPath) {
this.setSastContainerHeadPath(this.sastContainerHeadPath);
this.fetchSastContainerReports()
.then(() => {
this.$emit('updateBadgeCount', this.sastContainer.newIssues.length);
})
.catch(() =>
createFlash(s__('ciReport|There was an error loading container scanning report')),
);
}
if (this.dastHeadPath) {
this.setDastHeadPath(this.dastHeadPath);
this.fetchDastReports()
.then(() => {
this.$emit('updateBadgeCount', this.dast.newIssues.length);
})
.catch(() =>
createFlash(s__('ciReport|There was an error loading DAST report')),
);
}
},
methods: {
......@@ -105,8 +159,12 @@ export default {
'setHeadBlobPath',
'setSastHeadPath',
'setDependencyScanningHeadPath',
'setSastContainerHeadPath',
'setDastHeadPath',
'fetchSastReports',
'fetchDependencyScanningReports',
'fetchSastContainerReports',
'fetchDastReports',
'setVulnerabilityFeedbackPath',
'setVulnerabilityFeedbackHelpPath',
'setPipelineId',
......@@ -164,6 +222,32 @@ export default {
:popover-options="dependencyScanningPopover"
/>
<report-section
v-if="sastContainerHeadPath"
class="js-dependency-scanning-widget split-report-section"
:type="$options.sastContainer"
:status="checkReportStatus(sastContainer.isLoading, sastContainer.hasError)"
:loading-text="translateText('Container scanning').loading"
:error-text="translateText('Container scanning').error"
:success-text="sastContainerText"
:unresolved-issues="sastContainer.newIssues"
:has-issues="sastContainer.newIssues.length > 0"
:popover-options="sastContainerPopover"
/>
<report-section
v-if="dastHeadPath"
class="js-dast-widget split-report-section"
:type="$options.dast"
:status="checkReportStatus(dast.isLoading, dast.hasError)"
:loading-text="translateText('DAST').loading"
:error-text="translateText('DAST').error"
:success-text="dastText"
:unresolved-issues="dast.newIssues"
:has-issues="dast.newIssues.length > 0"
:popover-options="dastPopover"
/>
<issue-modal />
</div>
</template>
......@@ -170,7 +170,7 @@ export default {
state.dast.isLoading = false;
state.summaryCounts.added += newIssues.length;
state.summaryCounts.fixed += resolvedIssues.length;
} else if (reports.head && !reports.base) {
} else if (reports.head && reports.head.site && !reports.base) {
const newIssues = parseDastIssues(reports.head.site.alerts, reports.enrichData);
state.dast.newIssues = newIssues;
......
......@@ -40,6 +40,25 @@ module EE
path: Ci::Build::DEPENDENCY_SCANNING_FILE)
end
# sast_container_artifact_url is deprecated and replaced with container_scanning_artifact_url (#5778)
def sast_container_artifact_url(pipeline)
raw_project_build_artifacts_url(pipeline.project,
pipeline.sast_container_artifact,
path: Ci::Build::SAST_CONTAINER_FILE)
end
def container_scanning_artifact_url(pipeline)
raw_project_build_artifacts_url(pipeline.project,
pipeline.container_scanning_artifact,
path: Ci::Build::CONTAINER_SCANNING_FILE)
end
def dast_artifact_url(pipeline)
raw_project_build_artifacts_url(pipeline.project,
pipeline.dast_artifact,
path: Ci::Build::DAST_FILE)
end
def license_management_artifact_url(pipeline)
raw_project_build_artifacts_url(pipeline.project,
pipeline.license_management_artifact,
......
---
title: Render container scanning and dast reports in pipeline view
merge_request:
author:
type: added
......@@ -3,7 +3,7 @@ import store from 'ee/vue_shared/security_reports/store';
import state from 'ee/vue_shared/security_reports/store/state';
import reportSummary from 'ee/pipelines/components/security_reports/report_summary_widget.vue';
import { createComponentWithStore } from 'spec/helpers/vue_mount_component_helper';
import { sastIssues } from '../../vue_shared/security_reports/mock_data';
import { sastIssues, dast, dockerReport } from '../../vue_shared/security_reports/mock_data';
describe('Report summary widget', () => {
const Component = Vue.extend(reportSummary);
......@@ -30,9 +30,13 @@ describe('Report summary widget', () => {
beforeEach(() => {
vm.$store.dispatch('setSastHeadPath', 'head.json');
vm.$store.dispatch('setDependencyScanningHeadPath', 'head.json');
vm.$store.dispatch('setDastHeadPath', 'head.json');
vm.$store.dispatch('setSastContainerHeadPath', 'head.json');
vm.$store.dispatch('requestSastReports');
vm.$store.dispatch('requestDependencyScanningReports');
vm.$store.dispatch('requestDastReports');
vm.$store.dispatch('requestSastContainerReports');
});
it('renders loading icon and text for sast', done => {
......@@ -62,15 +66,47 @@ describe('Report summary widget', () => {
done();
});
});
it('renders loading icon and text for container scanning', done => {
vm.$nextTick(() => {
expect(
vm.$el
.querySelector('.js-sast-container-summary')
.textContent.trim()
.replace(/\s\s+/g, ' '),
).toEqual('Container scanning is loading');
expect(vm.$el.querySelector('.js-sast-container-summary .fa-spinner')).not.toBeNull();
done();
});
});
it('renders loading icon and text for dast', done => {
vm.$nextTick(() => {
expect(
vm.$el
.querySelector('.js-dast-summary')
.textContent.trim()
.replace(/\s\s+/g, ' '),
).toEqual('DAST is loading');
expect(vm.$el.querySelector('.js-dast-summary .fa-spinner')).not.toBeNull();
done();
});
});
});
describe('with error', () => {
beforeEach(() => {
vm.$store.dispatch('setSastHeadPath', 'head.json');
vm.$store.dispatch('setDependencyScanningHeadPath', 'head.json');
vm.$store.dispatch('setDastHeadPath', 'head.json');
vm.$store.dispatch('setSastContainerHeadPath', 'head.json');
vm.$store.dispatch('receiveSastError');
vm.$store.dispatch('receiveDependencyScanningError');
vm.$store.dispatch('receiveSastContainerError');
vm.$store.dispatch('receiveDastError');
});
it('renders warning icon and error text for sast', done => {
......@@ -102,12 +138,46 @@ describe('Report summary widget', () => {
.then(done)
.catch(done.fail);
});
it('renders warnin icon and error text for container scanning', done => {
vm.$nextTick()
.then(() => {
expect(
vm.$el
.querySelector('.js-sast-container-summary')
.textContent.trim()
.replace(/\s\s+/g, ' '),
).toEqual('Container scanning resulted in error while loading results');
expect(vm.$el.querySelector('.js-sast-container-summary .js-ci-status-icon-warning')).not.toBeNull();
})
.then(done)
.catch(done.fail);
});
it('renders warnin icon and error text for DAST', done => {
vm.$nextTick()
.then(() => {
expect(
vm.$el
.querySelector('.js-dast-summary')
.textContent.trim()
.replace(/\s\s+/g, ' '),
).toEqual('DAST resulted in error while loading results');
expect(vm.$el.querySelector('.js-dast-summary .js-ci-status-icon-warning')).not.toBeNull();
})
.then(done)
.catch(done.fail);
});
});
describe('with vulnerabilities', () => {
beforeEach(() => {
vm.$store.dispatch('setSastHeadPath', 'head.json');
vm.$store.dispatch('setDependencyScanningHeadPath', 'head.json');
vm.$store.dispatch('setDastHeadPath', 'head.json');
vm.$store.dispatch('setSastContainerHeadPath', 'head.json');
vm.$store.dispatch('receiveSastReports', {
head: sastIssues,
......@@ -115,6 +185,12 @@ describe('Report summary widget', () => {
vm.$store.dispatch('receiveDependencyScanningReports', {
head: sastIssues,
});
vm.$store.dispatch('receiveSastContainerReports', {
head: dockerReport,
});
vm.$store.dispatch('receiveDastReports', {
head: dast,
});
});
it('renders warning icon and vulnerabilities text for sast', done => {
......@@ -144,12 +220,42 @@ describe('Report summary widget', () => {
done();
});
});
it('renders warning icon and vulnerabilities text for container scanning', done => {
vm.$nextTick(() => {
expect(
vm.$el
.querySelector('.js-sast-container-summary')
.textContent.trim()
.replace(/\s\s+/g, ' '),
).toEqual('Container scanning detected 2 vulnerabilities');
expect(vm.$el.querySelector('.js-sast-container-summary .js-ci-status-icon-warning')).not.toBeNull();
done();
});
});
it('renders warning icon and vulnerabilities text for dast', done => {
vm.$nextTick(() => {
expect(
vm.$el
.querySelector('.js-dast-summary')
.textContent.trim()
.replace(/\s\s+/g, ' '),
).toEqual('DAST detected 2 vulnerabilities');
expect(vm.$el.querySelector('.js-dast-summary .js-ci-status-icon-warning')).not.toBeNull();
done();
});
});
});
describe('without vulnerabilities', () => {
beforeEach(() => {
vm.$store.dispatch('setSastHeadPath', 'head.json');
vm.$store.dispatch('setDependencyScanningHeadPath', 'head.json');
vm.$store.dispatch('setDastHeadPath', 'head.json');
vm.$store.dispatch('setSastContainerHeadPath', 'head.json');
vm.$store.dispatch('receiveSastReports', {
head: [],
......@@ -157,6 +263,12 @@ describe('Report summary widget', () => {
vm.$store.dispatch('receiveDependencyScanningReports', {
head: [],
});
vm.$store.dispatch('receiveSastContainerReports', {
head: [],
});
vm.$store.dispatch('receiveDastReports', {
head: [],
});
});
it('renders success icon and vulnerabilities text for sast', done => {
......@@ -186,5 +298,33 @@ describe('Report summary widget', () => {
done();
});
});
it('renders success icon and vulnerabilities text for container scanning', done => {
vm.$nextTick(() => {
expect(
vm.$el
.querySelector('.js-sast-container-summary')
.textContent.trim()
.replace(/\s\s+/g, ' '),
).toEqual('Container scanning detected no vulnerabilities');
expect(vm.$el.querySelector('.js-sast-container-summary .js-ci-status-icon-success')).not.toBeNull();
done();
});
});
it('renders success icon and vulnerabilities text for dast', done => {
vm.$nextTick(() => {
expect(
vm.$el
.querySelector('.js-dast-summary')
.textContent.trim()
.replace(/\s\s+/g, ' '),
).toEqual('DAST detected no vulnerabilities');
expect(vm.$el.querySelector('.js-dast-summary .js-ci-status-icon-success')).not.toBeNull();
done();
});
});
});
});
......@@ -5,7 +5,7 @@ import component from 'ee/vue_shared/security_reports/split_security_reports_app
import createStore from 'ee/vue_shared/security_reports/store';
import state from 'ee/vue_shared/security_reports/store/state';
import { mountComponentWithStore } from '../../helpers/vue_mount_component_helper';
import { sastIssues } from './mock_data';
import { sastIssues, dast, dockerReport } from './mock_data';
describe('Slipt security reports app', () => {
const Component = Vue.extend(component);
......@@ -34,6 +34,8 @@ describe('Slipt security reports app', () => {
beforeEach(() => {
mock.onGet('sast_head.json').reply(200, sastIssues);
mock.onGet('dss_head.json').reply(200, sastIssues);
mock.onGet('dast_head.json').reply(200, dast);
mock.onGet('sast_container_head.json').reply(200, dockerReport);
mock.onGet('vulnerability_feedback_path.json').reply(200, []);
vm = mountComponentWithStore(Component, {
......@@ -43,10 +45,14 @@ describe('Slipt security reports app', () => {
baseBlobPath: 'path',
sastHeadPath: 'sast_head.json',
dependencyScanningHeadPath: 'dss_head.json',
dastHeadPath: 'dast_head.json',
sastContainerHeadPath: 'sast_container_head.json',
sastHelpPath: 'path',
dependencyScanningHelpPath: 'path',
vulnerabilityFeedbackPath: 'vulnerability_feedback_path.json',
vulnerabilityFeedbackHelpPath: 'path',
dastHelpPath: 'path',
sastContainerHelpPath: 'path',
pipelineId: 123,
},
});
......@@ -57,6 +63,8 @@ describe('Slipt security reports app', () => {
expect(vm.$el.textContent).toContain('SAST is loading');
expect(vm.$el.textContent).toContain('Dependency scanning is loading');
expect(vm.$el.textContent).toContain('Container scanning is loading');
expect(vm.$el.textContent).toContain('DAST is loading');
setTimeout(() => {
done();
......@@ -68,6 +76,8 @@ describe('Slipt security reports app', () => {
beforeEach(() => {
mock.onGet('sast_head.json').reply(200, sastIssues);
mock.onGet('dss_head.json').reply(200, sastIssues);
mock.onGet('dast_head.json').reply(200, dast);
mock.onGet('sast_container_head.json').reply(200, dockerReport);
mock.onGet('vulnerability_feedback_path.json').reply(200, []);
vm = mountComponentWithStore(Component, {
......@@ -77,10 +87,14 @@ describe('Slipt security reports app', () => {
baseBlobPath: 'path',
sastHeadPath: 'sast_head.json',
dependencyScanningHeadPath: 'dss_head.json',
dastHeadPath: 'dast_head.json',
sastContainerHeadPath: 'sast_container_head.json',
sastHelpPath: 'path',
dependencyScanningHelpPath: 'path',
vulnerabilityFeedbackPath: 'vulnerability_feedback_path.json',
vulnerabilityFeedbackHelpPath: 'path',
dastHelpPath: 'path',
sastContainerHelpPath: 'path',
pipelineId: 123,
},
});
......@@ -95,6 +109,12 @@ describe('Slipt security reports app', () => {
expect(removeBreakLine(vm.$el.textContent)).toContain(
'Dependency scanning detected 3 vulnerabilities',
);
// Renders container scanning result
expect(vm.$el.textContent).toContain('Container scanning detected 2 vulnerabilities');
// Renders DAST result
expect(vm.$el.textContent).toContain('DAST detected 2 vulnerabilities');
done();
}, 0);
});
......@@ -104,6 +124,8 @@ describe('Slipt security reports app', () => {
beforeEach(() => {
mock.onGet('sast_head.json').reply(500);
mock.onGet('dss_head.json').reply(500);
mock.onGet('dast_head.json').reply(500);
mock.onGet('sast_container_head.json').reply(500);
mock.onGet('vulnerability_feedback_path.json').reply(500, []);
vm = mountComponentWithStore(Component, {
......@@ -113,10 +135,14 @@ describe('Slipt security reports app', () => {
baseBlobPath: 'path',
sastHeadPath: 'sast_head.json',
dependencyScanningHeadPath: 'dss_head.json',
dastHeadPath: 'dast_head.json',
sastContainerHeadPath: 'sast_container_head.json',
sastHelpPath: 'path',
dependencyScanningHelpPath: 'path',
vulnerabilityFeedbackPath: 'vulnerability_feedback_path.json',
vulnerabilityFeedbackHelpPath: 'path',
dastHelpPath: 'path',
sastContainerHelpPath: 'path',
pipelineId: 123,
},
});
......@@ -130,6 +156,10 @@ describe('Slipt security reports app', () => {
expect(removeBreakLine(vm.$el.textContent)).toContain(
'Dependency scanning resulted in error while loading results',
);
expect(vm.$el.textContent).toContain(
'Container scanning resulted in error while loading results',
);
expect(vm.$el.textContent).toContain('DAST resulted in error while loading results');
done();
}, 0);
});
......
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