Skip to content

Commit 8771a85

Browse files
hyperpolymathclaude
andcommitted
feat(bridge): implement Patch Bridge MVP — CVE triage with reachability analysis
Adds the `panic-attack bridge` subcommand family (behind --features http): - `bridge triage <dir>` — full CVE assessment: 1. Parse Cargo.lock for all dependencies 2. Query OSV API (api.osv.dev) for known vulnerabilities 3. Scan .rs files for imports to detect phantom dependencies 4. Classify: Mitigable / Unmitigable / Informational - `bridge status <dir>` — view mitigation registry Key capability: phantom dependency detection. If a crate is in Cargo.toml but never imported in any .rs file, the CVE is classified as Informational with action "remove unused dependency." Validated against Hypatia (octocrab/rsa) and VeriSimDB (lru, bincode, paste). New modules: src/bridge/mod.rs — orchestrator, core types, BridgeReport src/bridge/lockfile.rs — Cargo.lock parser src/bridge/intelligence.rs — OSV API batch queries src/bridge/reachability.rs — import scanning for phantom dep detection src/bridge/classify.rs — three-way classification engine src/bridge/registry.rs — mitigation lifecycle registry 14 unit tests across all modules. Zero compiler warnings. JSON output matches RSR template static-analysis-gate.yml schema. Design document: docs/patch-bridge-design.md Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 54fc5e0 commit 8771a85

9 files changed

Lines changed: 1523 additions & 2 deletions

File tree

.claude/CLAUDE.md

Lines changed: 62 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,13 @@ src/
5656
├── abduct/ # Isolation + time-skew
5757
├── adjudicate/ # Campaign verdict aggregation
5858
├── axial/ # Reaction observation
59+
├── bridge/ # Patch Bridge — CVE mitigation lifecycle (feature: http)
60+
│ ├── mod.rs # Triage orchestrator, core types (BridgeReport, AssessedCve)
61+
│ ├── lockfile.rs # Cargo.lock parser
62+
│ ├── intelligence.rs # OSV API batch queries (CVE feed)
63+
│ ├── reachability.rs # Import scanning for phantom dependency detection
64+
│ ├── classify.rs # Three-way classification: Mitigable/Unmitigable/Informational
65+
│ └── registry.rs # Mitigation lifecycle registry (JSON persistence)
5966
├── a2ml/ # AI manifest protocol
6067
├── panll/ # PanLL event-chain export
6168
├── storage/ # Filesystem + VerisimDB persistence
@@ -66,8 +73,9 @@ src/
6673
## Build & Test
6774

