batuta/bug_hunter/
modes_falsify.rs1use super::types::*;
4use std::path::Path;
5
6pub(super) fn run_falsify_mode(project_path: &Path, config: &HuntConfig, result: &mut HuntResult) {
8 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 for target in &config.targets {
34 let target_path = project_path.join(target);
35 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
49pub(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
58pub(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
106pub(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}