Skip to content

feat(osv): dependency-CVE adapter — reachable dependency CVEs (Python + Java/Maven) - #190

Open
Nehanth wants to merge 13 commits into
vercel-labs:mainfrom
Nehanth:osv-adapter-upstream
Open

Nehanth wants to merge 13 commits into
vercel-labs:mainfrom
Nehanth:osv-adapter-upstream

Conversation

@Nehanth

@Nehanth Nehanth commented Oct 20, 2026 •

Copy link
Copy Markdown

What

Adds an osv-based dependency-CVE step to setup so known-vulnerable dependencies are surfaced, reasoned about for reachability, and turned into located findings — not just listed. Covers Python (pyproject ranges) and Java/Maven (pom.xml).

  1. Threat-model injection — run osv-scanner over dependencies before the threat model is written, persist osv-findings.json, and inject a "Known-vulnerable dependencies" section into the repository-analysis prompt so the agent judges reachability per advisory.
  2. Python floor-resolution — osv-scanner only reads lockfiles; a range-only pyproject.toml reports "no package sources". Resolve lowest+highest allowed versions with uv pip compile and hand osv the resolved lockfiles.
  3. Java/Maven SBOM — osv can't read a bare pom.xml; syft resolves the Maven project into a CycloneDX SBOM that osv then matches (reuses osv, not grype).
  4. CVE-steered matchers — matcher generation receives the threat model so a reachable dependency CVE gets a matcher and becomes a scan candidate.

Security & robustness hardening (per review)

  • uv sandboxed on repo-controlled manifests: --no-build, --no-sources, isolated cwd (no arbitrary code execution).
  • Sanitize package name/version before the prompt (prompt-injection defense).
  • Timeouts on uv, syft, and osv-scanner; probe each tool on PATH (missing tool reported distinctly, not as "no vulns").
  • Skip floor-resolution when a real lockfile exists (no false positives); resolve all manifests in a monorepo; parseOsv dedup.
  • Checkpoints: bump rubricVersion so existing projects adopt it; don't overwrite the CVE-enriched INFO.md; run the dep-CVE matcher pass once (not every resume).

Validation (end-to-end)

  • Python / FastAPI (range-only pyproject): 0 → 22 advisories floor-resolved; produced a located Starlette multipart-DoS finding the baseline never generates.
  • Java / WebGoat (xstream 1.4.5): syft→SBOM→osv surfaced 36 advisories; matcher steering generated xstream-fromxml-deserialization.
  • commons-cli/-collections, snakeyaml, nimbus-jose-jwt, commons-fileupload: SBOMs resolved, 0 CVEs (correct, deps are clean).

Degrades cleanly throughout: missing tool, no vulnerable deps, or no manifest → empty context, setup unchanged.

(The Java work was briefly opened as a stacked PR #191; folded into this PR instead — one combined adapter.)

🤖 Generated with Claude Code

Nehanth and others added 3 commits October 20, 2026 10:21
Run osv-scanner during setup, persist the result to osv-findings.json, and inject
a summary of known-vulnerable dependencies into the threat-model prompt so the
model writes the Threat model and patterns-to-flag sections with those advisories
in view (and reasons about whether each is reachable, not just present).

- osv-enrich.ts: run osv-scanner, parse results, drop vendored sources
  (.deepsec/node_modules/vendor), keep each advisory's id + one-line summary so
  the ids are self-explanatory, and write osv-findings.json as a first-class
  artifact.
- repository-analysis.ts: optional dependencyContext appended to the threat-model
  prompt.
- coordinator.ts: run osv before the info phase and pass the context through.

Degrades cleanly when osv-scanner is absent or finds no package sources. Hermetic
tests cover parsing, vendored-source filtering, summaries, and prompt injection.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…ning

osv-scanner only reads pinned lockfiles, so a source package that declares
dependency ranges in a manifest (no lockfile) reports "no package sources"
and every dependency CVE is missed.

Resolve the manifest to its lowest allowed versions — the worst-case /
applicable-CVE floor, since a `>=` constraint permits a build to land on that
floor — with `uv pip compile --resolution lowest`, then hand osv-scanner the
resolved lockfile alongside the recursive source walk. The resolved lockfile is
written to a temp dir (so its osv source path is not treated as vendored) and a
copy is kept as deps-lowest.txt evidence next to osv-findings.json.

Degrades cleanly: when uv is missing or the manifest cannot be resolved, the
scan falls back to the source walk unchanged.

On a bare pyproject.toml (fastapi, range-only) this surfaces 22 advisories
across anyio/idna/starlette where the source walk alone found none.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…ories (phase 2)

Phase 1 fed dependency CVEs into the threat model, which shapes the per-finding
investigation. But matcher generation — the step that decides which code even
becomes a scan candidate — only ever filled surface-coverage gaps; it never saw
the CVEs. So a dependency advisory was only surfaced when its code happened to
also be an uncovered surface (incidental), and a CVE in already-covered or
non-surface code was never flagged at all.

Pass the threat model (INFO.md) and the known-vulnerable dependency count into
proposeMatchers. When the project has vulnerable deps, the prompt asks the agent
to also propose matchers anchored to the reachable advisories' call sites (e.g. a
form-parsing call with no size/field limit), skipping the ones the threat model
marks not-reachable. The coordinator runs one matcher pass even when every
surface is already covered, so a dependency CVE in non-gap code still gets a
matcher. Degrades cleanly: no vulnerable deps or no threat model → unchanged.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@vercel

