Safeguard's software composition analysis (SCA, generally available) reads a Ruby project's Gemfile and Gemfile.lock, resolves every direct and transitive gem the lockfile pins, and matches each one against vulnerability advisories enriched with FIRST EPSS scores and the CISA Known Exploited Vulnerabilities catalog. For Ruby, Safeguard's docs list static and dynamic reachability. When a fixed version exists, Griffin can open a pull request that updates both files, but autonomous remediation ships switched off, with every category set to Manual.
This post is for the AppSec lead or platform engineer with a Rails monolith, a few smaller Ruby services and some internal gems. It covers the files Safeguard reads, how deep it resolves, what reachability can tell you for Ruby, how EPSS and KEV change the order of work, what a Bundler fix pull request contains, license data, and the CLI and CI commands as the docs show them, then the limits.
Key takeaways
- SCA (GA) supports Ruby Bundler. The CLI support matrix lists
Gemfileas the manifest andGemfile.lockas the lockfile, with full support. Safeguard honours both, so resolution matches what Bundler actually installs. - Deep dependency scanning (GA) resolves
Gemfile.lockand walks the dependency graph up to 100 levels, keeping the full path from your app to each vulnerable gem. - Vulnerability management with EPSS and KEV (GA) puts exploited-in-the-wild issues first. Each finding shows CVE ID, CVSS, EPSS, exploitable status, affected gem and version, fixed version and reachability.
- Reachability analysis (GA as a capability): for Ruby the docs list static and dynamic reachability, with "conservative resolution for metaprogramming". Anything the analyser cannot decide is marked Unknown and treated as potentially reachable.
- Autonomous remediation and AI Remediate fix pull requests (GA, ships off): Ruby Bundler has full AI Remediate support in the docs. Every category starts at Manual until you change it.
- License compliance (GA) classifies each gem's license and enforces your allow and deny lists.
safeguard scanwrites alicense.jsonalongside its other results. - Policies and gates (GA) decide whether a build passes. The CLI's
safeguard scandoes not fail on findings; gating happens in the platform or withsafeguard ci scan --wait --fail-on. - CLI (GA), local runner (GA) and IDE extensions (GA) are the ways to run it. Griffin natural language search inside the app is part of in-app agentic search, which is GA.
Why Ruby SCA has to start from Gemfile.lock
A Rails application's Gemfile usually lists a few dozen gems. The application actually runs several hundred. Rails alone pulls in Action Pack, Active Record, Active Support and their own dependencies, and each of those brings Rack, Nokogiri, concurrent-ruby, i18n and more. The vulnerabilities that cause trouble are rarely in the gems you typed. They sit two or three levels down, in a gem you have never opened.
What Bundler records, and why the lockfile is the source of truth
The RubyGems and Bundler guides describe the mechanism plainly. When you run bundle install, Bundler resolves every requirement in the Gemfile, installs what it found, and writes a snapshot of every gem and version it installed to Gemfile.lock. The guide calls the lockfile a record of the third-party code your application ran "the last time you know for sure that everything worked."
Two details from that guide matter for security scanning.
First, version constraints in the Gemfile are ranges. The pessimistic operator ~> 2.0.3 means "at least 2.0.3 and below 2.1", according to the Gemfile reference. Gems declare ranges for their own dependencies too. A scanner that reads only the Gemfile has to guess which version inside each range you got, and for transitive gems it has nothing to read at all.
Second, Bundler treats applications and libraries differently. The guide says applications should commit Gemfile.lock, while a library that other applications depend on should not, because "a library's lockfile is ignored by any application that depends on it." That has a direct consequence for SCA: your internal gems are judged by the lockfile of the application that consumes them, not by their own repository.
So the lockfile is what tells you the truth about a deployed Ruby application. The GEM section lists each resolved gem with its exact version and its own requirements. PLATFORMS lists the platforms the bundle was resolved for. DEPENDENCIES repeats what the Gemfile asked for. Recent Bundler versions can also write a CHECKSUMS section; bundle lock --add-checksums adds it, per the bundle-lock reference. A trimmed lockfile looks like this:
GEM
remote: https://rubygems.org/
specs:
actionpack (7.1.3)
actionview (= 7.1.3)
activesupport (= 7.1.3)
rack (>= 2.2.4)
rack-session (>= 1.0.1)
nokogiri (1.16.2-x86_64-linux)
racc (~> 1.4)
rack (3.0.9)
racc (1.7.3)
PLATFORMS
x86_64-linux
DEPENDENCIES
rails (~> 7.1)
puma (~> 6.4)
BUNDLED WITH
2.5.6
The versions above are illustrative, chosen to show the format. Notice that rack never appears in DEPENDENCIES. Only the GEM specs tell you it is present and which version it is.
What the standards ask for
None of the major secure development frameworks say "scan your Gemfile.lock". They describe outcomes, and the lockfile is how a Ruby team meets them.
NIST SP 800-218, the Secure Software Development Framework (SSDF) version 1.1, published February 2022, includes several tasks that land directly on dependency management:
- PS.3.2 asks producers to "collect, safeguard, maintain, and share provenance data for all components of each software release", and gives an SBOM as the example. For Ruby, every gem in the lockfile is a component.
- PW.4.4 asks you to verify that acquired open source and other third-party components comply with your requirements throughout their life cycles. Its first example is regularly checking for publicly known vulnerabilities.
- RV.1.1 asks you to gather information on potential vulnerabilities in the software and the third-party components it uses, including by monitoring vulnerability databases.
- RV.2.2 asks you to plan and implement risk responses for each vulnerability, which is where prioritisation comes in.
OWASP Top 10:2025 moved the old "Using Components with Known Vulnerabilities" category into A03:2025 Software Supply Chain Failures. The page says you are likely vulnerable if you do not carefully track the versions of all components you use, "both client-side and server-side", including nested transitive dependencies. Its mapped CWEs include CWE-1104 (use of unmaintained third-party components) and CWE-1395 (dependency on vulnerable third-party component). Among its prevention steps: centrally generate and manage an SBOM, remove unused dependencies, and continuously inventory component versions.
CISA's Known Exploited Vulnerabilities catalog is, in CISA's words, "the authoritative source of vulnerabilities that have been exploited in the wild", and CISA asks organisations to use it as an input to their prioritisation. FIRST's Exploit Prediction Scoring System (EPSS) publishes a daily probability, between 0 and 1, that a published CVE will be exploited in the next 30 days. Together they let you rank a Ruby backlog by evidence of exploitation instead of by CVSS alone.
The community side of Ruby has its own advisory source. The Ruby Advisory Database (rubysec/ruby-advisory-db) describes itself as "a database of vulnerable Ruby Gems", keyed by gem name with one file per CVE or GHSA identifier, and it is the data behind bundler-audit. The OSV schema names RubyGems as an ecosystem, so OSV records for gems carry the same structure as any other ecosystem. Safeguard's vulnerabilities docs list NVD, GitHub Security Advisories, OSV, CISA KEV, vendor bulletins and researcher disclosures as its data sources.
Which Ruby files does Safeguard read?
Only what the docs list, and for Ruby the list is short and clear.
| Source | What Safeguard reads for Ruby | Where the docs say so |
|---|---|---|
| CLI support matrix | Bundler: manifest Gemfile, lockfile Gemfile.lock, status Full Support | /docs/cli/support-matrix |
| SCA page | "Ruby Bundler" among supported ecosystems; manifests and lockfiles both honoured | /docs/sca |
| Deep dependency scanning | "Ruby Bundler: resolves Gemfile.lock" | /docs/deep-dependency-scanning |
| AI Remediate | Ruby, Bundler, AI Remediate support: Full | /docs/esscm/auto-fix |
| Container images | Components found inside image layers are covered by SCA | /docs/sca |
Three things are absent from that list, and you should not assume them:
- A
.gemspecon its own. The support matrix listsGemfileandGemfile.lockfor Bundler. It does not list.gemspecas a separate manifest. A gem repository that usesgemspecinside itsGemfilestill has aGemfile, but if you do not commit a lockfile for that library (as Bundler recommends), what Safeguard can see is the range, not a resolved set. gems.rbandgems.locked. Bundler accepts these alternative file names. The Safeguard docs do not mention them, so treat them as unsupported until you confirm otherwise in a trial.- Installed gems on a host. Scanning a server's installed gem directory is not described for the CLI's directory scan. If the gems are inside a container image you build, scan the image instead, because SCA covers "components discovered inside container image layers".
The practical rule follows: commit Gemfile.lock for every application, keep it current, and point Safeguard at the repository or the built image.
How does Safeguard resolve transitive gems?
Safeguard's deep dependency scanning (GA) walks every edge in the resolved dependency graph up to level 100. At each node, the docs say it resolves the package URL and version against its vulnerability knowledge graph, computes license and provenance, feeds the node into reachability analysis, and records the full path from root to leaf, so you can see why a gem is present.
For Bundler that graph comes from Gemfile.lock. Because the lockfile already lists every resolved gem with exact versions, a Ruby project does not depend on Safeguard guessing versions inside ranges. The CLI's --resolve-transitive flag, which resolves transitive dependencies through an external dependency service, exists for manifests that lack a full lockfile. For a Bundler application with a committed, current lockfile, it is usually unnecessary.
When you open a finding in the ESSCM interface, you see the chain that pulls the vulnerable gem in, whether that chain is reachable, whether another path to the same gem exists, and what the safe version would change elsewhere.
That matters in Ruby because parents often pin children tightly (Action Pack pins its sibling Rails frameworks with =). If the fixed version is outside the parent's range, you move the parent, and the path view shows that before anyone edits a file.
Deep scanning also produces a reverse signal: gems present in the tree but never imported, which are candidates for removal. The docs note there is no CLI command for this yet, so it is an in-app view.
Reachability for Ruby: static and dynamic
Safeguard's reachability analysis and prioritisation is GA as a capability. Language support varies, and the docs are specific about Ruby:
| Language | Static reachability | Dynamic reachability | Docs note |
|---|---|---|---|
| Ruby | GA | GA | Conservative resolution for metaprogramming |
The three kinds of reachability, as the docs describe them:
- Static reachability builds a call graph from your source, lockfile and bytecode and checks whether any path leads to the vulnerable code. For Ruby it is GA, with conservative resolution for metaprogramming.
- Dynamic reachability uses a runtime collector that records which modules and functions are loaded and invoked in your production workloads, and flags never-executed vulnerable paths as cold. For Ruby this is GA, but it only helps where that collector runs in your environment. If you have not deployed it, you have static reachability and nothing else.
- Configuration reachability parses framework configuration to decide whether code is wired in at all. The docs give Spring bean definitions and Next.js route files as examples. They do not name a Rails example, so do not assume Rails routes or initialisers are parsed.
Each finding is classed as Reachable, Conditionally reachable, Unreachable or Unknown. Unknown covers the cases the analyser cannot decide, such as dynamic dispatch, reflection and native code, and the docs say Unknown is marked conservatively as potentially reachable.
Why Ruby is hard for static reachability
Ruby code calls methods that do not exist in the source as written: send calls a method named by a runtime value, method_missing answers undefined calls, define_method creates methods at load time, Rails autoloading resolves constants by name, and DSLs such as before_action :authenticate turn symbols into calls. A call graph built from source will miss some of these edges.
The docs' answer is "conservative resolution": when Safeguard cannot prove a path does not exist, it does not claim the code is unreachable. That is the right default for security, and it has a cost you should plan for. Expect a larger share of Unknown and Conditionally reachable findings in a Ruby codebase than in a Go or Java one, and do not treat Ruby reachability as a filter that lets you ignore large parts of the backlog.
The docs also list where reachability is weakest in any language: code run through reflection, eval or unsafe dispatch; data-driven vulnerabilities, where a parser is dangerous because of the input it receives even if the obvious entry point looks unreachable; and paths that only run under configurations the analyser cannot enumerate. Many Ruby parser and regular expression flaws are data-driven. For those, rely on KEV, EPSS and exposure.
Prioritising Ruby findings with EPSS and KEV
Safeguard's vulnerability management (GA) puts each finding into one of five categories: Critical, High, Medium, Low and Exploitable, where Exploitable means known active exploitation in the wild. Each finding carries a Priority Score from 0 to 100, computed from base severity (CVSS 3.1), EPSS probability, KEV listing, public proof-of-concept availability, the reachability class, the environment, exposure, data sensitivity and business criticality. Safeguard ingests the EPSS feed daily.
The weights are configurable per tenant under Settings › Prioritization. The documented defaults:
| Signals | Default priority band |
|---|---|
| Reachable, KEV-listed, production | 90 to 100 (P0) |
| Reachable, EPSS above 0.5, production | 75 to 89 (P1) |
| Reachable, high CVSS, staging | 50 to 74 (P2) |
| Unreachable, any severity | 0 to 30 (backlog) |
Because Unknown counts as potentially reachable, few Ruby findings fall into the backlog band, and KEV and EPSS do more of the sorting work for Ruby than for languages with GA static reachability.
In the Vulnerabilities tab of a project, the table has Priority and Reachability columns and sorts by Priority by default. The docs list these filters:
reachable:true
kev:true
epss:>0.3
env:production reachable:true kev:true
The last line is the documented "P0 backlog" query. You can also filter by severity, CVSS range, EPSS range, exploitable status, component name, fix availability, reachability status and date range.
Griffin natural language search, part of in-app agentic search (GA), accepts questions such as "Show my P0 findings in production that are reachable." It is a convenience on top of the filters, not a different data source.
For a deeper treatment of how the score is built and how to defend the order in an audit, see prioritise vulnerabilities with reachability, EPSS and KEV in Safeguard.
Step by step: scan a Ruby application with Safeguard
The walkthrough below uses only commands and screens shown in Safeguard's docs, plus standard Bundler commands from the RubyGems guides. Where the docs do not show a screen, the step describes the outcome instead.
1. Make the lockfile trustworthy before you scan it
Safeguard can only be as accurate as the Gemfile.lock it reads. Before the first scan:
- Confirm
Gemfile.lockis committed for every application repository. For libraries, follow Bundler's guidance and scan the applications that consume them. - Confirm it matches the
Gemfile. In CI, Bundler's deployment mode requires an up-to-date lockfile and errors if it is stale:
bundle config set --local deployment true
bundle install
- If you deploy to Linux but develop on macOS, make sure the lockfile lists your production platform. The bundle-lock reference documents
bundle lock --add-platformfor this. A lockfile resolved only forarm64-darwincan name different native gem builds from the ones your servers install. - Optionally run
bundle lock --add-checksumsso the lockfile records checksums for each gem.
Our earlier post on Bundler frozen and deployment mode in CI covers the CI side in detail.
2. Install the Safeguard CLI and sign in
The CLI (GA) is a single static binary for macOS, Linux and Windows. It needs no Ruby, Node, Python or JVM runtime. From the CLI docs:
# macOS and Linux
curl -fsSL https://cli.safeguard.sh/install | bash
# Windows
irm https://cli.safeguard.sh/install.ps1 | iex
Then open the interactive terminal and sign in:
safeguard
Type /login to authenticate with the OAuth device-code flow, then /scan . to scan the folder you are in. From the command line, safeguard login does the same, and safeguard login --no-browser prints a URL and code for a machine without a browser. safeguard whoami shows who you are signed in as.
3. Run a first local scan and read the output
From the root of the Rails application:
safeguard scan --dir .
safeguard scan builds an SBOM for the target, matches it against vulnerability databases and writes results as JSON files under ./output, in a folder per scan. The docs name vuln.json, license.json and collection.json among the files. Useful flags:
| Flag | Effect |
|---|---|
--output <dir> | Where results are written (default output) |
--scanners <list> | Run only these scanners |
--org-namespace <list> | Namespaces that identify your own private packages |
--verbose | More detail while the scan runs |
safeguard scan exits 0 whenever the scan ran, whatever it found; it exits 1 when the command itself failed and 2 when results were written but the vulnerability scan did not actually run. There is no severity threshold flag. The docs are explicit that gating on findings belongs to policy gates in the platform.
A command-line scan keeps results on your machine. To see them in the platform next to your other projects, scan from the interactive terminal with /scan . while signed in. The docs say the runner then uploads the SBOM and findings, never your source. If you would rather the platform hand scan jobs to your machine, the local runner (GA) built into the CLI executes them locally and sends only results. For the trade-off between connecting a repository and running locally, see repo connection, CLI or local runner.
4. Connect the repository as a project
In the web app, go to Integrations in the sidebar. The page has tabs for Add Project/Product, Deployment, Configure Scanner and Notification, and filters for Source Code (SCM), Container Image, AI Model and Upload. Under Source Code Management the docs list GitHub, GitLab (self-hosted or cloud), Bitbucket and a generic Git URL.
For a public repository you enter its URL; for a private one you verify credentials, then pick repositories. On the review step you set, per repository, the branch or tag, the project name and the version. Click Connect, and the configuration appears in the Project Configurations table. The project then shows under Projects, with its version, status, finding counts and vulnerability count.
The generation options worth setting for Ruby:
| Option | What to choose for a Bundler app |
|---|---|
| Branch/Tag | The branch you deploy from, not a feature branch |
| Include Dev Dependencies | Decide deliberately (see below) |
| Deep Analysis | Enables Griffin deep analysis of the SBOM |
Include Dev Dependencies needs a decision for Ruby. Bundler groups such as :development and :test are recorded in the same Gemfile.lock as production gems; the bundle-install reference notes that even skipped groups are still resolved. The Safeguard docs describe the toggle but do not describe how Bundler groups map onto it, so check the first SBOM: if your RSpec, RuboCop and Capybara gems appear when the toggle is off, adjust your expectations and filters accordingly.
The Upload filter also has Upload Manifest File, but its docs examples do not name Gemfile.lock, so prefer the CLI route from step 3 for Bundler.
5. Turn on continuous scanning
Safeguard re-scans every project daily so newly disclosed CVEs are matched against gems you already ship. On top of the daily run you can enable three triggers: On Push (every commit to monitored branches), Scheduled (daily, weekly or custom) and On Release (when a tag is created).
For Ruby, On Release matters most: scanning the release tag gives you an SBOM and findings that match the lockfile production runs. Our SBOM generation and analysis post covers the formats (SPDX and CycloneDX among them) and what each export contains.
6. Triage in the Vulnerabilities tab
Open the project and go to Vulnerabilities. For each finding the docs list these fields: CVE ID, CVSS score, EPSS score, Exploitable, Affected Component (gem name and version), Fixed Version, Description, References and Reachability.
A sensible first pass for a Ruby application:
- Filter
kev:true. Anything here is being exploited somewhere. Fix it first, whatever its reachability class. - Filter
env:production reachable:true, then sort by EPSS. Unknown findings remain in this view because they are treated as potentially reachable. - Filter by component name for the gems that appear most often (Rails frameworks, Rack, Nokogiri). One upgrade often clears several findings.
- Filter by fix availability. Findings without a fixed version go to a separate track: mitigation, a VEX statement if the code is not affected, or a fork.
For each finding you can click it and choose View Fix to see upgrade options and breaking change warnings, Mark False Positive with a justification (submitted for approval if policy requires), or Accept Risk with an expiration date and a documented business justification.
When you accept risk on an Unknown Ruby finding, write a justification an auditor can check, such as "only reachable through a send call whose argument comes from a fixed allowlist".
7. Open fix pull requests
Safeguard's autonomous remediation and AI Remediate are GA. Autonomous remediation ships switched off: every category starts at Manual, and nothing is applied until the master Autonomous Remediation toggle is enabled. The manual paths, from the AI Remediate docs:
- Single vulnerability: open the vulnerability, click AI Remediate, review the proposed changes, click Create Pull Request.
- Bulk: select vulnerabilities, click AI Remediate Selected. Griffin batches compatible fixes and creates one pull request per repository.
- Page level: click AI Remediate at the top of the Vulnerabilities page, review the plan, approve, and create pull requests.
The docs describe five stages: analysis, solution generation (the safest upgrade path), impact assessment (breaking changes and compatibility), pull request creation, and validation (automated tests, if configured). For dependency upgrades, Griffin can upgrade vulnerable packages, handle transitive dependency conflicts and update lockfiles. Breaking changes are classed Safe, Minor, Major or Critical.
Settings live under Settings › AI Remediate: Auto-Create PRs, Require Approval, Test Integration, Breaking Change Policy and Branch Naming. Turn on Test Integration before you trust any automated Ruby upgrade.
The autonomous remediation post covers the eight categories and their strategies.
8. Gate in CI
The docs' CI/CD integration runs one step, safeguard ci scan. Each push becomes a new version of the Safeguard project named after the branch and commit. Setup takes three steps:
- Create a secret key under Settings › Developer with the scopes
scans:triggerandscans:read. You can lock it to one project. - Store it in your CI as a secret named
SAFEGUARD_API_KEY. - Add the step from your CI system's guide and push.
The docs give templates for GitHub Actions, GitLab CI/CD, Bitbucket Pipelines, Jenkins and Azure Pipelines, plus a Set up CI/CD button on the source control cards in the app. For any other CI, the documented generic step is:
curl -fsSL https://cli.safeguard.sh/install | bash
SAFEGUARD_API_KEY=... safeguard ci scan --wait --fail-on critical \
--repository-url "$REPO_URL" --branch "$BRANCH" --commit "$COMMIT"
Pin a CLI version with SAFEGUARD_VERSION=<version> on the install line so a CLI release does not change a build under you.
Two modes matter for Ruby teams:
| Mode | Command | Needs | Scans |
|---|---|---|---|
| On Safeguard (default) | safeguard ci scan | The repository connected as an integration, or public on a major Git host | SAST, SBOM, dependency vulnerabilities |
| On the runner | safeguard ci scan --local | A checkout in the job; no integration | SBOM, dependency vulnerabilities |
With --local, only results leave the runner, and the docs say it reads dependencies from lockfiles and manifests. The SAST in the default mode is generally available, and the docs note SAST findings are not part of --fail-on yet.
The flags that fail the build:
| Flag | Fails the step when |
|---|---|
--wait | The scan fails or takes longer than --timeout (default 30m) |
--fail-on <severity> | With --wait: a dependency vulnerability at or above the chosen severity |
--fail-on-gate <gate> | With --wait: a policy gate is not Passed or Warning |
If you would rather scan on the build machine and keep files as artifacts, the docs show safeguard scan --dir . --output out/ followed by a jq check on vuln.json:
safeguard scan --dir . --output out/
jq -e '[.data.matches[].vulnerability.severity | ascii_downcase]
| index("critical") | not' out/*/vuln.json
On a fresh CI container that has never opened the interactive terminal, the scanners safeguard scan relies on are not yet downloaded. The docs say to put them on the PATH or point TOOLS_DIR at a directory that holds them.
9. Write the policy, not just the threshold
Policies (GA) let you gate on more than severity. Create one under Settings › Policies with the visual builder or YAML. Documented conditions include vulnerability severity, CVSS score, EPSS score, license, component age, maintenance (no abandoned packages) and SLSA level. Each policy's action decides what a failure does to the gate:
| Action | Effect on the gate |
|---|---|
| Block | The gate fails |
| Warn | The gate passes, with a warning naming the policy |
| Notify | The gate passes, and a notification is sent |
The reachability docs show a production admission policy that blocks reachable or conditionally reachable KEV findings:
- id: no-reachable-kev-in-prod
condition: |
env == "production"
AND any(vulnerabilities,
reachability_class in ["reachable", "conditional"]
AND kev == true
)
effect: BLOCK
For Ruby, consider widening that list to include unknown, given how often Ruby findings land there. Then use safeguard ci scan --wait --fail-on-gate <gate> so the build fails on the policy rather than on a raw severity. You can also write policies in plain English; see plain English security policies to release gates.
Example: what a Ruby finding and its fix look like
The offering files and docs show the structure of a finding and a fix pull request, not a specific Ruby case, so the example below is illustrative. The gem names are real; the versions and the advisory identifier are placeholders.
The transitive path view, in the format the deep scanning docs show:
your-rails-app
└─ rails @7.1.x
└─ actionpack @7.1.x
└─ rack @3.0.x ← vulnerable here (CVE-YYYY-NNNNN)
Here, rack is not in your Gemfile, and Action Pack allows a range of Rack versions. If the patched Rack release is inside that range, the smallest fix is a lockfile-only change, the equivalent of Bundler's conservative update:
bundle update rack --conservative
The bundle-update reference says --conservative keeps the update conservative and does "not allow indirect dependencies to be updated", and --patch or --minor with --strict caps how far a gem may move. If the fix requires a newer Action Pack, the change moves Rails, and Breaking Change Detection should flag it Minor or Major.
What a Griffin pull request contains, per the AI Remediate docs: a title with the CVE reference, a description of the vulnerability, a summary of changes, breaking change warnings if any, test results if configured, and links to the advisories. For a Bundler project, expect the diff to touch Gemfile.lock and, when a top-level constraint has to move, Gemfile. Review both before merging.
Fix pull requests for Bundler: what to check before you merge
AI Remediate support for Ruby Bundler is listed as Full in the docs. That covers the mechanics. The judgement is still yours. Before you merge a gem upgrade, check:
- The lockfile diff is the size you expected. A one-gem fix that rewrites thirty lines of
Gemfile.lockhas moved more than the vulnerable gem. Read theGEMspecs section, not just the summary. PLATFORMSdid not change. If a platform disappears, native gems resolved for it disappear too, and your Linux build may fail or pick different builds.- Native gems were rebuilt and tested. Gems with C extensions compile at install time. Your CI has to run
bundle installand the test suite on the production platform, not only on a developer laptop. - Pessimistic constraints did not widen more than needed when a
~>line in theGemfilewas edited to admit the fix.
Start with Require Approval on and Auto-Create PRs off, and keep Major and Critical breaking-change fixes manual even later.
License data for gems
License compliance (GA) identifies each component's license from several sources and reconciles them: package metadata, SPDX identifiers, full-text matching of LICENSE, LICENCE, COPYING and NOTICE files, source file headers, and license strings in container layers. When those signals disagree, Safeguard raises a license-discrepancy finding.
Licenses are classified as permissive, weak copyleft, strong copyleft, network copyleft, commercial, public domain or unknown. Some gems use Ruby's own license terms alongside or instead of an SPDX-listed license. Safeguard understands full SPDX expressions, including OR and AND, and the docs say that for OR the most permissive option that matches your policy is chosen.
You declare an allow, deny and require-review policy in YAML (kind: LicensePolicy in the docs' example), and policies can be scoped to asset labels, so an SDK you distribute can follow stricter rules than a SaaS backend.
Enforcement happens at CI time (builds fail on a denied license), at admission (images with denied licenses cannot deploy to production) and at pull request time (a new direct dependency with a denied license gets a blocking check). Add the Ruby license identifier your legal team has approved to allow or require_review before the first enforced run, or gems that declare it will surface as unknown or need review.
From the CLI, safeguard scan writes license.json with the licenses it found. The docs say there is no CLI command yet for license listing, reports or policy evaluation, and none for generating attribution files; NOTICE files, a licenses directory and an SPDX document per release are generated in the platform. For the obligations side, see open source license obligations with Safeguard.
Which Safeguard surface for which Ruby job
| You want to | Use | Label |
|---|---|---|
| A quick look at one app on your laptop | safeguard scan --dir . | CLI, GA |
| Findings in the platform without granting repo access | Interactive terminal /scan . while signed in, or the local runner | CLI and local runner, GA |
| Continuous monitoring of every Rails repo | Connect the repository under Integrations, enable On Push and On Release | SCA, GA |
| Fail a pull request on a critical gem CVE | safeguard ci scan --wait --fail-on critical | CLI in CI, GA |
| Fail on your own rules (KEV, reachability, license) | A policy plus --fail-on-gate | Policies and gates, GA |
| Rank a large Ruby backlog | Vulnerabilities tab, Priority sort, kev:true and EPSS filters | Vulnerability management, GA |
| Know whether a vulnerable gem method is called | Reachability column | Ruby static and dynamic: GA |
| Open upgrade pull requests | AI Remediate single, bulk or page level | GA, ships off |
| Security insight while you code | IDE extension (check its language list for Ruby) | GA |
| Ask about findings from Claude or ChatGPT | Safeguard from the assistant's connector directory | MCP server, GA; community connector in Claude's directory |
| Look up a gem CVE's fixed version for free | gold.safeguard.sh | Gold Open Source, GA |
Requirement, what Safeguard does, what you still own
| Requirement | What Safeguard does | What you still own |
|---|---|---|
| SSDF PS.3.2: provenance data for all components, for example an SBOM | Builds an SBOM per scan and per release tag from Gemfile.lock | Committing a current lockfile; scanning the tag you deploy |
| SSDF PW.4.4: verify third-party components against your requirements through their life cycle | Daily continuous scanning, license policy, policies and gates | Defining the requirements; deciding what Block, Warn or Notify means |
| SSDF RV.1.1: monitor vulnerability databases for components you use | Matches gems against NVD, GHSA, OSV, KEV and vendor bulletins | Advisories for private gems, which no public database covers |
| SSDF RV.2.2: plan and implement risk responses | Priority Score, Accept Risk with expiry, False Positive with justification, fix pull requests | The risk decision itself and its sign-off |
| OWASP A03:2025: track versions of all components, including transitive | Full transitive path to each vulnerable gem | Gems loaded outside Bundler, for example vendored code |
| OWASP A03:2025: remove unused dependencies | In-app unused dependency view | Deciding what to remove and testing the removal |
| CISA KEV as a prioritisation input | KEV flag, Exploitable category, kev:true filter | Your remediation timeline for KEV findings |
What Safeguard does not do
- It does not prove a Ruby vulnerability unreachable with static analysis alone. Metaprogramming limits what static analysis can prove. Dynamic reachability is GA, but only where the runtime collector runs in your workloads. Metaprogramming,
send,method_missing, autoloading and callbacks push many findings into Unknown, which Safeguard treats as potentially reachable. - It does not read files the docs do not list. For Bundler that means
GemfileandGemfile.lock. The docs do not list.gemspecon its own or the alternativegems.rbandgems.lockednames. A library repository without a committed lockfile can only be judged by version ranges. - It does not fix a stale or wrong lockfile. If
Gemfile.lockis out of date, missing your production platform, or regenerated differently in the Docker build, Safeguard reports what the file says, not what production runs. Scan the built image as well when the two might differ. - It does not describe how
git:andpath:gem sources are matched. Public advisories are keyed to gem names and released versions. A gem pinned to a Git commit or a local path may have no version an advisory can match. Treat these as a gap and confirm the behaviour in a trial; our post on Gemfile git and path sources explains why. - It does not invent advisories for private gems. Your internal gems will not have CVEs unless someone files them. Safeguard can inventory them and apply license and policy rules; their vulnerabilities still depend on your own review.
- It does not separately describe C libraries vendored inside native gems. Some gems compile or bundle C libraries. The docs do not describe matching those embedded libraries independently of the gem's own advisories, so do not assume it.
- The CLI does not gate on its own.
safeguard scanexits 0 whenever it ran, and has no severity threshold flag. Gating issafeguard ci scan --wait --fail-onor a policy gate in the platform. - It does not merge for you. Fix pull requests are proposals. Autonomous remediation ships off, and even when enabled, Require Approval and your branch protection decide what lands.
- It does not replace upgrading Ruby or Rails itself. The Ruby interpreter and default gems shipped with it, and Rails versions that leave their maintenance window, are platform decisions. SCA can tell you a gem version is vulnerable; it cannot plan a Rails major upgrade.
Common mistakes
Scanning a library repository and trusting the result. A gem repository should not commit its lockfile, per Bundler's guidance, so a scan of it sees ranges. The versions that matter are in the applications that consume it. Scan those.
Letting the CI lockfile drift from the committed one. If CI runs bundle install without deployment or frozen settings and the lockfile changes during the job, the scan sees a different set from the one in Git. Enable deployment mode in CI.
Gating on safeguard scan exit codes. It exits 0 when findings exist. Use safeguard ci scan --wait --fail-on, a policy gate, or a jq check on vuln.json.
Running bundle update with no arguments to fix one CVE. The guide says to run it without arguments only when you intend to move every gem. Update the named gem, conservatively, and let the transitive path view tell you whether a parent must move.
Leaving the Ruby license out of the policy. If your allow list only names SPDX licenses like MIT and BSD, gems under Ruby's own terms will land in unknown or review. Decide on them up front.
Assuming dev and test gems are out of scope. They are in Gemfile.lock, and some run in CI with access to secrets and artifacts. Decide whether Include Dev Dependencies is on, and why.
FAQ
Does Safeguard support Ruby and Bundler for SCA?
Yes. SCA is generally available, and the CLI support matrix lists Bundler with Gemfile as the manifest and Gemfile.lock as the lockfile, both with full support. The SCA and deep dependency scanning docs also list Ruby Bundler.
Does Safeguard need Gemfile.lock, or is the Gemfile enough?
Commit and scan Gemfile.lock. The docs say Safeguard honours both manifests and lockfiles "so resolution matches what actually installs", and deep scanning resolves Ruby from Gemfile.lock. A Gemfile alone contains ranges and no transitive gems.
Is reachability analysis available for Ruby?
Partly. Reachability is GA, for Ruby static and dynamic reachability, with conservative resolution for metaprogramming. Findings the analyser cannot decide are marked Unknown and treated as potentially reachable.
How does Safeguard prioritise Ruby gem vulnerabilities?
Each finding gets a Priority Score from 0 to 100 from CVSS, the daily EPSS probability, CISA KEV listing, public proof-of-concept availability, reachability class, environment, exposure, data sensitivity and business criticality. Weights are configurable under Settings › Prioritization.
Can Safeguard open a pull request to fix a vulnerable gem?
Yes. AI Remediate lists Ruby Bundler with full support. You can fix one vulnerability, a selection (one pull request per repository) or a whole page. Autonomous remediation ships switched off with every category at Manual, so pull requests open when you ask until you change that.
How do I fail a CI build on a critical Ruby CVE with Safeguard?
Store a key with scans:trigger and scans:read as SAFEGUARD_API_KEY, then run safeguard ci scan --wait --fail-on critical. Use --local to scan the checkout on the runner. For your own rules, use --fail-on-gate with a policy gate.
Does Safeguard scan gems inside Docker images?
SCA covers components discovered inside container image layers, and the CLI can scan an image with safeguard scan --image. Scanning the built image is a useful check that the gems in production match the lockfile you committed.
Is there a free way to check a gem CVE?
Gold Open Source at gold.safeguard.sh is free and needs no login. Search a CVE or package to see affected and fixed versions. The Portal free entry tier also offers SCA for one existing repository plus one open source repository, one project per month.
Every fix above assumes a patched gem version exists. When it does not, or when the vulnerable code sits deep in a dependency you cannot move quickly, the options change, and the section below sets out what Safeguard offers for both cases.
Fix it without waiting for upstream
Whatever the vulnerability, most of the risk sits in two gaps. The first is the time between a fix being published and that fix actually running in your production builds. The second, and worse, is the time before any fix exists at all, while a maintainer works out a patch and a release schedule. Waiting through both gaps is a habit, not a requirement. Safeguard gives teams three ways to close them.
1. Autonomous remediation: pull requests, not tickets
When a fixed version exists, Safeguard's Griffin models open a tested, policy gated pull request that moves the affected dependency to the safe version. You can run it on one repository or in bulk across every project that carries the vulnerable version. Remediation categories include dependencies, licenses, security posture and findings, and each one is set separately. Reachability analysis ranks the vulnerable code paths your application actually calls first, so the first pull requests you merge are the ones that matter most. Autonomous remediation ships switched off, with every category set to Manual, so your team decides how much runs on its own and what still needs a human approval.
2. Gold Open Source: the safe version in seconds, free
gold.safeguard.sh is free and needs no login. Search any CVE or package to see affected and fixed versions, CVSS, EPSS, CISA KEV status and a recommended safe version, or use the "Am I affected?" checker. The same data is available through a free read API, README status badges and RSS threat feeds.
3. Gold Registry: start from components that are already fixed
Gold Registry provides hardened, drop in replacements for upstream packages and container images. Every Gold artifact is held continuously to zero exploitable critical or high CVEs and a completed license check, with signed build provenance (signed with Safeguard's key and verifiable with cosign, SLSA Build Level 3 as the design target) and a public evidence page you can hand to an auditor. An artifact loses its Gold badge if it regresses. Teams can point their own package directory at the registry so hardened equivalents resolve without changing how developers install anything.
When upstream has no fix yet, or will not ship one, Safeguard first works to get the fix accepted upstream. If the issue is severe or upstream will not move, Safeguard builds a fork, fixes the vulnerability there and publishes a hardened Gold build, so you are not stuck on a maintainer's release schedule. You can also request a hardened, compatibility tested build of a specific package. Ask which packages it covers before you plan around it.
| Your situation | What to use |
|---|---|
| A fixed version exists | Autonomous remediation opens the upgrade pull request; Gold Open Source confirms the safe version |
| You need to know today whether you are affected | Gold Open Source lookup and "Am I affected?" checker, free and without login |
| You want vulnerable versions to stop shipping | Safeguard policies and gates in CI |
| Upstream has no fix yet, or will not ship one | A custom Gold request; fork and patch |
| You want to start clean on new projects | Pull hardened components and images from Gold Registry |
One honest limit: when the vulnerable code lives in a vendor appliance, a hosted service or an operating system runtime you do not build yourself, the fix still has to come from that vendor or distribution. The steps above apply to the open source dependencies and container images you build and ship, which is where most teams carry the largest and least tracked share of their exposure.
See how Safeguard would help
The first demo is a real Safeguard CLI session. You install the CLI, run safeguard to open the interactive terminal and ask it to "fix the lodash CVE in this repo". It reads the finding and the advisory, asks before any command that changes something, edits package.json to lodash 4.17.21, runs npm install and npm test with approval, prints a summary and pushes nothing, then shows /permissions, /model and /scan .. The demo uses npm, but the loop is the same one you would run on a Rails app: scan, read the finding, bump the gem, run bundle install and your tests, review the diff.
Click through it at your own pace, or open it full screen.
The second demo sets up the local runner on your own machine, approves it with its device code, then scans from the terminal. This is the route for Ruby teams that cannot grant Safeguard access to a repository: the runner reads Gemfile.lock where the code lives and sends only results to the platform.
Click through it at your own pace, or open it full screen.
The third demo connects Claude to Safeguard with OAuth and chooses which tools Claude can use. In your own workspace you add Safeguard from Claude's connector directory, where it is listed as a community connector, rather than configuring a custom server. Once connected, and where your account's tools allow it, you can ask Claude about your Ruby findings and confirm the answer in the Vulnerabilities tab. For the IDE and ChatGPT side of the same idea, see Safeguard in your IDE and in the Claude and ChatGPT connector directories.
Click through it at your own pace, or open it full screen.
To try it on your own Bundler projects, download the CLI and apps at safeguard.sh/download and follow the setup guides at docs.safeguard.sh. If you want background on the community advisory data Ruby teams already use, our post on the Ruby Advisory Database and bundler-audit explains how gem advisories are written and matched, and reachability analysis for Ruby and RubyGems goes deeper on why Ruby call graphs are hard.
Quick check
A Ruby finding is marked Unknown for reachability and EPSS is low. Can you move it to the backlog?
Not on reachability grounds. Safeguard treats Unknown as potentially reachable because the analyser could not decide, which is common in Ruby because of metaprogramming and dynamic dispatch. Check KEV first; if it is not listed, you can schedule it by EPSS and exposure, but record an expiring risk acceptance with a specific justification rather than "reachability unknown".
Your CI step runs safeguard scan and the build passes even though vuln.json lists a critical gem CVE. Why?
safeguard scan exits 0 whenever the scan ran, whatever it found, and has no severity threshold flag. To fail the build, use safeguard ci scan --wait --fail-on critical, a policy gate with --fail-on-gate, or a jq check against vuln.json as the CI docs show.
Sources
- Safeguard docs: Software Composition Analysis (SCA)
- Safeguard docs: Vulnerability Prioritization (Reachability Analysis)
- Safeguard docs: Deep Dependency Scanning
- Safeguard docs: CLI support matrix
- Safeguard docs: Safeguard CLI
- Safeguard docs: CLI usage
- Safeguard docs: CI/CD integration
- Safeguard docs: Generate SBOM
- Safeguard docs: Vulnerabilities
- Safeguard docs: AI Remediate
- Safeguard docs: Policies and gates
- Safeguard docs: License compliance
- Safeguard docs: Continuous scanning
- Gold Open Source
- RubyGems guides: Bundler dependency management and Gemfile.lock
- RubyGems guides: Gemfile reference
- RubyGems guides: bundle update
- RubyGems guides: bundle lock
- RubyGems guides: bundle install
- RubyGems guides: bundle config
- RubyGems guides: patterns
- Ruby Advisory Database (rubysec)
- OSV schema
- NIST SP 800-218, Secure Software Development Framework version 1.1
- OWASP Top 10:2025 A03 Software Supply Chain Failures
- CISA Known Exploited Vulnerabilities Catalog
- FIRST Exploit Prediction Scoring System (EPSS)
- CycloneDX specification overview
Last reviewed: 25 October 2026. Product capabilities change; check docs.safeguard.sh for the current state.