Skip to content

Lockfiles & transitive dependencies + v1.0.0 polish - #4

Merged
1816x merged 7 commits into
mainfrom
claude/session-dh25dl
Jul 21, 2026
Merged

1816x merged 7 commits into
mainfrom
claude/session-dh25dl

Conversation

@1816x

@1816x 1816x commented Jul 21, 2026 •

Copy link
Copy Markdown
Owner

Summary

This is the v1.0.0 release push. The headline is transitive dependency support: until now VTA only saw direct dependencies at their unresolved constraints, so the risk that hides one level down — a vulnerable package you never chose — was invisible. It now reads lockfiles, resolves the transitive tree, and extends the reachability model to it.

A. Lockfiles / transitive dependencies (the big feature)

  • Lockfile parsing (Rust parser). New parser/src/lockfiles.rs reads package-lock.json (v2/v3 + v1), yarn.lock (classic v1), poetry.lock, Cargo.lock, and the flat Pipfile.lock. It stamps each direct dependency with its exact resolved version and emits dependencyEdges — the flattened parent→child package graph, mirroring the existing importEdges. Malformed lockfiles warn and are skipped, never fatal.
  • Data model, backward compatible. Dependency gains resolved/direct; ParsedProject gains dependencyEdges. With no lockfile there are no edges and every dependency is direct, so the JSON and every downstream stage behave exactly as before.
  • Reachability by inheritance. A transitive is as reachable as the most reachable direct that introduces it, with the introducing chain (data-utils → lodash) recorded and dev inherited when every introducer is dev-only. Conservative on purpose: it never hides a transitive of a reachable direct. Orphan transitives (e.g. from the tree-less Pipfile.lock) stay unused.
  • Precise matching. OSV matching uses the lockfile's exact resolved version instead of an approximated range.
  • Surfaced everywhere. Terminal report, HTML dashboard, SARIF, and GitHub issues all label direct vs transitive and show the chain; a transitive's remediation targets the direct parent to bump (you can't upgrade a transitive on its own).
  • Fixture. examples/vulnerable-transitive-app: a reachable transitive lodash (critical) outranks a moderate direct express, while a critical-but-unreachable transitive ejs is noise.

B. Polish (audit loose ends)

  • OSV last_affected is now treated as an inclusive upper bound (distinct from the exclusive fixed), and Vulnerability.fix_for(version) picks the fix for the range the installed version is actually in, so a multi-range advisory never recommends a downgrade.
  • Parser coverage: npm peerDependencies/optionalDependencies; Python setup.py/setup.cfg/Pipfile; requirements.txt name @ url direct references; Cargo [workspace.dependencies] and [target.*.dependencies]; .mts/.cts TypeScript.
  • Viewer hardening: coerceReport normalizes malformed/old-schema reports so rendering never dereferences undefined; escapeHtml now also escapes the single quote.
  • CI reproducibility: ruff/mypy pinned exactly. .gitignore now ignores report.json.
  • Docs + version: README, analyzer/README.md, and docs/design-decisions.md document the feature; all three components bumped to 1.0.0.

Verification

All green locally:

  • Rust: cargo fmt --check · cargo clippy --all-targets -- -D warnings · cargo test — 38 tests
  • Python: ruff check · mypy src · pytest — 71 tests
  • TypeScript: npm run typecheck · npm test — 9 tests
  • End-to-end: vta triage examples/vulnerable-transitive-app surfaces the reachable transitive lodash with its chain and renders through the viewer to terminal + HTML; the direct-only vulnerable-express-app output is unchanged.

claude added 7 commits July 21, 2026 17:15
Extend the ParsedProject contract so a lockfile's resolved tree rides
alongside the manifest-declared dependencies, leaving the no-lockfile case
byte-for-byte unchanged:

- model: Dependency gains resolved (the exact version a lockfile pinned;
  None keeps the declared-constraint behavior) and direct (false for a
  lockfile-only transitive). ParsedProject gains dependency_edges, the
  flattened package graph, mirroring import_edges. Add direct/transitive
  constructors and route the manifest parsers through direct().