vercel Bot commented Oct 20, 2026

Copy link
Copy Markdown
Contributor

@Nehanth is attempting to deploy a commit to the Vercel Labs Team on Vercel.

A member of the Team first needs to authorize it.

@vercel-internal-security-reviewer

Copy link
Copy Markdown

Security review details

@auto-maintain

auto-maintain Bot commented Oct 20, 2026 •

Copy link
Copy Markdown

🤖 auto-maintain review

Automated, advisory triage for @Nehanth's PR. Facts below are read from the GitHub API.

Check Result
Author's merged PRs (this repo) 0 — ⚠️ first-time contributor
Account established ✅ (age 3914d · 37 followers · 25 public repos)
Commits signed/verified ⚠️ 0/13

Review panel: 🔴 high highest severity

deepsec maintainer code review: 🟡 medium

The enrichment pipeline can persist stale verdicts, silently miss advisories, and fail to recover after missing tooling; these correctness issues should be fixed before merge.

  • packages/deepsec/src/setup/coordinator.ts:592 — A successful clean OSV refresh preserves the previous reachability block because empty findings and scanner failure both produce no fresh block. After a dependency is patched, stale HIGH verdicts remain in INFO.md and can be promoted as false findings.
  • packages/deepsec/src/setup/repository-analysis.ts:128 — The required per-dependency reachability block is never validated. A syntactically valid analysis that omits or partially emits it is accepted, leaving matcher generation without the advisory details needed to produce candidates.
  • packages/deepsec/src/setup/coordinator.ts:553 — OSV failures still allow the info checkpoint to complete, while its digest records only the enabled flag. Installing a previously missing scanner/resolver and resuming unchanged source therefore skips enrichment permanently instead of retrying it.
  • packages/deepsec/src/setup/osv-enrich.ts:366 — Maven projects silently skip SBOM generation when syft is absent. OSV can then persist an empty result as if the project were clean, contrary to the promised distinct missing-tool reporting.

General code review: 🟡 medium

Dependency-CVE enrichment can retain obsolete reachability verdicts and silently discard current advisory context.

  • packages/deepsec/src/setup/coordinator.ts:592 — A successful clean OSV rescan produces no fresh reachability block, so this branch preserves the previous block. After a dependency is patched or removed, stale HIGH verdicts can continue forcing obsolete CVE findings; clean scans should remove the old block, while scanner failures need a distinct status.
  • packages/deepsec/src/setup/repository-analysis.ts:128 — Validation accepts analysis output that omits the required Dependency reachability subsection. Because the raw OSV context is not subsequently passed to matcher generation, one noncompliant agent response silently loses all advisory details despite persisted hits and leaves the CVE matcher pass unable to target them.

Scanner and matcher quality: 🔴 high

The Maven scan path is broken, and checkpoint/preservation behavior can both miss new advisories and retain patched ones as findings.

  • packages/deepsec/src/setup/osv-enrich.ts:442 — The generated CycloneDX file is appended as a positional source path rather than supplied through osv-scanner's SBOM input, so Maven components are not scanned and some versions reject the invocation entirely.
  • packages/deepsec/src/setup/coordinator.ts:592 — A successful clean OSV refresh produces no reachability block, but this branch preserves the previous block. After upgrading a vulnerable dependency, stale HIGH verdicts and generated candidates can still be promoted as false vulnerability findings.
  • packages/deepsec/src/setup/coordinator.ts:560 — OSV enrichment only runs when the repository-analysis checkpoint is stale. Rerunning setup with unchanged source never queries the updated advisory database, so newly disclosed CVEs remain undetected indefinitely.

