Skip to content

fix(bridge): distinguish direct vs transitive phantom deps (closes #47)#50

Merged
hyperpolymath merged 2 commits into
mainfrom
fix/bridge-direct-vs-transitive-47
May 26, 2026
Merged

fix(bridge): distinguish direct vs transitive phantom deps (closes #47)#50
hyperpolymath merged 2 commits into
mainfrom
fix/bridge-direct-vs-transitive-47

Conversation

@hyperpolymath
Copy link
Copy Markdown
Owner

Summary

Closes #47. bridge triage's phantom-classified findings were uniformly recommending "Remove unused dependency <pkg> from Cargo.toml", but the 2026-05-26 estate sweep showed every sampled phantom (28/28 in a 6-repo sample, 157 phantoms total) was a transitive dep pulled in by an upstream crate — never declared in any Cargo.toml. The action was unactionable.

Fix

Parse the project's Cargo.toml dependency tables once per triage run, then route phantom findings to one of two action strings based on whether the package appears as a direct dependency.

Case Old action New action
Phantom + direct dep "Remove unused dependency <pkg> from Cargo.toml" (unchanged)
Phantom + transitive "Remove unused dependency <pkg> from Cargo.toml" ❌ "Transitive — run cargo update -p <pkg> to pull a non-vulnerable version if one is published, or upgrade the upstream crate that pulls it in. Otherwise informational: code unreachable from this project."
Unreachable / Reachable (unchanged) (unchanged)

Implementation

File Change
src/bridge/lockfile.rs New collect_direct_cargo_dependencies() walks the root Cargo.toml + each workspace.members manifest. Indexes [dependencies], [dev-dependencies], [build-dependencies], [workspace.dependencies], and target-prefixed variants ([target.cfg(...).dependencies] etc.). Crate names normalised to hyphen + lowercase so a CVE feed reporting serde_json matches a manifest line serde-json.
src/bridge/classify.rs classify() signature gains is_direct: bool. Phantom arm splits on the flag; reachable arms unchanged.
src/bridge/mod.rs triage() builds the direct-deps set once (outside the per-CVE loop) and passes the lookup result into classify.

Regression coverage

New tests:

  • direct_deps_skips_transitive_only_crates — direct repro of the lru / ratatui case from the issue.
  • direct_deps_collects_dev_and_build_sections — all three direct sections.
  • direct_deps_handles_target_sections[target.cfg(unix).dependencies] etc.
  • direct_deps_handles_workspace_members — root manifest declares members; member deps are reachable.
  • direct_deps_normalises_underscore_to_hyphenserde_jsonserde-json.
  • direct_deps_ignores_commented_lines_and_strings_with_hash# foo = \"1.0\" does not count.
  • direct_deps_empty_when_no_manifest — graceful degradation (returns empty set, all phantoms treated as transitive).
  • test_phantom_transitive_recommends_cargo_update — the bug behaviour from bridge triage: 'Remove unused dependency' action assumes direct dep, fires on transitive deps #47 is gone.
  • test_phantom_direct_recommends_removal — direct case unchanged.
  • test_reachable_classification_unaffected_by_is_direct — invariant.

Test plan

  • cargo test --bin panic-attack --features signing,http bridge:: — 28 passed (10 new + 18 existing)
  • Full binary test suite: 236 passed, 0 failed
  • cargo clippy --all-targets --features signing,http -- -D warnings — clean
  • cargo fmt --check — clean
  • GPG-signed commit

Out of scope (per the issue)

For unmitigable + reachable: actionable warning (this is the real-risk category — 8 in the 2026-05-26 sweep, all advisory-issued without fixed versions).

The current unmitigable rationale already describes import sites + lack-of-fix. Whether the action wording for that category needs further sharpening is a separate concern from the wrong-action-on-phantom-transitive bug.

🤖 Generated with Claude Code

…action

Closes #47.

`bridge triage` was telling users to "Remove unused dependency
`<pkg>` from Cargo.toml" for any phantom-classified CVE, but the
2026-05-26 estate sweep showed every sampled phantom (28/28 in a 6-repo
sample, 157 phantoms total) was a *transitive* dep pulled in by some
upstream crate — never declared in any Cargo.toml. The action string was
unactionable.

Fix: parse the project's Cargo.toml dependency tables to distinguish
direct from transitive deps, then emit the right action for each.

Changes:

- `src/bridge/lockfile.rs`: new `collect_direct_cargo_dependencies()`
  walks the root Cargo.toml + each `workspace.members` manifest, indexing
  `[dependencies]`, `[dev-dependencies]`, `[build-dependencies]`,
  `[workspace.dependencies]`, and target-prefixed variants
  (`[target.cfg(...).dependencies]`). Crate names normalised to
  hyphen+lowercase for CVE-feed matching (serde_json ↔ serde-json).
- `src/bridge/classify.rs`: `classify()` takes a new `is_direct: bool`.
  - Phantom + direct → unchanged "Remove unused dependency" action.
  - Phantom + transitive → new action: "Transitive — run `cargo update -p
    <pkg>` ... Otherwise informational: code unreachable from this
    project."
  - Reachable arms unchanged.
- `src/bridge/mod.rs`: `triage()` builds the direct-deps set once
  (outside the per-CVE loop) and passes the lookup result into classify.

Regression coverage:

- `direct_deps_skips_transitive_only_crates` — repro of the `lru` /
  ratatui case from the issue.
- `direct_deps_collects_dev_and_build_sections`,
  `direct_deps_handles_target_sections`,
  `direct_deps_handles_workspace_members`,
  `direct_deps_normalises_underscore_to_hyphen`,
  `direct_deps_ignores_commented_lines_and_strings_with_hash`,
  `direct_deps_empty_when_no_manifest` — parser edge cases.
- `test_phantom_transitive_recommends_cargo_update` — asserts no "Remove
  unused dependency" string for the transitive phantom case.
- `test_phantom_direct_recommends_removal` — direct case unchanged.
- `test_reachable_classification_unaffected_by_is_direct` — sanity check
  that the new flag only affects the Phantom arm.

All 236 binary tests pass. cargo clippy clean. cargo fmt clean.

Out of scope (per the issue):
- "For unmitigable + reachable: actionable warning" — the current
  rationale already describes the import sites + lack of fix. Whether
  the action wording needs further sharpening is a separate concern
  from the wrong-action-on-phantom-transitive bug.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@hyperpolymath hyperpolymath enabled auto-merge (squash) May 26, 2026 10:06
@github-actions
Copy link
Copy Markdown

🔍 Hypatia Security Scan

Findings: 49 issues detected

Severity Count
🔴 Critical 4
🟠 High 16
🟡 Medium 29

⚠️ Action Required: Critical security issues found!

View findings
[
  {
    "reason": "Action hyperpolymath/standards/.github/workflows/governance-reusable.yml@main needs attention",
    "type": "unpinned_action",
    "file": "governance.yml",
    "action": "pin_sha",
    "rule_module": "workflow_audit",
    "severity": "high"
  },
  {
    "reason": "Nickel file missing SPDX-License-Identifier header (1 occurrences, CWE-1104)",
    "type": "ncl_missing_spdx",
    "file": "/home/runner/work/panic-attack/panic-attack/reports/panic-attack-20260211180017.ncl",
    "action": "flag",
    "rule_module": "code_safety",
    "severity": "medium"
  },
  {
    "reason": "expect() in hot path (2 occurrences, CWE-754)",
    "type": "expect_in_hot_path",
    "file": "/home/runner/work/panic-attack/panic-attack/src/attestation/chain.rs",
    "action": "flag",
    "rule_module": "code_safety",
    "severity": "medium"
  },
  {
    "reason": "unwrap_or(0) with dangerous default (1 occurrences, CWE-754)",
    "type": "unwrap_dangerous_default",
    "file": "/home/runner/work/panic-attack/panic-attack/src/attestation/evidence.rs",
    "action": "flag",
    "rule_module": "code_safety",
    "severity": "critical"
  },
  {
    "reason": "unwrap_or(0) with dangerous default (1 occurrences, CWE-754)",
    "type": "unwrap_dangerous_default",
    "file": "/home/runner/work/panic-attack/panic-attack/src/ambush/mod.rs",
    "action": "flag",
    "rule_module": "code_safety",
    "severity": "critical"
  },
  {
    "reason": "unwrap_or(0) with dangerous default (3 occurrences, CWE-754)",
    "type": "unwrap_dangerous_default",
    "file": "/home/runner/work/panic-attack/panic-attack/src/kanren/strategy.rs",
    "action": "flag",
    "rule_module": "code_safety",
    "severity": "critical"
  },
  {
    "reason": "unwrap_or(0) with dangerous default (3 occurrences, CWE-754)",
    "type": "unwrap_dangerous_default",
    "file": "/home/runner/work/panic-attack/panic-attack/src/axial/mod.rs",
    "action": "flag",
    "rule_module": "code_safety",
    "severity": "critical"
  },
  {
    "reason": "expect() in hot path (4 occurrences, CWE-754)",
    "type": "expect_in_hot_path",
    "file": "/home/runner/work/panic-attack/panic-attack/src/assail/analyzer.rs",
    "action": "flag",
    "rule_module": "code_safety",
    "severity": "medium"
  },
  {
    "reason": "unwrap() without prior check -- DoS via panic (4 occurrences, CWE-754)",
    "type": "unwrap_without_check",
    "file": "/home/runner/work/panic-attack/panic-attack/benches/scan_bench.rs",
    "action": "flag",
    "rule_module": "code_safety",
    "severity": "high"
  },
  {
    "reason": "expect() in hot path (2 occurrences, CWE-754)",
    "type": "expect_in_hot_path",
    "file": "/home/runner/work/panic-attack/panic-attack/benches/scan_bench.rs",
    "action": "flag",
    "rule_module": "code_safety",
    "severity": "medium"
  }
]

Powered by Hypatia Neurosymbolic CI/CD Intelligence

@github-actions
Copy link
Copy Markdown

🔍 Hypatia Security Scan

Findings: 49 issues detected

Severity Count
🔴 Critical 4
🟠 High 16
🟡 Medium 29

⚠️ Action Required: Critical security issues found!

View findings
[
  {
    "reason": "Action hyperpolymath/standards/.github/workflows/governance-reusable.yml@main needs attention",
    "type": "unpinned_action",
    "file": "governance.yml",
    "action": "pin_sha",
    "rule_module": "workflow_audit",
    "severity": "high"
  },
  {
    "reason": "Nickel file missing SPDX-License-Identifier header (1 occurrences, CWE-1104)",
    "type": "ncl_missing_spdx",
    "file": "/home/runner/work/panic-attack/panic-attack/reports/panic-attack-20260211180017.ncl",
    "action": "flag",
    "rule_module": "code_safety",
    "severity": "medium"
  },
  {
    "reason": "expect() in hot path (2 occurrences, CWE-754)",
    "type": "expect_in_hot_path",
    "file": "/home/runner/work/panic-attack/panic-attack/src/attestation/chain.rs",
    "action": "flag",
    "rule_module": "code_safety",
    "severity": "medium"
  },
  {
    "reason": "unwrap_or(0) with dangerous default (1 occurrences, CWE-754)",
    "type": "unwrap_dangerous_default",
    "file": "/home/runner/work/panic-attack/panic-attack/src/attestation/evidence.rs",
    "action": "flag",
    "rule_module": "code_safety",
    "severity": "critical"
  },
  {
    "reason": "unwrap_or(0) with dangerous default (1 occurrences, CWE-754)",
    "type": "unwrap_dangerous_default",
    "file": "/home/runner/work/panic-attack/panic-attack/src/ambush/mod.rs",
    "action": "flag",
    "rule_module": "code_safety",
    "severity": "critical"
  },
  {
    "reason": "unwrap_or(0) with dangerous default (3 occurrences, CWE-754)",
    "type": "unwrap_dangerous_default",
    "file": "/home/runner/work/panic-attack/panic-attack/src/kanren/strategy.rs",
    "action": "flag",
    "rule_module": "code_safety",
    "severity": "critical"
  },
  {
    "reason": "unwrap_or(0) with dangerous default (3 occurrences, CWE-754)",
    "type": "unwrap_dangerous_default",
    "file": "/home/runner/work/panic-attack/panic-attack/src/axial/mod.rs",
    "action": "flag",
    "rule_module": "code_safety",
    "severity": "critical"
  },
  {
    "reason": "expect() in hot path (4 occurrences, CWE-754)",
    "type": "expect_in_hot_path",
    "file": "/home/runner/work/panic-attack/panic-attack/src/assail/analyzer.rs",
    "action": "flag",
    "rule_module": "code_safety",
    "severity": "medium"
  },
  {
    "reason": "unwrap() without prior check -- DoS via panic (4 occurrences, CWE-754)",
    "type": "unwrap_without_check",
    "file": "/home/runner/work/panic-attack/panic-attack/benches/scan_bench.rs",
    "action": "flag",
    "rule_module": "code_safety",
    "severity": "high"
  },
  {
    "reason": "expect() in hot path (2 occurrences, CWE-754)",
    "type": "expect_in_hot_path",
    "file": "/home/runner/work/panic-attack/panic-attack/benches/scan_bench.rs",
    "action": "flag",
    "rule_module": "code_safety",
    "severity": "medium"
  }
]

Powered by Hypatia Neurosymbolic CI/CD Intelligence

@hyperpolymath hyperpolymath merged commit ea02d50 into main May 26, 2026
26 checks passed
@hyperpolymath hyperpolymath deleted the fix/bridge-direct-vs-transitive-47 branch May 26, 2026 10:12
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.

bridge triage: 'Remove unused dependency' action assumes direct dep, fires on transitive deps

1 participant