Skip to main content

Dependencies & CVEs

Vulkro matches your dependency manifests against a local CVE bundle. Today it parses five ecosystems:

  • npm: package.json (with package-lock.json for the resolved version).
  • PyPI: requirements*.txt, Pipfile, pyproject.toml (with poetry.lock / Pipfile.lock for the resolved version).
  • Go: go.mod (the require block; the module-graph-resolved versions, matched against the OSV Go ecosystem). The leading v is 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]), with Cargo.lock for the resolved version, matched against the OSV crates.io ecosystem. 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 as groupId:artifactId against the OSV Maven ecosystem. ${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.)

Offline bundles

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 | Low
  • cvss - base score
  • description - short summary
  • kev_added - date if listed in CISA Known Exploited Vulnerabilities
  • epss_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:

  1. Call-graph name match. Vulkro builds a call graph and collects every function and callee name in it. The catalogue's vulnerable_functions are matched against those names suffix-style (so _.merge matches a merge node). A match means the name appears somewhere in the graph; it does not prove the vulnerable code is reached from an entry point.
  2. 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.

Ecosystem scope

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.