OSV-Scanner: vulnerabilities with a source of truth
Table of contents
- Key takeaways
- The version-based scanner problem
- OSV as a structured format
- How OSV-Scanner works
- Practical CI use
- What about uncovered ecosystems
- The relationship with SBOMs
- Real limits
- My take
- Conclusion
- Frequently asked questions
- Which criteria should fail the CI build with OSV-Scanner without blocking every commit?
- Which manifests and formats can OSV-Scanner scan?
- What does OSV-Scanner miss even when it runs in CI?
- Sources
OSV-Scanner is Google's dependency scanner, announced in 2022, that queries the open OSV.dev database rather than raw CVE advisories. Because OSV pins each vulnerability to precise commit or semantic version ranges, matching against your actual dependencies becomes deterministic and produces far less noise. It reads npm, Python, Go, Ruby, PHP, Rust and Maven manifests.
OSV-Scanner[1] is the Google tool, announced in 2022[2], that directly queries the open OSV.dev database to identify vulnerabilities in project dependencies. At first glance it looks like any other dependency scanner, and indeed the output is similar. The difference lies beneath, in how vulnerability data is represented and how cross-referencing with real dependencies happens. After two years using it project after project, this article collects why this structure produces less noise than other tools and where friction remains.
Key takeaways
-
Version-based scanners generate false positives in volume because the affected-version lists in original CVE advisories tend to be imprecise or generic.
-
OSV represents each vulnerability with precise commit or semantic version ranges, enabling deterministic comparisons.
-
OSV-Scanner supports the most common manifests: npm, Python, Go, Ruby, PHP, Rust, Maven, Gradle; also container images and SBOMs in SPDX or CycloneDX.
-
The most valuable CI integration is failing only on new vulnerabilities introduced by the current change, not on preexisting ones.
-
The scanner is an important part of a system, not the whole system.
The version-based scanner problem
That kind of scanner works by comparing the version you use against a list of affected versions for each CVE. That sounds reasonable but generates a surprising number of false positives. The reasons are these:
-
The affected-version lists in original CVE advisories tend to be imprecise or generic.
-
Linux distributions backport patches without changing the public package version, so a scanner can think a version is vulnerable when it has actually already been patched.
-
An advisory may apply only to certain configurations or uses of the component, and the scanner does not distinguish between them.
The practical result is familiar to any team. Dependency scanners produce hundreds or thousands of alerts, a mix of real and false. Filtering the real ones consumes so much time that the team ends up ignoring the scanner. Signal is lost in noise.
It is the same underlying problem we describe when talking about CVE-based attack surface management: a list of identifiers with no context is not a prioritized work queue.
OSV as a structured format
The idea behind OSV is to change how vulnerability data is represented. Instead of a free-prose advisory with a vague list of affected versions, each vulnerability in OSV has a structured format with:
-
Precise commit or semantic version ranges.
-
Stable identifiers for each package and ecosystem.
-
Cross-references to the original advisories.
This structure allows deterministic comparisons between what a project has and what is listed as affected. A concrete example: an OSV advisory for a vulnerability in an npm package specifies exactly that versions between 1.2.0 and 1.4.5 are affected, in exact SemVer format. If your project uses version 1.4.6, the comparison is deterministic: not affected. No room for ambiguity.
OSV.dev[3] aggregates advisories from multiple sources while keeping this structure: GitHub Security Advisories, the PyPI Advisory Database, ecosystem-specific sources like RustSec, and the Global Security Database. Aggregating is not just pooling them together, it means normalizing them to the OSV format[4] and cross-referencing them.
How OSV-Scanner works
OSV-Scanner takes the dependency manifest of your project, resolves the exact versions, and queries OSV.dev for each package-and-version combination. The query returns the list of OSV advisories that apply exactly to those versions. There is no heuristic version matching and no free interpretation: if it shows up in the response, it applies.
The scanner supports the most common manifests:
-
npm’s
package-lock.json. -
Python’s
requirements.txtandpoetry.lock. -
Go’s
go.modandgo.sum. -
Ruby’s
Gemfile.lock. -
PHP’s
composer.lock. -
Rust’s
Cargo.lock. -
Maven’s
pom.xml. -
Gradle’s
gradle.lockfile.
It can also scan container images by inspecting installed packages, and SBOM files in SPDX or CycloneDX format.
Practical CI use
CI integration is one of the places where it shines most. OSV-Scanner is a static binary that can run in any environment. The typical pattern is adding a pipeline step that runs osv-scanner against the repository and fails the build if vulnerabilities matching certain criteria show up.
The most useful criteria in practice are two:
-
Fail if any vulnerability is listed in the CISA KEV catalog[5] (Known Exploited Vulnerabilities), because that indicates it is already being exploited in production.
-
Fail if vulnerabilities introduced by the current change appear, meaning vulnerabilities that did not exist on the main branch and show up because of dependencies added or updated on this branch. This second criterion is far more useful than failing on preexisting vulnerabilities, because it lets the baseline move forward without every existing security fix blocking every commit.
Another useful practice is running the scan at a higher cadence than commits, for example once a day against the main branch. Vulnerabilities published after a commit passed CI can appear at any time. OSV-Scanner covers declared dependencies; for vulnerabilities in your own code, the complementary layer is a static analyzer like Semgrep, which checks for insecure code patterns instead of dependency manifests.
What about uncovered ecosystems
A valid criticism is that ecosystem coverage is not uniform. npm, PyPI, Maven, and Go are covered well; so are Rust and Ruby; PHP and NuGet are covered adequately. But there are ecosystems where coverage is partial or nonexistent.
For these cases there are two strategies: complementing OSV-Scanner with other ecosystem-specific tools, or contributing to OSV.dev, which accepts advisory contributions.
For a project with dependencies in the largest ecosystems, OSV-Scanner is enough as the primary tool. For projects with dependencies in less mainstream ecosystems, it is worth having at least a second layer. This connects with the defense-in-depth approach we describe in guardrails in LLMs: frameworks and their real cost.
The relationship with SBOMs
OSV-Scanner works well with SBOMs. You can pass it an SBOM file in SPDX or CycloneDX format as input, and it produces the same kind of report as if it had started from the manifests. This is especially useful when the build process generates SBOMs and you want to scan starting from them, or when you want to scan artifacts already deployed for which you only have the SBOM.
The architecture that works project after project is this. The build chain generates SBOMs as part of the artifact and stores them in a registry alongside it. A periodic job runs OSV-Scanner against all the stored SBOMs to detect new vulnerabilities in artifacts already deployed.
Real limits
Three areas where OSV-Scanner keeps improving but is not yet enough:
-
The reach of runtime-based scanners: OSV-Scanner works on static declarations, manifests and SBOMs. It does not detect dependencies loaded dynamically at runtime, native system libraries the binary links against, or plugins.
-
Lack of context on whether a vulnerability affects your specific use: a CVE in an XML function of a library you only use for JSON does not affect you in practice. OSV-Scanner will still report it.
-
The prioritization layer: OSV-Scanner reports vulnerabilities but does not rank them by real risk. Turning a list of vulnerabilities into a prioritized work queue still needs a layer on top.
My take
OSV-Scanner is the best available piece today for the basic step of scanning dependencies. Its value is in the source of truth it queries, OSV.dev, more than in the scanner itself. The combination brings the false-positive count down compared with traditional scanners.
The concrete recommendation is that any project with open source dependencies should have OSV-Scanner running in CI, at least in the mode that detects new vulnerabilities introduced by the current change. It is free, it is fast, and the data source is good.
Thinking that having it enabled already covers dependency scanning is a common mistake. The tool is an important part of a system, not the whole system. Seen that way, it is useful, stable, and reduces the alert fatigue that does so much damage to practical security.
Conclusion
Alert fatigue is the enemy of real security. OSV-Scanner attacks that problem at the root, using structured data instead of version heuristics. For teams today with a dependency scanner producing hundreds of ignored alerts, migrating to OSV-Scanner cuts noise to a manageable fraction without losing real coverage.
Frequently asked questions
Which criteria should fail the CI build with OSV-Scanner without blocking every commit?
Two criteria. The first: fail if any vulnerability is listed in the CISA KEV catalog, already exploited in production; the second: fail only on new vulnerabilities, the ones that did not exist on the main branch. The second criterion lets the baseline move forward without every pending fix blocking every commit. Also scan the main branch once a day.
Which manifests and formats can OSV-Scanner scan?
The most common ones: npm's package-lock.json, Python's requirements.txt and poetry.lock, Go's go.mod and go.sum, Ruby's Gemfile.lock, PHP's composer.lock, Rust's Cargo.lock, Maven's pom.xml and Gradle's gradle.lockfile. It also scans container images by inspecting installed packages and SBOM files in SPDX or CycloneDX format, producing the same kind of report.
What does OSV-Scanner miss even when it runs in CI?
It works on static declarations (manifests and SBOMs), so it does not detect dependencies loaded dynamically at runtime, native system libraries the binary links against, or plugins. It also cannot tell whether a vulnerability affects your specific use of a package, does not rank by real risk, and does not cover your own code. For that, the complementary layer is a static analyzer like Semgrep. It is part of a system, not the whole system.