6875
```bash
69-
cargo build --release
70-
cargo test
76+
cargo build --release --features http # With Patch Bridge (needs network)
77+
cargo build --release # Without Patch Bridge
78+
cargo test --features http # All tests including bridge
7179

7280
# Run scan:
7381
panic-attack assail /path/to/repo
@@ -96,6 +104,58 @@ The kanren module provides:
96104
- **Forward chaining**: Derives new vulnerability facts from rules applied to existing facts
97105
- **Backward queries**: Given a vulnerability type, finds which files could cause it
98106

107+
## Patch Bridge (feature: `http`)
108+
109+
CVE mitigation lifecycle engine. Requires `--features http` at build time.
110+
Design document: `docs/patch-bridge-design.md`
111+
112+
### Subcommands
113+
114+
```bash
115+
# Full CVE triage with reachability analysis
116+
panic-attack bridge triage /path/to/project
117+
panic-attack bridge triage /path/to/project --output report.json
118+
panic-attack bridge triage /path/to/project --register # update mitigation registry
119+
panic-attack bridge triage /path/to/project --offline # skip API calls
120+
121+
# View mitigation registry
122+
panic-attack bridge status /path/to/project
123+
```
124+
125+
### How it works
126+
127+
1. **Parse Cargo.lock** — extract all dependencies with versions
128+
2. **Query OSV API** — batch query api.osv.dev for known vulnerabilities
129+
3. **Reachability scan** — grep .rs files for imports of vulnerable crates
130+
4. **Classify** — Mitigable (fix available) / Unmitigable (no fix) / Informational (phantom dep)
131+
5. **Report** — JSON output compatible with RSR template static-analysis-gate.yml
132+
133+
### Key capability: phantom dependency detection
134+
135+
If a crate is in Cargo.toml but never imported in any .rs file, it's a "phantom" dependency.
136+
The CVE is compiled into the binary but unreachable. Classification: Informational.
137+
Action: remove the unused dependency. This was validated against Hypatia (octocrab/rsa).
138+
139+
### Mitigation registry
140+
141+
Active mitigations tracked at `.machine_readable/patch-bridge/registry.json`.
142+
Lifecycle: Pending → Active → Retiring → Retired / AcceptedRisk.
143+
Phase 2 adds VeriSimDB hexad persistence and auto-retire on upstream fix.
144+
145+
### Output format
146+
147+
```json
148+
{
149+
"schema_version": "0.1.0",
150+
"total_dependencies": 486,
151+
"cves": [...],
152+
"mitigated": 1,
153+
"unmitigable": 0,
154+
"concatenative": 0,
155+
"informational": 2
156+
}
157+
```
158+
99159
## Deployment Modes
100160

101161
Three self-contained modes — none requires the others:

src/bridge/classify.rs

Lines changed: 205 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,205 @@
1+
// SPDX-License-Identifier: PMPL-1.0-or-later
2+
3+
//! Three-way CVE classification engine.
4+
//!
5+
//! Combines vulnerability data with reachability evidence to produce
6+
//! one of three classifications:
7+
//!
8+
//! - **Mitigable**: A fix exists (semver-compatible or manual upgrade)
9+
//! - **Unmitigable**: No fix available and dependency is reachable
10+
//! - **Informational**: Dependency is phantom or unreachable
11+
//!
12+
//! Phase 2 will add **Concatenative** classification for CVE×CVE
13+
//! interactions across shared trust boundaries.
14+
15+
use super::{Classification, ReachabilityEvidence, ReachabilityStatus, Vulnerability};
16+
17+
/// Classify a vulnerability given its reachability evidence.
18+
///
19+
/// Returns (classification, rationale, suggested_action).
20+
pub fn classify(
21+
vuln: &Vulnerability,
22+
evidence: &ReachabilityEvidence,
23+
) -> (Classification, String, String) {
24+
match evidence.status {
25+
// ─── Phantom dependency: declared but never imported ───
26+
ReachabilityStatus::Phantom => (
27+
Classification::Informational,
28+
format!(
29+
"{} {} is declared in Cargo.toml but never imported in any .rs file. \
30+
The vulnerable code is compiled but unreachable. \
31+
Removing the dependency from Cargo.toml eliminates this CVE entirely.",
32+
vuln.package, vuln.version
33+
),
34+
format!("Remove unused dependency `{}` from Cargo.toml", vuln.package),
35+
),
36+
37+
// ─── Unreachable: imported but no taint flow (Phase 2) ───
38+
ReachabilityStatus::Unreachable => (
39+
Classification::Informational,
40+
format!(
41+
"{} {} is imported but no data flow reaches the vulnerable code path. \
42+
(Note: Phase 2 kanren taint analysis will provide higher confidence.)",
43+
vuln.package, vuln.version
44+
),
45+
"Monitor — no immediate action required".to_string(),
46+
),
47+
48+
// ─── Reachable: imported and potentially exploitable ───
49+
ReachabilityStatus::Reachable => classify_reachable(vuln, evidence),
50+
}
51+
}
52+
53+
/// Classify a reachable vulnerability as mitigable or unmitigable.
54+
fn classify_reachable(
55+
vuln: &Vulnerability,
56+
evidence: &ReachabilityEvidence,
57+
) -> (Classification, String, String) {
58+
let import_summary = if evidence.import_sites.len() <= 3 {
59+
evidence
60+
.import_sites
61+
.iter()
62+
.map(|s| format!("{}:{}", s.file.display(), s.line))
63+
.collect::<Vec<_>>()
64+
.join(", ")
65+
} else {
66+
format!(
67+
"{} and {} more",
68+
evidence
69+
.import_sites
70+
.iter()
71+
.take(2)
72+
.map(|s| format!("{}:{}", s.file.display(), s.line))
73+
.collect::<Vec<_>>()
74+
.join(", "),
75+
evidence.import_sites.len() - 2
76+
)
77+
};
78+
79+
if vuln.fixed_versions.is_empty() {
80+
// No fix available — unmitigable
81+
(
82+
Classification::Unmitigable,
83+
format!(
84+
"{} {} has {} ({}) with NO upstream fix available. \
85+
The dependency is imported at: {}. \
86+
The vulnerable code is reachable in this project.",
87+
vuln.package,
88+
vuln.version,
89+
vuln.id,
90+
vuln.summary,
91+
import_summary
92+
),
93+
format!(
94+
"Replace `{}` with an alternative or accept the risk. \
95+
No version upgrade can fix this.",
96+
vuln.package
97+
),
98+
)
99+
} else if vuln.semver_fix_available {
100+
// Semver-compatible fix — easiest mitigation
101+
let fix_version = vuln.fixed_versions.first().unwrap();
102+
(
103+
Classification::Mitigable,
104+
format!(
105+
"{} {} has {} ({}). \
106+
A semver-compatible fix is available in version {}. \
107+
Run `cargo update {}` to apply.",
108+
vuln.package, vuln.version, vuln.id, vuln.summary,
109+
fix_version, vuln.package
110+
),
111+
format!("Run `cargo update {}`", vuln.package),
112+
)
113+
} else {
114+
// Fix exists but requires major version bump
115+
let fix_versions = vuln.fixed_versions.join(", ");
116+
(
117+
Classification::Mitigable,
118+
format!(
119+
"{} {} has {} ({}). \
120+
Fix available in version(s) {} but requires a breaking upgrade. \
121+
The dependency is imported at: {}.",
122+
vuln.package, vuln.version, vuln.id, vuln.summary,
123+
fix_versions, import_summary
124+
),
125+
format!(
126+
"Upgrade `{}` to {} in Cargo.toml (breaking change — review API differences)",
127+
vuln.package, fix_versions
128+
),
129+
)
130+
}
131+
}
132+
133+
#[cfg(test)]
134+
mod tests {
135+
use super::*;
136+
use crate::bridge::{ImportSite, SeverityLabel, SourceTier};
137+
use std::path::PathBuf;
138+
139+
fn mock_vuln(has_fix: bool, semver_fix: bool) -> Vulnerability {
140+
Vulnerability {
141+
id: "RUSTSEC-2026-0001".to_string(),
142+
cve: Some("CVE-2026-00001".to_string()),
143+
summary: "Test vulnerability".to_string(),
144+
package: "test-crate".to_string(),
145+
version: "1.0.0".to_string(),
146+
severity: Some(7.5),
147+
severity_label: SeverityLabel::High,
148+
fixed_versions: if has_fix {
149+
vec!["1.0.1".to_string()]
150+
} else {
151+
vec![]
152+
},
153+
semver_fix_available: semver_fix,
154+
source_tier: SourceTier::Tier1,
155+
references: vec![],
156+
}
157+
}
158+
159+
fn phantom_evidence() -> ReachabilityEvidence {
160+
ReachabilityEvidence {
161+
is_imported: false,
162+
import_sites: vec![],
163+
status: ReachabilityStatus::Phantom,
164+
}
165+
}
166+
167+
fn reachable_evidence() -> ReachabilityEvidence {
168+
ReachabilityEvidence {
169+
is_imported: true,
170+
import_sites: vec![ImportSite {
171+
file: PathBuf::from("src/main.rs"),
172+
line: 5,
173+
statement: "use test_crate::Thing;".to_string(),
174+
}],
175+
status: ReachabilityStatus::Reachable,
176+
}
177+
}
178+
179+
#[test]
180+
fn test_phantom_is_informational() {
181+
let (cls, _, action) = classify(&mock_vuln(false, false), &phantom_evidence());
182+
assert_eq!(cls, Classification::Informational);
183+
assert!(action.contains("Remove"));
184+
}
185+
186+
#[test]
187+
fn test_reachable_no_fix_is_unmitigable() {
188+
let (cls, _, _) = classify(&mock_vuln(false, false), &reachable_evidence());
189+
assert_eq!(cls, Classification::Unmitigable);
190+
}
191+
192+
#[test]
193+
fn test_reachable_semver_fix_is_mitigable() {
194+
let (cls, _, action) = classify(&mock_vuln(true, true), &reachable_evidence());
195+
assert_eq!(cls, Classification::Mitigable);
196+
assert!(action.contains("cargo update"));
197+
}
198+
199+
#[test]
200+
fn test_reachable_breaking_fix_is_mitigable() {
201+
let (cls, _, action) = classify(&mock_vuln(true, false), &reachable_evidence());
202+
assert_eq!(cls, Classification::Mitigable);
203+
assert!(action.contains("breaking change"));
204+
}
205+
}

0 commit comments

Comments
 (0)