The go-to resource for upgrading Ruby, Rails, and your dependencies.

How to Use Bundler-Audit to Catch Vulnerabilities During CI/CD


During the Middle Ages, walled cities relied on gatehouses not solely to keep opposing armies out, but to rigorously inspect the goods coming in. A merchant bringing shipments of grain might unknowingly introduce plague-carrying rats into the populace. To prevent this, gate guards acted as a mandatory inspection checkpoint, verifying that nothing dangerous slipped through the gates.

Similarly, for a Ruby and Rails application with hundreds of third-party dependencies, we face a comparable risk of supply chain contamination. We pull in libraries to handle everything from authentication to parsing XML, and we trust that these gems are safe. However, a developer might unknowingly introduce a library with a known Common Vulnerabilities and Exposures (CVE) identifier into the codebase. To prevent these attack vectors from reaching the production environment, engineering teams should integrate automated audits into their Continuous Integration (CI/CD) pipelines.

Before we get into that, though, let’s take a step back and examine the structural philosophy of dependency management: decisions and ideas that are central to maintaining technical health are also central to understanding why automated security audits are necessary.

The Threat of Outdated Dependencies

Security risks in a legacy application often originate from the code other people write. When organizations fall behind on a Ruby and Rails upgrade, they accumulate technical debt that frequently manifests as outdated, vulnerable dependencies. There are three major risks that arise from this situation.

One is immediate security vulnerability. Attack vectors multiply as these older libraries are discovered to have flaws, and without a reliable maintenance routine, these code vulnerabilities remain unpatched in your production environment.

Another important risk is compliance failure. If your application processes sensitive user data or payment information, running End-of-Life (EOL) software or gems with known vulnerabilities can lead to immediate audit findings and failures for standards like PCI DSS or HIPAA.

The cost of maintaining software, of course, brings us to the third major risk: compounding technical debt. When you delay updating dependencies, the eventual required update becomes significantly more complex and error-prone.

There are, of course, many ways to approach a security audit. For organizations looking to secure a legacy codebase, however, establishing an automated gatehouse in your CI/CD pipeline is the most pragmatic first step.

Introducing Bundler-Audit

At a basic level, Bundler-Audit is a command-line tool that checks your dependencies for known vulnerabilities. Strictly speaking, however, Bundler-Audit is a patch-level verification tool designed specifically for Ruby applications. Rather than running your application and probing it for weaknesses from the outside, it analyzes your dependency manifest — specifically, your Gemfile.lock — and compares it against the community-maintained Ruby Advisory Database.

There’s no charge for using the Ruby Advisory Database; it’s a free service maintained by the community that aggregates advisories from sources like the CVE and GitHub Security Advisories. If your organization depends on the Ruby Advisory Database for compliance or security, it is a nice gesture to provide support or contribute back to the project—though whether that means opening pull requests, reporting vulnerabilities, or something else is up to you.

Usage

To install Bundler-Audit, we can use the gem install command:

$ gem install bundler-audit

The general usage syntax for Bundler-Audit takes the following form:

$ bundle audit check [options]

In its simplest form, you can run the check directly in an application’s root directory:

$ bundle audit check

However, the vulnerability database changes frequently. Before scanning your project, you will often want to ensure that the tool fetches the latest vulnerability data. You can do this by passing the --update flag:

$ bundle audit check --update

To see this in action, let’s illustrate how this works with a somewhat contrived project. First, we’ll create our root directory and initialize a new project:

$ mkdir audit-test
$ cd audit-test
$ bundle init

Next, let’s intentionally add an older, vulnerable version of Rack to our project:

$ bundle add rack -v 2.0.4

When we run our audit check, we see output detailing specific gems that require attention:

$ bundle audit check --update
Updating ruby-advisory-db ...
ruby-advisory-db: 752 commits, 321 advisories
Name: rack
Version: 2.0.4
Advisory: CVE-2020-8184
Criticality: High
URL: https://groups.google.com/g/rubyonrails-security/c/OWtmozPH9Ak
Title: Percent-encoded cookies can be used to overwrite existing prefixed cookie names
Solution: upgrade to >= 2.1.4, ~> 2.0.9

..snip..

Vulnerabilities found!

We can notice a few things from this output. I’ve abbreviated the above output for the sake of brevity, as Rack 2.0.4 has several known vulnerabilities. First, as expected, the ruby-advisory-db updates before the scan begins. After that, Bundler-Audit provides precise, actionable intelligence telling us exactly which gem requires a version bump, its criticality, and the targeted minimum version required to remediate the risk.

One may wonder: does running this audit modify our Gemfile or Gemfile.lock to fix the vulnerabilities? Let’s check:

