Skip to content
Projects
Groups
Snippets
Help
Loading...
Help
Support
Keyboard shortcuts
?
Submit feedback
Contribute to GitLab
Sign in / Register
Toggle navigation
G
gitlab-ce
Project overview
Project overview
Details
Activity
Releases
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Issues
0
Issues
0
List
Boards
Labels
Milestones
Merge Requests
1
Merge Requests
1
Analytics
Analytics
Repository
Value Stream
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Create a new issue
Commits
Issue Boards
Open sidebar
nexedi
gitlab-ce
Commits
8b585bde
Commit
8b585bde
authored
Jan 05, 2018
by
Dmitriy Zaporozhets
Committed by
Tim Zallmann
Jan 05, 2018
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
Resolve "Show DAST results in the MR widget"
parent
752657df
Changes
29
Hide whitespace changes
Inline
Side-by-side
Showing
29 changed files
with
748 additions
and
9 deletions
+748
-9
app/assets/javascripts/lib/utils/text_utility.js
app/assets/javascripts/lib/utils/text_utility.js
+9
-0
app/assets/javascripts/vue_shared/components/expand_button.vue
...ssets/javascripts/vue_shared/components/expand_button.vue
+46
-0
app/assets/stylesheets/pages/merge_requests.scss
app/assets/stylesheets/pages/merge_requests.scss
+8
-0
changelogs/unreleased-ee/4348-show-dast-results-in-the-mr-widget.yml
...unreleased-ee/4348-show-dast-results-in-the-mr-widget.yml
+5
-0
doc/ci/examples/README.md
doc/ci/examples/README.md
+4
-0
doc/ci/examples/dast.md
doc/ci/examples/dast.md
+39
-0
doc/user/project/merge_requests/dast.md
doc/user/project/merge_requests/dast.md
+44
-0
doc/user/project/merge_requests/img/dast-all.png
doc/user/project/merge_requests/img/dast-all.png
+0
-0
doc/user/project/merge_requests/img/dast-single.png
doc/user/project/merge_requests/img/dast-single.png
+0
-0
ee/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_dast_modal.vue
..._merge_request_widget/components/mr_widget_dast_modal.vue
+108
-0
ee/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_report_collapsible_section.vue
...idget/components/mr_widget_report_collapsible_section.vue
+8
-0
ee/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_report_issues.vue
...rge_request_widget/components/mr_widget_report_issues.vue
+84
-7
ee/app/assets/javascripts/vue_merge_request_widget/mr_widget_options.js
...javascripts/vue_merge_request_widget/mr_widget_options.js
+52
-0
ee/app/assets/javascripts/vue_merge_request_widget/stores/mr_widget_store.js
...cripts/vue_merge_request_widget/stores/mr_widget_store.js
+22
-0
ee/app/models/ee/ci/build.rb
ee/app/models/ee/ci/build.rb
+12
-0
ee/app/models/ee/ci/pipeline.rb
ee/app/models/ee/ci/pipeline.rb
+4
-0
ee/app/models/ee/merge_request.rb
ee/app/models/ee/merge_request.rb
+5
-0
ee/app/models/license.rb
ee/app/models/license.rb
+1
-0
ee/app/serializers/ee/merge_request_widget_entity.rb
ee/app/serializers/ee/merge_request_widget_entity.rb
+14
-0
spec/ee/spec/models/ee/ci/build_spec.rb
spec/ee/spec/models/ee/ci/build_spec.rb
+2
-1
spec/ee/spec/models/ee/ci/pipeline_spec.rb
spec/ee/spec/models/ee/ci/pipeline_spec.rb
+2
-1
spec/ee/spec/models/ee/merge_request_spec.rb
spec/ee/spec/models/ee/merge_request_spec.rb
+14
-0
spec/javascripts/lib/utils/text_utility_spec.js
spec/javascripts/lib/utils/text_utility_spec.js
+10
-0
spec/javascripts/vue_mr_widget/components/mr_widget_dast_modal_spec.js
...pts/vue_mr_widget/components/mr_widget_dast_modal_spec.js
+45
-0
spec/javascripts/vue_mr_widget/components/mr_widget_report_issues_spec.js
.../vue_mr_widget/components/mr_widget_report_issues_spec.js
+19
-0
spec/javascripts/vue_mr_widget/ee_mr_widget_options_spec.js
spec/javascripts/vue_mr_widget/ee_mr_widget_options_spec.js
+80
-0
spec/javascripts/vue_mr_widget/mock_data.js
spec/javascripts/vue_mr_widget/mock_data.js
+60
-0
spec/javascripts/vue_mr_widget/stores/mr_widget_store_spec.js
.../javascripts/vue_mr_widget/stores/mr_widget_store_spec.js
+19
-0
spec/javascripts/vue_shared/components/expand_button_spec.js
spec/javascripts/vue_shared/components/expand_button_spec.js
+32
-0
No files found.
app/assets/javascripts/lib/utils/text_utility.js
View file @
8b585bde
...
...
@@ -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
);
app/assets/javascripts/vue_shared/components/expand_button.vue
0 → 100644
View file @
8b585bde
<
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
>
app/assets/stylesheets/pages/merge_requests.scss
View file @
8b585bde
...
...
@@ -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
;
}
}
}
}
changelogs/unreleased-ee/4348-show-dast-results-in-the-mr-widget.yml
0 → 100644
View file @
8b585bde
---
title
:
Show results from DAST scan in the merge request widget
merge_request
:
3885
author
:
type
:
added
doc/ci/examples/README.md
View file @
8b585bde
...
...
@@ -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
)
...
...
doc/ci/examples/dast.md
0 → 100644
View file @
8b585bde
# 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/
doc/user/project/merge_requests/dast.md
0 → 100644
View file @
8b585bde
# 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
doc/user/project/merge_requests/img/dast-all.png
0 → 100644
View file @
8b585bde
96 KB
doc/user/project/merge_requests/img/dast-single.png
0 → 100644
View file @
8b585bde
293 KB
ee/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_dast_modal.vue
0 → 100644
View file @
8b585bde
<
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"
>
×
</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
>
ee/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_report_collapsible_section.vue
View file @
8b585bde
...
...
@@ -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
...
...
ee/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_report_issues.vue
View file @
8b585bde
<
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>
ee/app/assets/javascripts/vue_merge_request_widget/mr_widget_options.js
View file @
8b585bde
...
...
@@ -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
...
...
ee/app/assets/javascripts/vue_merge_request_widget/stores/mr_widget_store.js
View file @
8b585bde
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
=>
({
...
...
ee/app/models/ee/ci/build.rb
View file @
8b585bde
...
...
@@ -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
)
...
...
ee/app/models/ee/ci/pipeline.rb
View file @
8b585bde
...
...
@@ -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
ee/app/models/ee/merge_request.rb
View file @
8b585bde
...
...
@@ -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
ee/app/models/license.rb
View file @
8b585bde
...
...
@@ -56,6 +56,7 @@ class License < ActiveRecord::Base
EEU_FEATURES
=
EEP_FEATURES
+
%i[
sast
sast_container
dast
epics
]
.
freeze
...
...
ee/app/serializers/ee/merge_request_widget_entity.rb
View file @
8b585bde
...
...
@@ -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
spec/ee/spec/models/ee/ci/build_spec.rb
View file @
8b585bde
...
...
@@ -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
|
...
...
spec/ee/spec/models/ee/ci/pipeline_spec.rb
View file @
8b585bde
...
...
@@ -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
|
...
...
spec/ee/spec/models/ee/merge_request_spec.rb
View file @
8b585bde
...
...
@@ -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
spec/javascripts/lib/utils/text_utility_spec.js
View file @
8b585bde
...
...
@@ -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 .
'
);
});
});
});
spec/javascripts/vue_mr_widget/components/mr_widget_dast_modal_spec.js
0 → 100644
View file @
8b585bde
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
'
);
});
});
spec/javascripts/vue_mr_widget/components/mr_widget_report_issues_spec.js
View file @
8b585bde
...
...
@@ -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
);
});
});
});
spec/javascripts/vue_mr_widget/ee_mr_widget_options_spec.js
View file @
8b585bde
...
...
@@ -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
'
,
()
=>
{
...
...
spec/javascripts/vue_mr_widget/mock_data.js
View file @
8b585bde
...
...
@@ -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
spec/javascripts/vue_mr_widget/stores/mr_widget_store_spec.js
View file @
8b585bde
...
...
@@ -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
);
});
});
});
spec/javascripts/vue_shared/components/expand_button_spec.js
0 → 100644
View file @
8b585bde
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
();
});
});
});
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment