Skip to content
DevOps AI ToolKit
Newsletter
All guides
AI for DevOps Security & Hardening By James Joyner IV · · 10 min read

Triaging Dependency Vulnerabilities With OSV-Scanner Without Drowning

Scan source lockfiles with OSV-Scanner, triage findings by reachability and fix availability, and suppress non-exploitable noise with VEX to keep CI honest.

  • #security
  • #hardening
  • #dependencies
  • #vulnerability-management
  • #supply-chain

The first time I pointed a scanner at a mature monorepo’s lockfiles, it came back with 412 vulnerabilities. My honest reaction was to close the laptop. That number is a lie of omission: it counts every advisory that touches a package in my dependency graph, whether or not my code ever calls the vulnerable function. Drowning in that list is how teams end up ignoring the scanner entirely — which is strictly worse than not running one. This is the defensive playbook I use to scan source dependencies, not container images, and to walk 412 findings down to the handful that can actually hurt me.

Source scanning is a different job than image scanning

Container image scanning tells you what OS packages and baked-in binaries shipped in a layer. Source dependency scanning reads your lockfiles — package-lock.json, go.mod/go.sum, Cargo.lock, requirements.txt, poetry.lock, Gemfile.lock — and resolves the exact pinned versions your build will pull. That precision matters: lockfiles encode the transitive graph, so a CVE three levels deep in a sub-dependency you never chose directly still shows up.

OSV-Scanner queries the OSV.dev database, an open, version-range-aware advisory feed that aggregates GitHub Security Advisories, RustSec, PyPA, Go’s vuln DB, and more. Because OSV speaks affected version ranges rather than vague “this package is bad” warnings, the match is exact.

osv-scanner scan source --lockfile package-lock.json
osv-scanner scan source --recursive ./   # walk every lockfile in the tree

For one specific manifest you can be explicit about the parser:

osv-scanner scan source \
  --lockfile 'requirements.txt:./services/api/requirements.txt'

Presence is not exploitability

A finding means a vulnerable version is present in your resolved graph. It does not mean the vulnerable code path is reachable from your application. A deserialization RCE in a YAML library only matters if you actually parse untrusted YAML with it; if you only ever emit YAML from trusted, static config, the advisory is real but inert in your context.

This is the single highest-leverage filter you have. OSV-Scanner ships experimental call-graph reachability analysis for some ecosystems (Go is the most mature):

osv-scanner scan source \
  --call-analysis=all \
  --recursive ./

When reachability says a vulnerable symbol is never called, that finding drops to the bottom of the queue. When it says the symbol is on a live path, it jumps to the top. Reachability data isn’t available for every ecosystem yet, so for the rest you fall back to manual reasoning — which is exactly where an AI assistant pulls its weight (more on that below).

Pro Tip: Sort your triage by two axes only — reachable? and fix available?. Reachable plus a published fixed version is a same-day patch. Unreachable with no fix is a VEX entry and a calendar reminder. Everything else lands in between. Most “412 findings” collapse into a dozen real decisions once you apply this grid.

Read the JSON, not the pretty table

The terminal table is for humans glancing once. For automation, emit structured output and parse it:

osv-scanner scan source --recursive ./ \
  --format json > osv-results.json

A quick triage slice with jq — pull the package, the advisory ID, and whether a fixed version exists:

