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. Cargo and Maven are
dependency-CVE only: they extend supply-chain coverage but do not add
Rust or Java source (taint) analysis - see Supported languages.
RubyGems, NuGet, and Gradle (build.gradle) manifests are not yet parsed for
CVEs; support is on the roadmap. (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; build a wider bundle with the internal bundler's
--ecosystems npm,PyPI,Go,Maven,crates.io.
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 (src/security/sca_reachable.rs) 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.