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. 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.)

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; 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 | 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 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.