//! Surface retrieval for absence-based controls. //! //! Some CRA controls are violated by an *absence* — no rate limiting on login, no //! security logging, no signature check on an update — so there's no offending //! pattern for semgrep to match. Instead we deterministically locate the code //! *surface* the control governs (a login route, a logging setup, update/download //! code) by identifier/route terms, then hand each surface region to the grounded //! judge, which decides whether the control is satisfied there. The resulting //! finding grounds to the surface snippet, so nothing fabricated survives. //! //! Retrieval is intentionally cheap and bounded: keyword match + a fixed window, //! capped per control to keep the downstream LLM cost predictable. use std::path::Path; use compliance_core::control_check::CandidateRegion; /// An absence-based control and the case-insensitive terms that mark the code /// surface it governs. pub struct Surface { pub control_id: &'static str, pub terms: &'static [&'static str], } /// The absence-based CRA controls we retrieve surfaces for — the grounded half of /// the hybrid coverage (the pattern-expressible half is custom semgrep rules). pub const SURFACES: &[Surface] = &[ Surface { control_id: "cra-ai-6", // Integritaetspruefung terms: &[ "checksum", "sha256", "signature", "hmac", "integrity", "verify", ], }, Surface { control_id: "cra-ai-11", // Brute-Force-Schutz terms: &[ "login", "signin", "authenticate", "/auth", "password", "ratelimit", ], }, Surface { control_id: "cra-ai-12", // Rollenbasierte Autorisierung (RBAC) terms: &[ "authorize", "permission", "role", "rbac", "require_role", "has_role", ], }, Surface { control_id: "cra-ai-24", // Security-Logging terms: &["login", "authorize", "permission", "role", "admin", "audit"], }, Surface { control_id: "cra-ai-27", // Log-Integritaet und -Aufbewahrung terms: &["logging", "logger", "getlogger", "audit_log"], }, Surface { control_id: "cra-ai-28", // Sichere Update-Mechanismen terms: &["update", "upgrade", "download", "firmware"], }, Surface { control_id: "cra-ai-29", // Update-Authentizitaet terms: &["update", "signature", "verify", "pubkey", "certificate"], }, Surface { control_id: "cra-ai-30", // Update-Integritaet terms: &["update", "checksum", "digest", "integrity", "verify"], }, ]; /// Source file extensions worth reading (skip binaries/assets/lockfiles). const CODE_EXTS: &[&str] = &[ "py", "js", "ts", "tsx", "jsx", "go", "java", "rb", "php", "rs", "cs", "kt", ]; /// Directories never worth walking. const SKIP_DIRS: &[&str] = &[ ".git", "node_modules", "target", "vendor", ".venv", "__pycache__", "dist", "build", ]; /// Lines of context on each side of a hit. const WINDOW: usize = 6; /// Cap on regions per control, to bound downstream LLM calls. const MAX_REGIONS_PER_CONTROL: usize = 8; /// Skip files larger than this (generated/minified). const MAX_FILE_BYTES: u64 = 512 * 1024; /// Deterministically retrieve up to [`MAX_REGIONS_PER_CONTROL`] code regions in /// `repo_path` whose lines mention any of `terms`. Hits close together within a /// file are merged into one region; results are capped to bound LLM cost. pub fn retrieve(repo_path: &Path, terms: &[&str]) -> Vec { let lowered: Vec = terms.iter().map(|t| t.to_lowercase()).collect(); let mut regions = Vec::new(); for entry in walk(repo_path) { if regions.len() >= MAX_REGIONS_PER_CONTROL { break; } let path = entry.path(); if !has_code_ext(path) { continue; } let Ok(meta) = entry.metadata() else { continue }; if !meta.is_file() || meta.len() > MAX_FILE_BYTES { continue; } let Ok(content) = std::fs::read_to_string(path) else { continue; }; let rel = path .strip_prefix(repo_path) .unwrap_or(path) .to_string_lossy() .to_string(); let lines: Vec<&str> = content.lines().collect(); let hits: Vec = lines .iter() .enumerate() .filter(|(_, line)| { let ll = line.to_lowercase(); lowered.iter().any(|t| ll.contains(t.as_str())) }) .map(|(i, _)| i) .collect(); for center in merge_centers(&hits) { if regions.len() >= MAX_REGIONS_PER_CONTROL { break; } let start = center.saturating_sub(WINDOW); let end = (center + WINDOW + 1).min(lines.len()); regions.push(CandidateRegion { file: rel.clone(), start_line: (start as u32) + 1, content: lines[start..end].join("\n"), }); } } regions } /// Collapse ascending hit indices that fall within one window into a single /// representative center, so overlapping regions aren't judged repeatedly. fn merge_centers(hits: &[usize]) -> Vec { let mut out: Vec = Vec::new(); for &h in hits { match out.last() { Some(&last) if h.saturating_sub(last) <= WINDOW => {} _ => out.push(h), } } out } fn has_code_ext(path: &Path) -> bool { path.extension() .and_then(|e| e.to_str()) .is_some_and(|e| CODE_EXTS.contains(&e)) } fn walk(root: &Path) -> Vec { walkdir::WalkDir::new(root) .into_iter() .filter_entry(|e| { let name = e.file_name().to_string_lossy(); !SKIP_DIRS.contains(&name.as_ref()) }) .filter_map(|e| e.ok()) .collect() } #[cfg(test)] #[allow(clippy::unwrap_used)] mod tests { use super::*; fn write(dir: &Path, rel: &str, body: &str) { let p = dir.join(rel); if let Some(parent) = p.parent() { std::fs::create_dir_all(parent).unwrap(); } std::fs::write(p, body).unwrap(); } fn terms_for(control_id: &str) -> &'static [&'static str] { SURFACES .iter() .find(|s| s.control_id == control_id) .unwrap() .terms } #[test] fn surfaces_cover_the_absence_based_controls() { assert_eq!(SURFACES.len(), 8); for id in [ "cra-ai-6", "cra-ai-11", "cra-ai-12", "cra-ai-24", "cra-ai-27", "cra-ai-28", "cra-ai-29", "cra-ai-30", ] { assert!(SURFACES.iter().any(|s| s.control_id == id), "{id} missing"); } } #[test] fn retrieves_matching_region_with_context() { let dir = std::env::temp_dir().join(format!("surface-{}", uuid::Uuid::new_v4())); std::fs::create_dir_all(&dir).unwrap(); write( &dir, "app/auth.py", "import x\n\n\n\n\n\n\ndef login(u, p):\n return check(u, p)\n", ); let regions = retrieve(&dir, terms_for("cra-ai-11")); assert_eq!(regions.len(), 1); assert!(regions[0].content.contains("def login")); assert_eq!(regions[0].file, "app/auth.py"); let _ = std::fs::remove_dir_all(&dir); } #[test] fn skips_non_code_and_vendored() { let dir = std::env::temp_dir().join(format!("surface-{}", uuid::Uuid::new_v4())); std::fs::create_dir_all(&dir).unwrap(); write(&dir, "README.md", "login and password and audit\n"); // not code ext write(&dir, "node_modules/pkg/index.js", "function login() {}\n"); // vendored assert!(retrieve(&dir, terms_for("cra-ai-11")).is_empty()); let _ = std::fs::remove_dir_all(&dir); } #[test] fn merges_adjacent_hits_into_one_region() { // Two hits one line apart collapse to a single center/region. assert_eq!(merge_centers(&[10, 11, 30]), vec![10, 30]); assert_eq!(merge_centers(&[]), Vec::::new()); assert_eq!(merge_centers(&[5]), vec![5]); } }