Skip to content

feat(osv): Java/Maven dependency-CVE resolution via syft SBOM (stacked on #190) - #191

Closed
Nehanth wants to merge 5 commits into
vercel-labs:mainfrom
Nehanth:osv-java-upstream
Closed

Nehanth wants to merge 5 commits into
vercel-labs:mainfrom
Nehanth:osv-java-upstream

Conversation

@Nehanth

@Nehanth Nehanth commented Oct 20, 2026

Copy link
Copy Markdown

Stacked on #190. The first four commits are #190 (the Python osv dependency-CVE adapter + review hardening); this PR adds the Java/Maven path on top — review the final commit. Please merge #190 first; its commits drop out of this diff once it lands.

What

Extends the osv dependency-CVE adapter to Java/Maven. osv-scanner can't read a bare pom.xml ("no package sources"), so Maven dependency CVEs were invisible.

syft resolves the Maven project into a CycloneDX SBOM, which osv-scanner then matches against advisories — reusing the existing osv tool rather than adding grype. Runs alongside the Python floor-resolver; excludes mirror the vendored-tree filter; the SBOM is kept as deps-sbom.cdx.json evidence.

Hardening (consistent with #190)

  • syft is probed on PATH and run with a timeout; it only catalogs (no build/exec), so no sandbox flags are required.
  • parseOsv dedupes by name@version@ecosystem — osv-scanner parses a simple pom.xml during the recursive walk and the SBOM, so the same package can appear twice.

Validation (end-to-end, real harness)

  • WebGoat (xstream 1.4.5): syft→SBOM→osv surfaced 36 advisories; the threat model pinned reachability; matcher steering generated an xstream-fromxml-deserialization matcher.
  • commons-cli / commons-collections / snakeyaml / nimbus-jose-jwt / commons-fileupload: SBOMs resolved, 0 CVEs — correct (deps are current/clean; no false positives).

Scope

  • Maven/pom only. Gradle-without-lockfile won't resolve (syft needs a lockfile for Gradle) — noted follow-up.
  • Degrades cleanly: no pom, or syft missing → falls back to the source walk unchanged.

🤖 Generated with Claude Code

Nehanth and others added 5 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>
…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>
@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.

Comment on lines +223 to +229
"--exclude",
"./vendor/**",
"--exclude",
"./node_modules/**",
"--exclude",
"./third_party/**",
"--exclude",

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.

In Maven monorepos with nested vendor, node_modules, or .deepsec directories, the root-only Syft exclusions allow unrelated dependencies into the SBOM and report their CVEs as project dependencies.

Fix on Vercel

@Nehanth

Nehanth commented Oct 20, 2026

Copy link
Copy Markdown
Author

Folded into #190 — the Java/Maven commit now lives directly in #190 (one combined dependency-CVE adapter PR instead of a stack). Closing this.

@Nehanth Nehanth closed this Oct 20, 2026
@Nehanth
Nehanth deleted the osv-java-upstream branch October 20, 2026 19:26
@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 · 36 followers · 25 public repos)
Commits signed/verified ⚠️ 0/5

Review panel: 🔴 high highest severity

deepsec maintainer code review: 🟡 medium

The enrichment path introduces false-clean checkpoints, destructive persisted-data behavior, and resume/cancellation regressions that should be fixed before merge.

  • packages/deepsec/src/setup/osv-enrich.ts:288 — All osv-scanner failures—including timeout, network failure, and invalid arguments—are converted into an empty findings file. Setup then checkpoints this as a clean scan and will not retry, silently hiding dependency CVEs.
  • packages/deepsec/src/setup/coordinator.ts:540 — The upgrade overwrites an existing complete, potentially hand-edited INFO.md whenever OSV finds anything. A routine resumed setup can therefore destroy persisted project threat-model customizations.
  • packages/deepsec/src/setup/coordinator.ts:651 — The dependency matcher pass is gated on there having never been any matcher attempt. Upgraded projects with prior coverage-repair attempts will enrich INFO.md but never generate CVE-specific matchers once coverage already passes.
  • packages/deepsec/src/setup/coordinator.ts:524 — Dependency enrichment uses synchronous subprocesses outside the abortable workflow. Each manifest can block twice for 120 seconds before syft and osv-scanner run, so --max-duration and cancellation cannot stop setup near the requested boundary.
  • packages/deepsec/src/setup/osv-enrich.ts:47 — requirements.txt is treated as an authoritative lockfile solely by filename. Common unpinned or partial requirements files therefore suppress pyproject range resolution and leave applicable Python dependency CVEs undiscovered.

General code review: 🔴 high

The Maven scan is invoked incorrectly and the resume gate omits CVE matcher generation for existing projects, producing security-review false negatives.

  • packages/deepsec/src/setup/osv-enrich.ts:281 — `scan source` accepts one source directory; appending the CycloneDX file as a second positional argument causes osv-scanner to fail instead of scanning it. The catch then treats the empty stdout as zero findings, so Maven enrichment—and Python findings in mixed projects—silently disappears. Scan the SBOM via the dedicated SBOM command and merge both JSON results.
  • packages/deepsec/src/setup/coordinator.ts:651 — Using global `matcherAttempts.length` to gate the CVE pass skips enrichment for upgraded projects that previously attempted unrelated coverage matchers. If their coverage now passes, the newly discovered dependency CVEs never receive matcher generation; persist a dedicated dependency-enrichment checkpoint instead.

Scanner and matcher quality: 🔴 high

The Maven path is silently broken at the osv-scanner boundary, and upgrade/filtering gaps can also miss or misattribute dependency-CVE matchers.

  • packages/deepsec/src/setup/osv-enrich.ts:281 — Pass the generated CycloneDX file with osv-scanner's `--sbom` option. Appending it as a second positional source after the project root makes `scan source` fail; the catch then converts that stderr-only failure into an empty findings artifact, silently disabling Maven CVE detection.
  • packages/deepsec/src/setup/coordinator.ts:651 — Existing projects with any prior matcher attempt never receive the new dependency-CVE matcher pass. The rubric bump reruns enrichment, but `state.matcherAttempts.length === 0` remains false; when coverage already passes, the loop is skipped entirely.
  • packages/deepsec/src/setup/osv-enrich.ts:29 — The vendored-source filter omits `third_party`, although the new Syft path explicitly treats it as vendored. osv-scanner's recursive source walk can therefore feed third-party lockfile CVEs into the threat model and generate false-positive project matchers.

Sandbox and credential boundaries: 🔴 high

The Java/Maven syft path omits the cwd pinning that the adjacent uv invocation uses, letting an untrusted repo's .syft.yaml re-enable network cataloging and defeat exclusions; the excludes and vendored-source filter are also ineffective for SBOM results, and osv failures silently produce a clean-looking artifact.

  • packages/deepsec/src/setup/osv-enrich.ts:235 — `syft` is invoked without a `cwd`, so it inherits the deepsec process cwd — which is the `.deepsec/` workspace inside the scanned repository. syft loads its configuration from `.syft.yaml` / `.syft/config.yaml` in the working directory, so an untrusted repo can ship a config that re-enables network cataloging (e.g. `java.use-network` with an attacker-controlled `maven-base-url`, giving egress/SSRF plus disclosure of the project's dependency graph) and clears `exclude`. This is exactly the attack the `uv` invocation 40 lines above deliberately defends against (`cwd: tempDir`, documented as "no `uv.toml`/`[tool.uv]` from the project tree is honored"); the syft path drops that protection. Pin `cwd` to the temp dir — the `dir:` target is already absolute.
  • packages/deepsec/src/setup/osv-enrich.ts:224 — The syft excludes do not "mirror the vendored-tree filter" as the comment claims: `./vendor/`, `./node_modules/`, `./third_party/` are root-anchored globs and do not match nested trees (`frontend/node_modules/`, `src/vendor/**`). Worse, every package from the SBOM arrives with `source.path` equal to the temp SBOM file, so the `VENDORED_SOURCE` guard in `parseOsv` can never drop them. Vendored/third-party dependencies are therefore attributed to the project, inflating `dependencyCveCount`, forcing an extra matcher-generation pass plus full rescan, and polluting the threat-model prompt with packages the project does not ship.
  • packages/deepsec/src/setup/osv-enrich.ts:288 — Any osv-scanner failure is collapsed into a clean-looking artifact: on timeout (120 s, now much more likely with a whole-tree SBOM added), crash, or `maxBuffer` overflow, the catch keeps only partial stdout, `parseOsv` fails to `JSON.parse` it and returns `[]`, and `osv-findings.json` is written as `[]`. Readers and `readOsvFindingCount` cannot distinguish that from "no vulnerable dependencies" — the same misleading-empty-artifact problem the missing-tool branch 30 lines above explicitly refuses to create.
  • packages/deepsec/src/setup/coordinator.ts:524 — `runOsvEnrich` is fully synchronous (`execFileSync` throughout) and receives neither `workflowSignal` nor the duration budget, so it blocks the event loop and cannot be aborted. Its cost is repository-controlled: `findManifests` collects every `pyproject.toml` within depth 3 with no cap, and each lockfile-less one spends up to 2 × 120 s in `uv`, plus syft and osv-scanner. A repo with a few dozen `pyproject.toml` stubs stalls setup for tens of minutes past `maxDurationMs`, since the abort timer cannot fire while the loop is blocked.
  • packages/deepsec/src/setup/generated-matchers.ts:55 — The entire INFO.md is appended verbatim and unclamped to the end of the matcher-generation prompt — the highest-leverage position for instruction override. INFO.md is agent prose derived from untrusted repository content, so repo text can be laundered into this prompt, yet every other repo-derived field in this feature (package name, version, advisory summary) is whitespace-collapsed and length-clamped by `sanitizeField` specifically "to block newline/markdown prompt injection". Apply the same collapse/clamp (or inject only the advisory-reachability section) rather than the raw file.

Durability and distributed state: 🟡 medium

The enrichment path has stale-state, cancellation, checkpoint, and interrupted-resume defects that should be fixed before merge.

  • packages/deepsec/src/setup/osv-enrich.ts:262 — A missing osv-scanner returns without clearing the prior osv-findings.json. After dependencies change, the old artifact is still counted and can generate matchers for removed CVEs.
  • packages/deepsec/src/setup/osv-enrich.ts:271 — The synchronous uv/syft/osv subprocesses ignore the workflow AbortSignal and block the timer event loop. With multiple manifests, setup can exceed --max-duration by many sequential 120-second timeouts.
  • packages/deepsec/src/setup/coordinator.ts:705 — Generated matchers are persisted before matcherAttempts records completion. An interruption during the final scan leaves matcherAttempts empty, so resume treats the dependency-CVE pass as unrun and appends another semantic duplicate under a new slug.
  • packages/deepsec/src/setup/coordinator.ts:504 — Tool absence and scan failures are committed as a completed info checkpoint keyed only by repository inputs. Installing syft/osv-scanner or recovering from a transient failure will not retry enrichment until repository content changes.

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

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