- lockfiles.rs: parse package-lock.json (v2/v3 packages + v1 nested tree),
  yarn.lock (classic v1; Berry skipped gracefully), poetry.lock, Cargo.lock,
  and the flat Pipfile.lock (versions, no tree). resolve() merges them per
  ecosystem, stamps each direct with its exact resolved version, appends the
  lockfile-only packages as transitives, and returns the edges. Malformed
  lockfiles warn and are skipped, never fatal.
- examples/vulnerable-transitive-app: a fixture whose real risk is a
  reachable transitive lodash (via data-utils) with a critical-but-unused
  ejs (via an unused dev tool) as contrast.

Covered by 9 lockfile unit tests plus a parser integration test.
Consume the parser's resolved versions and dependency graph, and surface
the direct/transitive distinction everywhere:

- models: Dependency gains resolved/direct and a match_version property;
  add DependencyEdge and ParsedProject.dependency_edges; Finding gains
  direct/dependency_chain, surfaced as direct/dependencyChain in the JSON.
  All new fields default so pre-lockfile JSON still loads.
- reachability: DependencyGraph carries reachability from a direct to the
  transitives it introduces — a transitive is as reachable as the most
  reachable direct that pulls it in, with the introducing chain recorded and
  dev inherited when every introducer is dev-only; orphan transitives stay
  unused (conservative).
- osv: match against match_version (a lockfile's exact version beats an
  approximated range). Treat last_affected as an inclusive upper bound via
  VersionRange.contains, distinct from the exclusive fixed.
- models.fix_for(version): pick the fixed bound of the range the installed
  version is actually in, so a multi-range advisory never recommends a
  downgrade; triage remediation uses it and, for a transitive, names the
  direct parent to upgrade. report/SARIF/issues label direct vs transitive
  and show the chain.

With no lockfile there are no edges, every dependency is direct, and the
classification is exactly the previous import-graph result. Adds transitive
reachability, end-to-end, last_affected/fix_for, and issue-body tests.
Terminal and HTML render now tag transitive findings and show the
introducing chain (express → qs). The new direct/dependencyChain fields
are optional, so reports produced before lockfile support still render
(treated as direct). Adds terminal + HTML transitive tests and a
backward-compatible default test.
- npm: read peerDependencies and optionalDependencies (both runtime-facing,
  so prod not dev).
- Python: parse setup.py (best-effort install_requires string list),
  setup.cfg ([options] install_requires + [options.extras_require], keyed on
  indentation so requirement operators aren't mistaken for INI keys), and
  Pipfile ([packages]/[dev-packages], with "*" as an empty constraint).
- requirements.txt: capture the name from a PEP 508 direct reference
  (name @ url) instead of skipping the whole line.
- Cargo: also read [workspace.dependencies] and every
  [target.<cfg>.dependencies] table (build-deps already handled).
- TypeScript: treat .mts/.cts as TypeScript in the parser and add them to
  the analyzer's _JS_EXTS so their relative imports resolve.

Adds unit tests for each new manifest path plus .mts/.cts detection.
- Add coerceReport, which normalizes any parsed value into a well-formed
  TriageReport (missing/mistyped results, summary, or per-result arrays
  become sane defaults). Both renderTerminal and renderHtml run it, so a
  truncated write or old-schema report annotates instead of dereferencing
  undefined; index.ts rejects a non-object payload with a clean error.
- escapeHtml now also escapes the single quote (&#39;), closing an
  attribute-injection gap in the HTML dashboard.

Adds malformed-report and single-quote-escaping tests.
Pin ruff==0.15.22 and mypy==2.3.0 in the analyzer's dev extra (the CI
installs .[dev], so the pins flow straight through) — a new lint rule or
type check in a patch release can otherwise fail CI on unchanged code.
Also add report.json to .gitignore so a local 'vta triage --out report.json'
never gets committed.
- README: new 'Transitive dependencies' section explaining the
  reachability-by-inheritance rule with the fixture's demo; note lockfile and
  setup.py/cfg/Pipfile support in the capabilities, architecture diagram, and
  'what it is NOT'; refresh test counts (Rust 38, Python 71, TypeScript 9).
- design-decisions: record why lockfile parsing lives in the Rust parser and
  how a transitive inherits reachability from its introducer.
- Bump all three components to 1.0.0 (Cargo.toml, pyproject.toml,
  package.json, vta.__version__).
@1816x
1816x merged commit 35ded2d into main Jul 21, 2026
6 checks passed
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.

2 participants