$ git status
On branch main
nothing to commit, working tree clean

As we can see, running bundle audit check does not alter our dependency files; it is a read-only process. It strictly reports on the state of your application, leaving the actual upgrading decisions to you.

Programmatic Integration

For organizations that need to generate custom reports or trigger automated alerting systems, using the command-line interface might be insufficient. Fortunately, Bundler-Audit exposes a programmatic API.

Let’s write a small script to interact with the scanner directly:

require 'bundler/audit/database'
require 'bundler/audit/scanner'

# Ensure we have the latest advisories
Bundler::Audit::Database.update!

scanner = Bundler::Audit::Scanner.new
scanner.scan.each do |result|
  case result
  when Bundler::Audit::Results::UnpatchedGem
    puts "Vulnerable Gem Found: #{result.gem.name} (#{result.gem.version}) - #{result.advisory.id}"
  when Bundler::Audit::Results::InsecureSource
    puts "Insecure Source Detected: #{result.source}"
  end
end

Running this script yields output similar to our CLI tool, but with the added flexibility of formatting the results however we please:

$ ruby custom_audit.rb
Vulnerable Gem Found: rack (2.0.4) - CVE-2018-16470
Vulnerable Gem Found: rack (2.0.4) - CVE-2018-16471
..snip..

This flexibility is particularly useful for extracting structured data to send to a Slack channel or a central logging service.

Managing False Positives

You may occasionally encounter false positives. Bundler-Audit must report vulnerabilities based strictly on the version numbers in your lockfile. However, your specific implementation might not actually exercise the vulnerable code path, or you might have applied a temporary mitigation.

There are two major approaches to managing these situations; depending on your deployment environment and team workflow, one may be more useful than the other.

The first approach is to explicitly ignore specific CVEs using the command line with an --ignore flag:

$ bundle audit check --update --ignore CVE-2020-8184

This is most useful, in my experience, for one-off checks or temporary bypasses on a local development machine.

The second option is to create a .bundler-audit.yml configuration file in your project root to persist these exceptions across your entire team:

---
ignore:
  - CVE-2020-8184

Generally speaking, the second option makes more sense if you’re working in a shared codebase, as it permanently configures your CI pipeline to respect the ignored CVE without breaking local checks. This is significant because it means we can configure our pipeline to fail only when new, un-ignored vulnerabilities are introduced, effectively halting the accumulation of new security debt without blocking legitimate deployments.

Integrating into CI/CD

A security audit is not a one-time event; it is an ongoing process. To effectively secure a production environment, these automated checks must be integrated into your Continuous Integration (CI/CD) pipeline.

There are, of course, many CI/CD providers we could use—GitLab CI, CircleCI, or Jenkins—but for this example, we will use GitHub Actions. Often, this is used in automated scripts with configuration similar to the following GitHub Actions snippet:

name: Security Audit
on: [push, pull_request]
jobs:
  audit:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3
      - uses: ruby/setup-ruby@v1
        with:
          ruby-version: '3.2'
          bundler-cache: true
      - name: Run Bundler-Audit
        run: |
          gem install bundler-audit
          bundle audit check --update

If Bundler-Audit detects a vulnerability, it will exit with a non-zero status code. This behavior is deliberate; the non-zero exit code instructs your CI/CD runner to fail the build, preventing the vulnerable dependencies from being merged into the main branch or deployed to servers.

The Limitations of Automation

Automation is powerful, but it is not a panacea.

While Bundler-Audit is excellent at identifying known technical flaws in third-party gems, it lacks visibility into your proprietary code. For example, if your application contains an unescaped string in an ERB template or a flaw in its authorization logic, Bundler-Audit will not flag it as an error.

Therefore, while establishing automated dependency scanning is a necessary baseline, it is rarely sufficient on its own. For large and complex applications, organizations often combine it with static analysis tools like Brakeman to detect code vulnerabilities in their own logic.

Furthermore, if Bundler-Audit does find an issue with a widely used gem, one must assume that malicious actors are also aware of the vulnerability. Yanking the gem or upgrading the version is a start, but if the vulnerability involved exposed credentials or secrets, those credentials should be immediately rotated.

Ultimately, automating these routine dependency checks frees up your engineering team—or external Rails upgrade specialists—to focus their attention on the complex, systemic security challenges that truly require human insight. By implementing Bundler-Audit today, you take a significant, pragmatic step toward a healthier, more secure application.

Sponsored by Durable Programming

Need help maintaining or upgrading your Ruby on Rails application? Durable Programming specializes in keeping Rails apps secure, performant, and up-to-date.

Hire Durable Programming