Commit c8c88126 authored by Dmitriy Zaporozhets's avatar Dmitriy Zaporozhets

Merge branch '5105_split_dependency_scanning_from_sast' into 'master'

Add Dependency Scanning feature and expose its artifacts in Merge Request

See merge request gitlab-org/gitlab-ee!5051
parents 1c21d6d1 408ab6bf
......@@ -8,7 +8,7 @@ import pipelineHeader from './components/header_component.vue';
import eventHub from './event_hub';
import SecurityReportApp from 'ee/pipelines/components/security_reports/security_report_app.vue'; // eslint-disable-line import/first
import SastSummaryWidget from 'ee/pipelines/components/security_reports/sast_report_summary_widget.vue'; // eslint-disable-line import/first
import SastSummaryWidget from 'ee/pipelines/components/security_reports/report_summary_widget.vue'; // eslint-disable-line import/first
Vue.use(Translate);
......@@ -81,24 +81,51 @@ export default () => {
const securityTab = document.getElementById('js-security-report-app');
const sastSummary = document.querySelector('.js-sast-summary');
const updateBadgeCount = (count) => {
const badge = document.querySelector('.js-sast-counter');
if (badge.textContent !== '') {
badge.textContent = parseInt(badge.textContent, 10) + count;
} else {
badge.textContent = count;
}
badge.classList.remove('hidden');
};
// They are being rendered under the same condition
if (securityTab && sastSummary) {
const datasetOptions = securityTab.dataset;
const endpoint = datasetOptions.endpoint;
const blobPath = datasetOptions.blobPath;
const dependencyScanningEndpoint = datasetOptions.dependencyScanningEndpoint;
mediator.fetchSastReport(endpoint, blobPath)
if (endpoint) {
mediator.fetchSastReport(endpoint, blobPath)
.then(() => {
// update the badge
if (mediator.store.state.securityReports.sast.newIssues.length) {
const badge = document.querySelector('.js-sast-counter');
badge.textContent = mediator.store.state.securityReports.sast.newIssues.length;
badge.classList.remove('hidden');
updateBadgeCount(mediator.store.state.securityReports.sast.newIssues.length);
}
})
.catch(() => {
Flash(__('Something went wrong while fetching SAST.'));
});
}
if (dependencyScanningEndpoint) {
mediator.fetchDependencyScanningReport(dependencyScanningEndpoint)
.then(() => {
// update the badge
if (mediator.store.state.securityReports.dependencyScanning.newIssues.length) {
updateBadgeCount(
mediator.store.state.securityReports.dependencyScanning.newIssues.length,
);
}
})
.catch(() => {
Flash(__('Something went wrong while fetching Dependency Scanning.'));
});
}
// Widget summary
// eslint-disable-next-line no-new
......@@ -115,7 +142,11 @@ export default () => {
render(createElement) {
return createElement('sast-summary-widget', {
props: {
unresolvedIssues: this.mediator.store.state.securityReports.sast.newIssues,
hasDependencyScanning: dependencyScanningEndpoint !== undefined,
hasSast: endpoint !== undefined,
sastIssues: this.mediator.store.state.securityReports.sast.newIssues.length,
dependencyScanningIssues:
this.mediator.store.state.securityReports.dependencyScanning.newIssues.length,
},
});
},
......@@ -137,6 +168,8 @@ export default () => {
return createElement('security-report-app', {
props: {
securityReports: this.mediator.store.state.securityReports,
hasDependencyScanning: dependencyScanningEndpoint !== undefined,
hasSast: endpoint !== undefined,
},
});
},
......
......@@ -67,4 +67,12 @@ export default class pipelinesMediator {
this.store.storeSastReport(data, blobPath);
});
}
fetchDependencyScanningReport(endpoint, blobPath) {
return PipelineService.getSecurityReport(endpoint)
.then(response => response.json())
.then((data) => {
this.store.storeDependencyScanningReport(data, blobPath);
});
}
}
......@@ -26,4 +26,11 @@ export default class PipelineStore {
setSastReport({ head: data, headBlobPath: blobPath }),
);
}
storeDependencyScanningReport(data, blobPath) {
Object.assign(
this.state.securityReports.dependencyScanning,
setSastReport({ head: data, headBlobPath: blobPath }),
);
}
}
#js-pipeline-header-vue.pipeline-header-container
- sast_artifact = @pipeline.sast_artifact
- dependecy_artifact = @pipeline.dependency_scanning_artifact
- if @commit.present?
.commit-box
......@@ -35,5 +36,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
- if sast_artifact || dependecy_artifact
.js-sast-summary
- failed_builds = @pipeline.statuses.latest.failed
- expose_sast_data = @pipeline.expose_sast_data?
- expose_dependency_data = @pipeline.expose_dependency_scanning_data?
- blob_path = project_blob_path(@project, @pipeline.sha)
.tabs-holder
......@@ -16,7 +17,7 @@
= link_to failures_project_pipeline_path(@project, @pipeline), data: { target: '#js-tab-failures', action: 'failures', toggle: 'tab' }, class: 'failures-tab' do
= _("Failed Jobs")
%span.badge.js-failures-counter= failed_builds.count
- if expose_sast_data
- if expose_sast_data || expose_dependency_data
%li.js-security-tab-link
= link_to security_project_pipeline_path(@project, @pipeline), data: { target: '#js-tab-security', action: 'security', toggle: 'tab' }, class: 'security-tab' do
= _("Security report")
......@@ -60,6 +61,8 @@
%span.build-name
= link_to build.name, pipeline_job_url(pipeline, build)
%pre.build-log= build_summary(build, skip: index >= 10)
- if expose_sast_data
- if expose_sast_data || expose_dependency_data
#js-tab-security.build-security.tab-pane
#js-security-report-app{ data: { endpoint: sast_artifact_url(@pipeline), blob_path: blob_path } }
#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} }
......@@ -49,6 +49,10 @@ There's also a collection of repositories with [example projects](https://gitlab
**(Ultimate)** [Scan your code for vulnerabilities](sast.md)
## Dependency Scanning
**(Ultimate)** [Scan your dependencies for vulnerabilities](dependency_scanning.md)
## Container Scanning
[Scan your Docker images for vulnerabilities](container_scanning.md)
......
# Dependency Scanning with GitLab CI/CD
NOTE: **Note:**
In order to use this tool, a [GitLab Ultimate][ee] license
is needed.
This example shows how to run Dependency Scanning on your
project's dependencies by using GitLab CI/CD.
First, you need GitLab Runner with [docker-in-docker executor](https://docs.gitlab.com/ee/ci/docker/using_docker_build.html#use-docker-in-docker-executor).
You can then add a new job to `.gitlab-ci.yml`, called `dependency_scanning`:
```yaml
dependency_scanning:
image: docker:latest
variables:
DOCKER_DRIVER: overlay2
allow_failure: true
services:
- docker:dind
script:
- export SP_VERSION=$(echo "$CI_SERVER_VERSION" | sed 's/^\([0-9]*\)\.\([0-9]*\).*/\1-\2-stable/')
- docker run
--env DEP_SCAN_DISABLE_REMOTE_CHECKS="${DEP_SCAN_DISABLE_REMOTE_CHECKS:-false}" \
--volume "$PWD:/code" \
--volume /var/run/docker.sock:/var/run/docker.sock \
"registry.gitlab.com/gitlab-org/security-products/dependency-scanning:$SP_VERSION" /code
artifacts:
paths: [gl-dependency-scanning-report.json]
```
The above example will create a `dependency_scanning` job in the `test` stage and will create the required report artifact. Check the
[Auto-DevOps template](https://gitlab.com/gitlab-org/gitlab-ci-yml/blob/master/Auto-DevOps.gitlab-ci.yml)
for a full reference.
The results are sorted by the priority of the vulnerability:
1. High
1. Medium
1. Low
1. Unknown
1. Everything else
Behind the scenes, the [GitLab Dependency Scanning Docker image](https://gitlab.com/gitlab-org/security-products/dependency-scanning)
is used to detect the languages/package managers and in turn runs the matching scan tools.
Some security scanners require to send a list of project dependencies to GitLab
central servers to check for vulnerabilities. To learn more about this or to
disable it, check the [GitLab Dependency Scanning documentation](https://gitlab.com/gitlab-org/security-products/dependency-scanning#remote-checks).
TIP: **Tip:**
Starting with [GitLab Ultimate][ee] 10.7, this information will
be automatically extracted and shown right in the merge request widget. To do
so, the CI job must be named `dependency_scanning` and the artifact path must be
`gl-dependency-scanning-report.json`. Make sure your pipeline has a stage nammed `test`,
or specify another existing stage inside the `dependency_scanning` job.
[Learn more on dependency scanning results shown in merge requests](../../user/project/merge_requests/dependency_scanning.md).
## Supported languages and package managers
See [the full list of supported languages and package managers](../../user/project/merge_requests/dependency_scanning.md#supported-languages-and-frameworks).
[ee]: https://about.gitlab.com/products/
......@@ -20,13 +20,12 @@ sast:
services:
- docker:dind
script:
- export SAST_VERSION=$(echo "$CI_SERVER_VERSION" | sed 's/^\([0-9]*\)\.\([0-9]*\).*/\1-\2-stable/')
- docker run
--env SAST_CONFIDENCE_LEVEL="${SAST_CONFIDENCE_LEVEL:-3}"
--env SAST_DISABLE_REMOTE_CHECKS="${SAST_DISABLE_REMOTE_CHECKS:-false}"
--volume "$PWD:/code"
--volume /var/run/docker.sock:/var/run/docker.sock
"registry.gitlab.com/gitlab-org/security-products/sast:$SAST_VERSION" /app/bin/run /code
- export SP_VERSION=$(echo "$CI_SERVER_VERSION" | sed 's/^\([0-9]*\)\.\([0-9]*\).*/\1-\2-stable/')
- docker run
--env SAST_CONFIDENCE_LEVEL="${SAST_CONFIDENCE_LEVEL:-3}"
--volume "$PWD:/code"
--volume /var/run/docker.sock:/var/run/docker.sock
"registry.gitlab.com/gitlab-org/security-products/sast:$SP_VERSION" /app/bin/run /code
artifacts:
paths: [gl-sast-report.json]
```
......
......@@ -20,6 +20,7 @@ project in an easy and automatic way:
1. [Auto Test](#auto-test)
1. [Auto Code Quality](#auto-code-quality)
1. [Auto SAST (Static Application Security Testing)](#auto-sast)
1. [Auto Dependency Scanning](#auto-dependency-scanning)
1. [Auto Container Scanning](#auto-container-scanning)
1. [Auto Review Apps](#auto-review-apps)
1. [Auto DAST (Dynamic Application Security Testing)](#auto-dast)
......@@ -217,6 +218,19 @@ check out.
In GitLab Ultimate, any security warnings are also
[shown in the merge request widget](../../user/project/merge_requests/sast.md).
### Auto Dependency Scanning
> Introduced in [GitLab Ultimate][ee] 10.7.
Dependency Scanning uses the
[Dependency Scanning Docker image](https://gitlab.com/gitlab-org/security-products/dependency-scanning)
to run analysis on the project dependencies and checks for potential security issues. Once the
report is created, it's uploaded as an artifact which you can later download and
check out.
In GitLab Ultimate, any security warnings are also
[shown in the merge request widget](../../user/project/merge_requests/dependency_scanning.md).
### Auto Container Scanning
> Introduced in GitLab 10.4.
......@@ -454,7 +468,7 @@ also be customized, and you can easily use a [custom buildpack](#custom-buildpac
| `POSTGRES_DB` | The PostgreSQL database name; defaults to the value of [`$CI_ENVIRONMENT_SLUG`](../../ci/variables/README.md#predefined-variables-environment-variables). Set it to use a custom database name. |
| `BUILDPACK_URL` | The buildpack's full URL. It can point to either Git repositories or a tarball URL. For Git repositories, it is possible to point to a specific `ref`, for example `https://github.com/heroku/heroku-buildpack-ruby.git#v142`|
| `SAST_CONFIDENCE_LEVEL` | The minimum confidence level of security issues you want to be reported; `1` for Low, `2` for Medium, `3` for High; defaults to `3`.|
| `SAST_DISABLE_REMOTE_CHECKS` | Whether remote SAST checks are disabled; defaults to `"false"`. Set to `"true"` to disable SAST checks that send data to GitLab central servers. [Read more about remote checks](https://gitlab.com/gitlab-org/security-products/sast#remote-checks).|
| `DEP_SCAN_DISABLE_REMOTE_CHECKS` | Whether remote Dependency Scanning checks are disabled; defaults to `"false"`. Set to `"true"` to disable checks that send data to GitLab central servers. [Read more about remote checks](https://gitlab.com/gitlab-org/security-products/dependency-scanning#remote-checks).|
TIP: **Tip:**
Set up the replica variables using a
......
# Dependency Scanning
> [Introduced][ee-5105] in [GitLab Ultimate][ee] 10.7.
## Overview
If you are using [GitLab CI/CD][ci], you can analyze your dependencies for known
vulnerabilities using Dependency Scanning, either by
including the CI job in your [existing `.gitlab-ci.yml` file][cc-docs] or
by implicitly using [Auto Dependency Scanning](../../../topics/autodevops/index.md#auto-dependency-scanning)
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.
## Use cases
It helps you automatically find security vulnerabilities in your dependencies
while you are developing and testing your applications. E.g. your application
is using an external (open source) library which is known to be vulnerable.
## Supported languages and dependency managers
The following languages and dependency managers are supported.
| Language (package managers) | Scan tool |
|-----------------------------------------------------------------------------|-----------------------------------------------------------------------------------------------------------------------------------|
| JavaScript ([npm](https://www.npmjs.com/), [yarn](https://yarnpkg.com/en/)) | [gemnasium](https://gitlab.com/gitlab-org/security-products/gemnasium/general), [Retire.js](https://retirejs.github.io/retire.js) |
| Python ([pip](https://pip.pypa.io/en/stable/)) | [gemnasium](https://gitlab.com/gitlab-org/security-products/gemnasium/general) |
| Ruby ([gem](https://rubygems.org/)) | [gemnasium](https://gitlab.com/gitlab-org/security-products/gemnasium/general), [bundler-audit](https://github.com/rubysec/bundler-audit) |
| Java ([Maven](https://maven.apache.org/)) | [gemnasium](https://gitlab.com/gitlab-org/security-products/gemnasium/general) |
| PHP ([Composer](https://getcomposer.org/)) | [gemnasium](https://gitlab.com/gitlab-org/security-products/gemnasium/general) |
Some scanners require to send a list of project dependencies to GitLab central servers to check for vulnerabilities. To learn more about this or to disable it please
check [GitLab Dependency Scanning documentation](https://gitlab.com/gitlab-org/security-products/dependency-scanning#remote-checks).
## How it works
First of all, you need to define a job named `dependency_scanning` in your
`.gitlab-ci.yml` file. [Check how the `dependency_scanning` job should look like][cc-docs].
In order for the report to show in the merge request, there are two
prerequisites:
- the specified job **must** be named `dependency_scanning`
- the resulting report **must** be named `gl-dependency-scanning-report.json`
and uploaded as an artifact
The `dependency_scanning` job will perform an analysis on the application
dependencies, the resulting JSON file will be uploaded as an artifact, and
GitLab will then check this file and show the information inside the merge
request.
![Dependency Scanning Widget](img/dependency_scanning.png)
[ee-4682]: https://gitlab.com/gitlab-org/gitlab-ee/issues/4682
[ee-5105]: https://gitlab.com/gitlab-org/gitlab-ee/issues/5105
[ee]: https://about.gitlab.com/products/
[ci]: ../../../ci/README.md
[cc-docs]: ../../../ci/examples/dependency_scanning.md
......@@ -35,7 +35,11 @@ With **[GitLab Enterprise Edition][ee]**, you can also:
- View the deployment process across projects with [Multi-Project Pipeline Graphs](https://docs.gitlab.com/ee/ci/multi_project_pipeline_graphs.html#multi-project-pipeline-graphs) (available only in GitLab Premium)
- Request [approvals](https://docs.gitlab.com/ee/user/project/merge_requests/merge_request_approvals.html) from your managers (available in GitLab Starter)
- [Squash and merge](https://docs.gitlab.com/ee/user/project/merge_requests/squash_and_merge.html) for a cleaner commit history (available in GitLab Starter)
- Analyze the impact of your changes with [Code Quality reports](https://docs.gitlab.com/ee/user/project/merge_requests/code_quality_diff.html) (available in GitLab Starter)
- Analyze the impact of your changes with [Code Quality](#code-quality) (available in GitLab Starter)
- Analyze your source code for vulnerabilities with [Static Application Security Testing](#static-application-security-testing) (available in GitLab Ultimate)
- Analyze your dependencies for vulnerabilities with [Dependency Scanning](#dependency-scanning) (available in GitLab Ultimate)
- Analyze your Docker images for vulnerabilities with [Container Scanning](#container-scanning) (available in GitLab Ultimate)
- Analyze your running web applications for vulnerabilities with [Dynamic Application Security Testing](#dynamic-application-security-testing) (available in GitLab Ultimate)
- Determine the performance impact of changes with [Browser Performance Testing](#browser-performance-testing) (available in GitLab Premium)
## Use cases
......@@ -44,7 +48,7 @@ A. Consider you are a software developer working in a team:
1. You checkout a new branch, and submit your changes through a merge request
1. You gather feedback from your team
1. You work on the implementation optimizing code with [Code Quality reports](#code-quality-reports)
1. You work on the implementation optimizing code with [Code Quality](#code-quality)
1. You build and test your changes with GitLab CI/CD
1. You request the [approval](#merge-request-approvals) from your manager
1. Your manager pushes a commit with his final review, [approves the merge request](#merge-request-approvals), and set it to [merge when pipeline succeeds](#merge-when-pipeline-succeeds)
......@@ -207,7 +211,7 @@ list of approvers that will need to approve every merge request in a project.
[Read more about merge request approvals.](merge_request_approvals.md)
## Code Quality reports
## Code Quality
> Introduced in [GitLab Starter][products] 9.3.
......@@ -228,6 +232,17 @@ merge request widget area.
[Read more about Static Application Security Testing reports.](sast.md)
## Dependency Scanning
> Introduced in [GitLab Ultimate][products] 10.7.
If you are using [GitLab CI/CD][ci], you can analyze your dependencies for known
vulnerabilities using Dependency Scanning.
Going a step further, GitLab can show the vulnerability report right in the
merge request widget area.
[Read more about Dependency Scanning reports.](dependency_scanning.md)
## Container Scanning
> Introduced in [GitLab Ultimate][products] 10.4.
......
......@@ -25,17 +25,12 @@ request widget area.
The following languages and frameworks are supported.
| Language (package managers) / framework | Scan tool |
| ---------------------- | --------- |
| JavaScript ([npm](https://www.npmjs.com/), [yarn](https://yarnpkg.com/en/)) | [gemnasium](https://gitlab.com/gitlab-org/security-products/gemnasium), [Retire.js](https://retirejs.github.io/retire.js)
| Python ([pip](https://pip.pypa.io/en/stable/)) | [gemnasium](https://gitlab.com/gitlab-org/security-products/gemnasium), [bandit](https://github.com/openstack/bandit) |
| Ruby ([gem](https://rubygems.org/)) | [gemnasium](https://gitlab.com/gitlab-org/security-products/gemnasium), [bundler-audit](https://github.com/rubysec/bundler-audit) |
| Ruby on Rails | [brakeman](https://brakemanscanner.org) |
| Java ([Maven](https://maven.apache.org/)) | [gemnasium](https://gitlab.com/gitlab-org/security-products/gemnasium), [find-sec-bugs](https://find-sec-bugs.github.io/) |
| PHP ([Composer](https://getcomposer.org/)) | [gemnasium](https://gitlab.com/gitlab-org/security-products/gemnasium) |
Some security scanners require to send a list of project dependencies to GitLab central servers to check for vulnerabilities. To learn more about this or to disable it please
check [GitLab SAST documentation](https://gitlab.com/gitlab-org/security-products/sast#remote-checks).
| Language / framework | Scan tool |
|----------------------|----------------------------------------------------|
| C/C++ | [Flawfinder](https://www.dwheeler.com/flawfinder/) |
| Python | [bandit](https://github.com/openstack/bandit) |
| Ruby on Rails | [brakeman](https://brakemanscanner.org) |
| Java | [find-sec-bugs](https://find-sec-bugs.github.io/) |
## How it works
......@@ -53,7 +48,7 @@ The `sast` job will perform an analysis on the running web application, the
resulting JSON file will be uploaded as an artifact, and GitLab will then check
this file and show the information inside the merge request.
![SAST Widget](img/gemnasium.png)
![SAST Widget](img/sast.png)
## Security report under pipelines
......
<script>
import $ from 'jquery';
import { n__, s__ } from '~/locale';
import CiIcon from '~/vue_shared/components/ci_icon.vue';
export default {
name: 'SummaryReport',
components: {
CiIcon,
},
props: {
sastIssues: {
type: Number,
required: false,
default: 0,
},
dependencyScanningIssues: {
type: Number,
required: false,
default: 0,
},
hasDependencyScanning: {
type: Boolean,
required: false,
default: false,
},
hasSast: {
type: Boolean,
required: false,
default: false,
},
},
computed: {
sastLink() {
return this.link(this.sastIssues);
},
dependencyScanningLink() {
return this.link(this.dependencyScanningIssues);
},
sastIcon() {
return this.statusIcon(this.sastIssues);
},
dependencyScanningIcon() {
return this.statusIcon(this.dependencyScanningIssues);
},
},
methods: {
openTab() {
// This opens a tab outside of this Vue application
// It opens the securty report tab in the pipelines page and updates the URL
// This is needed because the tabs are built in haml+jquery
$('.pipelines-tabs a[data-action="security"]').tab('show');
},
link(issues) {
if (issues > 0) {
return n__(
'%d vulnerability',
'%d vulnerabilities',
issues,
);
}
return s__('ciReport|no vulnerabilities');
},
statusIcon(issues) {
if (issues > 0) {
return {
group: 'warning',
icon: 'status_warning',
};
}
return {
group: 'success',
icon: 'status_success',
};
},
},
};
</script>
<template>
<div>
<div
class="well-segment flex js-sast-summary"
v-if="hasSast"
>
<ci-icon
:status="sastIcon"
class="flex flex-align-self-center"
/>
<span
class="prepend-left-10 flex flex-align-self-center"
>
{{ s__('ciReport|SAST detected') }}
<button
type="button"
class="btn-link btn-blank prepend-left-5"
@click="openTab"
>
{{ sastLink }}
</button>
</span>
</div>
<div
class="well-segment flex js-dss-summary"
v-if="hasDependencyScanning"
>
<ci-icon
:status="dependencyScanningIcon"
class="flex flex-align-self-center"
/>
<span
class="prepend-left-10 flex flex-align-self-center"
>
{{ s__('ciReport|Dependency scanning detected') }}
<button
type="button"
class="btn-link btn-blank prepend-left-5"
@click="openTab"
>
{{ dependencyScanningLink }}
</button>
</span>
</div>
</div>
</template>
<script>
import $ from 'jquery';
import { n__, s__ } from '~/locale';
import ciIcon from '~/vue_shared/components/ci_icon.vue';
export default {
name: 'SastSummaryReport',
components: {
ciIcon,
},
props: {
unresolvedIssues: {
type: Array,
required: false,
default: () => ([]),
},
},
computed: {
sastText() {
if (this.unresolvedIssues.length) {
return s__('ciReport|SAST degraded on');
}
return s__('ciReport|SAST detected');
},
sastLink() {
if (this.unresolvedIssues.length) {
return n__(
'%d security vulnerability',
'%d security vulnerabilities',
this.unresolvedIssues.length,
);
}
return s__('ciReport|no security vulnerabilities');
},
statusIcon() {
if (this.unresolvedIssues.length) {
return {
group: 'warning',
icon: 'status_warning',
};
}
return {
group: 'success',
icon: 'status_success',
};
},
},
methods: {
openTab() {
// This opens a tab outside of this Vue application
// It opens the securty report tab in the pipelines page and updates the URL
// This is needed because the tabs are built in haml+jquery
$('.pipelines-tabs a[data-action="security"]').tab('show');
},
},
};
</script>
<template>
<div class="well-segment flex">
<ci-icon
:status="statusIcon"
class="flex flex-align-self-center"
/>
<span
class="prepend-left-10 flex flex-align-self-center"
>
{{ sastText }}
<button
type="button"
class="btn-link btn-blank prepend-left-5"
@click="openTab"
>
{{ sastLink }}
</button>
</span>
</div>
</template>
......@@ -17,12 +17,23 @@
type: Object,
required: true,
},
hasDependencyScanning: {
type: Boolean,
required: false,
default: false,
},
hasSast: {
type: Boolean,
required: false,
default: false,
},
},
};
</script>
<template>
<div class="pipeline-tab-content">
<report-section
v-if="hasSast"
class="js-sast-widget"
:type="$options.sast"
:status="checkReportStatus(securityReports.sast.isLoading, securityReports.sast.hasError)"
......@@ -32,7 +43,26 @@
:unresolved-issues="securityReports.sast.newIssues"
:resolved-issues="securityReports.sast.resolvedIssues"
:all-issues="securityReports.sast.allIssues"
:is-collapsible="false"
/>
<report-section
v-if="hasDependencyScanning"
class="js-dependency-scanning-widget"
:class="{ 'prepend-top-20': hasSast }"
:type="$options.sast"
:status="checkReportStatus(
securityReports.dependencyScanning.isLoading,
securityReports.dependencyScanning.hasError
)"
:loading-text="translateText('dependency scanning').loading"
:error-text="translateText('dependency scanning').error"
:success-text="depedencyScanningText(
securityReports.dependencyScanning.newIssues,
securityReports.dependencyScanning.resolvedIssues
)"
:unresolved-issues="securityReports.dependencyScanning.newIssues"
:resolved-issues="securityReports.dependencyScanning.resolvedIssues"
:all-issues="securityReports.dependencyScanning.allIssues"
/>
</div>
</template>
......@@ -17,9 +17,7 @@ export default {
'mr-widget-geo-secondary-node': GeoSecondaryNode,
ReportSection,
},
mixins: [
securityMixin,
],
mixins: [securityMixin],
dast: DAST,
sast: SAST,
sastContainer: SAST_CONTAINER,
......@@ -30,11 +28,13 @@ export default {
isLoadingSecurity: false,
isLoadingDocker: false,
isLoadingDast: false,
isLoadingDependencyScanning: false,
loadingCodequalityFailed: false,
loadingPerformanceFailed: false,
loadingSecurityFailed: false,
loadingDockerFailed: false,
loadingDastFailed: false,
loadingDependencyScanningFailed: false,
};
},
computed: {
......@@ -53,10 +53,13 @@ export default {
return this.mr.sast && this.mr.sast.head_path;
},
shouldRenderDockerReport() {
return this.mr.sastContainer;
return this.mr.sastContainer && this.mr.sastContainer.head_path;
},
shouldRenderDastReport() {
return this.mr.dast;
return this.mr.dast && this.mr.dast.head_path;
},
shouldRenderDependencyReport() {
return this.mr.dependencyScanning && this.mr.dependencyScanning.head_path;
},
codequalityText() {
const { newIssues, resolvedIssues } = this.mr.codeclimateMetrics;
......@@ -68,11 +71,13 @@ export default {
text.push(s__('ciReport|Code quality'));
if (resolvedIssues.length) {
text.push(n__(
' improved on %d point',
' improved on %d points',
resolvedIssues.length,
));
text.push(
n__(
' improved on %d point',
' improved on %d points',
resolvedIssues.length,
),
);
}
if (newIssues.length > 0 && resolvedIssues.length > 0) {
......@@ -80,11 +85,13 @@ export default {
}
if (newIssues.length) {
text.push(n__(
' degraded on %d point',
' degraded on %d points',
newIssues.length,
));
text.push(
n__(
' degraded on %d point',
' degraded on %d points',
newIssues.length,
),
);
}
}
......@@ -101,11 +108,13 @@ export default {
text.push(s__('ciReport|Performance metrics'));
if (improved.length) {
text.push(n__(
' improved on %d point',
' improved on %d points',
improved.length,
));
text.push(
n__(
' improved on %d point',
' improved on %d points',
improved.length,
),
);
}
if (improved.length > 0 && degraded.length > 0) {
......@@ -113,11 +122,13 @@ export default {
}
if (degraded.length) {
text.push(n__(
' degraded on %d point',
' degraded on %d points',
degraded.length,
));
text.push(
n__(
' degraded on %d point',
' degraded on %d points',
degraded.length,
),
);
}
}
......@@ -129,6 +140,11 @@ export default {
return this.sastText(newIssues, resolvedIssues, allIssues);
},
dependencyScanningText() {
const { newIssues, resolvedIssues, allIssues } = this.mr.dependencyScanningReport;
return this.depedencyScanningText(newIssues, resolvedIssues, allIssues);
},
dockerText() {
const { vulnerabilities, approved, unapproved } = this.mr.dockerReport;
return this.sastContainerText(vulnerabilities, approved, unapproved);
......@@ -139,24 +155,43 @@ export default {
},
codequalityStatus() {
return this.checkReportStatus(this.isLoadingCodequality, this.loadingCodequalityFailed);
return this.checkReportStatus(
this.isLoadingCodequality,
this.loadingCodequalityFailed,
);
},
performanceStatus() {
return this.checkReportStatus(this.isLoadingPerformance, this.loadingPerformanceFailed);
return this.checkReportStatus(
this.isLoadingPerformance,
this.loadingPerformanceFailed,
);
},
securityStatus() {
return this.checkReportStatus(this.isLoadingSecurity, this.loadingSecurityFailed);
return this.checkReportStatus(
this.isLoadingSecurity,
this.loadingSecurityFailed,
);
},
dockerStatus() {
return this.checkReportStatus(this.isLoadingDocker, this.loadingDockerFailed);
return this.checkReportStatus(
this.isLoadingDocker,
this.loadingDockerFailed,
);
},
dastStatus() {
return this.checkReportStatus(this.isLoadingDast, this.loadingDastFailed);
},
dependencyScanningStatus() {
return this.checkReportStatus(
this.isLoadingDependencyScanning,
this.loadingDependencyScanningFailed,
);
},
},
methods: {
fetchCodeQuality() {
......@@ -168,7 +203,7 @@ export default {
this.service.fetchReport(head_path),
this.service.fetchReport(base_path),
])
.then((values) => {
.then(values => {
this.mr.compareCodeclimateMetrics(
values[0],
values[1],
......@@ -192,7 +227,7 @@ export default {
this.service.fetchReport(head_path),
this.service.fetchReport(base_path),
])
.then((values) => {
.then(values => {
this.mr.comparePerformanceMetrics(values[0], values[1]);
this.isLoadingPerformance = false;
})
......@@ -216,7 +251,7 @@ export default {
this.service.fetchReport(sast.head_path),
this.service.fetchReport(sast.base_path),
])
.then((values) => {
.then(values => {
this.handleSecuritySuccess({
head: values[0],
headBlobPath: this.mr.headBlobPath,
......@@ -226,8 +261,9 @@ export default {
})
.catch(() => this.handleSecurityError());
} else if (sast.head_path) {
this.service.fetchReport(sast.head_path)
.then((data) => {
this.service
.fetchReport(sast.head_path)
.then(data => {
this.handleSecuritySuccess({
head: data,
headBlobPath: this.mr.headBlobPath,
......@@ -237,6 +273,46 @@ export default {
}
},
fetchDependencyScanning() {
const { dependencyScanning } = this.mr;
this.isLoadingDependencyScanning = true;
if (dependencyScanning.base_path && dependencyScanning.head_path) {
Promise.all([
this.service.fetchReport(dependencyScanning.head_path),
this.service.fetchReport(dependencyScanning.base_path),
])
.then(values => {
this.mr.setDependencyScanningReport({
head: values[0],
headBlobPath: this.mr.headBlobPath,
base: values[1],
baseBlobPath: this.mr.baseBlobPath,
});
this.isLoadingDependencyScanning = false;
})
.catch(() => {
this.isLoadingDependencyScanning = false;
this.loadingDependencyScanningFailed = true;
});
} else if (dependencyScanning.head_path) {
this.service
.fetchReport(dependencyScanning.head_path)
.then(data => {
this.mr.setDependencyScanningReport({
head: data,
headBlobPath: this.mr.headBlobPath,
});
this.isLoadingDependencyScanning = false;
})
.catch(() => {
this.isLoadingDependencyScanning = false;
this.loadingDependencyScanningFailed = true;
});
}
},
handleSecuritySuccess(data) {
this.mr.setSecurityReport(data);
this.isLoadingSecurity = false;
......@@ -251,8 +327,9 @@ export default {
const { head_path } = this.mr.sastContainer;
this.isLoadingDocker = true;
this.service.fetchReport(head_path)
.then((data) => {
this.service
.fetchReport(head_path)
.then(data => {
this.mr.setDockerReport(data);
this.isLoadingDocker = false;
})
......@@ -265,8 +342,9 @@ export default {
fetchDastReport() {
this.isLoadingDast = true;
this.service.fetchReport(this.mr.dast.head_path)
.then((data) => {
this.service
.fetchReport(this.mr.dast.head_path)
.then(data => {
this.mr.setDastReport(data);
this.isLoadingDast = false;
})
......@@ -296,6 +374,10 @@ export default {
if (this.shouldRenderDastReport) {
this.fetchDastReport();
}
if (this.shouldRenderDependencyReport) {
this.fetchDependencyScanning();
}
},
template: `
<div class="mr-state-widget prepend-top-default">
......@@ -351,6 +433,18 @@ export default {
:resolved-issues="mr.securityReport.resolvedIssues"
:all-issues="mr.securityReport.allIssues"
/>
<report-section
class="js-dependency-scanning-widget"
v-if="shouldRenderDependencyReport"
:type="$options.sast"
:status="dependencyScanningStatus"
:loading-text="translateText('dependency scanning').loading"
:error-text="translateText('dependency scanning').error"
:success-text="dependencyScanningText"
:unresolved-issues="mr.dependencyScanningReport.newIssues"
:resolved-issues="mr.dependencyScanningReport.resolvedIssues"
:all-issues="mr.dependencyScanningReport.allIssues"
/>
<report-section
class="js-docker-widget"
v-if="shouldRenderDockerReport"
......
......@@ -20,6 +20,7 @@ export default class MergeRequestStore extends CEMergeRequestStore {
this.initSecurityReport(data);
this.initDockerReport(data);
this.initDastReport(data);
this.initDependencyScanningReport(data);
}
setData(data) {
......@@ -95,6 +96,15 @@ export default class MergeRequestStore extends CEMergeRequestStore {
this.dastReport = [];
}
initDependencyScanningReport(data) {
this.dependencyScanning = data.dependency_scanning;
this.dependencyScanningReport = {
newIssues: [],
resolvedIssues: [],
allIssues: [],
};
}
setSecurityReport(data) {
const report = setSastReport(data);
this.securityReport.newIssues = report.newIssues;
......@@ -113,6 +123,13 @@ export default class MergeRequestStore extends CEMergeRequestStore {
this.dastReport = setDastReport(data);
}
setDependencyScanningReport(data) {
const report = setSastReport(data);
this.dependencyScanningReport.newIssues = report.newIssues;
this.dependencyScanningReport.resolvedIssues = report.resolvedIssues;
this.dependencyScanningReport.allIssues = report.allIssues;
}
compareCodeclimateMetrics(headIssues, baseIssues, headBlobPath, baseBlobPath) {
const parsedHeadIssues = parseCodeclimateMetrics(headIssues, headBlobPath);
const parsedBaseIssues = parseCodeclimateMetrics(baseIssues, baseBlobPath);
......
......@@ -155,7 +155,7 @@
class="media-body space-children"
>
<span
class="js-code-text"
class="js-code-text code-text"
>
{{ successText }}
</span>
......
......@@ -16,4 +16,11 @@ export default {
newIssues: [],
resolvedIssues: [],
},
dependencyScanning: {
isLoading: false,
hasError: false,
newIssues: [],
resolvedIssues: [],
allIssues: [],
},
};
......@@ -36,6 +36,40 @@ export default {
return text.join('');
},
depedencyScanningText(newIssues = [], resolvedIssues = [], allIssues = []) {
const text = [];
if (!newIssues.length && !resolvedIssues.length && !allIssues.length) {
text.push(s__('ciReport|Dependency scanning detected no security vulnerabilities'));
} else if (!newIssues.length && !resolvedIssues.length && allIssues.length) {
text.push(s__('ciReport|Dependency scanning detected no new security vulnerabilities'));
} else if (newIssues.length || resolvedIssues.length) {
text.push(s__('ciReport|Dependency scanning'));
}
if (resolvedIssues.length) {
text.push(n__(
' improved on %d security vulnerability',
' improved on %d security vulnerabilities',
resolvedIssues.length,
));
}
if (newIssues.length > 0 && resolvedIssues.length > 0) {
text.push(__(' and'));
}
if (newIssues.length) {
text.push(n__(
' degraded on %d security vulnerability',
' degraded on %d security vulnerabilities',
newIssues.length,
));
}
return text.join('');
},
translateText(type) {
return {
error: sprintf(s__('ciReport|Failed to load %{reportName} report'), { reportName: type }),
......
......@@ -2,11 +2,16 @@
.space-children,
.space-children > span {
display: flex;
align-self: center;
}
.media {
align-items: center;
}
.code-text {
width: 100%;
}
}
.report-block-container {
......
......@@ -33,5 +33,11 @@ module EE
pipeline.sast_artifact,
path: Ci::Build::SAST_FILE)
end
def dependency_scanning_artifact_url(pipeline)
raw_project_build_artifacts_url(pipeline.project,
pipeline.dependency_scanning_artifact,
path: Ci::Build::DEPENDENCY_SCANNING_FILE)
end
end
end
......@@ -8,6 +8,7 @@ module EE
extend ActiveSupport::Concern
CODEQUALITY_FILE = 'codeclimate.json'.freeze
DEPENDENCY_SCANNING_FILE = 'gl-dependency-scanning-report.json'.freeze
SAST_FILE = 'gl-sast-report.json'.freeze
PERFORMANCE_FILE = 'performance.json'.freeze
SAST_CONTAINER_FILE = 'gl-sast-container-report.json'.freeze
......@@ -17,6 +18,7 @@ module EE
scope :codequality, -> { where(name: %w[codequality codeclimate]) }
scope :performance, -> { where(name: %w[performance deploy]) }
scope :sast, -> { where(name: 'sast') }
scope :dependency_scanning, -> { where(name: 'dependency_scanning') }
scope :sast_container, -> { where(name: 'sast:container') }
scope :dast, -> { where(name: 'dast') }
......@@ -52,6 +54,10 @@ module EE
has_artifact?(SAST_FILE)
end
def has_dependency_scanning_json?
has_artifact?(DEPENDENCY_SCANNING_FILE)
end
def has_sast_container_json?
has_artifact?(SAST_CONTAINER_FILE)
end
......
......@@ -24,6 +24,10 @@ module EE
@sast_artifact ||= artifacts.sast.find(&:has_sast_json?)
end
def dependency_scanning_artifact
@dependency_scanning_artifact ||= artifacts.dependency_scanning.find(&:has_dependency_scanning_json?)
end
def sast_container_artifact
@sast_container_artifact ||= artifacts.sast_container.find(&:has_sast_container_json?)
end
......@@ -40,6 +44,10 @@ module EE
sast_artifact&.success?
end
def has_dependency_scanning_data?
dependency_scanning_artifact&.success?
end
def has_sast_container_data?
sast_container_artifact&.success?
end
......@@ -61,6 +69,11 @@ module EE
has_sast_data?
end
def expose_dependency_scanning_data?
project.feature_available?(:dependency_scanning) &&
has_dependency_scanning_data?
end
def expose_sast_container_data?
project.feature_available?(:sast_container) &&
has_sast_container_data?
......
......@@ -16,6 +16,8 @@ module EE
delegate :performance_artifact, to: :base_pipeline, prefix: :base, allow_nil: true
delegate :sast_artifact, to: :head_pipeline, prefix: :head, allow_nil: true
delegate :sast_artifact, to: :base_pipeline, prefix: :base, allow_nil: true
delegate :dependency_scanning_artifact, to: :head_pipeline, prefix: :head, allow_nil: true
delegate :dependency_scanning_artifact, to: :base_pipeline, prefix: :base, allow_nil: true
delegate :sast_container_artifact, to: :head_pipeline, prefix: :head, allow_nil: true
delegate :sast_container_artifact, to: :base_pipeline, prefix: :base, allow_nil: true
delegate :dast_artifact, to: :head_pipeline, prefix: :head, allow_nil: true
......@@ -23,9 +25,11 @@ module EE
delegate :sha, to: :head_pipeline, prefix: :head_pipeline, allow_nil: true
delegate :sha, to: :base_pipeline, prefix: :base_pipeline, allow_nil: true
delegate :has_sast_data?, to: :base_pipeline, prefix: :base, allow_nil: true
delegate :has_dependency_scanning_data?, to: :base_pipeline, prefix: :base, allow_nil: true
delegate :has_sast_container_data?, to: :base_pipeline, prefix: :base, allow_nil: true
delegate :has_dast_data?, to: :base_pipeline, prefix: :base, allow_nil: true
delegate :expose_sast_data?, to: :head_pipeline, allow_nil: true
delegate :expose_dependency_scanning_data?, to: :head_pipeline, allow_nil: true
delegate :expose_sast_container_data?, to: :head_pipeline, allow_nil: true
delegate :expose_dast_data?, to: :head_pipeline, allow_nil: true
end
......
......@@ -59,6 +59,7 @@ class License < ActiveRecord::Base
].freeze
EEU_FEATURES = EEP_FEATURES + %i[
dependency_scanning
sast
sast_container
cluster_health
......
......@@ -55,6 +55,20 @@ module EE
end
end
expose :dependency_scanning, if: -> (mr, _) { mr.expose_dependency_scanning_data? } do
expose :head_path, if: -> (mr, _) { can?(current_user, :read_build, mr.head_dependency_scanning_artifact) } do |merge_request|
raw_project_build_artifacts_url(merge_request.source_project,
merge_request.head_dependency_scanning_artifact,
path: Ci::Build::DEPENDENCY_SCANNING_FILE)
end
expose :base_path, if: -> (mr, _) { mr.base_has_dependency_scanning_data? && can?(current_user, :read_build, mr.base_dependency_scanning_artifact) } do |merge_request|
raw_project_build_artifacts_url(merge_request.target_project,
merge_request.base_dependency_scanning_artifact,
path: Ci::Build::DEPENDENCY_SCANNING_FILE)
end
end
expose :sast_container, if: -> (mr, _) { mr.expose_sast_container_data? } do
expose :head_path, if: -> (mr, _) { can?(current_user, :read_build, mr.head_sast_container_artifact) } do |merge_request|
raw_project_build_artifacts_url(merge_request.source_project,
......
---
title: Render dependency scanning in MR widget and CI view
merge_request:
author:
type: added
......@@ -137,15 +137,16 @@ describe Ci::Build do
end
end
ARTIFACTS_METHODS = {
BUILD_ARTIFACTS_METHODS = {
has_codeclimate_json?: Ci::Build::CODEQUALITY_FILE,
has_performance_json?: Ci::Build::PERFORMANCE_FILE,
has_sast_json?: Ci::Build::SAST_FILE,
has_dependency_scanning_json?: Ci::Build::DEPENDENCY_SCANNING_FILE,
has_sast_container_json?: Ci::Build::SAST_CONTAINER_FILE,
has_dast_json?: Ci::Build::DAST_FILE
}.freeze
ARTIFACTS_METHODS.each do |method, filename|
BUILD_ARTIFACTS_METHODS.each do |method, filename|
describe "##{method}" do
context 'valid build' do
let!(:build) do
......
......@@ -17,15 +17,16 @@ describe Ci::Pipeline do
end
end
ARTIFACTS_METHODS = {
PIPELINE_ARTIFACTS_METHODS = {
codeclimate_artifact: [Ci::Build::CODEQUALITY_FILE, 'codequality'],
performance_artifact: [Ci::Build::PERFORMANCE_FILE, 'performance'],
sast_artifact: [Ci::Build::SAST_FILE, 'sast'],
dependency_scanning_artifact: [Ci::Build::DEPENDENCY_SCANNING_FILE, 'dependency_scanning'],
sast_container_artifact: [Ci::Build::SAST_CONTAINER_FILE, 'sast:container'],
dast_artifact: [Ci::Build::DAST_FILE, 'dast']
}.freeze
ARTIFACTS_METHODS.each do |method, options|
PIPELINE_ARTIFACTS_METHODS.each do |method, options|
describe method.to_s do
context 'has corresponding job' do
let!(:build) do
......
......@@ -47,6 +47,19 @@ describe MergeRequestWidgetEntity do
expect(subject.as_json[:sast]).to include(:base_path)
end
it 'has dependency_scanning data' do
build = create(:ci_build, name: 'dependency_scanning', pipeline: pipeline)
allow(merge_request).to receive(:expose_dependency_scanning_data?).and_return(true)
allow(merge_request).to receive(:base_has_dependency_scanning_data?).and_return(true)
allow(merge_request).to receive(:base_dependency_scanning_artifact).and_return(build)
allow(merge_request).to receive(:head_dependency_scanning_artifact).and_return(build)
expect(subject.as_json).to include(:dependency_scanning)
expect(subject.as_json[:dependency_scanning]).to include(:head_path)
expect(subject.as_json[:dependency_scanning]).to include(:base_path)
end
it 'has sast_container data' do
build = create(:ci_build, name: 'sast:image', pipeline: pipeline)
......
import Vue from 'vue';
import reportSummary from 'ee/pipelines/components/security_reports/sast_report_summary_widget.vue';
import reportSummary from 'ee/pipelines/components/security_reports/report_summary_widget.vue';
import mountComponent from 'spec/helpers/vue_mount_component_helper';
import { parsedSastIssuesHead } from 'spec/vue_shared/security_reports/mock_data';
describe('SAST report summary widget', () => {
describe('Report summary widget', () => {
let vm;
let Component;
......@@ -18,25 +17,40 @@ describe('SAST report summary widget', () => {
describe('with vulnerabilities', () => {
beforeEach(() => {
vm = mountComponent(Component, {
unresolvedIssues: parsedSastIssuesHead,
sastIssues: 2,
dependencyScanningIssues: 4,
hasSast: true,
hasDependencyScanning: true,
});
});
it('renders summary text with warning icon', () => {
expect(vm.$el.textContent.trim().replace(/\s\s+/g, ' ')).toEqual('SAST degraded on 2 security vulnerabilities');
expect(vm.$el.querySelector('span').classList).toContain('ci-status-icon-warning');
it('renders summary text with warning icon for sast', () => {
expect(vm.$el.querySelector('.js-sast-summary').textContent.trim().replace(/\s\s+/g, ' ')).toEqual('SAST detected 2 vulnerabilities');
expect(vm.$el.querySelector('.js-sast-summary span').classList).toContain('ci-status-icon-warning');
});
it('renders summary text with warning icon for dependency scanning', () => {
expect(vm.$el.querySelector('.js-dss-summary').textContent.trim().replace(/\s\s+/g, ' ')).toEqual('Dependency scanning detected 4 vulnerabilities');
expect(vm.$el.querySelector('.js-dss-summary span').classList).toContain('ci-status-icon-warning');
});
});
describe('without vulnerabilities', () => {
beforeEach(() => {
vm = mountComponent(Component, {
hasSast: true,
hasDependencyScanning: true,
});
});
it('render summary text with success icon', () => {
expect(vm.$el.textContent.trim().replace(/\s\s+/g, ' ')).toEqual('SAST detected no security vulnerabilities');
expect(vm.$el.querySelector('span').classList).toContain('ci-status-icon-success');
it('render summary text with success icon for sast', () => {
expect(vm.$el.querySelector('.js-sast-summary').textContent.trim().replace(/\s\s+/g, ' ')).toEqual('SAST detected no vulnerabilities');
expect(vm.$el.querySelector('.js-sast-summary span').classList).toContain('ci-status-icon-success');
});
it('render summary text with success icon for dependecy scanning', () => {
expect(vm.$el.querySelector('.js-dss-summary').textContent.trim().replace(/\s\s+/g, ' ')).toEqual('Dependency scanning detected no vulnerabilities');
expect(vm.$el.querySelector('.js-dss-summary span').classList).toContain('ci-status-icon-success');
});
});
});
......@@ -26,15 +26,34 @@ describe('Security Report App', () => {
resolvedIssues: [],
allIssues: [],
},
dependencyScanning: {
isLoading: false,
hasError: false,
newIssues: parsedSastIssuesHead,
resolvedIssues: [],
allIssues: [],
},
},
hasDependencyScanning: true,
hasSast: true,
});
});
it('renders the sast report', () => {
expect(vm.$el.querySelector('.js-code-text').textContent.trim()).toEqual('SAST degraded on 2 security vulnerabilities');
expect(vm.$el.querySelectorAll('.js-mr-code-new-issues li').length).toEqual(parsedSastIssuesHead.length);
expect(vm.$el.querySelector('.js-sast-widget .js-code-text').textContent.trim()).toEqual('SAST degraded on 2 security vulnerabilities');
expect(vm.$el.querySelectorAll('.js-sast-widget .js-mr-code-new-issues li').length).toEqual(parsedSastIssuesHead.length);
const issue = vm.$el.querySelector('.js-sast-widget .js-mr-code-new-issues li').textContent;
expect(issue).toContain(parsedSastIssuesHead[0].message);
expect(issue).toContain(parsedSastIssuesHead[0].path);
});
it('renders the dependency scanning report', () => {
expect(vm.$el.querySelector('.js-dependency-scanning-widget .js-code-text').textContent.trim()).toEqual('Dependency scanning degraded on 2 security vulnerabilities');
expect(vm.$el.querySelectorAll('.js-dependency-scanning-widget .js-mr-code-new-issues li').length).toEqual(parsedSastIssuesHead.length);
const issue = vm.$el.querySelector('.js-mr-code-new-issues li').textContent;
const issue = vm.$el.querySelector('.js-dependency-scanning-widget .js-mr-code-new-issues li').textContent;
expect(issue).toContain(parsedSastIssuesHead[0].message);
expect(issue).toContain(parsedSastIssuesHead[0].path);
......
......@@ -158,6 +158,128 @@ describe('ee merge request widget options', () => {
});
});
describe('dependency scanning widget', () => {
beforeEach(() => {
gl.mrWidgetData = {
...mockData,
dependency_scanning: {
base_path: 'path.json',
head_path: 'head_path.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-dependency-scanning-widget').textContent.trim(),
).toContain('Loading dependency scanning report');
});
});
describe('with successful request', () => {
let mock;
beforeEach(() => {
mock = mock = new MockAdapter(axios);
mock.onGet('path.json').reply(200, sastIssuesBase);
mock.onGet('head_path.json').reply(200, sastIssues);
vm = mountComponent(Component);
});
afterEach(() => {
mock.restore();
});
it('should render provided data', (done) => {
setTimeout(() => {
expect(
vm.$el.querySelector('.js-dependency-scanning-widget .js-code-text').textContent.trim(),
).toEqual('Dependency scanning improved on 1 security vulnerability and degraded on 2 security vulnerabilities');
done();
}, 0);
});
});
describe('with full report and no added or fixed issues', () => {
let mock;
beforeEach(() => {
mock = mock = new MockAdapter(axios);
mock.onGet('path.json').reply(200, sastBaseAllIssues);
mock.onGet('head_path.json').reply(200, sastHeadAllIssues);
vm = mountComponent(Component);
});
afterEach(() => {
mock.restore();
});
it('renders no new vulnerabilities message', (done) => {
setTimeout(() => {
expect(
vm.$el.querySelector('.js-dependency-scanning-widget .js-code-text').textContent.trim(),
).toEqual('Dependency scanning detected no new security vulnerabilities');
done();
}, 0);
});
});
describe('with empty successful request', () => {
let mock;
beforeEach(() => {
mock = mock = new MockAdapter(axios);
mock.onGet('path.json').reply(200, []);
mock.onGet('head_path.json').reply(200, []);
vm = mountComponent(Component);
});
afterEach(() => {
mock.restore();
});
it('should render provided data', (done) => {
setTimeout(() => {
expect(
vm.$el.querySelector('.js-dependency-scanning-widget .js-code-text').textContent.trim(),
).toEqual('Dependency scanning detected no security vulnerabilities');
done();
}, 0);
});
});
describe('with failed request', () => {
let mock;
beforeEach(() => {
mock = mock = new MockAdapter(axios);
mock.onGet('path.json').reply(500, []);
mock.onGet('head_path.json').reply(500, []);
vm = mountComponent(Component);
});
afterEach(() => {
mock.restore();
});
it('should render error indicator', (done) => {
setTimeout(() => {
expect(
vm.$el.querySelector('.js-dependency-scanning-widget').textContent.trim(),
).toContain('Failed to load dependency scanning report');
done();
}, 0);
});
});
});
describe('code quality', () => {
beforeEach(() => {
gl.mrWidgetData = {
......
......@@ -118,6 +118,26 @@ describe('MergeRequestStore', () => {
});
});
describe('setDependencyScanningReport', () => {
it('should set security issues with head', () => {
store.setDependencyScanningReport({ head: sastIssues, headBlobPath: 'path' });
expect(store.dependencyScanningReport.newIssues).toEqual(parsedSastIssuesStore);
});
it('should set security issues with head and base', () => {
store.setDependencyScanningReport({
head: sastIssues,
headBlobPath: 'path',
base: sastIssuesBase,
baseBlobPath: 'path',
});
expect(store.dependencyScanningReport.newIssues).toEqual(parsedSastIssuesHead);
expect(store.dependencyScanningReport.resolvedIssues).toEqual(parsedSastBaseStore);
expect(store.dependencyScanningReport.allIssues).toEqual(allIssuesParsed);
});
});
describe('isNothingToMergeState', () => {
it('returns true when nothingToMerge', () => {
store.state = stateKey.nothingToMerge;
......
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