Automating Vulnerable Dependency Checking in CI Using Open Source

Varrun Ramani

In 2017, a data breach exposed the private data of millions of consumers online. A little known fact is that the Equifax Data Breach was the result of an unpatched version of Apache Struts that was vulnerable to an RCE. Also, in a report published by Aspect Security on the security analysis of JAVA libraries, it was found that 26% of all downloads have some known vulnerability in them.

In this article, we will cover ways to prevent this by integrating dependency checking for application security and strategies to automate it as part of the CI pipeline with a focus on JAVA projects.

Dependency Checking

Today's web applications rely on a ton of third-party dependencies to avoid redundant code. These can include:

  • Web frameworks like Spring and Django

  • Cryptographic libraries like OpenSSL and BouncyCastle

  • Serialization libraries like Jackson

  • Utility libraries like Apache Commons and Guava

While you can control the security of your codebase through audits, code review, and static analysis, your application has no direct control over vulnerabilities present in third party code. These dependencies are typically specified through a descriptor, like an Apache Maven POM. Vulnerabilities can either be present in the code of upstream dependencies configured explicitly or through transitive dependencies. A sample dependency tree may look like this:

+- org.springframework.security:spring-security-core:jar:5.3.3.RELEASE:compile
|  +- org.springframework:spring-aop:jar:5.2.6.RELEASE:compile
|  +- org.springframework:spring-beans:jar:5.2.6.RELEASE:compile
|  +- org.springframework:spring-context:jar:5.2.6.RELEASE:compile
|  +- org.springframework:spring-core:jar:5.2.6.RELEASE:compile
|  |  \- org.springframework:spring-jcl:jar:5.2.6.RELEASE:compile
|  \- org.springframework:spring-expression:jar:5.2.6.RELEASE:compile

Here, spring-security-core is the direct dependency and the ones below it are transitive dependencies that are pulled in.

If not patched in due time, you could be exposing your web application to be remotely exploited

When vulnerabilities are found through a reported CVE in any of these, upstream maintainers typically push out new versions with security patches. Patching the vulnerability involves updating your POM dependencies to the non-vulnerable version. If not patched in due time, you could be exposing your web application to be remotely exploited.

Detecting Vulnerabilities

While there are a plethora of proprietary tools, I’m an advocate of free and open-source software and no OSS does a better job at this than OWASP Dependency-Check. The tool checks project dependencies against configured CVE databases and publishes a report of vulnerable dependencies in various formats.

To run dependency check using the Maven plugin,

mvn dependency-check:check          

The tool also has support for different languages and package managers. For a full reference on how to use the tool and its implementation, see the docs.

Automating Dependency Checking in CI

Today's development process typically includes running commits through a Continuous Integration (CI) system that runs automated tests to help facilitate faster and iterative development while lowering the chance of regressions. In a similar vein, it is increasingly becoming important to shift security left, to find vulnerabilities as early in the development lifecycle as possible. There are two reasons why you should automate dependency checking into CI

  • Prevention vs Remediation - It's easier and cheaper to prevent introducing a vulnerable dependency before merging code than finding and fixing it post-merge

  • Library upgrades can be expensive - Security updates are often batched into the next software release. New versions can have breaking API changes or changes in functionality that can take significant effort for migration

CI Integration

While GH has native support for alerts for vulnerable dependencies using Dependabot, I’ll be using Github Workflows as an example to show this can be automated in CI including facilitating notifications to collaboration tools like Slack and Jira. To build the workflow, there are a few aspects to consider. These can be adapted to use any CI that fits your development workflows.

When to Scan

Choose when to trigger the scan. For example,

  • Scan on merges to master

  • Scan on all pull requests off the master branch

  • Run on a cron schedule

# Controls when the action will run. Triggers the workflow on push or pull request 
# events but only for the master branch
on:
  pull_request:
    branches: [ master ]

Running Dependency-Check

Running the tool in CI can be slow as it downloads the CVE database on every run in a fresh container. Fortunately, the "cache" GH action allows the workflow to cache Maven artifacts which includes the downloaded CVE database.

  # Also caches CVE database stored by Dependency-Check
    # Default path is ~/.m2/repository/org/owasp/dependency-check-data/
    - name: Cache Maven artifacts
      uses: actions/cache@v1
      with:
        path: ~/.m2/repository
        key: ${{ runner.os }}-maven-${{ hashFiles('**/pom.xml') }}
        restore-keys: |
          ${{ runner.os }}-maven-

    - name: Run Dependency Check
      run: mvn dependency-check:check

Security Workflows

There are several strategies one can adopt based on preference and security model.

Blocking Merge

Block all merges to the mainline branch unless the status check succeeds. This can be achieved using a Branch Protection Rule.

To make the task fail on vulnerabilities, set the

failBuildOnCVSS
or
failBuildOnAnyVulnerability
configuration parameters. The latter can be used to control failure only for vulnerabilities above a certain CVSS score.

This provides the best security control but can create developer friction if the scanning has sufficient false positives.

Self-Service Reports

In order to empower the developer to own the security of their own code, the dependency check report can be uploaded as an artifact.

 - name: Upload Dependency Check Report
      uses: actions/upload-artifact@v1.0.0
      with:
        # Artifact name
        name: dependency-check-report.html
        # Destination path
        path: dependency-check-report.html

If the task fails, the report can be downloaded from the task.

Security Alerts

If you prefer an approach with less friction, you can choose an alert-only approach where the security team is alerted of vulnerabilities present in a new branch. This typically requires some manual triage by a security engineer. In the following example, alerts are sent to a Slack channel for triage by a script that parses the JSON output of the dependency check.

 - name: Slack Notify
        run: python parse_dep_check_result.py dependency-check-report.json ${{ secrets.slack_webhook_url }}

As an extension, you can also file issues in a bug reporting system like Jira automatically on detection.

Managing False Positives

There can be false-positives for various reasons like:

  • The dependency is vulnerable but the application does not use the affected code path

  • The vulnerability is tracked in a bug reporting system but not yet fixed

  • A scan detects a newly reported vulnerability in the mainline branch

Such CVEs can be suppressed by adding an entry to the XML suppression files. The HTML report has a button to generate this XML.

Overall, choose the approach that best suits your development and security philosophy. For the full workflow, click here.

Varrun Ramani
Staff Software Engineer, Security

Varrun is the tech lead of the Engineering Security team at Okta where he embraces SecDevOps principles. His interest in security started with participating in CTFs and organizing them, leading to a stint on the offensive side as a security engineer at VMware. His passion for building things led him to Okta where he builds features and tools to keep the platform and product secure and evangelizes security amongst developers.