jq -r '
  .results[].packages[]
  | .package.name as $pkg
  | .vulnerabilities[]
  | [$pkg, .id, (.affected[0].ranges[0].events
      | map(.fixed) | map(select(.)) | first // "NO FIX")]
  | @tsv
' osv-results.json

That one command turns the wall of findings into a sortable list of “package, advisory, fix-or-not” — the raw material for the reachable/fixable grid.

Suppress non-exploitable findings with VEX, not silence

When you’ve genuinely determined a finding is not exploitable in your product, the wrong move is to delete it or pipe the scanner to || true. The right move is to record an auditable justification with VEX (Vulnerability Exploitability eXchange). VEX is a standardized way to say “we ship this package, we know about CVE-X, and here’s why it doesn’t affect us.” OpenVEX is a lightweight, signable JSON format for exactly this.

{
  "@context": "https://openvex.dev/ns/v0.2.0",
  "@id": "https://example.com/vex/2026-osv-001",
  "author": "Platform Security <secops@example.com>",
  "timestamp": "2026-06-17T00:00:00Z",
  "version": 1,
  "statements": [
    {
      "vulnerability": { "name": "GHSA-xxxx-yyyy-zzzz" },
      "products": [
        { "@id": "pkg:npm/your-service@2.4.0" }
      ],
      "status": "not_affected",
      "justification": "vulnerable_code_not_in_execute_path",
      "impact_statement": "The affected YAML.load path is never invoked; we only call YAML.dump on trusted static config."
    }
  ]
}

The justification field is a controlled vocabulary (vulnerable_code_not_in_execute_path, component_not_present, inline_mitigations_already_exist, and a few others) — so your suppressions are queryable and reviewable, not buried in a comment. Feed it back to the scanner so suppressed findings stop failing the build:

osv-scanner scan source --recursive ./ \
  --openvex openvex.json \
  --format json > osv-results.json

Now CI fails only on findings nobody has triaged. A suppression has an author, a timestamp, and a stated reason — when the auditor (or future you) asks “why did we ship a known CVE,” the answer is in version control.

Wire it into CI so it gates without nagging

The goal is a gate that breaks the build on new, untriaged findings and stays quiet about the ones you’ve already reasoned through. A minimal GitHub Actions step:

name: dependency-scan
on:
  pull_request:
  schedule:
    - cron: "0 6 * * 1"   # weekly re-scan: ranges and DB change over time
jobs:
  osv:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - name: Run OSV-Scanner
        run: |
          osv-scanner scan source \
            --recursive ./ \
            --openvex openvex.json \
            --call-analysis=all \
            --format sarif > results.sarif || true
      - name: Upload SARIF
        uses: github/codeql-action/upload-sarif@v3
        with:
          sarif_file: results.sarif

The scheduled re-scan matters: a dependency you froze today picks up new advisories tomorrow without a single line of your code changing. Uploading SARIF surfaces findings inline on the PR instead of forcing engineers to dig through CI logs — the closer the finding is to the diff, the more likely it gets handled. If you want a second pair of eyes on the diff itself before it merges, the code review dashboard pairs naturally with this gate.

AI as a fast junior engineer for advisory triage

The reachability question — “can a request actually reach this vulnerable call?” — is tedious text-reasoning over advisory descriptions and your own call sites. That’s precisely the kind of grunt work where an AI copilot shines: treat it like a sharp junior engineer who reads fast and never gets bored, not an authority whose word you ship unverified.

I paste the advisory text and the relevant source files and ask a narrow question:

Here is GHSA-xxxx and the three files in our repo that import this package. Does our code call the affected function, directly or transitively? Quote the exact lines that do or explain why none of them reach it.

The model drafts a reachability hypothesis and a candidate VEX justification in seconds. Then a human verifies — I read the cited lines myself before that suppression ever lands. The model is allowed to be wrong; the merge gate is not. A tool like Claude does this well, and a saved, structured triage prompt keeps the output consistent across the team — I keep mine in a prompt workspace and lean on a few ready-made ones from the prompt packs.

Pro Tip: Strictly defensive, and never hand the model real secrets. Advisory IDs, version numbers, and public source are fine to share. API keys, internal hostnames, and .env contents are not — redact before you paste, every time.

Wrapping up

Source dependency scanning only pays off if the output stays actionable. OSV-Scanner against your lockfiles gives you exact, version-aware matches; reachability analysis and the reachable/fixable grid cut the list to what’s real; VEX records why the rest is safe in an auditable, reviewable way; and CI gates on the untriaged remainder. Let an AI assistant draft the tedious advisory reasoning, but keep a human on the verification step and your secrets off the wire. Do that, and 412 findings becomes a short, honest list you’ll actually work — which is the whole point. For more in this vein, browse the security hardening category.

Free download · 368-page PDF

Download the Free 500-Prompt DevOps AI Toolkit

500 battle-tested, copy-paste AI prompts engineered by a senior systems engineer — every one with fill-in placeholders and safety/back-out notes. Drop your email and it's yours.

  • 500 prompts: Linux · Kubernetes · Terraform · OpenStack · GitLab · Docker · Monitoring · Incident Response
  • Instant PDF download — yours free, forever
  • Plus one practical AI-workflow email a week (no spam)

Single opt-in · unsubscribe anytime · no spam.