Sandbox and credential boundaries: 🟡 medium

The opt-in osv step is well-bounded overall (off by default, timeouts, tool probes, sanitized prompt fields), but the uv SSRF guard misses manifest-controlled index configuration, the new subprocesses bypass the repo's established minimal-env credential boundary, and the resolved-version caveat is lost before the promote step — fix or consciously accept before merge.

  • packages/deepsec/src/setup/osv-enrich.ts:279 — The SSRF guard for `uv pip compile` is incomplete. `--no-sources` only neutralizes `[tool.uv.sources]`, and `DIRECT_REF` only matches PEP 508 `name @ <url>` forms — neither covers index/link configuration that uv reads from the same repository-controlled `pyproject.toml` (`[[tool.uv.index]]`, `[tool.uv.pip] index-url` / `extra-index-url` / `find-links`). Since no `--index-url`/`--no-index` is passed to pin the resolver, a malicious manifest can still steer the resolution fetch to an attacker-chosen or internal host, which is exactly the outbound-request class `manifestHasDirectRefs` was added to block.
  • packages/deepsec/src/setup/osv-enrich.ts:284 — `uv`, `syft`, `osv-scanner` and `which` are spawned with no `env` option, so they inherit the orchestrator's full `process.env` (GITHUB_TOKEN, AWS_, VERCEL_TOKEN, API_KEY). This contradicts the credential boundary the repo already enforces for untrusted-repo-facing subprocesses via `buildCodexEnv` (packages/processor/src/agents/codex-sdk.ts:268), and these are network-reaching tools whose endpoint can be influenced by the repository-controlled manifest and by inherited `UV`/`HTTP_PROXY` settings. uv also echoes any configured index URL (credentials included) into the compiled requirements file that is then copied to `data/<id>/setup/deps-.txt`.
  • packages/deepsec/src/setup/osv-enrich.ts:573 — The required reachability line format drops the `(resolved range version — not necessarily installed)` marker that `buildContext` emits one block earlier, so the hypothetical/confirmed distinction is lost once the verdict is written into INFO.md. The new processor prompt then tells the agent that a HIGH verdict means "reachability is already established upstream" and must be promoted without re-litigation (packages/processor/src/prompt/core.ts:76), so an advisory against a `lowest-direct`/`highest` version the project does not actually install is reported as a real finding.

Durability and distributed state: 🟡 medium

Two resumability issues can lose CVE matcher work or prevent bounded cancellation during enrichment.

  • packages/deepsec/src/setup/coordinator.ts:754 — The CVE pass digest is committed before its proposed matchers are persisted. A crash after this write but before writeGeneratedMatchers makes a resume skip the pass when coverage already passes, permanently losing the proposal.
  • packages/deepsec/src/setup/osv-enrich.ts:426 — OSV enrichment uses synchronous subprocesses without the workflow AbortSignal. These calls block the event loop, so cancellation and --max-duration cannot take effect until potentially several minutes of uv/syft/osv timeouts finish.

Posted by auto-maintain. This automated code review is advisory; a human maintainer makes the call.

Comment on lines +133 to +137
maxBuffer: 64 * 1024 * 1024,
stdio: ["ignore", "pipe", "ignore"],
});
} catch (error) {
const stdout = (error as { stdout?: unknown })?.stdout;

@vercel vercel Bot Oct 20, 2026 •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

With --osv or osv enabled in config, dependency enrichment still blocks workflow cancellation and the configured setup duration limit because its subprocesses execute synchronously without the workflow signal.

Fix on Vercel

…kpoints)

Addresses the automated review on the dependency-CVE adapter:

Security:
- uv is now sandboxed when resolving a repo-controlled manifest: `--no-build`
  (no PEP 517 build hooks / dynamic-metadata project builds), `--no-sources`
  (ignore attacker `[tool.uv.sources]`), and an empty temp cwd (no project
  `uv.toml`/`[tool.uv]`). Resolution still works; repo code never executes.
- Sanitize package name/version (collapse whitespace, clamp length) before they
  reach the threat-model prompt — blocks newline/markdown injection from
  attacker-controlled lockfile fields.
