batuta/bug_hunter/
modes_fuzz.rs1use super::types::*;
4use std::path::Path;
5
6pub(super) fn source_forbids_unsafe(path: &Path) -> bool {
9 let Ok(content) = std::fs::read_to_string(path) else {
10 return false;
11 };
12 content.lines().take(50).any(|line| {
13 let t = line.trim();
14 t.starts_with("#![") && t.contains("forbid") && t.contains("unsafe_code")
15 })
16}
17
18pub(super) fn crate_forbids_unsafe(project_path: &Path) -> bool {
21 for entry in ["src/lib.rs", "src/main.rs"] {
22 if source_forbids_unsafe(&project_path.join(entry)) {
24 return true;
25 }
26 }
27 if let Ok(content) = std::fs::read_to_string(project_path.join("Cargo.toml")) {
28 if content.contains("unsafe_code") && content.contains("forbid") {
30 return true;
31 }
32 }
33 false
34}
35
36pub(super) fn scan_file_for_unsafe_blocks(
38 entry: &Path,
39 finding_id: &mut usize,
40 unsafe_inventory: &mut Vec<(std::path::PathBuf, usize)>,
41 result: &mut HuntResult,
42) {
43 let Ok(content) = std::fs::read_to_string(entry) else {
44 return;
45 };
46 let mut in_unsafe = false;
47 let mut unsafe_start = 0;
48
49 for (line_num, line) in content.lines().enumerate() {
50 let line_num = line_num + 1;
51
52 if line.contains("unsafe ") && line.contains('{') {
54 in_unsafe = true;
55 unsafe_start = line_num;
56 }
57
58 if in_unsafe {
60 if line.contains('*') && (line.contains("ptr") || line.contains("as *")) {
61 *finding_id += 1;
62 unsafe_inventory.push((entry.to_path_buf(), line_num));
63 result.add_finding(
64 Finding::new(
65 format!("BH-UNSAFE-{:04}", finding_id),
66 entry,
67 line_num,
68 "Pointer dereference in unsafe block",
69 )
70 .with_description(format!(
71 "Unsafe block starting at line {}; potential fuzzing target",
72 unsafe_start
73 ))
74 .with_severity(FindingSeverity::High)
75 .with_category(DefectCategory::MemorySafety)
76 .with_suspiciousness(0.75)
77 .with_discovered_by(HuntMode::Fuzz)
78 .with_evidence(FindingEvidence::fuzzing("N/A", "pointer_deref")),
79 );
80 }
81
82 if line.contains("transmute") {
83 *finding_id += 1;
84 result.add_finding(
85 Finding::new(
86 format!("BH-UNSAFE-{:04}", finding_id),
87 entry,
88 line_num,
89 "Transmute in unsafe block",
90 )
91 .with_description(
92 "std::mem::transmute bypasses type safety; high-priority fuzzing target",
93 )
94 .with_severity(FindingSeverity::Critical)
95 .with_category(DefectCategory::MemorySafety)
96 .with_suspiciousness(0.9)
97 .with_discovered_by(HuntMode::Fuzz)
98 .with_evidence(FindingEvidence::fuzzing("N/A", "transmute")),
99 );
100 }
101 }
102
103 if line.contains('}') && in_unsafe {
105 in_unsafe = false;
106 }
107 }
108}
109
110pub(super) fn run_fuzz_mode(project_path: &Path, config: &HuntConfig, result: &mut HuntResult) {
112 if crate_forbids_unsafe(project_path) {
114 result.add_finding(
115 Finding::new(
116 "BH-FUZZ-SKIPPED",
117 project_path.join("src/lib.rs"),
118 1,
119 "Fuzz targets not needed - crate forbids unsafe code",
120 )
121 .with_description("Crate uses #![forbid(unsafe_code)], no unsafe blocks to fuzz")
122 .with_severity(FindingSeverity::Info)
123 .with_category(DefectCategory::ConfigurationErrors)
124 .with_suspiciousness(0.0)
125 .with_discovered_by(HuntMode::Fuzz),
126 );
127 return;
128 }
129
130 let mut unsafe_inventory = Vec::new();
131 let mut finding_id = 0;
132
133 for target in &config.targets {
134 let target_path = project_path.join(target);
135 for pattern in &[
136 format!("{}/*.rs", target_path.display()),
137 format!("{}/**/*.rs", target_path.display()),
138 ] {
139 if let Ok(entries) = glob::glob(pattern) {
140 for entry in entries.flatten() {
141 scan_file_for_unsafe_blocks(
142 &entry,
143 &mut finding_id,
144 &mut unsafe_inventory,
145 result,
146 );
147 }
148 }
149 }
150 }
151
152 let fuzz_dir = project_path.join("fuzz");
153 if !fuzz_dir.exists() {
154 result.add_finding(
155 Finding::new(
156 "BH-FUZZ-NOTARGETS",
157 project_path.join("Cargo.toml"),
158 1,
159 "No fuzz directory found",
160 )
161 .with_description(format!(
162 "Create fuzz targets for {} identified unsafe blocks",
164 unsafe_inventory.len()
165 ))
166 .with_severity(FindingSeverity::Medium)
167 .with_category(DefectCategory::ConfigurationErrors)
168 .with_suspiciousness(0.4)
169 .with_discovered_by(HuntMode::Fuzz),
170 );
171 }
172
173 result.stats.mode_stats.fuzz_coverage = if unsafe_inventory.is_empty() { 100.0 } else { 0.0 };
175}
176
177pub(super) fn scan_file_for_deep_conditionals(
179 entry: &Path,
180 finding_id: &mut usize,
181 result: &mut HuntResult,
182) {
183 let Ok(content) = std::fs::read_to_string(entry) else {
184 return;
185 };
186 let mut complexity: usize = 0;
187 let mut complex_start: usize = 0;
188
189 for (line_num, line) in content.lines().enumerate() {
190 let line_num = line_num + 1;
191
192 if line.contains("if ") || line.contains("match ") {
193 complexity += 1;
194 if complexity == 1 {
195 complex_start = line_num;
196 }
197 }
198
199 if complexity >= 3 && line.contains("if ") {
200 *finding_id += 1;
201 result.add_finding(
202 Finding::new(
203 format!("BH-DEEP-{:04}", *finding_id),
204 entry,
205 line_num,
206 "Deeply nested conditional",
207 )
208 .with_description(format!(
209 "Complexity {} starting at line {}; concolic execution recommended",
210 complexity, complex_start
211 ))
212 .with_severity(FindingSeverity::Medium)
213 .with_category(DefectCategory::LogicErrors)
214 .with_suspiciousness(0.6)
215 .with_discovered_by(HuntMode::DeepHunt)
216 .with_evidence(FindingEvidence::concolic(format!("depth={}", complexity))),
217 );
218 }
219
220 if line.contains(" && ") && line.contains(" || ") {
221 *finding_id += 1;
222 result.add_finding(
223 Finding::new(
224 format!("BH-DEEP-{:04}", *finding_id),
225 entry,
226 line_num,
227 "Complex boolean guard",
228 )
229 .with_description("Mixed AND/OR logic; path explosion potential")
230 .with_severity(FindingSeverity::Medium)
231 .with_category(DefectCategory::LogicErrors)
232 .with_suspiciousness(0.55)
233 .with_discovered_by(HuntMode::DeepHunt)
234 .with_evidence(FindingEvidence::concolic("complex_guard")),
235 );
236 }
237
238 if line.contains('}') && complexity > 0 {
239 complexity -= 1;
240 }
241 }
242}
243
244pub(super) fn run_deep_hunt_mode(
246 project_path: &Path,
247 config: &HuntConfig,
248 result: &mut HuntResult,
249) {
250 let mut finding_id = 0;
251
252 for target in &config.targets {
253 let target_path = project_path.join(target);
254 for pattern in &[
255 format!("{}/*.rs", target_path.display()),
256 format!("{}/**/*.rs", target_path.display()),
257 ] {
258 if let Ok(entries) = glob::glob(pattern) {
259 for entry in entries.flatten() {
260 scan_file_for_deep_conditionals(&entry, &mut finding_id, result);
261 }
262 }
263 }
264 }
265
266 super::modes_hunt::run_hunt_mode(project_path, config, result);
267}