Vulnerability Management

bundler-audit Production Setup

A practical guide to running bundler-audit in production CI pipelines, including advisory database updates, exception handling, and integration with remediation workflows.

Nayan Dey
Senior Security Engineer
7 min read

bundler-audit has been the default answer for Ruby CVE scanning for the better part of a decade, and it remains the most widely deployed tool in the Ruby ecosystem for catching known vulnerabilities in your Gemfile.lock. What has changed over the years is the production story around it. The gem itself, currently at version 0.9.2 as of mid-2024, has matured slowly and deliberately, but the surrounding practices, how you keep the advisory database fresh, how you handle exceptions, how you integrate results into your remediation workflow, have evolved much more. This post is a field guide to running bundler-audit as a reliable gate in a modern CI pipeline, not just as a scary command you run once and forget.

The tool itself is simple. It cross-references the gems pinned in your Gemfile.lock against the Ruby Advisory Database maintained by Ruby Central, and it prints any matching advisories. But the gap between running it locally and running it as a production gate is substantial, and teams routinely get tripped up by the same handful of issues.

How do you keep the advisory database current?

bundler-audit ships with a bundled copy of the Ruby Advisory Database, but that snapshot is frozen at the time the gem was released. For production use, you need to fetch the latest advisories before every run. The command is bundler-audit update, which pulls the latest database from the ruby-advisory-db GitHub repository. In a CI environment, this is non-negotiable. Without it, you will miss advisories published after the bundler-audit version you have installed.

The practical wrinkle is that the update command talks to GitHub, and GitHub rate-limiting can bite you in high-frequency CI setups. Teams running hundreds of builds per hour have seen transient failures from anonymous rate limits. The fix is to either authenticate the clone using a GitHub token in the GIT_ASKPASS environment variable, or to cache the database in your build system and refresh it on a schedule rather than per-build. A common pattern is to run bundler-audit update in a nightly job and commit the advisory directory to an internal artifact store, then have each build pull from the artifact store. This also protects you from a GitHub outage taking down your security gate.

What does a robust CI invocation look like?

The minimum viable invocation is bundle exec bundler-audit check --update, but production setups need more. A flag that gets overlooked is --ignore, which takes a space-separated list of advisory IDs to skip. This is how you handle known-false-positives or accepted-risk situations without silencing the tool entirely. The IDs correspond to the CVE or GHSA identifier in the advisory database, for example CVE-2024-26144 for the Rack HTTP range header vulnerability disclosed in March 2024.

A more durable pattern is to commit a .bundler-audit.yml file in the repository root that lists ignored advisories with expiration dates and commented justifications. The tool respects this file automatically. Example:

ignore:
  - CVE-2024-26144
  - GHSA-54rr-7fvw-6x8f

Pair this with a scheduled job that re-audits the ignore list monthly and fails if an entry is older than 90 days without a documented extension. This forces exceptions to be revisited rather than accumulating silently, which is how most security gates become useless over time.

How should you handle the exit code?

bundler-audit returns exit code 0 for clean runs and 1 for any finding, regardless of severity. For production gates, this is often too blunt. A CVSS 3.1 finding that is not exploitable in your configuration should not block a hotfix deploy at 2 a.m., even if a CVSS 9.8 finding should. The tool does not currently ship a severity-aware exit code, so teams wrap it.

A minimal wrapper reads the JSON output with bundler-audit check --format json, filters by severity, and sets its own exit code. The JSON format was stabilized in bundler-audit 0.9.0 released in October 2022 and has been stable since. A production script typically treats critical and high findings as blocking, medium findings as warnings that fail only if they have been open more than 14 days, and low findings as informational. The 14-day grace period is configurable but should not exceed 30 days without justification.

What about transitive dependencies?

bundler-audit audits every gem in Gemfile.lock, which includes transitive dependencies. This is the right default, but it means that a low-severity finding in a gem pulled in by Rails three levels deep will appear in your report even if you cannot upgrade Rails for months. The tool does not distinguish between direct and transitive dependencies in its output, which makes triage harder than it needs to be.

A workaround is to parse Gemfile.lock yourself and annotate findings with their dependency depth. The bundler gem exposes a programmatic interface via Bundler::LockfileParser that lets you reconstruct the dependency graph. With that in hand, you can sort findings by depth and spotlight the ones in your direct dependencies first, where your remediation leverage is highest. Transitive findings often resolve themselves when you upgrade the parent gem, so burning triage cycles on them prematurely is wasteful.

How do you integrate with ticketing?

The tool's output is designed for humans reading a terminal, not for feeding into Jira or GitHub Issues. Teams that want one ticket per advisory need to build their own mapping layer. The JSON output gives you the advisory ID, CVSS score, affected gem, and fix version, which is enough to template a ticket description.

The tricky part is deduplication. If your audit runs on every pull request, you do not want 30 Jira tickets for the same advisory. The standard approach is to use the advisory ID as a stable key, check if an open ticket already exists, and update it rather than creating a new one. The ticket body should include the current affected gem versions in your lockfile and a link to the fix version in the advisory. When the gem is upgraded and the advisory no longer matches, the ticket auto-closes based on the next clean audit run.

What does it not catch?

bundler-audit only catches advisories that exist in the Ruby Advisory Database. Zero-days, advisories that have been disclosed but not yet ingested, and vulnerabilities in dependencies outside the Ruby ecosystem, native libraries, Node packages pulled in by gems with embedded JavaScript, are all invisible to it. The advisory database has a latency between upstream CVE publication and local ingestion that has averaged 6 days through 2023 and 2024, with occasional spikes when a big CVE hits and the volunteer maintainer team gets backlogged.

For gems that wrap native libraries, OpenSSL, libxml2, libsqlite3, the advisory coverage is particularly patchy because the vulnerability is often in the native library rather than the gem wrapper. A gem like nokogiri may be on a safe version by its own advisory record even if the libxml2 it links against has an unpatched CVE. You need a separate tool, typically an SBOM-based scanner, to catch these.

Putting it together

A production-grade bundler-audit setup looks like: advisory database cached and refreshed nightly from an internal artifact store, invocation in CI with --format json and a severity-aware wrapper, .bundler-audit.yml for explicit exceptions with expiration dates, transitive-finding annotation using the lockfile parser, and deduplicated ticket creation keyed on advisory ID. This takes a couple of days to set up the first time and then runs with minimal babysitting. The investment pays off the first time a critical CVE hits a popular gem and you have confidence that your gate will block it.

How Safeguard Helps

Safeguard ingests bundler-audit output alongside SBOM-based scanning and native-library CVE feeds, giving you a single triage surface that catches both gem advisories and the upstream native issues bundler-audit misses. We auto-deduplicate findings across repositories, track advisory exceptions against their expiration dates, and route tickets into the workflow your team already uses. When a new advisory lands in the Ruby Advisory Database, we cross-reference it against every Gemfile.lock we have scanned and alert on real exposure rather than generic presence.

Never miss an update

Weekly insights on software supply chain security, delivered to your inbox.