How OSV-Scanner's Version Matching Algorithm Works for Accurate Vulnerability Detection

OSV-Scanner determines if a package version is vulnerable by evaluating OSV range events (introduced, fixed, last_affected, limit) against semantic version comparisons, with fallback to explicit version lists and commit-hash matching for Git-based packages.

The accuracy of vulnerability detection in google/osv-scanner depends on its sophisticated version matching algorithm, which bridges the gap between extracted package metadata and Open Source Vulnerability (OSV) database records. According to the source code, this logic resides primarily in the internal/utility/vulns package, where it processes ecosystem-specific versioning schemes and complex range definitions to minimize false negatives while accounting for edge cases.

Overview of the Matching Pipeline

The algorithm follows a deterministic multi-step pipeline when scanning dependencies. In osvscanner/internal/clients/clientimpl/osvmatcher/osvmatcher.go, the pkgToQuery function (lines 37-52) transforms each discovered package into an OSV Query structure containing the package name, ecosystem, and version. The scanner then retrieves matching vulnerability records and delegates the actual matching decision to vulns.IsAffected in internal/utility/vulns/vulnerability.go (lines 33-73).

The process unfolds through four distinct evaluation stages: exact version verification, range-based semantic comparison, ecosystem-specific parsing, and Git commit matching.

Exact Version Verification

Before evaluating complex ranges, the algorithm checks for explicit version lists in the OSV record. As implemented in vulnerability.go (lines 56-60), a simple slice lookup using slices.Contains determines if the package version appears directly in the versions array of the affected range. This provides O(1) matching for vulnerabilities with discrete affected versions, bypassing expensive semantic comparisons when not needed.

Range-Based Event Processing

If exact version matching fails, the algorithm evaluates ECOSYSTEM or SEMVER ranges via rangeAffectsVersion (lines 84-96). The rangeContainsVersion function orchestrates the core logic:

  1. Event extraction: Calls eventVersion to retrieve version strings from range events, preferring introduced, then fixed, limit, or last_affected fields.
  2. Chronological sorting: Sorts events using semantic version comparison to establish temporal order.
  3. Range walking: Iterates through ordered events, toggling an "affected" flag when passing an introduced event and clearing it on fixed or last_affected events.

This event-driven approach correctly handles complex vulnerability histories with multiple fixed and re-introduced cycles.

Semantic Version Parsing

Version strings undergo strict parsing using the github.com/google/osv-scalibr/semantic library. For Go modules specifically, the custom semverlike.ParseSemverLikeVersion function in internal/utility/semverlike/version-semver-like.go handles non-standard formats like "vX.Y.Z-patch" with unlimited numeric components. The parsed versions support numeric-aware comparison through semantic.MustParse(...).CompareStr(), ensuring that pre-release identifiers (like -rc1) are evaluated correctly according to SemVer 2.0 rules.

Git-Based Package Handling

For packages without ecosystems (or with GIT explicitly specified), the algorithm checks commit hashes against Git ranges via hasGitRangeForRepo (lines 36-42). This allows precise vulnerability detection for vendored dependencies or direct Git references by comparing the package's commit hash against the vulnerable commit ranges defined in the OSV record, bypassing version string parsing entirely.

Edge Cases and Safety Defaults

The version matching algorithm implements defensive defaults to prevent false negatives:

  • No version specified: The scanner assumes the package is vulnerable (biased toward false positives)
  • Version "0" introduced: Treated as "always vulnerable" (affects all versions of the package)
  • Empty ranges: Logged as warnings indicating potential data errors in the OSV record

These safeguards ensure that missing metadata does not result in undetected vulnerabilities.

Practical Implementation Example

You can reuse the core matching logic outside the full scanner context. The following example demonstrates direct usage of vulns.IsAffected:

package main

import (
	"context"
	"fmt"

	"github.com/google/osv-scalibr/extractor"
	"github.com/google/osv-scanner/v2/internal/utility/vulns"
	"github.com/ossf/osv-schema/bindings/go/osvschema"
)

func main() {
	// 1️⃣ Build a dummy package (normally created by a scanner plugin)
	pkg := &extractor.Package{
		Name:      "github.com/example/foo",
		Version:   "1.3.2",
		Ecosystem: "Go", // matches osvconstants.EcosystemGo
		PURL:      &purl.PackageURL{Type: "golang", Namespace: "github.com/example", Name: "foo", Version: "v1.3.2"},
	}

	// 2️⃣ Retrieve a vulnerability from the OSV API (omitted here – assume we already have it)
	// For illustration, we create a simple vulnerability with a single range:
	v := &osvschema.Vulnerability{
		Id: "GHSA-xxxx-xxxx-xxxx",
		Affected: []*osvschema.Affected{
			{
				Package: &osvschema.Package{
					Name:      "github.com/example/foo",
					Ecosystem: "Go",
				},
				Ranges: []*osvschema.Range{
					{
						Type: osvschema.Range_ECOSYSTEM,
						Events: []*osvschema.Event{
							{Introduced: "1.0.0"},
							{Fixed: "1.4.0"},
						},
					},
				},
			},
		},
	}

	// 3️⃣ Run the core matcher
	affected := vulns.IsAffected(v, pkg)
	fmt.Printf("Package %s@%s affected? %v\n", pkg.Name, pkg.Version, affected)
}

Output: Package github.com/example/foo@1.3.2 affected? true

The example demonstrates creation of an extractor.Package, construction of an OSV Vulnerability with an ECOSYSTEM range, and the direct call to vulns.IsAffected, which internally uses the range-event algorithm described above.

Summary

  • OSV-Scanner's version matching algorithm evaluates vulnerability ranges through event-based semantic comparison in internal/utility/vulns/vulnerability.go
  • The IsAffected function checks explicit version lists first, then falls back to range-based evaluation using introduced, fixed, limit, and last_affected events
  • Semantic parsing handles ecosystem-specific formats, including Go module extensions via semverlike.ParseSemverLikeVersion
  • Git-based packages match against commit ranges when ecosystem data is unavailable
  • Safety defaults treat missing versions as vulnerable to ensure comprehensive detection

Frequently Asked Questions

How does osv-scanner handle different versioning schemes like SemVer and CalVer?

OSV-Scanner delegates version parsing to the semantic library, which normalizes versions for numeric comparison. For ecosystem-specific variations (such as Go's pseudo-versions or complex npm ranges), the scanner uses specialized parsers like semverlike.ParseSemverLikeVersion to extract comparable components before evaluating range membership against the vulnerability database.

What happens if a vulnerability database entry lacks fixed version information?

When the fixed or last_affected event is absent from a range, the algorithm assumes all versions from the introduced event onward remain vulnerable indefinitely. Additionally, if the introduced version is "0" (as implemented in vulnerability.go lines 50-53), the scanner treats every version of the package as affected, ensuring no vulnerable instances are missed due to incomplete data.

Can the matching logic be used independently of the full scanner?

Yes. The vulns.IsAffected function in internal/utility/vulns/vulnerability.go is exported and can be imported as a standalone library. Developers can construct extractor.Package structs and osvschema.Vulnerability objects to programmatically check version vulnerability status without running the complete osv-scanner CLI, making it suitable for integration into custom security tooling.

How does the algorithm prevent false negatives for packages with complex range histories?

By sorting range events chronologically and walking them sequentially (as seen in rangeContainsVersion), the algorithm correctly handles interleaved introduced/fixed cycles. The semantic comparison ensures that pre-release versions and build metadata are evaluated according to SemVer 2.0 rules, while Git commit matching provides precise identification for source-based dependencies that may not follow traditional versioning schemes.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →