Internal Architecture of the OSV-Scanner Security Scanning Engine: A Deep Dive into the Source Code
OSV-Scanner operates as a thin orchestration layer over the osv-scalibr extraction framework, transforming raw source trees, lockfiles, or container images into enriched vulnerability reports through a pipeline of plugin resolution, capability-gated scanning, and configurable filtering.
The internal architecture of the OSV-Scanner security scanning engine centers on modularity and clear separation of concerns. Rather than implementing extraction logic directly, the google/osv-scanner repository delegates heavy lifting to standalone scalibr plugins while managing the workflow through well-defined stages. Understanding this architecture reveals how the tool handles diverse inputs—from Git repositories to Docker images—while maintaining extensibility for new package ecosystems.
Core Architectural Components
The engine divides work into distinct logical stages, each handled by specific source files in the repository.
CLI Entry Point and Public API
The journey begins in cmd/osv-scanner/main.go, where command-line flags are parsed into a ScannerActions struct. This configuration object captures user intent: recursive directory scanning, specific lockfile parsing, or container image analysis.
The public API surface lives in pkg/osvscanner/osvscanner.go. Here, two primary functions drive execution: DoScan for filesystem-based operations and DoContainerScan for image-based operations. These functions initialize external accessors via initializeExternalAccessors, creating the vulnerability matcher (either an online OSV.dev client or offline SQLite database), an optional license matcher, and vendored extractor clients.
Plugin Resolution and Extraction Framework
At the heart of the architecture lies the plugin system. The function getPlugins in pkg/osvscanner/scan.go determines which scalibr extractors (and enrichers) are required based on user flags and built-in defaults. Each language or operating system maintains its own extractor—such as golang/gomod, javascript/packagelockjson, or dpkg—which scalibr resolves by name via scalibrplugin.Resolve.
When users explicitly specify lockfiles using the --lockfile path:parseAs syntax, the engine looks up the corresponding extractor in internal/scanners/lockfile.go. The osvscannerScalibrExtractionMapping map translates identifiers like "gomod" to their respective scalibr plugins, and ParseAsToPlugin forces that specific extractor for the exact file path.
Root Map Construction and Input Normalization
Before scanning, the engine normalizes diverse inputs through pathToRootMap and isDescendent in pkg/osvscanner/scan.go. This stage handles directories, individual lockfiles, SBOMs, git commits, and container images, converting them into a standardized map of filesystem roots to specific file paths. This abstraction allows the scanner to treat a Git repository and a Docker image layer with the same uniform interface.
Data Flow Through the Engine
Understanding the sequence of function calls clarifies how raw artifacts become vulnerability reports.
The pipeline follows this exact path through the source code:
-
CLI flags populate a
ScannerActionsinstance, triggeringinitializeExternalAccessorsto set up matchers. -
getPluginsresolves the scalibr plugin list, whilepathToRootMapbuilds the root map and override map for explicit file handling. -
scanner.Scan(orScanContainerfor images) invokes scalibr with the plugin list, capabilities, and an extractor-override closure that forces specific extractors for user-provided files. -
Filtering stages remove noise:
filterUnscannablePackagesdrops packages that cannot be analyzed,filterIgnoredPackagesapplies user-defined ignore rules fromosv-scanner.toml,filterNonContainerRelevantPackagestrims container-specific noise, andfilterResultsapplies final vulnerability filtering. -
makeVulnRequestWithMatchersends the sanitized inventory to the chosen matcher—osvmatcherfor live OSV.dev queries orlocalmatcherfor offline database reads—returningPackageVulns. -
Optional enrichment: When
--scan-licensesis enabled, thelicensematcherqueries Deps.dev for license data. -
finalizeScanResultconstructs themodels.VulnerabilityResults, applies configuration overrides (such as Go-version overrides), and determines exit codes likeErrVulnerabilitiesFoundorErrNoPackagesFound. -
Reporting: The finalized results pass to reporters in
internal/reporter(JSON, SARIF, HTML, CycloneDX) for output formatting.
Key Architectural Patterns
Several design patterns enable the scanner's flexibility and security posture.
Capability Gating and Security Boundaries
Before execution, the engine builds a plugin.Capabilities struct (lines 15-21 of pkg/osvscanner/scan.go). This security mechanism tells scalibr which permissions to grant plugins—distinguishing between NetworkOnline (live vulnerability database queries) and NetworkOffline (air-gapped environments). This gating ensures that offline-mode scans never attempt network access, even if plugins technically support it.
Matcher Abstraction for Vulnerability Sources
The clientinterfaces.VulnerabilityMatcher interface abstracts the vulnerability data source. According to the source code, osvmatcher.New creates a client for OSV.dev API calls, while localmatcher.NewLocalMatcher initializes a reader for pre-downloaded SQLite databases. This abstraction allows identical calling code regardless of whether the scan operates online or offline.
Config-Driven Filtering
The architecture supports complex ignore rules through osv-scanner.toml files. The config.Manager loads these configurations, and the filtering pipeline applies them at two levels: filterIgnoredPackages handles package-level exclusions, while filterPackageVulns applies vulnerability-specific rules (e.g., ignoring specific GHSA IDs). Unused ignore entries are reported back to the user for configuration validation.
Container Image Handling
Container scanning follows a specialized path through DoContainerScan. The engine uses imagehelpers.ExportDockerImage to extract image tarballs, creates an osv-scalibr image.Image object, and executes ScanContainer. Post-scan, it extracts container metadata via proto.ScanResultToProto, enabling analysis of installed OS packages alongside application dependencies.
Practical Implementation Examples
These code snippets demonstrate how to interact with the internal architecture programmatically.
Scanning a Directory Recursively
actions := osvscanner.ScannerActions{
DirectoryPaths: []string{"./my-project"},
Recursive: true,
HTTPClient: http.DefaultClient, // Enable online vulnerability lookup
}
results, err := osvscanner.DoScan(actions)
if err != nil {
log.Fatalf("scan failed: %v", err)
}
fmt.Printf("Found %d vulnerable packages\n", len(results.Results))
This invokes the full pipeline: from ScannerActions definition in osvscanner.go through DoScan to the file-based scan implementation in scan.go.
Scanning with Explicit Parser Override
actions := osvscanner.ScannerActions{
LockfilePaths: []string{"go.mod:gomod"},
}
res, err := osvscanner.DoScan(actions)
The "gomod" token resolves through ParseAsToPlugin in internal/scanners/lockfile.go, mapping to the gomod extractor via the osvscannerScalibrExtractionMapping map.
Analyzing Docker Images
actions := osvscanner.ScannerActions{
Image: "alpine:latest",
IsImageArchive: false, // Pull from Docker daemon
}
res, err := osvscanner.DoContainerScan(actions)
This executes the container-specific path: DoContainerScan → imagehelpers.ExportDockerImage → image.FromTarball → scanner.ScanContainer.
Applying Configuration-Based Ignores
Create an osv-scanner.toml file:
[[ignore]]
id = "GHSA-xxxx-xxxx-xxxx"
reason = "false positive in our use-case"
Place this adjacent to scanned code. The engine loads it via config.Manager within DoScan, and filterIgnoredPackages removes matching vulnerability IDs (see filter.go lines 79-94).
Summary
- OSV-Scanner acts as an orchestration layer over the osv-scalibr plugin framework, delegating extraction to specialized plugins while managing the workflow internally.
- The public API centers on
DoScanandDoContainerScaninpkg/osvscanner/osvscanner.go, which coordinate all scanning stages. - Plugin resolution occurs in
getPlugins(scan.go), with explicit lockfile mapping handled byosvscannerScalibrExtractionMappingin lockfile.go. - Capability gating through
plugin.Capabilitiesensures security boundaries between online and offline modes. - Data flows through root-map construction, scalibr invocation, multi-stage filtering, vulnerability matching via abstracted interfaces, and final result construction.
- Configuration-driven filtering supports
.tomlbased ignore rules applied at both package and vulnerability levels.
Frequently Asked Questions
How does OSV-Scanner decide which extractors to use for a given file?
The engine determines extractor selection through getPlugins in pkg/osvscanner/scan.go, which evaluates built-in defaults and user flags. When users explicitly specify files via --lockfile, the ParseAsToPlugin function in internal/scanners/lockfile.go looks up the identifier in osvscannerScalibrExtractionMapping to force the specific extractor. For directories, scalibr automatically identifies applicable extractors based on file patterns.
What is the difference between DoScan and DoContainerScan in the OSV-Scanner API?
DoScan handles filesystem-based scanning of directories, lockfiles, and SBOMs, while DoContainerScan specializes in container image analysis. According to the source code, DoContainerScan first exports the image tarball using imagehelpers.ExportDockerImage, creates an image.Image object, and invokes ScanContainer rather than the standard file scanner, enabling extraction of OS-level packages from container layers.
How does the scanner support offline vulnerability database queries?
The architecture abstracts vulnerability sources through the clientinterfaces.VulnerabilityMatcher interface. While online mode uses osvmatcher.New to query OSV.dev, offline mode instantiates localmatcher.NewLocalMatcher to read from a pre-downloaded SQLite database. Both implement the same interface, allowing makeVulnRequestWithMatcher to operate identically regardless of the data source.
Where does OSV-Scanner apply user-defined ignore rules from configuration files?
Ignore rules defined in osv-scanner.toml undergo two-phase filtering. First, filterIgnoredPackages in pkg/osvscanner/filter.go removes packages matching user criteria. Second, filterPackageVulns applies vulnerability-specific filters (such as specific GHSA IDs). The config.Manager loads these rules during DoScan initialization, and unused ignore entries are reported to help validate configuration accuracy.
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:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →