Dependencies & CVEs
Vulkro matches your dependency manifests against a local CVE bundle. Today it parses five ecosystems:
- npm:
package.json(withpackage-lock.jsonfor the resolved version). - PyPI:
requirements*.txt,Pipfile,pyproject.toml(withpoetry.lock/Pipfile.lockfor the resolved version). - Go:
go.mod(therequireblock; the module-graph-resolved versions, matched against the OSVGoecosystem). The leadingvis stripped to match OSV's version format. Go modules are not in the reachability call-shape set, so Go CVE findings carry no[reachable]/[unreachable]tag. - Cargo (Rust):
Cargo.toml([dependencies],[dev-dependencies],[build-dependencies],[workspace.dependencies]), withCargo.lockfor the resolved version, matched against the OSVcrates.ioecosystem. The lock is found next to the manifest or at the workspace root. SemVer requirement floors (serde = "1") without a lock resolve to Low confidence. - Maven (Java/JVM):
pom.xml<dependencies>, keyed asgroupId:artifactIdagainst the OSVMavenecosystem.${property}placeholders are resolved against<properties>. Direct declared dependencies only: parent-POM inheritance,<dependencyManagement>version pins, and transitive resolution are out of scope, so a version managed by a parent POM or BOM (e.g. a Spring Boot starter) is reported without a version and is not matched against CVEs.
CVE scanning covers the manifests above. Maven (pom.xml) gives a Java /
Spring project dependency coverage on top of its full source analysis - see
Java and Spring. Cargo is dependency-CVE only:
it extends supply-chain coverage but does not add Rust source (taint)
analysis - see Supported languages.
Gradle (build.gradle, build.gradle.kts) is detected as a Java project
and scanned for code, but its dependency declarations are not yet parsed for
CVEs: a Gradle-built Java project gets full source analysis but no
dependency-CVE findings until it also carries a pom.xml. RubyGems and NuGet
manifests are likewise not yet parsed for CVEs. (Secrets, IaC, and container
scanning still run on those repos.)
The live OSV lookup covers every ecosystem above out of the box. For fully
offline matching (VULKRO_OFFLINE=1), the local CVE bundle must include
the ecosystem's file (crates-io.json, maven.json, ...). The default bundle
ships npm + PyPI; a wider bundle covering Go, Maven, and crates.io is
distributed as a signed offline bundle you apply with
vulkro update --bundle <path> (see
Air-gap: bundle format).
The bundle aggregates OSV + NVD + CISA KEV + EPSS and is refreshed daily by Vulkro's internal bundler.
How matching works
Each detected package + version is looked up in the local CVE bundle
(~/.vulkro/data/cves/<ecosystem>.json). A finding is emitted per
matched (package, vulnerable-range) pair, carrying:
cve_id-CVE-2024-...severity-Critical | High | Medium | Lowcvss- base scoredescription- short summarykev_added- date if listed in CISA Known Exploited Vulnerabilitiesepss_score- exploit-prediction probability (0-1)vulnerable_symbols- for selected packages, the function names that carry the vulnerability (used by reachability)
The local bundle path is configurable via VULKRO_CDN_BASE_URL for
mirroring.
KEV / EPSS prioritisation
KEV-listed CVEs are bumped to Critical regardless of CVSS. EPSS >= 0.9 is bumped to High. The decoration modifies severity in place after the initial CVE match.
Your output looks like this:
DEPS
CVE-2021-23337 lodash 4.17.20
CISA KEV - actively exploited [reachable]
CVE-2020-14343 pyyaml 5.3.1
EPSS 91% [unreachable]
Reachability annotation
For a curated set of packages - lodash, axios, requests, pyyaml,
jinja2, etc. - Vulkro tags each CVE finding by whether a call shape for
the package appears in your source (a textual heuristic, not symbol resolution
inside the dependency):
[reachable] - a call shape for the package was found in your source
[unreachable] - no matching call shape was found
By default [unreachable] findings are demoted one severity tier, not dropped.
This cuts CVE noise on dependency-heavy projects. See
Reachability for the exact algorithm, the curated list, and
the limitations.
Reachability-based SCA gate (VULKRO_SCA_REACHABLE=1)
The reachability annotation above only decorates a finding. The opt-in SCA
reachability gate acts on it: an unreachable CVE is downgraded to Info /
Confidence::Low, and a reachable CVE is pinned to the public CVE severity (so
a catalogue that under-rates everything to Medium is overridden). It is opt-in
so existing baselines do not shift silently:
VULKRO_SCA_REACHABLE=1 vulkro scan .
How the gate decides reachability
The gate matches the CVE's known vulnerable-function names against your project, using two name-based signals. It is a heuristic, not a resolved call-graph traversal from entry points:
- Call-graph name match. Vulkro builds a call graph and collects every
function and callee name in it. The catalogue's
vulnerable_functionsare matched against those names suffix-style (so_.mergematches amergenode). A match means the name appears somewhere in the graph; it does not prove the vulnerable code is reached from an entry point. - Textual fallback. A module both imports the package AND its raw content
references the function name. Picks up dynamic
require(name), re-exports, and embeds the graph does not resolve.
Reachable findings carry evidence[].signal = "sca-reachable" with
detail = "call-graph" or detail = "<file-path>"; unreachable findings carry
signal = "sca-unreachable". SARIF and dashboard consumers can distinguish the
two.
The reachability gate only acts on npm and PyPI CVE findings: the
call-shape catalogue is npm/PyPI only. CVE matching itself is broader (npm,
PyPI, Go, Cargo, Maven - see the manifest list above), but Go, Cargo, and Maven
findings are not in the reachability set, so they carry no [reachable] /
[unreachable] tag (the CVE is still reported).
The catalogue also lists Maven, RubyGems, and NuGet call shapes that stay inert
until a future release wires reachability for them. Treat those as roadmap, not
shipped reachability coverage.
Refreshing the bundle
vulkro update # public CDN refresh
vulkro update --bundle ./vulkro-cve-2026-05-10.vkbundle # offline
VULKRO_OFFLINE=1 makes the no-flag form refuse the network. The
desktop console exposes a Quick Sync button that runs the same path.