Skip to main content

batuta/falsification/
auditors.rs

1//! Audit Utilities for Falsification Checks
2//!
3//! Provides reusable audit functions for the checklist evaluation.
4
5use std::path::{Path, PathBuf};
6use std::process::Command;
7
8/// Result of a file audit.
9#[derive(Debug, Clone)]
10pub struct FileAuditResult {
11    /// Files matching the pattern
12    pub matches: Vec<PathBuf>,
13    /// Total files scanned
14    pub scanned: usize,
15    /// Patterns used
16    pub patterns: Vec<String>,
17}
18
19impl FileAuditResult {
20    /// Check if any violations were found.
21    pub fn has_violations(&self) -> bool {
22        !self.matches.is_empty()
23    }
24
25    /// Get violation count.
26    pub fn violation_count(&self) -> usize {
27        self.matches.len()
28    }
29}
30
31/// Non-production paths to exclude from scripting audits
32const EXCLUDED_SCRIPT_DIRS: &[&str] = &[
33    "node_modules",
34    "venv",
35    ".venv",
36    "__pycache__",
37    "/target/",
38    "/dist/",
39    "/examples/",
40    "/migrations/",
41    "/book/",
42    "/docs/",
43    "/fixtures/",
44    "/testdata/",
45];
46
47/// Check if a path should be excluded from scripting audit
48fn is_excluded_script_path(path_str: &str) -> bool {
49    EXCLUDED_SCRIPT_DIRS.iter().any(|ex| path_str.contains(ex))
50}
51
52/// Audit for scripting language files.
53pub fn audit_scripting_files(project_path: &Path) -> FileAuditResult {
54    let patterns = vec![
55        "**/*.py".to_string(),
56        "**/*.js".to_string(),
57        "**/*.ts".to_string(),
58        "**/*.lua".to_string(),
59        "**/*.rb".to_string(),
60    ];
61
62    let mut matches = Vec::new();
63    let mut scanned = 0;
64
65    for pattern in &patterns {
66        let Ok(entries) = glob::glob(&format!("{}/{}", project_path.display(), pattern)) else {
67            continue;
68        };
69        for entry in entries.flatten() {
70            scanned += 1;
71            if !is_excluded_script_path(&entry.to_string_lossy()) {
72                matches.push(entry);
73            }
74        }
75    }
76
77    FileAuditResult { matches, scanned, patterns }
78}
79
80/// Audit for test framework files.
81pub fn audit_test_frameworks(project_path: &Path) -> FileAuditResult {
82    let patterns = vec![
83        // JavaScript test files
84        "**/*.test.js".to_string(),
85        "**/*.spec.js".to_string(),
86        "**/*.test.ts".to_string(),
87        "**/*.spec.ts".to_string(),
88        "**/jest.config.*".to_string(),
89        "**/vitest.config.*".to_string(),
90        // Python test files
91        "**/test_*.py".to_string(),
92        "**/*_test.py".to_string(),
93        "**/conftest.py".to_string(),
94        "**/pytest.ini".to_string(),
95    ];
96
97    let mut matches = Vec::new();
98    let mut scanned = 0;
99
100    for pattern in &patterns {
101        if let Ok(entries) = glob::glob(&format!("{}/{}", project_path.display(), pattern)) {
102            for entry in entries.flatten() {
103                scanned += 1;
104                let path_str = entry.to_string_lossy();
105
106                if !path_str.contains("node_modules") && !path_str.contains("venv") {
107                    matches.push(entry);
108                }
109            }
110        }
111    }
112
113    FileAuditResult { matches, scanned, patterns }
114}
115
116/// Audit for YAML configuration files.
117pub fn audit_yaml_configs(project_path: &Path) -> FileAuditResult {
118    let patterns = vec![
119        "*.yaml".to_string(),
120        "*.yml".to_string(),
121        "config/**/*.yaml".to_string(),
122        "config/**/*.yml".to_string(),
123        "examples/**/*.yaml".to_string(),
124        "examples/**/*.yml".to_string(),
125    ];
126
127    let mut matches = Vec::new();
128    let mut scanned = 0;
129
130    for pattern in &patterns {
131        if let Ok(entries) = glob::glob(&format!("{}/{}", project_path.display(), pattern)) {
132            for entry in entries.flatten() {
133                scanned += 1;
134                matches.push(entry);
135            }
136        }
137    }
138
139    FileAuditResult { matches, scanned, patterns }
140}
141
142/// Result of a dependency audit.
143#[derive(Debug, Clone)]
144pub struct DependencyAuditResult {
145    /// Forbidden dependencies found
146    pub forbidden: Vec<String>,
147    /// All dependencies checked
148    pub checked: Vec<String>,
149    /// Raw cargo tree output (if available)
150    pub cargo_tree_output: Option<String>,
151}
152
153impl DependencyAuditResult {
154    /// Check if any forbidden dependencies were found.
155    pub fn has_violations(&self) -> bool {
156        !self.forbidden.is_empty()
157    }
158}
159
160/// Audit Cargo.toml for forbidden dependencies.
161pub fn audit_cargo_dependencies(project_path: &Path, forbidden: &[&str]) -> DependencyAuditResult {
162    let cargo_toml = project_path.join("Cargo.toml");
163    let mut found_forbidden = Vec::new();
164    let mut checked = Vec::new();
165
166    if cargo_toml.exists() {
167        if let Ok(content) = std::fs::read_to_string(&cargo_toml) {
168            for dep in forbidden {
169                checked.push((*dep).to_string());
170
171                // Simple check - a proper implementation would parse TOML
172                if content.contains(&format!("{} =", dep))
173                    || content.contains(&format!("{}=", dep))
174                    || content.contains(&format!("\"{}\"", dep))
175                {
176                    found_forbidden.push((*dep).to_string());
177                }
178            }
179        }
180    }
181
182    // Try to get cargo tree output for more detailed analysis
183    let cargo_tree_output = Command::new("cargo")
184        .args(["tree", "--edges", "no-dev", "-p"])
185        .current_dir(project_path)
186        .output()
187        .ok()
188        .and_then(|output| {
189            if output.status.success() {
190                String::from_utf8(output.stdout).ok()
191            } else {
192                None
193            }
194        });
195
196    DependencyAuditResult { forbidden: found_forbidden, checked, cargo_tree_output }
197}
198
199/// Check if a Rust project has tests.
200pub fn has_rust_tests(project_path: &Path) -> bool {
201    project_path.join("tests").exists()
202        || super::helpers::files_contain_pattern(
203            project_path,
204            &["src/**/*.rs"],
205            &["#[test]", "#[cfg(test)]"],
206        )
207}
208
209/// Check if a project has WASM support.
210pub fn has_wasm_support(project_path: &Path) -> WasmSupport {
211    let cargo_toml = project_path.join("Cargo.toml");
212    let mut support = WasmSupport::default();
213
214    if cargo_toml.exists() {
215        if let Ok(content) = std::fs::read_to_string(&cargo_toml) {
216            support.has_wasm_feature = content.contains("[features]")
217                && (content.contains("wasm") || content.contains("web"));
218
219            support.has_wasm_bindgen = content.contains("wasm-bindgen");
220            support.has_web_sys = content.contains("web-sys");
221            support.has_wasm_pack = content.contains("wasm-pack");
222        }
223    }
224
225    // Check for wasm.rs module
226    support.has_wasm_module = project_path.join("src/wasm.rs").exists();
227
228    // Check for wasm target in .cargo/config.toml
229    let cargo_config = project_path.join(".cargo/config.toml");
230    if cargo_config.exists() {
231        if let Ok(content) = std::fs::read_to_string(&cargo_config) {
232            support.has_wasm_target = content.contains("wasm32");
233        }
234    }
235
236    support
237}
238
239/// WASM support detection result.
240#[derive(Debug, Clone, Default)]
241pub struct WasmSupport {
242    pub has_wasm_feature: bool,
243    pub has_wasm_bindgen: bool,
244    pub has_web_sys: bool,
245    pub has_wasm_pack: bool,
246    pub has_wasm_module: bool,
247    pub has_wasm_target: bool,
248}
249
250impl WasmSupport {
251    /// Check if any WASM support is present.
252    pub fn is_supported(&self) -> bool {
253        self.has_wasm_feature
254            || self.has_wasm_bindgen
255            || self.has_web_sys
256            || self.has_wasm_pack
257            || self.has_wasm_module
258    }
259
260    /// Get support level description.
261    pub fn level(&self) -> &'static str {
262        if self.has_wasm_bindgen && self.has_wasm_module {
263            "Full"
264        } else if self.has_wasm_bindgen || self.has_wasm_feature {
265            "Partial"
266        } else if self.has_wasm_module {
267            "Basic"
268        } else {
269            "None"
270        }
271    }
272}
273
274/// Scan source files for Deserialize structs and config patterns.
275fn scan_deserialize_structs(project_path: &Path) -> (bool, bool) {
276    let mut has_deserialize = false;
277    let mut has_config = false;
278    let Ok(entries) = glob::glob(&format!("{}/src/**/*.rs", project_path.display())) else {
279        return (false, false);
280    };
281    for entry in entries.flatten() {
282        let Ok(content) = std::fs::read_to_string(&entry) else {
283            continue;
284        };
285        if content.contains("#[derive") && content.contains("Deserialize") {
286            has_deserialize = true;
287            if content.to_lowercase().contains("config") {
288                has_config = true;
289            }
290        }
291    }
292    (has_deserialize, has_config)
293}
294
295/// Check for serde-based config validation.
296pub fn has_serde_config(project_path: &Path) -> SerdeConfigSupport {
297    let cargo_toml = project_path.join("Cargo.toml");
298    let content = std::fs::read_to_string(&cargo_toml).unwrap_or_default();
299    let (has_deserialize_structs, has_config_struct) = scan_deserialize_structs(project_path);
300
301    SerdeConfigSupport {
302        has_serde: content.contains("serde"),
303        has_serde_yaml: content.contains("serde_yaml")
304            || content.contains("serde_yml")
305            || content.contains("serde_yaml_ng"),
306        has_serde_json: content.contains("serde_json"),
307        has_toml: content.contains("toml"),
308        has_validator: content.contains("validator") || content.contains("garde"),
309        has_deserialize_structs,
310        has_config_struct,
311    }
312}
313
314/// Serde config support detection result.
315#[derive(Debug, Clone, Default)]
316pub struct SerdeConfigSupport {
317    pub has_serde: bool,
318    pub has_serde_yaml: bool,
319    pub has_serde_json: bool,
320    pub has_toml: bool,
321    pub has_validator: bool,
322    pub has_deserialize_structs: bool,
323    pub has_config_struct: bool,
324}
325
326impl SerdeConfigSupport {
327    /// Check if typed config is supported.
328    pub fn has_typed_config(&self) -> bool {
329        self.has_serde && self.has_config_struct
330    }
331
332    /// Check if validation is supported.
333    pub fn has_validation(&self) -> bool {
334        self.has_validator || self.has_serde // serde itself provides some validation
335    }
336}
337
338#[cfg(test)]
339#[path = "auditors_tests.rs"]
340mod tests;