Skip to content

Commit 1555f00

Browse files
authored
Merge pull request #54 from auths-dev/dev-refactorMain
refactor: extract deep analysis into deep.rs module from main.rs
2 parents 1d638b6 + 9544a0f commit 1555f00

5 files changed

Lines changed: 301 additions & 181 deletions

File tree

crates/cargo-capsec/src/config.rs

Lines changed: 42 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -277,26 +277,58 @@ pub fn resolve_classification(
277277
cargo_toml_classification
278278
}
279279

280+
/// Pre-compiled exclude patterns for efficient repeated matching.
281+
#[allow(dead_code)]
282+
pub struct CompiledExcludes {
283+
set: globset::GlobSet,
284+
}
285+
286+
#[allow(dead_code)]
287+
impl CompiledExcludes {
288+
/// Compiles exclude patterns once. Invalid patterns are silently skipped.
289+
pub fn new(patterns: &[String]) -> Self {
290+
let mut builder = globset::GlobSetBuilder::new();
291+
for p in patterns {
292+
if let Ok(glob) = globset::Glob::new(p) {
293+
builder.add(glob);
294+
}
295+
}
296+
Self {
297+
set: builder
298+
.build()
299+
.unwrap_or_else(|_| globset::GlobSetBuilder::new().build().unwrap()),
300+
}
301+
}
302+
303+
/// Returns `true` if a file path matches any compiled exclude pattern.
304+
pub fn is_excluded(&self, path: &Path) -> bool {
305+
let path_str = path.display().to_string();
306+
self.set.is_match(&path_str)
307+
|| path
308+
.file_name()
309+
.and_then(|n| n.to_str())
310+
.is_some_and(|name| self.set.is_match(name))
311+
}
312+
}
313+
280314
/// Returns `true` if a file path matches any `[analysis].exclude` glob pattern.
281315
///
282316
/// Uses the [`globset`] crate for correct glob semantics (supports `**`, `*`,
283317
/// `?`, and character classes).
284318
pub fn should_exclude(path: &Path, excludes: &[String]) -> bool {
285319
let path_str = path.display().to_string();
286-
excludes.iter().any(|pattern| {
287-
match globset::Glob::new(pattern) {
320+
excludes
321+
.iter()
322+
.any(|pattern| match globset::Glob::new(pattern) {
288323
Ok(glob) => match glob.compile_matcher().is_match(&path_str) {
289324
true => true,
290-
false => {
291-
// Also try matching against just the file name for simple patterns
292-
path.file_name()
293-
.and_then(|n| n.to_str())
294-
.is_some_and(|name| glob.compile_matcher().is_match(name))
295-
}
325+
false => path
326+
.file_name()
327+
.and_then(|n| n.to_str())
328+
.is_some_and(|name| glob.compile_matcher().is_match(name)),
296329
},
297330
Err(_) => path_str.contains(pattern),
298-
}
299-
})
331+
})
300332
}
301333

302334
#[cfg(test)]

crates/cargo-capsec/src/deep.rs

Lines changed: 231 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,231 @@
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+
}

crates/cargo-capsec/src/lib.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ pub mod authorities;
4646
pub mod baseline;
4747
pub mod config;
4848
pub mod cross_crate;
49+
pub mod deep;
4950
pub mod detector;
5051
pub mod discovery;
5152
pub mod export_map;

0 commit comments

Comments
 (0)