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'; ...@@ -8,7 +8,7 @@ import pipelineHeader from './components/header_component.vue';
import eventHub from './event_hub'; import eventHub from './event_hub';
import SecurityReportApp from 'ee/pipelines/components/security_reports/security_report_app.vue'; // eslint-disable-line import/first 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); Vue.use(Translate);
...@@ -81,24 +81,51 @@ export default () => { ...@@ -81,24 +81,51 @@ export default () => {
const securityTab = document.getElementById('js-security-report-app'); const securityTab = document.getElementById('js-security-report-app');
const sastSummary = document.querySelector('.js-sast-summary'); 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 // They are being rendered under the same condition
if (securityTab && sastSummary) { if (securityTab && sastSummary) {
const datasetOptions = securityTab.dataset; const datasetOptions = securityTab.dataset;
const endpoint = datasetOptions.endpoint; const endpoint = datasetOptions.endpoint;
const blobPath = datasetOptions.blobPath; const blobPath = datasetOptions.blobPath;
const dependencyScanningEndpoint = datasetOptions.dependencyScanningEndpoint;
if (endpoint) {
mediator.fetchSastReport(endpoint, blobPath) mediator.fetchSastReport(endpoint, blobPath)
.then(() => { .then(() => {
// update the badge // update the badge
if (mediator.store.state.securityReports.sast.newIssues.length) { if (mediator.store.state.securityReports.sast.newIssues.length) {
const badge = document.querySelector('.js-sast-counter'); updateBadgeCount(mediator.store.state.securityReports.sast.newIssues.length);
badge.textContent = mediator.store.state.securityReports.sast.newIssues.length;
badge.classList.remove('hidden');
} }
}) })
.catch(() => { .catch(() => {
Flash(__('Something went wrong while fetching SAST.')); 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 // Widget summary
// eslint-disable-next-line no-new // eslint-disable-next-line no-new
...@@ -115,7 +142,11 @@ export default () => { ...@@ -115,7 +142,11 @@ export default () => {
render(createElement) { render(createElement) {
return createElement('sast-summary-widget', { return createElement('sast-summary-widget', {
props: { 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 () => { ...@@ -137,6 +168,8 @@ export default () => {
return createElement('security-report-app', { return createElement('security-report-app', {
props: { props: {
securityReports: this.mediator.store.state.securityReports, securityReports: this.mediator.store.state.securityReports,
hasDependencyScanning: dependencyScanningEndpoint !== undefined,
hasSast: endpoint !== undefined,
}, },
}); });
}, },
......
...@@ -67,4 +67,12 @@ export default class pipelinesMediator { ...@@ -67,4 +67,12 @@ export default class pipelinesMediator {
this.store.storeSastReport(data, blobPath); 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 { ...@@ -26,4 +26,11 @@ export default class PipelineStore {
setSastReport({ head: data, headBlobPath: blobPath }), 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 #js-pipeline-header-vue.pipeline-header-container
- sast_artifact = @pipeline.sast_artifact - sast_artifact = @pipeline.sast_artifact
- dependecy_artifact = @pipeline.dependency_scanning_artifact
- if @commit.present? - if @commit.present?
.commit-box .commit-box
...@@ -35,5 +36,5 @@ ...@@ -35,5 +36,5 @@
= link_to @pipeline.sha, project_commit_path(@project, @pipeline.sha), class: "commit-sha commit-hash-full" = 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") = clipboard_button(text: @pipeline.sha, title: "Copy commit SHA to clipboard")
- if sast_artifact - if sast_artifact || dependecy_artifact
.js-sast-summary .js-sast-summary
- failed_builds = @pipeline.statuses.latest.failed - failed_builds = @pipeline.statuses.latest.failed
- expose_sast_data = @pipeline.expose_sast_data? - expose_sast_data = @pipeline.expose_sast_data?
- expose_dependency_data = @pipeline.expose_dependency_scanning_data?
- blob_path = project_blob_path(@project, @pipeline.sha) - blob_path = project_blob_path(@project, @pipeline.sha)
.tabs-holder .tabs-holder
...@@ -16,7 +17,7 @@ ...@@ -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 = link_to failures_project_pipeline_path(@project, @pipeline), data: { target: '#js-tab-failures', action: 'failures', toggle: 'tab' }, class: 'failures-tab' do
= _("Failed Jobs") = _("Failed Jobs")
%span.badge.js-failures-counter= failed_builds.count %span.badge.js-failures-counter= failed_builds.count
- if expose_sast_data - if expose_sast_data || expose_dependency_data
%li.js-security-tab-link %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 = link_to security_project_pipeline_path(@project, @pipeline), data: { target: '#js-tab-security', action: 'security', toggle: 'tab' }, class: 'security-tab' do
= _("Security report") = _("Security report")
...@@ -60,6 +61,8 @@ ...@@ -60,6 +61,8 @@
%span.build-name %span.build-name
= link_to build.name, pipeline_job_url(pipeline, build) = link_to build.name, pipeline_job_url(pipeline, build)
%pre.build-log= build_summary(build, skip: index >= 10) %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-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 ...@@ -49,6 +49,10 @@ There's also a collection of repositories with [example projects](https://gitlab
**(Ultimate)** [Scan your code for vulnerabilities](sast.md) **(Ultimate)** [Scan your code for vulnerabilities](sast.md)
## Dependency Scanning
**(Ultimate)** [Scan your dependencies for vulnerabilities](dependency_scanning.md)
## Container Scanning ## Container Scanning
[Scan your Docker images for vulnerabilities](container_scanning.md) [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: ...@@ -20,13 +20,12 @@ sast:
services: services:
- docker:dind - docker:dind
script: script:
- export SAST_VERSION=$(echo "$CI_SERVER_VERSION" | sed 's/^\([0-9]*\)\.\([0-9]*\).*/\1-\2-stable/') - export SP_VERSION=$(echo "$CI_SERVER_VERSION" | sed 's/^\([0-9]*\)\.\([0-9]*\).*/\1-\2-stable/')
- docker run - docker run
--env SAST_CONFIDENCE_LEVEL="${SAST_CONFIDENCE_LEVEL:-3}" --env SAST_CONFIDENCE_LEVEL="${SAST_CONFIDENCE_LEVEL:-3}"
--env SAST_DISABLE_REMOTE_CHECKS="${SAST_DISABLE_REMOTE_CHECKS:-false}"
--volume "$PWD:/code" --volume "$PWD:/code"
--volume /var/run/docker.sock:/var/run/docker.sock --volume /var/run/docker.sock:/var/run/docker.sock
"registry.gitlab.com/gitlab-org/security-products/sast:$SAST_VERSION" /app/bin/run /code "registry.gitlab.com/gitlab-org/security-products/sast:$SP_VERSION" /app/bin/run /code
artifacts: artifacts:
paths: [gl-sast-report.json] paths: [gl-sast-report.json]
``` ```
......
...@@ -20,6 +20,7 @@ project in an easy and automatic way: ...@@ -20,6 +20,7 @@ project in an easy and automatic way:
1. [Auto Test](#auto-test) 1. [Auto Test](#auto-test)
1. [Auto Code Quality](#auto-code-quality) 1. [Auto Code Quality](#auto-code-quality)
1. [Auto SAST (Static Application Security Testing)](#auto-sast) 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 Container Scanning](#auto-container-scanning)
1. [Auto Review Apps](#auto-review-apps) 1. [Auto Review Apps](#auto-review-apps)
1. [Auto DAST (Dynamic Application Security Testing)](#auto-dast) 1. [Auto DAST (Dynamic Application Security Testing)](#auto-dast)
...@@ -217,6 +218,19 @@ check out. ...@@ -217,6 +218,19 @@ check out.
In GitLab Ultimate, any security warnings are also In GitLab Ultimate, any security warnings are also
[shown in the merge request widget](../../user/project/merge_requests/sast.md). [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 ### Auto Container Scanning
> Introduced in GitLab 10.4. > Introduced in GitLab 10.4.
...@@ -454,7 +468,7 @@ also be customized, and you can easily use a [custom buildpack](#custom-buildpac ...@@ -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. | | `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`| | `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_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:** TIP: **Tip:**
Set up the replica variables using a 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: ...@@ -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) - 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) - 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) - [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) - Determine the performance impact of changes with [Browser Performance Testing](#browser-performance-testing) (available in GitLab Premium)
## Use cases ## Use cases
...@@ -44,7 +48,7 @@ A. Consider you are a software developer working in a team: ...@@ -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 checkout a new branch, and submit your changes through a merge request
1. You gather feedback from your team 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 build and test your changes with GitLab CI/CD
1. You request the [approval](#merge-request-approvals) from your manager 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) 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. ...@@ -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) [Read more about merge request approvals.](merge_request_approvals.md)
## Code Quality reports ## Code Quality
> Introduced in [GitLab Starter][products] 9.3. > Introduced in [GitLab Starter][products] 9.3.
...@@ -228,6 +232,17 @@ merge request widget area. ...@@ -228,6 +232,17 @@ merge request widget area.
[Read more about Static Application Security Testing reports.](sast.md) [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 ## Container Scanning
> Introduced in [GitLab Ultimate][products] 10.4. > Introduced in [GitLab Ultimate][products] 10.4.
......
...@@ -25,17 +25,12 @@ request widget area. ...@@ -25,17 +25,12 @@ request widget area.
The following languages and frameworks are supported. The following languages and frameworks are supported.
| Language (package managers) / framework | Scan tool | | Language / 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) | C/C++ | [Flawfinder](https://www.dwheeler.com/flawfinder/) |
| Python ([pip](https://pip.pypa.io/en/stable/)) | [gemnasium](https://gitlab.com/gitlab-org/security-products/gemnasium), [bandit](https://github.com/openstack/bandit) | | Python | [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) | | 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/) | | Java | [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).
## How it works ## How it works
...@@ -53,7 +48,7 @@ The `sast` job will perform an analysis on the running web application, the ...@@ -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 resulting JSON file will be uploaded as an artifact, and GitLab will then check
this file and show the information inside the merge request. this file and show the information inside the merge request.
![SAST Widget](img/gemnasium.png) ![SAST Widget](img/sast.png)
## Security report under pipelines ## 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 @@ ...@@ -17,12 +17,23 @@
type: Object, type: Object,
required: true, required: true,
}, },
hasDependencyScanning: {
type: Boolean,
required: false,
default: false,
},
hasSast: {
type: Boolean,
required: false,
default: false,
},
}, },
}; };
</script> </script>
<template> <template>
<div class="pipeline-tab-content"> <div class="pipeline-tab-content">
<report-section <report-section
v-if="hasSast"
class="js-sast-widget" class="js-sast-widget"
:type="$options.sast" :type="$options.sast"
:status="checkReportStatus(securityReports.sast.isLoading, securityReports.sast.hasError)" :status="checkReportStatus(securityReports.sast.isLoading, securityReports.sast.hasError)"
...@@ -32,7 +43,26 @@ ...@@ -32,7 +43,26 @@
:unresolved-issues="securityReports.sast.newIssues" :unresolved-issues="securityReports.sast.newIssues"
:resolved-issues="securityReports.sast.resolvedIssues" :resolved-issues="securityReports.sast.resolvedIssues"
:all-issues="securityReports.sast.allIssues" :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> </div>
</template> </template>
...@@ -17,9 +17,7 @@ export default { ...@@ -17,9 +17,7 @@ export default {
'mr-widget-geo-secondary-node': GeoSecondaryNode, 'mr-widget-geo-secondary-node': GeoSecondaryNode,
ReportSection, ReportSection,
}, },
mixins: [ mixins: [securityMixin],
securityMixin,
],
dast: DAST, dast: DAST,
sast: SAST, sast: SAST,
sastContainer: SAST_CONTAINER, sastContainer: SAST_CONTAINER,
...@@ -30,11 +28,13 @@ export default { ...@@ -30,11 +28,13 @@ export default {
isLoadingSecurity: false, isLoadingSecurity: false,
isLoadingDocker: false, isLoadingDocker: false,
isLoadingDast: false, isLoadingDast: false,
isLoadingDependencyScanning: false,
loadingCodequalityFailed: false, loadingCodequalityFailed: false,
loadingPerformanceFailed: false, loadingPerformanceFailed: false,
loadingSecurityFailed: false, loadingSecurityFailed: false,
loadingDockerFailed: false, loadingDockerFailed: false,
loadingDastFailed: false, loadingDastFailed: false,
loadingDependencyScanningFailed: false,
}; };
}, },
computed: { computed: {
...@@ -53,10 +53,13 @@ export default { ...@@ -53,10 +53,13 @@ export default {
return this.mr.sast && this.mr.sast.head_path; return this.mr.sast && this.mr.sast.head_path;
}, },
shouldRenderDockerReport() { shouldRenderDockerReport() {
return this.mr.sastContainer; return this.mr.sastContainer && this.mr.sastContainer.head_path;
}, },
shouldRenderDastReport() { 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() { codequalityText() {
const { newIssues, resolvedIssues } = this.mr.codeclimateMetrics; const { newIssues, resolvedIssues } = this.mr.codeclimateMetrics;
...@@ -68,11 +71,13 @@ export default { ...@@ -68,11 +71,13 @@ export default {
text.push(s__('ciReport|Code quality')); text.push(s__('ciReport|Code quality'));
if (resolvedIssues.length) { if (resolvedIssues.length) {
text.push(n__( text.push(
n__(
' improved on %d point', ' improved on %d point',
' improved on %d points', ' improved on %d points',
resolvedIssues.length, resolvedIssues.length,
)); ),
);
} }
if (newIssues.length > 0 && resolvedIssues.length > 0) { if (newIssues.length > 0 && resolvedIssues.length > 0) {
...@@ -80,11 +85,13 @@ export default { ...@@ -80,11 +85,13 @@ export default {
} }
if (newIssues.length) { if (newIssues.length) {
text.push(n__( text.push(
n__(
' degraded on %d point', ' degraded on %d point',
' degraded on %d points', ' degraded on %d points',
newIssues.length, newIssues.length,
)); ),
);
} }
} }
...@@ -101,11 +108,13 @@ export default { ...@@ -101,11 +108,13 @@ export default {
text.push(s__('ciReport|Performance metrics')); text.push(s__('ciReport|Performance metrics'));
if (improved.length) { if (improved.length) {
text.push(n__( text.push(
n__(
' improved on %d point', ' improved on %d point',
' improved on %d points', ' improved on %d points',
improved.length, improved.length,
)); ),
);
} }
if (improved.length > 0 && degraded.length > 0) { if (improved.length > 0 && degraded.length > 0) {
...@@ -113,11 +122,13 @@ export default { ...@@ -113,11 +122,13 @@ export default {
} }
if (degraded.length) { if (degraded.length) {
text.push(n__( text.push(
n__(
' degraded on %d point', ' degraded on %d point',
' degraded on %d points', ' degraded on %d points',
degraded.length, degraded.length,
)); ),
);
} }
} }
...@@ -129,6 +140,11 @@ export default { ...@@ -129,6 +140,11 @@ export default {
return this.sastText(newIssues, resolvedIssues, allIssues); return this.sastText(newIssues, resolvedIssues, allIssues);
}, },
dependencyScanningText() {
const { newIssues, resolvedIssues, allIssues } = this.mr.dependencyScanningReport;
return this.depedencyScanningText(newIssues, resolvedIssues, allIssues);
},
dockerText() { dockerText() {
const { vulnerabilities, approved, unapproved } = this.mr.dockerReport; const { vulnerabilities, approved, unapproved } = this.mr.dockerReport;
return this.sastContainerText(vulnerabilities, approved, unapproved); return this.sastContainerText(vulnerabilities, approved, unapproved);
...@@ -139,24 +155,43 @@ export default { ...@@ -139,24 +155,43 @@ export default {
}, },
codequalityStatus() { codequalityStatus() {
return this.checkReportStatus(this.isLoadingCodequality, this.loadingCodequalityFailed); return this.checkReportStatus(
this.isLoadingCodequality,
this.loadingCodequalityFailed,
);
}, },
performanceStatus() { performanceStatus() {
return this.checkReportStatus(this.isLoadingPerformance, this.loadingPerformanceFailed); return this.checkReportStatus(
this.isLoadingPerformance,
this.loadingPerformanceFailed,
);
}, },
securityStatus() { securityStatus() {
return this.checkReportStatus(this.isLoadingSecurity, this.loadingSecurityFailed); return this.checkReportStatus(
this.isLoadingSecurity,
this.loadingSecurityFailed,
);
}, },
dockerStatus() { dockerStatus() {
return this.checkReportStatus(this.isLoadingDocker, this.loadingDockerFailed); return this.checkReportStatus(
this.isLoadingDocker,
this.loadingDockerFailed,
);
}, },
dastStatus() { dastStatus() {
return this.checkReportStatus(this.isLoadingDast, this.loadingDastFailed); return this.checkReportStatus(this.isLoadingDast, this.loadingDastFailed);
}, },
dependencyScanningStatus() {
return this.checkReportStatus(
this.isLoadingDependencyScanning,
this.loadingDependencyScanningFailed,
);
},
}, },
methods: { methods: {
fetchCodeQuality() { fetchCodeQuality() {
...@@ -168,7 +203,7 @@ export default { ...@@ -168,7 +203,7 @@ export default {
this.service.fetchReport(head_path), this.service.fetchReport(head_path),
this.service.fetchReport(base_path), this.service.fetchReport(base_path),
]) ])
.then((values) => { .then(values => {
this.mr.compareCodeclimateMetrics( this.mr.compareCodeclimateMetrics(
values[0], values[0],
values[1], values[1],
...@@ -192,7 +227,7 @@ export default { ...@@ -192,7 +227,7 @@ export default {
this.service.fetchReport(head_path), this.service.fetchReport(head_path),
this.service.fetchReport(base_path), this.service.fetchReport(base_path),
]) ])
.then((values) => { .then(values => {
this.mr.comparePerformanceMetrics(values[0], values[1]); this.mr.comparePerformanceMetrics(values[0], values[1]);
this.isLoadingPerformance = false; this.isLoadingPerformance = false;
}) })
...@@ -216,7 +251,7 @@ export default { ...@@ -216,7 +251,7 @@ export default {
this.service.fetchReport(sast.head_path), this.service.fetchReport(sast.head_path),
this.service.fetchReport(sast.base_path), this.service.fetchReport(sast.base_path),
]) ])
.then((values) => { .then(values => {
this.handleSecuritySuccess({ this.handleSecuritySuccess({
head: values[0], head: values[0],
headBlobPath: this.mr.headBlobPath, headBlobPath: this.mr.headBlobPath,
...@@ -226,8 +261,9 @@ export default { ...@@ -226,8 +261,9 @@ export default {
}) })
.catch(() => this.handleSecurityError()); .catch(() => this.handleSecurityError());
} else if (sast.head_path) { } else if (sast.head_path) {
this.service.fetchReport(sast.head_path) this.service
.then((data) => { .fetchReport(sast.head_path)
.then(data => {
this.handleSecuritySuccess({ this.handleSecuritySuccess({
head: data, head: data,
headBlobPath: this.mr.headBlobPath, headBlobPath: this.mr.headBlobPath,
...@@ -237,6 +273,46 @@ export default { ...@@ -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) { handleSecuritySuccess(data) {
this.mr.setSecurityReport(data); this.mr.setSecurityReport(data);
this.isLoadingSecurity = false; this.isLoadingSecurity = false;
...@@ -251,8 +327,9 @@ export default { ...@@ -251,8 +327,9 @@ export default {
const { head_path } = this.mr.sastContainer; const { head_path } = this.mr.sastContainer;
this.isLoadingDocker = true; this.isLoadingDocker = true;
this.service.fetchReport(head_path) this.service
.then((data) => { .fetchReport(head_path)
.then(data => {
this.mr.setDockerReport(data); this.mr.setDockerReport(data);
this.isLoadingDocker = false; this.isLoadingDocker = false;
}) })
...@@ -265,8 +342,9 @@ export default { ...@@ -265,8 +342,9 @@ export default {
fetchDastReport() { fetchDastReport() {
this.isLoadingDast = true; this.isLoadingDast = true;
this.service.fetchReport(this.mr.dast.head_path) this.service
.then((data) => { .fetchReport(this.mr.dast.head_path)
.then(data => {
this.mr.setDastReport(data); this.mr.setDastReport(data);
this.isLoadingDast = false; this.isLoadingDast = false;
}) })
...@@ -296,6 +374,10 @@ export default { ...@@ -296,6 +374,10 @@ export default {
if (this.shouldRenderDastReport) { if (this.shouldRenderDastReport) {
this.fetchDastReport(); this.fetchDastReport();
} }
if (this.shouldRenderDependencyReport) {
this.fetchDependencyScanning();
}
}, },
template: ` template: `
<div class="mr-state-widget prepend-top-default"> <div class="mr-state-widget prepend-top-default">
...@@ -351,6 +433,18 @@ export default { ...@@ -351,6 +433,18 @@ export default {
:resolved-issues="mr.securityReport.resolvedIssues" :resolved-issues="mr.securityReport.resolvedIssues"
:all-issues="mr.securityReport.allIssues" :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 <report-section
class="js-docker-widget" class="js-docker-widget"
v-if="shouldRenderDockerReport" v-if="shouldRenderDockerReport"
......
...@@ -20,6 +20,7 @@ export default class MergeRequestStore extends CEMergeRequestStore { ...@@ -20,6 +20,7 @@ export default class MergeRequestStore extends CEMergeRequestStore {
this.initSecurityReport(data); this.initSecurityReport(data);
this.initDockerReport(data); this.initDockerReport(data);
this.initDastReport(data); this.initDastReport(data);
this.initDependencyScanningReport(data);
} }
setData(data) { setData(data) {
...@@ -95,6 +96,15 @@ export default class MergeRequestStore extends CEMergeRequestStore { ...@@ -95,6 +96,15 @@ export default class MergeRequestStore extends CEMergeRequestStore {
this.dastReport = []; this.dastReport = [];
} }
initDependencyScanningReport(data) {
this.dependencyScanning = data.dependency_scanning;
this.dependencyScanningReport = {
newIssues: [],
resolvedIssues: [],
allIssues: [],
};
}
setSecurityReport(data) { setSecurityReport(data) {
const report = setSastReport(data); const report = setSastReport(data);
this.securityReport.newIssues = report.newIssues; this.securityReport.newIssues = report.newIssues;
...@@ -113,6 +123,13 @@ export default class MergeRequestStore extends CEMergeRequestStore { ...@@ -113,6 +123,13 @@ export default class MergeRequestStore extends CEMergeRequestStore {
this.dastReport = setDastReport(data); 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) { compareCodeclimateMetrics(headIssues, baseIssues, headBlobPath, baseBlobPath) {
const parsedHeadIssues = parseCodeclimateMetrics(headIssues, headBlobPath); const parsedHeadIssues = parseCodeclimateMetrics(headIssues, headBlobPath);
const parsedBaseIssues = parseCodeclimateMetrics(baseIssues, baseBlobPath); const parsedBaseIssues = parseCodeclimateMetrics(baseIssues, baseBlobPath);
......
...@@ -155,7 +155,7 @@ ...@@ -155,7 +155,7 @@
class="media-body space-children" class="media-body space-children"
> >
<span <span
class="js-code-text" class="js-code-text code-text"
> >
{{ successText }} {{ successText }}
</span> </span>
......
...@@ -16,4 +16,11 @@ export default { ...@@ -16,4 +16,11 @@ export default {
newIssues: [], newIssues: [],
resolvedIssues: [], resolvedIssues: [],
}, },
dependencyScanning: {
isLoading: false,
hasError: false,
newIssues: [],
resolvedIssues: [],
allIssues: [],
},
}; };
...@@ -36,6 +36,40 @@ export default { ...@@ -36,6 +36,40 @@ export default {
return text.join(''); 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) { translateText(type) {
return { return {
error: sprintf(s__('ciReport|Failed to load %{reportName} report'), { reportName: type }), error: sprintf(s__('ciReport|Failed to load %{reportName} report'), { reportName: type }),
......
...@@ -2,11 +2,16 @@ ...@@ -2,11 +2,16 @@
.space-children, .space-children,
.space-children > span { .space-children > span {
display: flex; display: flex;
align-self: center;
} }
.media { .media {
align-items: center; align-items: center;
} }
.code-text {
width: 100%;
}
} }
.report-block-container { .report-block-container {
......
...@@ -33,5 +33,11 @@ module EE ...@@ -33,5 +33,11 @@ module EE
pipeline.sast_artifact, pipeline.sast_artifact,
path: Ci::Build::SAST_FILE) path: Ci::Build::SAST_FILE)
end 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
end end
...@@ -8,6 +8,7 @@ module EE ...@@ -8,6 +8,7 @@ module EE
extend ActiveSupport::Concern extend ActiveSupport::Concern
CODEQUALITY_FILE = 'codeclimate.json'.freeze CODEQUALITY_FILE = 'codeclimate.json'.freeze
DEPENDENCY_SCANNING_FILE = 'gl-dependency-scanning-report.json'.freeze
SAST_FILE = 'gl-sast-report.json'.freeze SAST_FILE = 'gl-sast-report.json'.freeze
PERFORMANCE_FILE = 'performance.json'.freeze PERFORMANCE_FILE = 'performance.json'.freeze
SAST_CONTAINER_FILE = 'gl-sast-container-report.json'.freeze SAST_CONTAINER_FILE = 'gl-sast-container-report.json'.freeze
...@@ -17,6 +18,7 @@ module EE ...@@ -17,6 +18,7 @@ module EE
scope :codequality, -> { where(name: %w[codequality codeclimate]) } scope :codequality, -> { where(name: %w[codequality codeclimate]) }
scope :performance, -> { where(name: %w[performance deploy]) } scope :performance, -> { where(name: %w[performance deploy]) }
scope :sast, -> { where(name: 'sast') } scope :sast, -> { where(name: 'sast') }
scope :dependency_scanning, -> { where(name: 'dependency_scanning') }
scope :sast_container, -> { where(name: 'sast:container') } scope :sast_container, -> { where(name: 'sast:container') }
scope :dast, -> { where(name: 'dast') } scope :dast, -> { where(name: 'dast') }
...@@ -52,6 +54,10 @@ module EE ...@@ -52,6 +54,10 @@ module EE
has_artifact?(SAST_FILE) has_artifact?(SAST_FILE)
end end
def has_dependency_scanning_json?
has_artifact?(DEPENDENCY_SCANNING_FILE)
end
def has_sast_container_json? def has_sast_container_json?
has_artifact?(SAST_CONTAINER_FILE) has_artifact?(SAST_CONTAINER_FILE)
end end
......
...@@ -24,6 +24,10 @@ module EE ...@@ -24,6 +24,10 @@ module EE
@sast_artifact ||= artifacts.sast.find(&:has_sast_json?) @sast_artifact ||= artifacts.sast.find(&:has_sast_json?)
end end
def dependency_scanning_artifact
@dependency_scanning_artifact ||= artifacts.dependency_scanning.find(&:has_dependency_scanning_json?)
end
def sast_container_artifact def sast_container_artifact
@sast_container_artifact ||= artifacts.sast_container.find(&:has_sast_container_json?) @sast_container_artifact ||= artifacts.sast_container.find(&:has_sast_container_json?)
end end
...@@ -40,6 +44,10 @@ module EE ...@@ -40,6 +44,10 @@ module EE
sast_artifact&.success? sast_artifact&.success?
end end
def has_dependency_scanning_data?
dependency_scanning_artifact&.success?
end
def has_sast_container_data? def has_sast_container_data?
sast_container_artifact&.success? sast_container_artifact&.success?
end end
...@@ -61,6 +69,11 @@ module EE ...@@ -61,6 +69,11 @@ module EE
has_sast_data? has_sast_data?
end end
def expose_dependency_scanning_data?
project.feature_available?(:dependency_scanning) &&
has_dependency_scanning_data?
end
def expose_sast_container_data? def expose_sast_container_data?
project.feature_available?(:sast_container) && project.feature_available?(:sast_container) &&
has_sast_container_data? has_sast_container_data?
......
...@@ -16,6 +16,8 @@ module EE ...@@ -16,6 +16,8 @@ module EE
delegate :performance_artifact, to: :base_pipeline, prefix: :base, allow_nil: true 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: :head_pipeline, prefix: :head, allow_nil: true
delegate :sast_artifact, to: :base_pipeline, prefix: :base, 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: :head_pipeline, prefix: :head, allow_nil: true
delegate :sast_container_artifact, to: :base_pipeline, prefix: :base, 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 delegate :dast_artifact, to: :head_pipeline, prefix: :head, allow_nil: true
...@@ -23,9 +25,11 @@ module EE ...@@ -23,9 +25,11 @@ module EE
delegate :sha, to: :head_pipeline, prefix: :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 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_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_sast_container_data?, to: :base_pipeline, prefix: :base, allow_nil: true
delegate :has_dast_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_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_sast_container_data?, to: :head_pipeline, allow_nil: true
delegate :expose_dast_data?, to: :head_pipeline, allow_nil: true delegate :expose_dast_data?, to: :head_pipeline, allow_nil: true
end end
......
...@@ -59,6 +59,7 @@ class License < ActiveRecord::Base ...@@ -59,6 +59,7 @@ class License < ActiveRecord::Base
].freeze ].freeze
EEU_FEATURES = EEP_FEATURES + %i[ EEU_FEATURES = EEP_FEATURES + %i[
dependency_scanning
sast sast
sast_container sast_container
cluster_health cluster_health
......
...@@ -55,6 +55,20 @@ module EE ...@@ -55,6 +55,20 @@ module EE
end end
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 :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| 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, 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 ...@@ -137,15 +137,16 @@ describe Ci::Build do
end end
end end
ARTIFACTS_METHODS = { BUILD_ARTIFACTS_METHODS = {
has_codeclimate_json?: Ci::Build::CODEQUALITY_FILE, has_codeclimate_json?: Ci::Build::CODEQUALITY_FILE,
has_performance_json?: Ci::Build::PERFORMANCE_FILE, has_performance_json?: Ci::Build::PERFORMANCE_FILE,
has_sast_json?: Ci::Build::SAST_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_sast_container_json?: Ci::Build::SAST_CONTAINER_FILE,
has_dast_json?: Ci::Build::DAST_FILE has_dast_json?: Ci::Build::DAST_FILE
}.freeze }.freeze
ARTIFACTS_METHODS.each do |method, filename| BUILD_ARTIFACTS_METHODS.each do |method, filename|
describe "##{method}" do describe "##{method}" do
context 'valid build' do context 'valid build' do
let!(:build) do let!(:build) do
......
...@@ -17,15 +17,16 @@ describe Ci::Pipeline do ...@@ -17,15 +17,16 @@ describe Ci::Pipeline do
end end
end end
ARTIFACTS_METHODS = { PIPELINE_ARTIFACTS_METHODS = {
codeclimate_artifact: [Ci::Build::CODEQUALITY_FILE, 'codequality'], codeclimate_artifact: [Ci::Build::CODEQUALITY_FILE, 'codequality'],
performance_artifact: [Ci::Build::PERFORMANCE_FILE, 'performance'], performance_artifact: [Ci::Build::PERFORMANCE_FILE, 'performance'],
sast_artifact: [Ci::Build::SAST_FILE, 'sast'], 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'], sast_container_artifact: [Ci::Build::SAST_CONTAINER_FILE, 'sast:container'],
dast_artifact: [Ci::Build::DAST_FILE, 'dast'] dast_artifact: [Ci::Build::DAST_FILE, 'dast']
}.freeze }.freeze
ARTIFACTS_METHODS.each do |method, options| PIPELINE_ARTIFACTS_METHODS.each do |method, options|
describe method.to_s do describe method.to_s do
context 'has corresponding job' do context 'has corresponding job' do
let!(:build) do let!(:build) do
......
...@@ -47,6 +47,19 @@ describe MergeRequestWidgetEntity do ...@@ -47,6 +47,19 @@ describe MergeRequestWidgetEntity do
expect(subject.as_json[:sast]).to include(:base_path) expect(subject.as_json[:sast]).to include(:base_path)
end 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 it 'has sast_container data' do
build = create(:ci_build, name: 'sast:image', pipeline: pipeline) build = create(:ci_build, name: 'sast:image', pipeline: pipeline)
......
import Vue from 'vue'; 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 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 vm;
let Component; let Component;
...@@ -18,25 +17,40 @@ describe('SAST report summary widget', () => { ...@@ -18,25 +17,40 @@ describe('SAST report summary widget', () => {
describe('with vulnerabilities', () => { describe('with vulnerabilities', () => {
beforeEach(() => { beforeEach(() => {
vm = mountComponent(Component, { vm = mountComponent(Component, {
unresolvedIssues: parsedSastIssuesHead, sastIssues: 2,
dependencyScanningIssues: 4,
hasSast: true,
hasDependencyScanning: true,
}); });
}); });
it('renders summary text with warning icon', () => { it('renders summary text with warning icon for sast', () => {
expect(vm.$el.textContent.trim().replace(/\s\s+/g, ' ')).toEqual('SAST degraded on 2 security vulnerabilities'); expect(vm.$el.querySelector('.js-sast-summary').textContent.trim().replace(/\s\s+/g, ' ')).toEqual('SAST detected 2 vulnerabilities');
expect(vm.$el.querySelector('span').classList).toContain('ci-status-icon-warning'); 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', () => { describe('without vulnerabilities', () => {
beforeEach(() => { beforeEach(() => {
vm = mountComponent(Component, { vm = mountComponent(Component, {
hasSast: true,
hasDependencyScanning: true,
}); });
}); });
it('render summary text with success icon', () => { it('render summary text with success icon for sast', () => {
expect(vm.$el.textContent.trim().replace(/\s\s+/g, ' ')).toEqual('SAST detected no security vulnerabilities'); expect(vm.$el.querySelector('.js-sast-summary').textContent.trim().replace(/\s\s+/g, ' ')).toEqual('SAST detected no vulnerabilities');
expect(vm.$el.querySelector('span').classList).toContain('ci-status-icon-success'); 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', () => { ...@@ -26,15 +26,34 @@ describe('Security Report App', () => {
resolvedIssues: [], resolvedIssues: [],
allIssues: [], allIssues: [],
}, },
dependencyScanning: {
isLoading: false,
hasError: false,
newIssues: parsedSastIssuesHead,
resolvedIssues: [],
allIssues: [],
},
}, },
hasDependencyScanning: true,
hasSast: true,
}); });
}); });
it('renders the sast report', () => { it('renders the sast report', () => {
expect(vm.$el.querySelector('.js-code-text').textContent.trim()).toEqual('SAST degraded on 2 security vulnerabilities'); expect(vm.$el.querySelector('.js-sast-widget .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.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].message);
expect(issue).toContain(parsedSastIssuesHead[0].path); expect(issue).toContain(parsedSastIssuesHead[0].path);
......
...@@ -158,6 +158,128 @@ describe('ee merge request widget options', () => { ...@@ -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', () => { describe('code quality', () => {
beforeEach(() => { beforeEach(() => {
gl.mrWidgetData = { gl.mrWidgetData = {
......
...@@ -118,6 +118,26 @@ describe('MergeRequestStore', () => { ...@@ -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', () => { describe('isNothingToMergeState', () => {
it('returns true when nothingToMerge', () => { it('returns true when nothingToMerge', () => {
store.state = stateKey.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