Skip to content

feat(bridge): split phantom into phantom-declared vs phantom-transitive#76

Merged
hyperpolymath merged 1 commit into
mainfrom
feat/bridge-phantom-declared-vs-transitive
May 27, 2026
Merged

feat(bridge): split phantom into phantom-declared vs phantom-transitive#76
hyperpolymath merged 1 commit into
mainfrom
feat/bridge-phantom-declared-vs-transitive

Conversation

@hyperpolymath
Copy link
Copy Markdown
Owner

Summary

The Track E sweep across 29 Cargo CVE issues revealed that the previous bridge classifier marked any crate without a direct use <crate> statement as reach=phantom, conflating two very different remediation paths:

  • True phantom (PhantomDeclared) — crate IS in a Cargo.toml [dependencies] / [workspace.dependencies] block but never imported. Fix: cargo machete --fix strips the dep and the whole subtree drops from Cargo.lock. file-soup#50 is the canonical case.
  • Misclassified (PhantomTransitive) — crate is NOT in any Cargo.toml; pulled in transitively by a parent. Local strip is impossible; the fix requires bumping the parent past the affected version. Of 28 non-pilot Track E repos, ~26 fell here (Batch A 7/7 misclassified, Batch B almost certainly similar, Cohort E-2 4/5 misclassified per patch-bridge: classify Dioxus/GTK transitive advisories as Informational (Cohort E-2) #75).

What changed

  • Enum split: ReachabilityStatus::PhantomPhantomDeclared / PhantomTransitive. JSON wire values (serde rename_all = kebab-case) are now phantom-declared / phantom-transitive.
  • Evidence shape: ReachabilityEvidence gains optional parent_dep (populated only when transitive AND identifiable via Cargo.lock).
  • Reachability scan: new check_reachability_with_manifest(project_dir, crate, declared_deps, parent_map) returns the correct variant. Lookup is normalised (underscore↔hyphen, lowercase) so a CVE feed reporting serde_json matches a manifest line serde-json.
  • Parent identification: lockfile::collect_cargo_parents parses Cargo.lock's dependencies = [...] arrays and BFS-walks from each direct dep, attributing every reachable transitive (first BFS to reach wins). Best-effort — returns empty map on missing/unparseable lockfile, and classify falls back to "an upstream parent dependency" phrasing.
  • Classifier: classify::classify no longer takes is_direct — the evidence's status variant already carries that info. Three-way output (Mitigable / Unmitigable / Informational) is unchanged. The rationale and fix-action diverge:
    • PhantomDeclared: "Strip from Cargo.toml — run cargo machete --fix (or remove the dependency line manually) for <crate>."
    • PhantomTransitive: "Pulled in transitively by <parent> — fix requires bumping the parent dependency past the affected version of <crate>. No local strip is possible."
  • Schema bump: BridgeReport::schema_version 0.1.0 → 0.2.0 to flag the reach-field semantics change.
  • No new deps: reuses the existing in-house TOML / Cargo.lock string parsers (consistent with collect_direct_cargo_dependencies).

Test coverage

13 new tests across the three bridge modules:

bridge::reachability (6 new):

  • phantom_declared_when_crate_in_cargo_toml_but_no_use
  • phantom_transitive_when_crate_not_in_cargo_toml (parent identified)
  • reachable_when_declared_and_used
  • phantom_transitive_with_unknown_parent_is_still_classified
  • phantom_classification_normalises_underscore_to_hyphen
  • phantom_declared_resolves_workspace_member_dep (workspace-member declared dep)

bridge::lockfile (3 new):

  • parent_map_identifies_direct_to_transitive_path
  • parent_map_returns_empty_when_no_lockfile
  • parent_map_normalises_underscore_to_hyphen

bridge::classify (4 new, rewritten old ones):

  • test_phantom_declared_recommends_machete_strip
  • test_phantom_transitive_recommends_parent_bump
  • test_phantom_transitive_unknown_parent_falls_back_gracefully
  • test_phantom_variants_both_classify_informational

All 38 bridge-module tests pass; full suite (327 lib + integration) green; cargo check clean on default features (bridge is feature-gated behind http).

Files touched

File +/-
src/bridge/mod.rs +35 / -14
src/bridge/reachability.rs +213 / -18
src/bridge/classify.rs +109 / -52
src/bridge/lockfile.rs +244 / -7

Related work

  • panic-attack#74 — vendored-pin allowlist (Cohort E-3). With this PR, the classifier no longer falsely recommends stripping vendored-TLS / build-script-only crates, because those would only be in scope when they're PhantomDeclared (i.e., genuinely in Cargo.toml). The allowlist still needs to ride on top to handle e.g. openssl-sys = { features = ["vendored"] }.
  • panic-attack#75 — Dioxus/GTK informational (Cohort E-2). With parent-dep identification, the genuine presswerk case will now be labelled with parent_dep: "dioxus" (or wry, whichever the BFS reaches first).
  • file-soup#50 — canonical PhantomDeclared case.

Closes #32 (Track C/E sweep parent).

Test plan

  • cargo check --features http clean
  • cargo check (default features) clean
  • cargo test --features http --lib bridge:: — 38 passed
  • cargo test --features http — full suite green (327 + integration)
  • Re-run Track E sweep against ~5 sample repos and confirm previously-phantom findings now split correctly into declared/transitive cohorts (post-merge validation)

The Track E sweep across 29 Cargo CVE issues revealed that the previous
classifier marked any crate without a direct `use <crate>` statement as
`reach=phantom`, conflating two very different remediation paths:

- True phantom (`PhantomDeclared`): crate IS in a Cargo.toml dependency
  block but never imported. Fix: `cargo machete --fix` strips the dep and
  the whole subtree drops from Cargo.lock. file-soup#50 is the canonical
  case.
- Misclassified (`PhantomTransitive`): crate is NOT in any Cargo.toml —
  pulled in by a parent. Local strip is impossible; the fix requires
  bumping the parent past the affected version. Of 28 non-pilot Track E
  repos, ~26 fell here (Batch A 7/7 misclassified, Cohort E-2 4/5).

Changes
-------

- `ReachabilityStatus::Phantom` split into `PhantomDeclared` /
  `PhantomTransitive`. Serde rename_all = kebab-case so the JSON wire
  values are `phantom-declared` / `phantom-transitive`.
- `ReachabilityEvidence` gains an optional `parent_dep` (set only on
  `PhantomTransitive` when identifiable via Cargo.lock).
- `reachability::check_reachability_with_manifest` is the new entry
  point — takes the declared-deps set and a parent map, returns the
  correct variant. The old `check_reachability` is `#[cfg(test)]` only.
- `lockfile::collect_cargo_parents` parses `Cargo.lock`'s `dependencies`
  arrays and BFS-walks from each direct dep, attributing every
  transitive to a parent (first match wins).
- `classify::classify` no longer takes `is_direct` — the variant on the
  evidence already carries that information. Three-way classifier
  output (Mitigable / Unmitigable / Informational) is unchanged. The
  rationale and fix-action diverge per variant:
  - PhantomDeclared: "Strip from Cargo.toml — run `cargo machete --fix`"
  - PhantomTransitive: "Pulled in transitively by `<parent>` — fix
    requires bumping the parent dependency past the affected version"
- `BridgeReport::schema_version` bumped 0.1.0 → 0.2.0 to flag the
  reach-field semantics change.

Tests
-----

Added 8 regression tests covering:
- `reachability::phantom_declared_when_crate_in_cargo_toml_but_no_use`
- `reachability::phantom_transitive_when_crate_not_in_cargo_toml`
  (with parent identified)
- `reachability::reachable_when_declared_and_used`
- `reachability::phantom_transitive_with_unknown_parent_is_still_classified`
- `reachability::phantom_classification_normalises_underscore_to_hyphen`
- `reachability::phantom_declared_resolves_workspace_member_dep`
- `lockfile::parent_map_identifies_direct_to_transitive_path`
- `lockfile::parent_map_normalises_underscore_to_hyphen`
- `lockfile::parent_map_returns_empty_when_no_lockfile`
- `classify::test_phantom_declared_recommends_machete_strip`
- `classify::test_phantom_transitive_recommends_parent_bump`
- `classify::test_phantom_transitive_unknown_parent_falls_back_gracefully`
- `classify::test_phantom_variants_both_classify_informational`

Existing tests rewritten to use the new evidence shape; all 38 bridge
tests pass + full suite (327 lib + integration) is green.

Refs: panic-attack#74 (vendored-pin allowlist), panic-attack#75
(Dioxus/GTK informational), file-soup#50 (true phantom-declared).
Closes #32 (Track C/E sweep parent).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@hyperpolymath hyperpolymath enabled auto-merge (squash) May 27, 2026 14:25
@hyperpolymath hyperpolymath merged commit 03fdd9f into main May 27, 2026
15 of 26 checks passed
@hyperpolymath hyperpolymath deleted the feat/bridge-phantom-declared-vs-transitive branch May 27, 2026 14:33
hyperpolymath added a commit that referenced this pull request May 27, 2026
…ansitive (#75) (#78)

## Summary

Two layered refinements on top of #76 (phantom-declared /
phantom-transitive split). Same three-way
Mitigable/Unmitigable/Informational output, but the Informational tier
now produces an accurate \`action\` field for two cohorts where the
generic message was misleading.

Closes #74 in part (build-script-only /
vendored-pin name-list portion).
Closes #75.

## Cohort E-3 — build-script-only / vendored-pin (#74, partial)

A naive \`cargo machete --fix\` strip of certain phantom-declared crates
breaks the build inscrutably (cross-compile TLS, native-lib resolution,
build-time codegen). New \`is_build_script_only_or_vendored_pin(name)\`
predicate covers crates that **have no \`use\` site by design**:

- Build-script side-effect crates: \`pkg-config\`, \`cc\`, \`bindgen\`,
\`cmake\`, \`autocfg\`, \`vcpkg\`, \`winres\`, \`embed-resource\`
- Canonical vendored-pin: \`openssl-src\`

When a phantom-declared crate matches, the action flips from \"Strip
from Cargo.toml\" to \"DO NOT STRIP — load-bearing via build.rs
side-effects or native-lib linkage\".

**Future follow-up**: feature-based detection (e.g. \`openssl-sys = {
features = [\"vendored\"] }\`) needs feature-set plumbing through
\`ReachabilityEvidence\` — left out of scope.

## Cohort E-2 — Dioxus/GTK transitive (#75)

Phantom-transitive advisories where the parent is in the Dioxus desktop
family (\`wry\`, \`dioxus-desktop\`, \`dioxus\`) and the affected crate
is in the GTK/webkit family now get a Cohort E-2 message naming the
no-local-fix path (wait for parent release, or swap the desktop
renderer). Sub-rule covers \`printpdf\`→\`kuchiki\`.

GTK/webkit family matched: \`atk*\`, \`gdk*\`, \`gtk*\`, \`glib\`,
\`glib-sys\`, \`gio\`, \`gio-sys\`, \`gobject-sys\`, \`gtk3-macros\`,
\`proc-macro-error\`, \`paste\`, \`fxhash\`, \`webkit2gtk\`,
\`webkit2gtk-sys\`.

## Bonus repair: src/assail/analyzer.rs test-module corruption

The squash-merge sequence of PRs #71 (Julia) → #77 (refile of #72
vendored-snapshot) → #73 (flake.lock) left \`src/assail/analyzer.rs\`
with an unclosed-delimiter at line 7962:

- \`count_julia_dce\` had \`flake_findings\` body
- \`julia_ext_jl_dce_is_exempt\` was missing closing braces
- Two flake tests (\`flake_without_lock_is_low_severity\`,
\`flake_with_narhash_has_no_finding\`) had landed inside the Julia
section

\`cargo test --lib\` was failing to compile on main as a result. This PR
reassembles each section in its intended location; no test logic
changed.

## Test plan

- [x] \`cargo test --features http --lib bridge::classify::\` — 14/14
pass (5 new + 9 existing)
- [x] \`cargo test --features http --lib\` — 343 lib tests pass (was
previously failing to compile)
- [x] \`cargo check --features http\` — green

## Changes

- \`src/bridge/classify.rs\`: +268 / -29 lines (3 predicate fns + 2
cohort override branches + 5 regression tests)
- \`src/assail/analyzer.rs\`: +/-109 lines, net wash (reassemble
corrupted test sections)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@github-actions
Copy link
Copy Markdown

🔍 Hypatia Security Scan

Findings: 74 issues detected

Severity Count
🔴 Critical 7
🟠 High 16
🟡 Medium 51

⚠️ Action Required: Critical security issues found!

View findings
[
  {
    "reason": "Issue in boj-build.yml",
    "type": "unknown",
    "file": "boj-build.yml",
    "action": "flag",
    "rule_module": "workflow_audit",
    "severity": "medium"
  },
  {
    "reason": "Issue in cargo-audit.yml",
    "type": "unknown",
    "file": "cargo-audit.yml",
    "action": "flag",
    "rule_module": "workflow_audit",
    "severity": "medium"
  },
  {
    "reason": "Issue in casket-pages.yml",
    "type": "unknown",
    "file": "casket-pages.yml",
    "action": "flag",
    "rule_module": "workflow_audit",
    "severity": "medium"
  },
  {
    "reason": "Issue in casket-pages.yml",
    "type": "unknown",
    "file": "casket-pages.yml",
    "action": "flag",
    "rule_module": "workflow_audit",
    "severity": "medium"
  },
  {
    "reason": "Issue in codeql.yml",
    "type": "unknown",
    "file": "codeql.yml",
    "action": "flag",
    "rule_module": "workflow_audit",
    "severity": "medium"
  },
  {
    "reason": "Issue in coverage.yml",
    "type": "unknown",
    "file": "coverage.yml",
    "action": "flag",
    "rule_module": "workflow_audit",
    "severity": "medium"
  },
  {
    "reason": "Issue in dependency-review.yml",
    "type": "unknown",
    "file": "dependency-review.yml",
    "action": "flag",
    "rule_module": "workflow_audit",
    "severity": "medium"
  },
  {
    "reason": "Issue in dogfood-gate.yml",
    "type": "unknown",
    "file": "dogfood-gate.yml",
    "action": "flag",
    "rule_module": "workflow_audit",
    "severity": "medium"
  },
  {
    "reason": "Issue in dogfood-gate.yml",
    "type": "unknown",
    "file": "dogfood-gate.yml",
    "action": "flag",
    "rule_module": "workflow_audit",
    "severity": "medium"
  },
  {
    "reason": "Issue in dogfood-gate.yml",
    "type": "unknown",
    "file": "dogfood-gate.yml",
    "action": "flag",
    "rule_module": "workflow_audit",
    "severity": "medium"
  }
]

Powered by Hypatia Neurosymbolic CI/CD Intelligence

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.

panic-attack estate sweep — 2026-05-26

1 participant