- osv-scanner now runs with a timeout (it had none) so a stalled scan can't wedge
  the info phase indefinitely.
- Probe for uv/osv-scanner on PATH; a missing tool is reported distinctly (warn,
  no artifact) instead of writing an empty osv-findings.json that reads as
  "no vulnerable dependencies".

Correctness:
- Skip floor resolution when an authoritative lockfile exists (uv.lock,
  poetry.lock, requirements.txt, …) so installed/patched versions aren't
  overridden by hypothetical floor versions (false positives).
- Resolve both lowest and highest allowed versions (widens range coverage).
- Resolve every manifest in a monorepo, not just the first.
- Dedupe parseOsv by name@version@ecosystem (a package can surface from multiple
  resolved lockfiles).
- Matcher prompt: require closesSurfaceIds (the schema rejects empty, which could
  abort setup) — point it at the surface the vulnerable call sits in.

Checkpoints / durability:
- Bump rubricVersion (2 -> 3) so existing projects re-run and gain the enrichment.
- Don't restore the pre-osv INFO.md over the CVE-enriched threat model.
- Run the dependency-CVE matcher pass only on first generation, not every resume.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…d on vercel-labs#190)

osv-scanner can't read a bare pom.xml. syft resolves the Maven project into a
CycloneDX SBOM, which osv-scanner then matches against advisories (reusing our
osv tool, not grype). syft is probed on PATH and run with a timeout; it only
catalogs (no build/exec). parseOsv dedup already merges a package seen from both
osv's pom parse and the SBOM.

Stacked on vercel-labs#190 (the Python osv adapter + hardening); this commit is the Java
addition on top. Validated end-to-end on WebGoat (xstream 1.4.5 -> 36 advisories,
SBOM resolved, CVE-targeted matcher generated).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@Nehanth Nehanth changed the title feat(osv): dependency-CVE adapter — surface reachable dependency CVEs in the threat model and matchers feat(osv): dependency-CVE adapter — reachable dependency CVEs (Python + Java/Maven) Oct 20, 2026
…-positives

Addresses the second review pass:

Security:
- Reject manifests with PEP 508 direct URL/VCS/path deps before resolving — uv
  would otherwise fetch attacker-chosen URLs (SSRF). (`--no-sources` only covered
  `[tool.uv.sources]`.)
- Write resolver evidence AND osv-findings.json under the gitignored `data/<id>/
  setup/` dir so they are never committed/pushed to the shared data repo (a
  compiled index URL could carry a private-index token).

Failure handling (no silent false negatives):
- Distinguish a real scan failure (timeout/crash/no parseable output) from a
  clean/no-sources scan: on failure, warn and do NOT overwrite osv-findings.json
  with an empty result (which would erase prior findings / read as "no vulns").
- Warn when uv is missing but range-only manifests exist (same treatment as a
  missing osv-scanner).

False positives / correctness:
- Mark resolved floor/ceiling versions as hypothetical ("resolved range version —
  not necessarily installed") in the threat-model context, so an up-to-date
  project isn't reported as actually vulnerable.
- Drop requirements.txt from the authoritative-lockfile list (it is often
  unpinned, which would wrongly skip resolution).

Durability / cancellation:
- Track the dependency-CVE matcher pass with its own state flag
  (`osvMatcherPassDone`) instead of matcherAttempts length — so it runs once, and
  upgraded projects with older matcher attempts still get it after rubricVersion 3.
- Cap resolved manifests (10) and add an overall resolve-time budget so a large
  monorepo can't stall setup for many minutes (the sync calls can't observe an
  abort signal; bounding total time is the pragmatic guard — full async
  cancellation is a follow-up).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The round-2 INFO.md merge appended the fresh osv advisory section to the prior
INFO.md, but that file already contained a previously appended section — so
refreshes accumulated duplicate sections and kept listing dependencies that had
since been patched or removed (false positives).

Extract mergeInfoWithOsvContext(): strip any previously appended section (located
by the shared OSV_CONTEXT_HEADING) before appending the current one, and drop the
section entirely when there are no current advisories. Curated content is kept.

