# Internal Architecture of the OSV-Scanner Security Scanning Engine: A Deep Dive into the Source Code

> Explore the internal architecture of the OSV-Scanner security scanning engine. Discover how it transforms source code into vulnerability reports via its pipeline. Learn more.

- Repository: [Google/osv-scanner](https://github.com/google/osv-scanner)
- Tags: deep-dive
- Published: 2026-04-25

---

**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`](https://github.com/google/osv-scanner/blob/main/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`](https://github.com/google/osv-scanner/blob/main/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`](https://github.com/google/osv-scanner/blob/main/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`](https://github.com/google/osv-scanner/blob/main/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`](https://github.com/google/osv-scanner/blob/main/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:

1. **CLI flags** populate a `ScannerActions` instance, triggering `initializeExternalAccessors` to set up matchers.

2. **`getPlugins`** resolves the scalibr plugin list, while `pathToRootMap` builds the root map and override map for explicit file handling.

3. **`scanner.Scan`** (or `ScanContainer` for images) invokes scalibr with the plugin list, capabilities, and an extractor-override closure that forces specific extractors for user-provided files.

4. **Filtering stages** remove noise: `filterUnscannablePackages` drops packages that cannot be analyzed, `filterIgnoredPackages` applies user-defined ignore rules from [`osv-scanner.toml`](https://github.com/google/osv-scanner/blob/main/osv-scanner.toml), `filterNonContainerRelevantPackages` trims container-specific noise, and `filterResults` applies final vulnerability filtering.

5. **`makeVulnRequestWithMatcher`** sends the sanitized inventory to the chosen matcher—`osvmatcher` for live OSV.dev queries or `localmatcher` for offline database reads—returning `PackageVulns`.

6. **Optional enrichment**: When `--scan-licenses` is enabled, the `licensematcher` queries Deps.dev for license data.

7. **`finalizeScanResult`** constructs the `models.VulnerabilityResults`, applies configuration overrides (such as Go-version overrides), and determines exit codes like `ErrVulnerabilitiesFound` or `ErrNoPackagesFound`.

8. **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`](https://github.com/google/osv-scanner/blob/main/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`](https://github.com/google/osv-scanner/blob/main/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

```go
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`](https://github.com/google/osv-scanner/blob/main/osvscanner.go) through `DoScan` to the file-based `scan` implementation in [`scan.go`](https://github.com/google/osv-scanner/blob/main/scan.go).

### Scanning with Explicit Parser Override

```go
actions := osvscanner.ScannerActions{
    LockfilePaths: []string{"go.mod:gomod"},
}
res, err := osvscanner.DoScan(actions)

```

The `"gomod"` token resolves through `ParseAsToPlugin` in [`internal/scanners/lockfile.go`](https://github.com/google/osv-scanner/blob/main/internal/scanners/lockfile.go), mapping to the `gomod` extractor via the `osvscannerScalibrExtractionMapping` map.

### Analyzing Docker Images

```go
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`](https://github.com/google/osv-scanner/blob/main/osv-scanner.toml) file:

```toml
[[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`](https://github.com/google/osv-scanner/blob/main/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 `DoScan` and `DoContainerScan` in [`pkg/osvscanner/osvscanner.go`](https://github.com/google/osv-scanner/blob/main/pkg/osvscanner/osvscanner.go), which coordinate all scanning stages.
- **Plugin resolution** occurs in `getPlugins` (scan.go), with explicit lockfile mapping handled by `osvscannerScalibrExtractionMapping` in lockfile.go.
- **Capability gating** through `plugin.Capabilities` ensures 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 `.toml` based 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`](https://github.com/google/osv-scanner/blob/main/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`](https://github.com/google/osv-scanner/blob/main/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`](https://github.com/google/osv-scanner/blob/main/osv-scanner.toml) undergo two-phase filtering. First, `filterIgnoredPackages` in [`pkg/osvscanner/filter.go`](https://github.com/google/osv-scanner/blob/main/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.