A tool that helps with scanning for security vulnerabilities in dependencies can be a great addition to a CI/CD stack. Using it poorly can leave you with a false sense of security.

1. OWASP DependencyCheck

Many software applications are constructed using third party libraries. From time to time, vulnerabilities are discovered in such libraries, and those are commonly reported and recorded in public data repositories such as the CVE (Common Vulnerabilities and Exposures) database.

This database can then be used by software developers to check if the libraries that they use contain vulnerabilities. Because this is a common concern that isn’t necessarily unique to the functional domain that a developer works in, it’s nice that there are organizations such as OWASP (Open Web Application Security Project, https://owasp.org/), that provide tools and support for addressing this concern.

One such tool that it provides is DependencyCheck (https://owasp.org/www-project-dependency-check).

Dependency-Check is a Software Composition Analysis (SCA) tool that attempts to detect publicly disclosed vulnerabilities contained within a project’s dependencies. It does this by determining if there is a Common Platform Enumeration (CPE) identifier for a given dependency. If found, it will generate a report linking to the associated CVE entries.
— OWASP on DependencyCheck

This is a great tool that developers can employ. But delegating the concern for vulnerabilities to a tool is certainly not without effort, nor is it without risk. Where without it, you can be in the dark on the vulnerabilities you may have; with it, you could unjustly be thinking you’re safe, which is arguably worse.

This blog will show the basic way of using DependencyCheck, and then highlight some pitfalls and how to address them.

2. Basic use of DependencyCheck

DependencyCheck can be used on a local development machine, or in a CI/CD pipeline. When running it using Gradle and CircleCI, this would look like the following:

Locally

./gradlew dependencyCheckAnalyze

As part of a CircleCI job (which would be in the config.yml)

owasp:
 docker:
   - image: cimg/openjdk:17.
 working_directory: ~/repo
 resource_class: medium
 steps:
   - checkout
   - run:
       name: Run owasp plugin
       command: ./gradlew dependencyCheckAnalyze
   - store_artifacts:
       name: Store OWASP Dependency-Check report
       path: service/build/reports/dependency-check-report.html

The CircleCI job does the following:

  1. Start up a docker container

  2. Checkout the code

  3. Run dependencyCheckAnalyze using the Gradle plugin

  4. Persist the generated DependencyCheck report in a working directory that is available after the job ends, so that you can watch the results later.

This job would then be run as part of your regular CircleCI build and/or deploy workflow. More on that later.

3. Problems introduced by OWASP DependencyCheck

When running DependencyCheck in your CI/CD, there are some gotchas that may leave you thinking you’re not vulnerable, when you are. This has to do with your configuration of what constitutes “failure” of its results - or more specifically: with the defaults that apply when you lack your own configuration.

3.1. Default CVSS

DependencyCheck uses something called CVSS (Common Vulnerability Scoring System) to grade CVE. When running as part of a CI/CD pipeline, this score is what determines whether the build of a certain version of your code is considered to have failed.

The configuration property for this is failBuildOnCVSS and as you can read in the DependencyCheck configuration documentation (https://jeremylong.github.io/DependencyCheck/dependency-check-gradle/configuration.html), there is a gotcha here:

  • The score range goes from 1 to 10.

  • The default score is 11.

Guitar amp volume control that goes to 11. Taken from the Spinal Tap film

In practice, this means that if you do not override the default, nothing is considered to be vulnerable enough to warrant failing the build. With the job mentioned in the last section, it would look like this:

CircleCI pipeline showing green checkbox hiding detected vulnerabilities

Even though the generated report contains high and critical CVE:

DependencyCheck report showing detected vulnerabilities

This is in the documentation, but many developers glance over it and leave it at the default. And thus CircleCI will tell them that they’re safe, even after DependencyCheck reports the most critical vulnerabilities.

The solution, when working with Gradle, is to add a dependencyCheck configuration to the build.gradle file:

dependencyCheck {
   failBuildOnCVSS = 7 // https://nvd.nist.gov/vuln-metrics/cvss
   ..
}
check.dependsOn dependencyCheckAnalyze

As you can see, the failBuildOnCVSS is set to 7 here. This will make the build fail on the highest and critical CVE. Depending on application and organization security requirements, you might want to set a lower value to catch more.

3.2. Default outputdirectory

Persisting the generated DependencyCheck analysis report is another thing that can fail silently and yet report a success.

Given this build pipeline status:

DependencyCheck report job with a green checkmark

You would think that the report was successfully generated and stored in CircleCI. But when you return to it later to read the results and see what you need to address, you’ll find that the Artifacts tab under which CircleCI makes a stored artifact available is empty. And that is because the persistence step actually failed:

DependencyCheck report job expanded to show it couldn’t find the generated report

DependencyCheck by default generates a report in build/reports/dependency-check-report.html whereas the CircleCI config.yml file shown above looks for it in services/build/reports…​

You could update the CircleCI config.yml step to reflect the proper output directory:

   - store_artifacts:
       name: Store OWASP Dependency-Check report
       path: build/reports/dependency-check-report.html

Or you could update build.gradle to manually set the outputDirectory to a value that you find more convenient:

dependencyCheck {
    failBuildOnCVSS = 7
    outputDirectory = “${projectDir}/<the location of your report>”
    ..
}

3.3. Multiproject repository scan

Even when you’ve followed the previous steps to properly configure failBuildOnCVSS and outputDirectory, you may find yourself in a situation where critical CVE are reported despite your pipeline being all green. This can be the case when you’re scanning a multiproject application using

./gradlew dependencyCheckAnalyze

and the CVE are found in any of the subprojects.

./gradlew dependencyCheckAggregate

Coincidentally, this works for singleproject applications too. I’m not aware of a good reason to not just always use this. Maybe a performance consideration? If you do decide to use the singleproject dependencyCheckAnalyze task and move to a multiproject application later, don’t forget to change to dependencyCheckAggregate as - again - DependencyCheck might end up making you think you’re not vulnerable, when you are.

Also don’t forget to adjust your build.gradle:

dependencyCheck {
    ..
}
check.dependsOn dependencyCheckAggregate

3.4. Dealing with reported CVSS threshold violations

Once you’ve ascertained that you’ve configured DependencyCheck properly and your build will fail on critical vulnerabilities, and you’ve got reports for the details of those vulnerabilities, you’re left with the next most important task: analyzing and addressing them. This too is a task that comes with some caveats and considerations.

Suppose a scan has reported 2 critical violations that are failing your build. There are several next steps you may be able to take.

Upgrade

Often, the solution to a reported vulnerability is to upgrade the affected library to its latest version. If you’re lucky, it’s an explicitly listed dependency. Sometimes, that’s not the case and it’s a transitive dependency that gets pulled in via another dependency. You can look at the report where it was found, and run

./gradlew <submodule>:dependencies | grep <affected libraryname>

to find it. You can then exclude it and/or add it as an explicit dependency with a pinned safe version.

Suppress

Sometimes a reported vulnerability is a false positive, and you can suppress it by adding it to a suppression file as follows:

dependencyCheck {
    failBuildOnCVSS = 7
    outputDirectory = “${projectDir}/<the location of your report>”
    suppressionFile = "${projectDir}/dependencyCheckSuppress.xml"
    ..
}

The format for the suppressionfile might seem daunting, but actually you can find the XML fragments to add in the HTML report that DependencyCheck generates for you by clicking the dependency in it and looking for the “Suppress” button:

DependencyCheck report offering to generate a suppress XML fragment

This will open a preview with XML that you can add to the dependencyCheckSuppress.xml file at the location that you’ve configured in your build.gradle.

Now before you do this, take the effort to browse the links in the report to more information on the CVE, and take a look at GitHub issues and discussions for the affected library. You may conclude that indeed it’s a false positive, or you may conclude that it’s a vulnerability that doesn’t affect you. For instance, the aforementioned CVE is something in the Spring Core framework that is not going to be fixed (see https://github.com/spring-projects/spring-framework/issues/24434), but arguably only is a concern if you use the HTTPInvokerServiceExporter or RemoteInvocationSerializingExporter classes from this library. If you do not use those classes, you can suppress this vulnerability.

Note, however, that if you suppress a CVE for a library because you don’t use it in a risky way right now, that you won’t be warned later should you decide to start using it in a way that the CVE specifically relates to. DependencyCheck does offer some kind of mitigation for this: by allowing you to add an expiration date to a suppression rule, it can remind you to reevaluate your suppressed CVE after some time.

<suppress until="2020-01-01Z">
Wait

If you’re using a library that is less actively maintained, a CVE might not be immediately addressed. You can’t upgrade, doing nothing fails your build, and suppressing the warning puts you at risk. That puts you in a bit of a pickle.

You could suppress the warning to make your pipeline succeed, and add a reminder to your task backlog to regularly check for updates.

To speed things up, it helps to file a bug report.

If the maintainer allows it, you could speed things up even more by providing a fix yourself and offering it via pull request.

As a last resort, if the library is abandoned, you could find a fork, or even make your own, onboarding the responsibility of maintenance.

Lower your tolerance

Once you’ve found a mode of operation that allows you to deal with CVE given the CVSS that you’ve configured (7, in the example in this blog), you could consider lowering the threshold of vulnerability scores that you find tolerable, and start addressing findings of lower severity.

Share concerns

In a microservices architecture, running DependencyCheck in every pipeline may result in developers across multiple teams having to address similar CVE findings. This is a reason to consider introducing a centralized library that governs those shared dependencies, with its own pipeline. That way, common CVE only have to be addressed once. Beside the challenges of ownership introduced by a such a shared library however, this does carry the risk that a CVE, like the Spring CVE mentioned earlier, is a false positive for one application because it doesn’t use an affected class, but is an actual risk to another because it does.

Nightly build

Analyzing and addressing CVE can be detrimental to your focus, and introducing DependencyCheck and having it fail your build may become frustrating to your ability to deliver. When you start that pipeline, your state of mind is usually centered on the impact it’s going to have, and not on a vulnerability in some obscure library you’ve been transitively depending on for months without problems.

Depending on the risk profile of your application and organization, your historical ability to stay up to date with security fixes and the rate of deployments, you may want to consider having a separate workflow to run the OWASP DependencyCheck job to scan your master branch. It does require discipline to monitor such jobs and address them in timely manner.

In CircleCI, given that you have the OWASP job as defined earlier in this blog, this workflow would look like this:

workflows:
 version: 2
 nightly-owasp-trigger:
   triggers:
     - schedule:
         cron: "0 0 * * *"
         filters:
           branches:
             only:
               - master
               - main
   jobs:
     - owasp

4. Conclusion

A tool that helps with scanning for security vulnerabilities in dependencies can be a great addition to a CI/CD stack. However, configuring and running it properly, analyzing its reports, forming an opinion on the findings therein and fixing them where needed is still not a trivial task. Spend some effort in setting it up correctly and fitting it into your software delivery way of working. This helps avoid it giving you a false sense of security.

shadow-left