(The related "upgraded projects never get the CVE matcher pass when coverage
passes" finding was already addressed by the osvMatcherPassDone checkpoint flag.)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Nehanth and others added 4 commits October 20, 2026 16:23
The osv step runs external, network-reaching tools (osv-scanner, uv, syft) during
setup, which crosses the documented read-only/no-network analysis boundary. Make
it opt-in per project: `osv: true` in the deepsec.config.ts project declaration.
Off by default — when unset, runOsvEnrich is never called (no network, no overhead).

Enabling it (osv: true) invalidates only that project's info checkpoint via the
digest, forcing one re-run to adopt the enrichment; projects that never enable it
keep their prior digest and are not re-run. hasDepCves is also gated on the flag.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Enable dependency-CVE enrichment per run with deepsec setup/init --osv, alongside
osv: true in deepsec.config.ts. Default off.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Add docs/dependency-cves.md (what it does, --osv flag / osv:true config, required
tools, Python ranges + Java/Maven coverage, how results surface, security notes,
limitations). Add the `osv` field to the ProjectDeclaration reference, list the
page in the docs nav, and note the dependency step as the opt-in exception to the
read-only/no-network analysis boundary in vercel-setup.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Make the dependency-CVE path deterministic at the two steps where it was
silently losing reachable advisories to run-to-run variance.

- threat model: buildContext now emits a required, graded "Dependency
  reachability" block — one line per dependency (HIGH/MEDIUM/LOW + call
  site + consequence), covering every advisory osv found, instead of a
  soft "note where it's used" prose instruction that collapsed into a
  vague summary on noisier runs.

- investigation: CORE_PROMPT now treats those grades as promotion
  guidance — HIGH with a confirmed call site is a finding to promote (not
  "just a library default"), MEDIUM is judged on its merits, LOW stays as
  context unless a missed call site is found. Reachability is gated
  upstream; the investigation confirms and writes it up.

Verified on fastapi: the Starlette unbounded-body DoS now promotes to a
located finding (dropped as context in prior runs), while transitive
anyio/idna stay LOW with zero false-positive findings.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…isting INFO.md

When osv was enabled on a project that already had a complete INFO.md, the
preserve path overwrote the freshly generated analysis with the old threat
model plus osvEnrichment.context. Since that context is now a prompt directive
(instructions to emit a graded Dependency reachability block), the persisted
INFO.md ended up carrying raw grading instructions and the real verdicts the
analysis agent just produced were discarded — so matcher generation saw the
stale threat model, not the reachability grades.

Enabling osv is a deliberate "re-analyze with dependencies in view" action, so
keep the fresh analysis (verdicts included) instead of restoring the preserved
copy. The non-osv preserve path is unchanged and still keeps curated content.

Adds a coordinator test for the osv + preserved-INFO.md path (verified to fail
without the fix).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Setup state and INFO.md handling:
- Remember the last explicit --osv / --no-osv choice in setup state, and
  carry --osv in the printed resume command. Previously a plain resume or a
  later bare init/setup flipped the info checkpoint digest, re-ran the
  analysis without dependency context, and dropped the reachability verdicts.
  --no-osv is new and is the way to turn the step back off.
- Keep a hand-edited INFO.md on every re-run. Only the agent's
  "### Dependency reachability" block is refreshed (spliced into the Threat
  model section), kept as-is when osv produced nothing, and removed when osv
  is off.
- Stop the osv section strip at the next "## " heading instead of cutting to
  end of file.
- Record the dependency matcher pass only after it completes, keyed to a
  digest of the vulnerable-dependency set, so a failed pass is retried and a
  newly found dependency gets its own pass.
- Ignore a leftover osv-findings.json when osv is disabled.

Scanning:
- Resolve ranges with --resolution lowest-direct instead of lowest, so only
  declared dependencies are floored. Plain lowest also pinned transitive
  dependencies to versions no real install picks; on fastapi this cut the
  report from 3 packages / 22 advisories to starlette / 14.
- Capture osv-scanner stderr so "No package sources found" (exit 128, empty
  stdout) is treated as a clean result and clears stale findings.
- Match vendored trees against the path relative to the project root, so a
  project checked out under a vendor/ directory is not filtered out.
- Warn when uv or syft fails to resolve a manifest instead of reporting it as
  clean.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Comment on lines +589 to +593
const merged = osvEnabled
? freshBlock
? spliceReachabilityBlock(curated, freshBlock)
: curated
: spliceReachabilityBlock(curated, undefined);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A successful OSV refresh with no vulnerable dependencies retains the previous HIGH reachability verdicts in INFO.md, so patched or removed dependencies remain promotion guidance for investigations.

Fix on Vercel

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant