Skip to main content

batuta/bug_hunter/
modes_falsify.rs

1//! BH-01: Mutation-based invariant falsification (FDV pattern).
2
3use super::types::*;
4use std::path::Path;
5
6/// BH-01: Mutation-based invariant falsification (FDV pattern)
7pub(super) fn run_falsify_mode(project_path: &Path, config: &HuntConfig, result: &mut HuntResult) {
8    // Check for cargo-mutants availability
9    let mutants_available = std::process::Command::new("cargo")
10        .args(["mutants", "--version"])
11        .output()
12        .map(|o| o.status.success())
13        .unwrap_or(false);
14
15    if !mutants_available {
16        result.add_finding(
17            Finding::new(
18                "BH-FALSIFY-UNAVAIL",
19                project_path.join("Cargo.toml"),
20                1,
21                "cargo-mutants not installed",
22            )
23            .with_description("Install with: cargo install cargo-mutants")
24            .with_severity(FindingSeverity::Info)
25            .with_category(DefectCategory::ConfigurationErrors)
26            .with_suspiciousness(0.1)
27            .with_discovered_by(HuntMode::Falsify),
28        );
29        return;
30    }
31
32    // Analyze Rust files for potential mutation targets
33    for target in &config.targets {
34        let target_path = project_path.join(target);
35        // Match .rs files both directly in the target dir and in subdirectories
36        for pattern in &[
37            format!("{}/*.rs", target_path.display()),
38            format!("{}/**/*.rs", target_path.display()),
39        ] {
40            if let Ok(entries) = glob::glob(pattern) {
41                for entry in entries.flatten() {
42                    analyze_file_for_mutations(&entry, config, result);
43                }
44            }
45        }
46    }
47}
48
49/// Detected mutation target with metadata.
50pub(super) struct MutationMatch {
51    pub(super) title: &'static str,
52    pub(super) description: &'static str,
53    pub(super) severity: FindingSeverity,
54    pub(super) suspiciousness: f64,
55    pub(super) prefix: &'static str,
56}
57
58/// Detect mutation targets in a single line of code.
59pub(super) fn detect_mutation_targets(line: &str) -> Vec<MutationMatch> {
60    let mut matches = Vec::new();
61
62    let has_comparison =
63        line.contains("< ") || line.contains("> ") || line.contains("<= ") || line.contains(">= ");
64    let has_len = line.contains("len()") || line.contains("size()") || line.contains(".len");
65    if has_comparison && has_len {
66        matches.push(MutationMatch {
67            title: "Boundary condition mutation target",
68            description: "Off-by-one errors are common; this comparison should be mutation-tested",
69            severity: FindingSeverity::Medium,
70            suspiciousness: 0.6,
71            prefix: "boundary",
72        });
73    }
74
75    let has_arith = line.contains(" + ") || line.contains(" - ") || line.contains(" * ");
76    let no_safe =
77        !line.contains("saturating_") && !line.contains("checked_") && !line.contains("wrapping_");
78    let has_cast = line.contains("as usize") || line.contains("as u") || line.contains("as i");
79    if has_arith && no_safe && has_cast {
80        matches.push(MutationMatch {
81            title: "Arithmetic operation mutation target",
82            description:
83                "Unchecked arithmetic with type cast; consider checked_* or saturating_* operations",
84            severity: FindingSeverity::Medium,
85            suspiciousness: 0.55,
86            prefix: "arith",
87        });
88    }
89
90    let has_logic = line.contains(" && ") || line.contains(" || ");
91    let has_predicate = line.contains('!') || line.contains("is_") || line.contains("has_");
92    if has_logic && has_predicate {
93        matches.push(MutationMatch {
94            title: "Boolean logic mutation target",
95            description:
96                "Complex boolean expression; verify test coverage catches negation mutations",
97            severity: FindingSeverity::Low,
98            suspiciousness: 0.4,
99            prefix: "bool",
100        });
101    }
102
103    matches
104}
105
106/// Analyze a file for mutation testing targets.
107pub(super) fn analyze_file_for_mutations(
108    file_path: &Path,
109    _config: &HuntConfig,
110    result: &mut HuntResult,
111) {
112    let Ok(content) = std::fs::read_to_string(file_path) else {
113        return;
114    };
115
116    let mut finding_id = 0;
117
118    for (line_num, line) in content.lines().enumerate() {
119        let line_num = line_num + 1;
120        for m in detect_mutation_targets(line) {
121            finding_id += 1;
122            result.add_finding(
123                Finding::new(format!("BH-MUT-{:04}", finding_id), file_path, line_num, m.title)
124                    .with_description(m.description)
125                    .with_severity(m.severity)
126                    .with_category(DefectCategory::LogicErrors)
127                    .with_suspiciousness(m.suspiciousness)
128                    .with_discovered_by(HuntMode::Falsify)
129                    .with_evidence(FindingEvidence::mutation(
130                        format!("{}_{}", m.prefix, finding_id),
131                        true,
132                    )),
133            );
134        }
135    }
136}