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')