Reachability annotation
Vulkro annotates each dependency-CVE finding with whether your code looks like it calls into the vulnerable package, using a textual call-shape heuristic (not a symbol-resolved call graph). The goal is noise reduction: demote the long tail of CVEs in packages you pull in but never exercise.
[reachable]- a call shape for the package (e.g._.merge(,yaml.load() appears somewhere in your source.[unreachable - vulnerable symbol not called from your code]- the package is in your dependency tree, but no matching call shape was found in your source.
This cuts CVE noise on dependency-heavy projects. It is a heuristic, not a proof: see How it works and Limitations.
Vulkro produces CVE findings for five ecosystems: npm (package.json),
PyPI (requirements*.txt, Pipfile, pyproject.toml), Go (go.mod),
Cargo (Cargo.toml + Cargo.lock), and Maven (pom.xml). The
reachability annotation below applies to npm and PyPI only: the call-shape
set has no Go, Cargo, or Maven fragments, so those findings are emitted without
a [reachable] / [unreachable] tag (the CVE is still reported). Gradle,
RubyGems, and NuGet are not yet scanned for CVEs. See
Supported languages.
Curated call-shape list
Reachability matches a pinned set of hand-written call-shape fragments for
high-traffic packages (the regex is (?:fragment)\s*( run over your file
text). The current fragments cover:
- npm: lodash (
_.merge(,_.set(), axios, ws, node-fetch, express, marked, handlebars, minimist, moment. - PyPI: requests (
requests.get(), urllib3, pyyaml (yaml.load(), jinja2, django, flask.
When a CVE record ships its own vulnerable_symbols, those names are also
matched as call shapes in your source. Outside this set, CVE findings are
emitted without [reachable] / [unreachable] tags - the same conservative
behaviour as Snyk / Dependabot.
How it works
- Parse the npm / PyPI manifest and match (package, version, CVE) tuples against the offline CVE bundle.
- For each package, pick its call-shape fragments: the curated list above,
plus any
vulnerable_symbolsthe CVE record carries. - Build a regex from those fragments and run it over each source file's text.
If any file matches, the finding is
[reachable]; otherwise[unreachable]. - By default,
[unreachable]findings are downgraded one severity tier and annotated (not dropped).vulkro scan --reachable-onlydrops them entirely;--no-reachability-filterturns the whole pass off.
This is a textual heuristic, deliberately biased toward [reachable]
(false-positive-safe): a call to the package that the regex does not recognise
still shows up, just without the demotion.
Where this matters
A typical vulnerability heavy-tail looks like:
Without reachability:
CVE-2021-23337 (lodash) your project | 1 finding
CVE-2020-14343 (pyyaml) your project | 1 finding
CVE-2023-32681 (requests) your project | 1 finding
CVE-2024-... x 47 more
With reachability:
CVE-2021-23337 (lodash) [reachable] <- a `_.template(` call was found
CVE-2020-14343 (pyyaml) [unreachable] <- no `yaml.load(` in source
CVE-2023-32681 (requests) [reachable] <- a `requests.get(` call was found
CVE-2024-... x 47 more mostly [unreachable]
Net effect on a typical Node monorepo: a large share of CVE findings move to
[unreachable] and out of the High-confidence default view.
Limitations
The heuristic is intentionally shallow. Be aware that:
- It is a substring match over your source text, not a resolved call graph.
A literal
_.merge(in a comment or string marks lodash reachable; it does not confirm the call binds to the vulnerable package. - It does not resolve the vulnerable symbol inside the dependency, and it does not require the file to import the package.
- A call reached through an alias (
const m = _.merge; m(x)) or a re-export is marked[unreachable]. - The reachability annotation covers npm and PyPI only; Go, Cargo, and
Maven CVE findings (and any future ecosystem) are emitted without a
[reachable]tag until call-shape fragments exist for them.
Treat [reachable] as "worth looking at first," not as proof of
exploitability. A resolved call-graph closure is on the roadmap.