gitlab-ci dependency scanning using npm audit
December 19, 2018
Evan Lucas <evanlucas@me.com>
Recently, GitLab introduced built in Dependency Scanning to CI pipelines.
In order to utilize this feature, it is necessary to add a job to .gitlab-ci.yml
that looks something like this:
dependency_scanning:
image: docker:stable
variables:
DOCKER_DRIVER: overlay2
allow_failure: true
services:
- docker:stable-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:
reports:
dependency_scanning: gl-dependency-scanning-report.json
GitLab uses retire.js under the hood
for these vulnerabilities. While retire.js
is extremely useful, npm
has a
built-in command (npm audit
) to audit your dependencies. What if we used that
instead?
One of the biggest reason for trying to get this to work was to reduce the
build time in our CI pipeline. Currently, the dependency_scanning
job
takes roughly a minute and a half to run. A lot of this time is spent
pulling the docker image. If we could use npm audit
to generate a report
similar to one generated by retire.js
, we may not even need an additional
job. We already have a test
job that looks similar to this:
test:integration:
image: node:10
stage: test
services:
- docker:dind
- rabbitmq:3-management-alpine
variables:
DOCKER_HOST: tcp://docker:2375/
DOCKER_DRIVER: overlay2
COVERAGE_DIR: /builds/$CI_PROJECT_PATH/coverage
except:
refs:
- tags
- master
variables:
- $CI_COMMIT_MESSAGE =~ /\[skip tests\]/
- $CLEANUP_FRONTEND
script:
- mkdir -p ${COVERAGE_DIR}
- npm ci
- npm run test:ci
artifacts:
expire_in: 3 days
paths:
- ${COVERAGE_DIR}
reports:
junit: [coverage/voltron.xml]
We can add this report to the test job above like so:
test:integration:
image: node:10
stage: test
services:
- docker:dind
- rabbitmq:3-management-alpine
variables:
DOCKER_HOST: tcp://docker:2375/
DOCKER_DRIVER: overlay2
COVERAGE_DIR: /builds/$CI_PROJECT_PATH/coverage
except:
refs:
- tags
- master
variables:
- $CI_COMMIT_MESSAGE =~ /\[skip tests\]/
- $CLEANUP_FRONTEND
script:
- mkdir -p ${COVERAGE_DIR}
- npm ci
- npm audit --json > audit.json
- node tools/audit-report.js
- npm run test:ci
artifacts:
expire_in: 3 days
paths:
- ${COVERAGE_DIR}
reports:
junit: [coverage/voltron.xml]
dependency_scanning: [gl-dependency-scanning-report.json]
Where tools/audit-report.js
looks like this:
'use strict'
const fs = require('fs')
const audit = require('../audit.json')
const advisories = Object.values(audit.advisories)
const result = []
function getPriority(priority) {
switch (priority.toLowerCase()) {
case 'moderate':
return 'Medium'
case 'low':
return 'Low'
default:
return 'High'
}
}
for (const advisory of advisories) {
const {title, overview, recommendation, severity, url} = advisory
const message = `${title}\n\n${overview}`
const cve = advisory.cves && advisory.cves.length
? advisory.cves[0]
: null
result.push({
message
, cve
, cwe: advisory.cwe
, solution: recommendation
, url
, priority: getPriority(severity)
})
}
const filename = 'gl-dependency-scanning-report.json'
fs.writeFileSync(filename, JSON.stringify(result), 'utf8')