Skip to content

Commit 345bb64

Browse files
committed
tests: add integration test for unified mir check
1 parent a7566f2 commit 345bb64

2 files changed

Lines changed: 245 additions & 152 deletions

File tree

crates/cargo-capsec/src/main.rs

Lines changed: 162 additions & 152 deletions
Original file line numberDiff line numberDiff line change
@@ -212,6 +212,166 @@ fn run_audit(args: AuditArgs) {
212212
}
213213
}
214214

215+
// ── Deep MIR analysis (runs before Phase 2 so findings feed into export maps) ──
216+
if args.deep {
217+
let output_path = std::env::temp_dir()
218+
.join(format!("capsec-deep-{}.jsonl", std::process::id()));
219+
220+
let driver_available = capsec_std::process::command("which", &spawn_cap)
221+
.ok()
222+
.and_then(|mut cmd| cmd.arg("capsec-driver").output().ok())
223+
.map(|o| o.status.success())
224+
.unwrap_or(false);
225+
226+
if !driver_available {
227+
eprintln!("Error: --deep requires capsec-driver (MIR analysis driver).");
228+
eprintln!("Install with: cd crates/capsec-deep && cargo install --path .");
229+
eprintln!("Continuing with syntactic-only analysis...");
230+
} else {
231+
let deep_target_dir = workspace_root.join("target/capsec-deep");
232+
let toolchain = {
233+
let pinned = "nightly-2026-02-17";
234+
let has_pinned = capsec_std::process::command("rustup", &spawn_cap)
235+
.ok()
236+
.and_then(|mut cmd| {
237+
cmd.arg("run")
238+
.arg(pinned)
239+
.arg("rustc")
240+
.arg("--version")
241+
.output()
242+
.ok()
243+
})
244+
.map(|o| o.status.success())
245+
.unwrap_or(false);
246+
if has_pinned { pinned } else { "nightly" }
247+
};
248+
249+
let _ = std::fs::remove_dir_all(&deep_target_dir);
250+
251+
let deep_result = capsec_std::process::command("cargo", &spawn_cap)
252+
.ok()
253+
.and_then(|mut cmd| {
254+
cmd.arg("check")
255+
.current_dir(&path_arg)
256+
.env("RUSTC_WRAPPER", "capsec-driver")
257+
.env("CAPSEC_DEEP_OUTPUT", &output_path)
258+
.env("CAPSEC_CRATE_VERSION", "0.0.0")
259+
.env("CARGO_TARGET_DIR", &deep_target_dir)
260+
.env("RUSTUP_TOOLCHAIN", toolchain)
261+
.output()
262+
.ok()
263+
});
264+
265+
// Build name/version lookup for patching MIR findings
266+
let crate_lookup: HashMap<String, (String, String)> = workspace_crates
267+
.iter()
268+
.chain(dep_crates.iter())
269+
.map(|c| {
270+
(
271+
discovery::normalize_crate_name(&c.name),
272+
(c.name.clone(), c.version.clone()),
273+
)
274+
})
275+
.collect();
276+
277+
let mut mir_findings: Vec<detector::Finding> = Vec::new();
278+
279+
match deep_result {
280+
Some(output) if output.status.success() || output_path.exists() => {
281+
if let Ok(contents) =
282+
capsec_std::fs::read_to_string(&output_path, &fs_read)
283+
{
284+
for line in contents.lines() {
285+
if line.trim().is_empty() {
286+
continue;
287+
}
288+
match serde_json::from_str::<detector::Finding>(line) {
289+
Ok(mut finding) => {
290+
let normalized = discovery::normalize_crate_name(
291+
&finding.crate_name,
292+
);
293+
if let Some((cargo_name, ver)) =
294+
crate_lookup.get(&normalized)
295+
{
296+
finding.crate_name = cargo_name.clone();
297+
if finding.crate_version == "0.0.0" {
298+
finding.crate_version = ver.clone();
299+
}
300+
}
301+
mir_findings.push(finding);
302+
}
303+
Err(e) => {
304+
eprintln!(
305+
"Warning: Failed to parse deep finding: {e}"
306+
);
307+
}
308+
}
309+
}
310+
}
311+
let _ = std::fs::remove_file(&output_path);
312+
}
313+
Some(output) => {
314+
let stderr = String::from_utf8_lossy(&output.stderr);
315+
eprintln!(
316+
"Warning: Deep analysis failed (cargo check returned non-zero)."
317+
);
318+
for line in stderr
319+
.lines()
320+
.filter(|l| l.contains("error") || l.contains("Error"))
321+
.take(5)
322+
{
323+
eprintln!(" {line}");
324+
}
325+
if stderr.contains("incompatible version of rustc") {
326+
eprintln!(
327+
" Hint: try `rm -rf target/capsec-deep` to clear stale artifacts."
328+
);
329+
}
330+
eprintln!("Continuing with syntactic-only findings.");
331+
}
332+
None => {
333+
eprintln!("Warning: Could not invoke cargo check for deep analysis.");
334+
eprintln!("Continuing with syntactic-only findings.");
335+
}
336+
}
337+
338+
if !mir_findings.is_empty() {
339+
eprintln!(
340+
"Deep analysis: {} MIR-level findings. Building export maps...",
341+
mir_findings.len()
342+
);
343+
344+
// Build export maps from MIR findings so they propagate to Phase 2
345+
let mut mir_by_crate: HashMap<String, Vec<detector::Finding>> = HashMap::new();
346+
for f in &mir_findings {
347+
mir_by_crate
348+
.entry(f.crate_name.clone())
349+
.or_default()
350+
.push(f.clone());
351+
}
352+
for (crate_name, findings) in &mir_by_crate {
353+
let normalized = discovery::normalize_crate_name(crate_name);
354+
let src_dir = dep_crates
355+
.iter()
356+
.chain(workspace_crates.iter())
357+
.find(|c| discovery::normalize_crate_name(&c.name) == normalized)
358+
.map(|c| c.source_dir.clone())
359+
.unwrap_or_default();
360+
let mir_emap = export_map::build_export_map(
361+
&normalized,
362+
&findings[0].crate_version,
363+
findings,
364+
&src_dir,
365+
);
366+
export_maps.push(mir_emap);
367+
}
368+
369+
// Add MIR findings to the main collection
370+
all_findings.extend(mir_findings);
371+
}
372+
}
373+
}
374+
215375
// Phase 2: Scan workspace crates with dependency export maps injected.
216376
// Process in topological order so workspace-to-workspace findings propagate
217377
// (e.g., radicle-cli depends on radicle → radicle scanned first).
@@ -357,158 +517,8 @@ fn run_audit(args: AuditArgs) {
357517
}
358518
}
359519

