tty/0·uid=audit gid=audit
docs.reachability()

most findings don't deserve your attention

scanners are judged by what they find, so they find everything. the result is the familiar wall: 200 findings, of which maybe five change what you do this week. virgil's job is deciding which five — this page explains the two mechanisms that do most of that work.

## reachability: is the vulnerable code even yours to worry about?

dependency scanners flag every CVE in every package in your lockfile. but a lockfile entry is not exposure — transitive dependencies, dev tooling, and optional extras pile up packages your code never touches. virgil cross-references each dependency CVE against your actual source: does any file import (require, use) the vulnerable package?

  • +reachable — your code imports the package: the CVE keeps its severity and its place in the queue
  • +unreachable — nothing imports it: the CVE drops one severity rung and hides by default (visible with one click, never deleted)

this works across five language families — python (requirements, poetry, pipfile), javascript/typescript (package.json + lockfiles), go (go.mod), ruby (gemfile), java/kotlin (maven, gradle). unreachable is a demotion, not a verdict of safe — which is exactly why it's reversible and logged.

## package-level vs. function-level reachability

it's worth being precise about which claim this is, because the two get conflated constantly. package-level reachability asks whether your code imports the vulnerable package at all — a static, cheap check against the import graph. function-level reachability (sometimes call-graph or taint reachability) asks the stronger question: does an actual call path reach the specific vulnerable function, with attacker-influenced data flowing into it? that requires building a call graph and often tracing data flow — significantly more expensive, and blind to anything loaded dynamically (reflection, eval, plugin registries resolved at runtime).

virgil runs package-level reachability. it's a conservative noise filter, not a soundness proof: it will essentially never mark something reachable when it isn't, but a package can be package-level reachable while the specific vulnerable function inside it is never actually called — that gap is why unreachable CVEs are demoted, not deleted.

## clusters: count problems, not symptoms

the second mechanism attacks duplication. when semgrep reports sql injection at 47 call sites, the useful unit isn't the call site — it's the one raw-query helper they all flow through. virgil groups findings that share a root cause into a single cluster whose headline points at the shared code:

#02 [ HIGH ]  SQL injection via raw query helper  ×12
      12 callsites share src/db/query.py — fix the helper, not callsites.

one fix, twelve findings resolved. the cluster keeps every member finding as evidence, so nothing is hidden — it's the same information, aggregated at the altitude you actually fix things.

## what survives gets ranked

after demotion and clustering, the queue is short enough to rank honestly: severity, how much of the codebase a cluster touches, whether the CVE is in CISA's known-exploited-vulnerabilities catalog (a KEV entry means someone is using it in the wild — it outranks a theoretical critical). each item carries a one-line rationale. with no model in the loop, a deterministic fallback produces the same queue shape — ranking is a product feature, not an llm trick.

## the numbers you see

 CRIT  HIGH  MED   LOW   INFO  KEV  unreach
   2     7    14    6     3     1     19

that unreach 19 is the point: nineteen dependency CVEs that would have been rows in another tool's report, demoted because your code never touches the vulnerable packages. the two criticals are what your week is for.

see the whole pipeline on how it works, or run it on your repo — 3 free audits, no card.