|
| 1 | +//! Deep MIR analysis integration. |
| 2 | +//! |
| 3 | +//! Invokes `capsec-driver` as `RUSTC_WRAPPER` to analyze all crates via MIR, |
| 4 | +//! then reads findings from JSONL and builds export maps for cross-crate propagation. |
| 5 | +
|
| 6 | +use crate::detector::Finding; |
| 7 | +use crate::discovery::{self, CrateInfo}; |
| 8 | +use crate::export_map::{self, CrateExportMap}; |
| 9 | +use std::collections::HashMap; |
| 10 | +use std::path::Path; |
| 11 | + |
| 12 | +/// Pinned nightly date for capsec-driver. Must match `crates/capsec-deep/rust-toolchain.toml`. |
| 13 | +const PINNED_NIGHTLY: &str = "nightly-2026-02-17"; |
| 14 | + |
| 15 | +/// Result of running deep MIR analysis. |
| 16 | +pub struct DeepResult { |
| 17 | + /// Findings from the MIR driver, with crate names/versions patched to match Cargo metadata. |
| 18 | + pub findings: Vec<Finding>, |
| 19 | + /// Export maps built from MIR findings, ready to inject into Phase 2. |
| 20 | + pub export_maps: Vec<CrateExportMap>, |
| 21 | + /// Warnings encountered during analysis (driver missing, parse errors, etc.). |
| 22 | + pub warnings: Vec<String>, |
| 23 | +} |
| 24 | + |
| 25 | +/// Runs the MIR-based deep analysis driver on the target project. |
| 26 | +/// |
| 27 | +/// Invokes `capsec-driver` via `RUSTC_WRAPPER` + `cargo check`, reads JSONL |
| 28 | +/// findings, patches crate names/versions, and builds export maps. |
| 29 | +/// |
| 30 | +/// Returns an empty `DeepResult` if the driver is not available or fails. |
| 31 | +/// Warnings are collected in `DeepResult::warnings` rather than printed directly. |
| 32 | +pub fn run_deep_analysis( |
| 33 | + path: &Path, |
| 34 | + workspace_root: &Path, |
| 35 | + workspace_crates: &[CrateInfo], |
| 36 | + dep_crates: &[CrateInfo], |
| 37 | + fs_read: &impl capsec_core::cap_provider::CapProvider<capsec_core::permission::FsRead>, |
| 38 | + spawn_cap: &impl capsec_core::cap_provider::CapProvider<capsec_core::permission::Spawn>, |
| 39 | +) -> DeepResult { |
| 40 | + let mut warnings: Vec<String> = Vec::new(); |
| 41 | + let output_path = |
| 42 | + std::env::temp_dir().join(format!("capsec-deep-{}.jsonl", std::process::id())); |
| 43 | + |
| 44 | + // Check if capsec-driver is available by trying to run it |
| 45 | + let driver_available = capsec_std::process::command("capsec-driver", spawn_cap) |
| 46 | + .ok() |
| 47 | + .and_then(|mut cmd| cmd.arg("--version").output().ok()) |
| 48 | + .map(|o| o.status.success()) |
| 49 | + .unwrap_or(false); |
| 50 | + |
| 51 | + if !driver_available { |
| 52 | + warnings.push( |
| 53 | + "--deep requires capsec-driver. Install with: cd crates/capsec-deep && cargo install --path .".to_string() |
| 54 | + ); |
| 55 | + return DeepResult { |
| 56 | + findings: Vec::new(), |
| 57 | + export_maps: Vec::new(), |
| 58 | + warnings, |
| 59 | + }; |
| 60 | + } |
| 61 | + |
| 62 | + let deep_target_dir = workspace_root.join("target/capsec-deep"); |
| 63 | + let toolchain = detect_nightly_toolchain(spawn_cap); |
| 64 | + |
| 65 | + // Clean to force full rebuild (incremental cache prevents driver from running) |
| 66 | + let _ = std::fs::remove_dir_all(&deep_target_dir); |
| 67 | + |
| 68 | + let deep_result = capsec_std::process::command("cargo", spawn_cap) |
| 69 | + .ok() |
| 70 | + .and_then(|mut cmd| { |
| 71 | + cmd.arg("check") |
| 72 | + .current_dir(path) |
| 73 | + .env("RUSTC_WRAPPER", "capsec-driver") |
| 74 | + .env("CAPSEC_DEEP_OUTPUT", &output_path) |
| 75 | + .env("CAPSEC_CRATE_VERSION", "0.0.0") |
| 76 | + .env("CARGO_TARGET_DIR", &deep_target_dir) |
| 77 | + .env("RUSTUP_TOOLCHAIN", toolchain) |
| 78 | + .output() |
| 79 | + .ok() |
| 80 | + }); |
| 81 | + |
| 82 | + // Build name/version lookup for patching MIR findings |
| 83 | + let crate_lookup: HashMap<String, (String, String)> = workspace_crates |
| 84 | + .iter() |
| 85 | + .chain(dep_crates.iter()) |
| 86 | + .map(|c| { |
| 87 | + ( |
| 88 | + discovery::normalize_crate_name(&c.name), |
| 89 | + (c.name.clone(), c.version.clone()), |
| 90 | + ) |
| 91 | + }) |
| 92 | + .collect(); |
| 93 | + |
| 94 | + let mir_findings = match deep_result { |
| 95 | + Some(output) if output.status.success() || output_path.exists() => { |
| 96 | + let findings = |
| 97 | + parse_findings_jsonl(&output_path, &crate_lookup, fs_read, &mut warnings); |
| 98 | + let _ = std::fs::remove_file(&output_path); |
| 99 | + findings |
| 100 | + } |
| 101 | + Some(output) => { |
| 102 | + let stderr = String::from_utf8_lossy(&output.stderr); |
| 103 | + let mut msg = "Deep analysis failed (cargo check returned non-zero).".to_string(); |
| 104 | + for line in stderr |
| 105 | + .lines() |
| 106 | + .filter(|l| l.contains("error") || l.contains("Error")) |
| 107 | + .take(5) |
| 108 | + { |
| 109 | + msg.push_str(&format!("\n {line}")); |
| 110 | + } |
| 111 | + if stderr.contains("incompatible version of rustc") { |
| 112 | + msg.push_str("\n Hint: try `rm -rf target/capsec-deep` to clear stale artifacts."); |
| 113 | + } |
| 114 | + warnings.push(msg); |
| 115 | + Vec::new() |
| 116 | + } |
| 117 | + None => { |
| 118 | + warnings.push("Could not invoke cargo check for deep analysis.".to_string()); |
| 119 | + Vec::new() |
| 120 | + } |
| 121 | + }; |
| 122 | + |
| 123 | + // Build export maps from MIR findings |
| 124 | + let export_maps = build_mir_export_maps(&mir_findings, workspace_crates, dep_crates); |
| 125 | + |
| 126 | + DeepResult { |
| 127 | + findings: mir_findings, |
| 128 | + export_maps, |
| 129 | + warnings, |
| 130 | + } |
| 131 | +} |
| 132 | + |
| 133 | +/// Parses JSONL findings from the MIR driver output file. |
| 134 | +/// Patches crate names (rustc → Cargo) and versions (0.0.0 → real) using the lookup. |
| 135 | +fn parse_findings_jsonl( |
| 136 | + output_path: &Path, |
| 137 | + crate_lookup: &HashMap<String, (String, String)>, |
| 138 | + fs_read: &impl capsec_core::cap_provider::CapProvider<capsec_core::permission::FsRead>, |
| 139 | + warnings: &mut Vec<String>, |
| 140 | +) -> Vec<Finding> { |
| 141 | + let mut findings = Vec::new(); |
| 142 | + let Ok(contents) = capsec_std::fs::read_to_string(output_path, fs_read) else { |
| 143 | + return findings; |
| 144 | + }; |
| 145 | + for line in contents.lines() { |
| 146 | + if line.trim().is_empty() { |
| 147 | + continue; |
| 148 | + } |
| 149 | + match serde_json::from_str::<Finding>(line) { |
| 150 | + Ok(mut finding) => { |
| 151 | + let normalized = discovery::normalize_crate_name(&finding.crate_name); |
| 152 | + if let Some((cargo_name, ver)) = crate_lookup.get(&normalized) { |
| 153 | + finding.crate_name = cargo_name.clone(); |
| 154 | + if finding.crate_version == "0.0.0" { |
| 155 | + finding.crate_version = ver.clone(); |
| 156 | + } |
| 157 | + } |
| 158 | + findings.push(finding); |
| 159 | + } |
| 160 | + Err(e) => { |
| 161 | + warnings.push(format!("Failed to parse deep finding: {e}")); |
| 162 | + } |
| 163 | + } |
| 164 | + } |
| 165 | + findings |
| 166 | +} |
| 167 | + |
| 168 | +/// Builds export maps from MIR findings, grouped by crate. |
| 169 | +fn build_mir_export_maps( |
| 170 | + findings: &[Finding], |
| 171 | + workspace_crates: &[CrateInfo], |
| 172 | + dep_crates: &[CrateInfo], |
| 173 | +) -> Vec<CrateExportMap> { |
| 174 | + if findings.is_empty() { |
| 175 | + return Vec::new(); |
| 176 | + } |
| 177 | + |
| 178 | + // Group findings by crate name |
| 179 | + let mut by_crate: HashMap<String, Vec<&Finding>> = HashMap::new(); |
| 180 | + for f in findings { |
| 181 | + by_crate.entry(f.crate_name.clone()).or_default().push(f); |
| 182 | + } |
| 183 | + |
| 184 | + let all_crates: Vec<&CrateInfo> = dep_crates.iter().chain(workspace_crates.iter()).collect(); |
| 185 | + |
| 186 | + let mut export_maps = Vec::new(); |
| 187 | + for (crate_name, crate_findings) in &by_crate { |
| 188 | + let normalized = discovery::normalize_crate_name(crate_name); |
| 189 | + let src_dir = all_crates |
| 190 | + .iter() |
| 191 | + .find(|c| discovery::normalize_crate_name(&c.name) == normalized) |
| 192 | + .map(|c| &c.source_dir); |
| 193 | + |
| 194 | + let Some(src_dir) = src_dir else { |
| 195 | + eprintln!( |
| 196 | + "Warning: MIR findings for unknown crate '{crate_name}', skipping export map" |
| 197 | + ); |
| 198 | + continue; |
| 199 | + }; |
| 200 | + |
| 201 | + // Collect owned findings for build_export_map (which takes &[Finding]) |
| 202 | + let owned: Vec<Finding> = crate_findings.iter().map(|f| (*f).clone()).collect(); |
| 203 | + let mir_emap = |
| 204 | + export_map::build_export_map(&normalized, &owned[0].crate_version, &owned, src_dir); |
| 205 | + export_maps.push(mir_emap); |
| 206 | + } |
| 207 | + export_maps |
| 208 | +} |
| 209 | + |
| 210 | +/// Detects the nightly toolchain to use for the MIR driver. |
| 211 | +fn detect_nightly_toolchain( |
| 212 | + spawn_cap: &impl capsec_core::cap_provider::CapProvider<capsec_core::permission::Spawn>, |
| 213 | +) -> &'static str { |
| 214 | + let has_pinned = capsec_std::process::command("rustup", spawn_cap) |
| 215 | + .ok() |
| 216 | + .and_then(|mut cmd| { |
| 217 | + cmd.arg("run") |
| 218 | + .arg(PINNED_NIGHTLY) |
| 219 | + .arg("rustc") |
| 220 | + .arg("--version") |
| 221 | + .output() |
| 222 | + .ok() |
| 223 | + }) |
| 224 | + .map(|o| o.status.success()) |
| 225 | + .unwrap_or(false); |
| 226 | + if has_pinned { |
| 227 | + PINNED_NIGHTLY |
| 228 | + } else { |
| 229 | + "nightly" |
| 230 | + } |
| 231 | +} |
0 commit comments