360-
// ── Deep MIR analysis (optional, requires nightly + capsec-driver) ──
361-
if args.deep {
362-
let output_path =
363-
std::env::temp_dir().join(format!("capsec-deep-{}.jsonl", std::process::id()));
364-
365-
// Check if capsec-driver is available
366-
let driver_check = capsec_std::process::command("capsec-driver", &spawn_cap)
367-
.ok()
368-
.and_then(|mut cmd| cmd.arg("--version").output().ok());
369-
370-
if driver_check.is_none() {
371-
// Try finding it relative to ourselves or via which
372-
let which_check = capsec_std::process::command("which", &spawn_cap)
373-
.ok()
374-
.and_then(|mut cmd| cmd.arg("capsec-driver").output().ok())
375-
.map(|o| o.status.success())
376-
.unwrap_or(false);
377-
378-
if !which_check {
379-
eprintln!("Error: --deep requires capsec-driver (MIR analysis driver).");
380-
eprintln!("Install it with: cd crates/capsec-deep && cargo install --path .");
381-
eprintln!("Requires nightly Rust toolchain with rustc-dev component.");
382-
eprintln!();
383-
eprintln!("Continuing with syntactic-only analysis...");
384-
}
385-
}
386-
387-
// Invoke cargo check with capsec-driver as RUSTC_WRAPPER (not WORKSPACE_WRAPPER).
388-
// RUSTC_WRAPPER wraps ALL crate compilations including dependencies,
389-
// so the MIR driver analyzes every crate in the dependency tree.
390-
// Must use the EXACT nightly that capsec-driver was compiled against.
391-
// Detect it by running capsec-driver to print its rustc version hash,
392-
// or fall back to the pinned nightly from capsec-deep's toolchain file.
393-
//
394-
// We use RUSTUP_TOOLCHAIN to override the target project's rust-toolchain.toml.
395-
// We also clean the deep target dir on first run to avoid stale .rmeta conflicts.
396-
let deep_target_dir = workspace_root.join("target/capsec-deep");
397-
398-
// Detect which nightly capsec-driver needs by checking its linked rustc version
399-
let toolchain = {
400-
// Try the pinned date-based nightly first (most reliable)
401-
let pinned = "nightly-2026-02-17";
402-
// Verify it exists
403-
let has_pinned = capsec_std::process::command("rustup", &spawn_cap)
404-
.ok()
405-
.and_then(|mut cmd| {
406-
cmd.arg("run")
407-
.arg(pinned)
408-
.arg("rustc")
409-
.arg("--version")
410-
.output()
411-
.ok()
412-
})
413-
.map(|o| o.status.success())
414-
.unwrap_or(false);
415-
416-
if has_pinned { pinned } else { "nightly" }
417-
};
418-
419-
// Clean the deep target dir to force a full rebuild.
420-
// Without this, `cargo check` sees cached .rmeta and doesn't invoke the driver,
421-
// producing zero MIR findings on subsequent runs.
422-
let _ = std::fs::remove_dir_all(&deep_target_dir);
423-
424-
let deep_result = capsec_std::process::command("cargo", &spawn_cap)
425-
.ok()
426-
.and_then(|mut cmd| {
427-
cmd.arg("check")
428-
.current_dir(&path_arg)
429-
.env("RUSTC_WRAPPER", "capsec-driver")
430-
.env("CAPSEC_DEEP_OUTPUT", &output_path)
431-
.env("CAPSEC_CRATE_VERSION", "0.0.0")
432-
.env("CARGO_TARGET_DIR", &deep_target_dir)
433-
.env("RUSTUP_TOOLCHAIN", toolchain)
434-
.output()
435-
.ok()
436-
});
437-
438-
match deep_result {
439-
Some(output) if output.status.success() || output_path.exists() => {
440-
// Build lookups: normalized_name → (cargo_name, version)
441-
// The MIR driver uses rustc's crate name (underscored: radicle_cli)
442-
// but the syntactic scanner uses Cargo's package name (hyphenated: radicle-cli).
443-
// We need to map MIR names back to the canonical Cargo names.
444-
let crate_lookup: HashMap<String, (String, String)> = workspace_crates
445-
.iter()
446-
.chain(dep_crates.iter())
447-
.map(|c| {
448-
(
449-
discovery::normalize_crate_name(&c.name),
450-
(c.name.clone(), c.version.clone()),
451-
)
452-
})
453-
.collect();
454-
455-
// Read JSONL findings
456-
if let Ok(contents) = capsec_std::fs::read_to_string(&output_path, &fs_read) {
457-
let mut deep_count = 0;
458-
for line in contents.lines() {
459-
if line.trim().is_empty() {
460-
continue;
461-
}
462-
match serde_json::from_str::<detector::Finding>(line) {
463-
Ok(mut finding) => {
464-
// Patch crate name and version: MIR driver uses rustc
465-
// identifiers (underscored) and doesn't know Cargo versions.
466-
let normalized =
467-
discovery::normalize_crate_name(&finding.crate_name);
468-
if let Some((cargo_name, ver)) = crate_lookup.get(&normalized) {
469-
finding.crate_name = cargo_name.clone();
470-
if finding.crate_version == "0.0.0" {
471-
finding.crate_version = ver.clone();
472-
}
473-
}
474-
all_findings.push(finding);
475-
deep_count += 1;
476-
}
477-
Err(e) => {
478-
eprintln!("Warning: Failed to parse deep finding: {e}");
479-
}
480-
}
481-
}
482-
if deep_count > 0 {
483-
eprintln!("Deep analysis: {deep_count} MIR-level findings added.");
484-
}
485-
}
486-
// Clean up temp file
487-
let _ = std::fs::remove_file(&output_path);
488-
}
489-
Some(output) => {
490-
let stderr = String::from_utf8_lossy(&output.stderr);
491-
eprintln!("Warning: Deep analysis failed (cargo check returned non-zero).");
492-
// Show last few meaningful error lines
493-
for line in stderr
494-
.lines()
495-
.filter(|l| l.contains("error") || l.contains("Error"))
496-
.take(5)
497-
{
498-
eprintln!(" {line}");
499-
}
500-
if stderr.contains("incompatible version of rustc") {
501-
eprintln!(" Hint: try `rm -rf target/capsec-deep` to clear stale artifacts.");
502-
}
503-
eprintln!("Continuing with syntactic-only findings.");
504-
}
505-
None => {
506-
eprintln!("Warning: Could not invoke cargo check for deep analysis.");
507-
eprintln!("Continuing with syntactic-only findings.");
508-
}
509-
}
510-
511-
// Dedup: if both syntactic and MIR found the same call site, keep one
520+
// Dedup: if both syntactic and MIR found the same call site, keep one
521+
{
512522
let mut seen = std::collections::HashSet::new();
513523
all_findings.retain(|f| {
514524
seen.insert((

0 commit comments

Comments
 (0)