threatflux_binary_analysis/
lib.rs1pub mod analysis;
34pub mod error;
35pub mod formats;
36pub mod types;
37
38#[cfg(any(feature = "disasm-capstone", feature = "disasm-iced"))]
39pub mod disasm;
40
41pub mod utils;
42
43#[cfg(any(feature = "disasm-capstone", feature = "disasm-iced"))]
45pub use disasm::DisassemblyEngine;
46pub use error::{BinaryError, Result};
47pub use types::{
48 AnalysisResult, Architecture, BasicBlock, BinaryFormat, BinaryFormatParser, BinaryFormatTrait,
49 BinaryMetadata, CallGraph, CallGraphConfig, CallGraphEdge, CallGraphNode, CallGraphStatistics,
50 ComplexityMetrics, ControlFlowGraph, EnhancedControlFlowAnalysis, EntropyAnalysis, Export,
51 Function, HalsteadMetrics, Import, Instruction, Loop, LoopType, NodeType, Section,
52 SecurityIndicators, Symbol,
53};
54
55pub struct BinaryAnalyzer {
57 config: AnalysisConfig,
58}
59
60#[derive(Debug, Clone)]
62pub struct AnalysisConfig {
63 pub enable_disassembly: bool,
65 #[cfg(any(feature = "disasm-capstone", feature = "disasm-iced"))]
67 pub disassembly_engine: DisassemblyEngine,
68 pub enable_control_flow: bool,
70 pub enable_call_graph: bool,
72 pub enable_cognitive_complexity: bool,
74 pub enable_advanced_loops: bool,
76 pub enable_entropy: bool,
78 pub enable_symbols: bool,
80 pub max_analysis_size: usize,
85 pub max_disassembly_instructions: usize,
90 pub architecture_hint: Option<Architecture>,
92 pub call_graph_config: Option<CallGraphConfig>,
94}
95
96impl Default for AnalysisConfig {
97 fn default() -> Self {
98 Self {
99 enable_disassembly: false,
100 #[cfg(any(feature = "disasm-capstone", feature = "disasm-iced"))]
101 disassembly_engine: DisassemblyEngine::Auto,
102 enable_control_flow: false,
103 enable_call_graph: false,
104 enable_cognitive_complexity: false,
105 enable_advanced_loops: false,
106 enable_entropy: false,
107 enable_symbols: false,
108 max_analysis_size: 100 * 1024 * 1024, max_disassembly_instructions: 10_000,
110 architecture_hint: None,
111 call_graph_config: None,
112 }
113 }
114}
115
116impl AnalysisConfig {
117 pub fn validate(&self) -> Result<()> {
124 #[cfg(not(any(feature = "disasm-capstone", feature = "disasm-iced")))]
125 if self.enable_disassembly {
126 return Err(BinaryError::feature_not_available(
127 "disassembly (enable disasm-capstone or disasm-iced)",
128 ));
129 }
130
131 #[cfg(not(feature = "control-flow"))]
132 if self.enable_control_flow
133 || self.enable_call_graph
134 || self.enable_cognitive_complexity
135 || self.enable_advanced_loops
136 {
137 return Err(BinaryError::feature_not_available("control-flow"));
138 }
139
140 #[cfg(not(feature = "entropy-analysis"))]
141 if self.enable_entropy {
142 return Err(BinaryError::feature_not_available("entropy-analysis"));
143 }
144
145 #[cfg(not(feature = "symbol-resolution"))]
146 if self.enable_symbols {
147 return Err(BinaryError::feature_not_available("symbol-resolution"));
148 }
149
150 Ok(())
151 }
152}
153
154impl BinaryAnalyzer {
155 pub fn new() -> Self {
157 Self::with_config(AnalysisConfig::default())
158 }
159
160 pub fn with_config(config: AnalysisConfig) -> Self {
162 Self { config }
163 }
164
165 pub fn config(&self) -> &AnalysisConfig {
167 &self.config
168 }
169
170 pub fn analyze(&self, data: &[u8]) -> Result<AnalysisResult> {
172 self.config.validate()?;
173 self.validate_input_size(data.len())?;
174 let binary_file = BinaryFile::parse(data)?;
175 self.analyze_binary(&binary_file)
176 }
177
178 pub fn analyze_binary(&self, binary: &BinaryFile) -> Result<AnalysisResult> {
180 self.config.validate()?;
181 self.validate_input_size(binary.data().len())?;
182
183 #[allow(unused_mut)] let mut result = AnalysisResult {
185 format: binary.format(),
186 architecture: binary.architecture(),
187 entry_point: binary.entry_point(),
188 sections: binary.sections().to_vec(),
189 symbols: binary.symbols().to_vec(),
190 imports: binary.imports().to_vec(),
191 exports: binary.exports().to_vec(),
192 metadata: binary.metadata().clone(),
193 ..Default::default()
194 };
195
196 if self.config.enable_disassembly {
198 #[cfg(any(feature = "disasm-capstone", feature = "disasm-iced"))]
199 {
200 result.disassembly = Some(self.perform_disassembly(binary)?);
201 }
202 }
203
204 #[cfg(feature = "control-flow")]
205 {
206 let enhanced_control_flow_requested =
207 self.config.enable_cognitive_complexity || self.config.enable_advanced_loops;
208 if self.config.enable_control_flow || enhanced_control_flow_requested {
209 let control_flow_graphs = self.perform_control_flow_analysis(binary)?;
210 match (
211 self.config.enable_control_flow,
212 enhanced_control_flow_requested,
213 ) {
214 (true, true) => {
215 result.control_flow = Some(control_flow_graphs.clone());
216 result.enhanced_control_flow =
217 Some(Self::summarize_enhanced_control_flow(control_flow_graphs));
218 }
219 (true, false) => result.control_flow = Some(control_flow_graphs),
220 (false, true) => {
221 result.enhanced_control_flow =
222 Some(Self::summarize_enhanced_control_flow(control_flow_graphs));
223 }
224 (false, false) => {}
225 }
226 }
227
228 if self.config.enable_call_graph {
229 result.call_graph = Some(self.perform_call_graph_analysis(binary)?);
230 }
231 }
232
233 if self.config.enable_entropy {
234 #[cfg(feature = "entropy-analysis")]
235 {
236 result.entropy = Some(self.perform_entropy_analysis(binary)?);
237 }
238 }
239
240 #[cfg(feature = "symbol-resolution")]
241 {
242 if self.config.enable_symbols {
243 analysis::symbols::demangle_symbols(&mut result.symbols);
244 }
245 }
246
247 Ok(result)
248 }
249
250 fn validate_input_size(&self, actual: usize) -> Result<()> {
251 if actual > self.config.max_analysis_size {
252 return Err(BinaryError::input_too_large(
253 actual,
254 self.config.max_analysis_size,
255 ));
256 }
257
258 Ok(())
259 }
260
261 #[cfg(any(feature = "disasm-capstone", feature = "disasm-iced"))]
262 fn perform_disassembly(&self, binary: &BinaryFile) -> Result<Vec<Instruction>> {
263 disasm::disassemble_binary(binary, &self.config)
264 }
265
266 #[cfg(feature = "control-flow")]
267 fn perform_control_flow_analysis(&self, binary: &BinaryFile) -> Result<Vec<ControlFlowGraph>> {
268 let analyzer = analysis::control_flow::ControlFlowAnalyzer::with_config(
269 binary.architecture(),
270 self.control_flow_analysis_config(),
271 );
272 analyzer.analyze_binary(binary)
273 }
274
275 #[cfg(feature = "control-flow")]
276 fn control_flow_analysis_config(&self) -> analysis::control_flow::AnalysisConfig {
277 analysis::control_flow::AnalysisConfig {
278 detect_loops: true,
279 calculate_metrics: true,
280 enable_cognitive_complexity: self.config.enable_cognitive_complexity,
281 enable_advanced_loops: self.config.enable_advanced_loops,
282 ..analysis::control_flow::AnalysisConfig::default()
283 }
284 }
285
286 #[cfg(feature = "control-flow")]
287 fn perform_call_graph_analysis(&self, binary: &BinaryFile) -> Result<CallGraph> {
288 let config = self.config.call_graph_config.clone().unwrap_or_default();
289 analysis::call_graph::analyze_binary_with_config(binary, config)
290 }
291
292 #[cfg(feature = "control-flow")]
293 fn summarize_enhanced_control_flow(
294 control_flow_graphs: Vec<ControlFlowGraph>,
295 ) -> EnhancedControlFlowAnalysis {
296 let mut total_cognitive_complexity = 0_u64;
298 let mut max_cognitive_complexity = 0_u32;
299 let mut most_complex_function = None;
300 let mut functions_analyzed = 0_usize;
301
302 let mut total_loops = 0_usize;
303 let mut natural_loops = 0_usize;
304 let mut irreducible_loops = 0_usize;
305 let mut nested_loops = 0_usize;
306 let mut max_nesting_depth = 0_u32;
307 let mut loops_by_type = std::collections::HashMap::new();
308
309 for cfg in &control_flow_graphs {
310 functions_analyzed = functions_analyzed.saturating_add(1);
311
312 let cognitive = cfg.complexity.cognitive_complexity;
314 total_cognitive_complexity =
315 total_cognitive_complexity.saturating_add(u64::from(cognitive));
316 if cognitive > max_cognitive_complexity {
317 max_cognitive_complexity = cognitive;
318 most_complex_function = Some(cfg.function.name.clone());
319 }
320
321 total_loops = total_loops.saturating_add(cfg.loops.len());
323 for loop_info in &cfg.loops {
324 match loop_info.loop_type {
325 LoopType::Natural => natural_loops = natural_loops.saturating_add(1),
326 LoopType::Irreducible => {
327 irreducible_loops = irreducible_loops.saturating_add(1);
328 }
329 _ => {}
330 }
331
332 if loop_info.nesting_level > 1 {
333 nested_loops = nested_loops.saturating_add(1);
334 }
335
336 if loop_info.nesting_level > max_nesting_depth {
337 max_nesting_depth = loop_info.nesting_level;
338 }
339
340 let count = loops_by_type
341 .entry(loop_info.loop_type.clone())
342 .or_insert(0_usize);
343 *count = count.saturating_add(1);
344 }
345 }
346
347 let average_cognitive_complexity = if functions_analyzed > 0 {
348 total_cognitive_complexity as f64 / functions_analyzed as f64
349 } else {
350 0.0
351 };
352
353 let cognitive_complexity_summary = types::CognitiveComplexityStats {
354 total_cognitive_complexity: u32::try_from(total_cognitive_complexity)
355 .unwrap_or(u32::MAX),
356 average_cognitive_complexity,
357 max_cognitive_complexity,
358 most_complex_function,
359 functions_analyzed,
360 };
361
362 let loop_analysis_summary = types::LoopAnalysisStats {
363 total_loops,
364 natural_loops,
365 irreducible_loops,
366 nested_loops,
367 max_nesting_depth,
368 loops_by_type,
369 };
370
371 EnhancedControlFlowAnalysis {
372 control_flow_graphs,
373 cognitive_complexity_summary,
374 loop_analysis_summary,
375 }
376 }
377
378 #[cfg(feature = "entropy-analysis")]
379 fn perform_entropy_analysis(&self, binary: &BinaryFile) -> Result<EntropyAnalysis> {
380 analysis::entropy::analyze_binary(binary)
381 }
382}
383
384impl Default for BinaryAnalyzer {
385 fn default() -> Self {
386 Self::new()
387 }
388}
389
390pub struct BinaryFile {
392 data: Vec<u8>,
393 parsed: Box<dyn BinaryFormatTrait>,
394}
395
396impl BinaryFile {
397 pub fn parse(data: &[u8]) -> Result<Self> {
399 let format = formats::detect_format(data)?;
400 let parsed = formats::parse_binary(data, format)?;
401
402 Ok(Self {
403 data: data.to_vec(),
404 parsed,
405 })
406 }
407
408 pub fn format(&self) -> BinaryFormat {
410 self.parsed.format_type()
411 }
412
413 pub fn architecture(&self) -> Architecture {
415 self.parsed.architecture()
416 }
417
418 pub fn entry_point(&self) -> Option<u64> {
420 self.parsed.entry_point()
421 }
422
423 pub fn sections(&self) -> &[Section] {
425 self.parsed.sections()
426 }
427
428 pub fn symbols(&self) -> &[Symbol] {
430 self.parsed.symbols()
431 }
432
433 pub fn imports(&self) -> &[Import] {
435 self.parsed.imports()
436 }
437
438 pub fn exports(&self) -> &[Export] {
440 self.parsed.exports()
441 }
442
443 pub fn metadata(&self) -> &BinaryMetadata {
445 self.parsed.metadata()
446 }
447
448 pub fn data(&self) -> &[u8] {
450 &self.data
451 }
452}
453
454#[cfg(test)]
455mod tests {
456 use super::*;
457
458 #[test]
459 fn test_analyzer_creation() {
460 let analyzer = BinaryAnalyzer::new();
461 assert!(!analyzer.config.enable_disassembly);
462 assert!(!analyzer.config.enable_control_flow);
463 assert!(!analyzer.config.enable_call_graph);
464 assert!(!analyzer.config.enable_cognitive_complexity);
465 assert!(!analyzer.config.enable_advanced_loops);
466 assert!(!analyzer.config.enable_entropy);
467 assert!(!analyzer.config.enable_symbols);
468 }
469
470 #[test]
471 fn test_custom_config() {
472 let config = AnalysisConfig {
473 enable_disassembly: false,
474 #[cfg(any(feature = "disasm-capstone", feature = "disasm-iced"))]
475 disassembly_engine: DisassemblyEngine::Auto,
476 enable_control_flow: true,
477 enable_call_graph: true,
478 enable_cognitive_complexity: false,
479 enable_advanced_loops: true,
480 enable_entropy: false,
481 enable_symbols: true,
482 max_analysis_size: 1024,
483 max_disassembly_instructions: 256,
484 architecture_hint: Some(Architecture::X86_64),
485 call_graph_config: Some(CallGraphConfig::default()),
486 };
487
488 let analyzer = BinaryAnalyzer::with_config(config);
489 assert!(!analyzer.config.enable_disassembly);
490 assert!(analyzer.config.enable_control_flow);
491 assert!(analyzer.config.enable_call_graph);
492 assert!(!analyzer.config.enable_cognitive_complexity);
493 assert!(analyzer.config.enable_advanced_loops);
494 assert!(!analyzer.config.enable_entropy);
495 assert!(analyzer.config.enable_symbols);
496 assert_eq!(analyzer.config.max_analysis_size, 1024);
497 assert_eq!(analyzer.config.max_disassembly_instructions, 256);
498 assert!(analyzer.config.call_graph_config.is_some());
499 }
500
501 #[test]
502 #[cfg(feature = "control-flow")]
503 fn high_level_control_flow_flags_map_to_low_level_semantics() {
504 let base_only = BinaryAnalyzer::with_config(AnalysisConfig {
505 enable_control_flow: true,
506 ..AnalysisConfig::default()
507 });
508 let base_config = base_only.control_flow_analysis_config();
509 assert!(base_config.detect_loops);
510 assert!(base_config.calculate_metrics);
511 assert!(!base_config.enable_cognitive_complexity);
512 assert!(!base_config.enable_advanced_loops);
513
514 let enhanced = BinaryAnalyzer::with_config(AnalysisConfig {
515 enable_cognitive_complexity: true,
516 enable_advanced_loops: true,
517 ..AnalysisConfig::default()
518 });
519 let enhanced_config = enhanced.control_flow_analysis_config();
520 assert!(enhanced_config.enable_cognitive_complexity);
521 assert!(enhanced_config.enable_advanced_loops);
522 }
523
524 #[test]
525 #[cfg(feature = "control-flow")]
526 fn enhanced_summary_uses_wide_accumulator_for_averages() {
527 let graph = |name: &str| ControlFlowGraph {
528 function: Function {
529 name: name.to_string(),
530 start_address: 0,
531 end_address: 0,
532 size: 0,
533 function_type: types::FunctionType::Normal,
534 calling_convention: None,
535 parameters: Vec::new(),
536 return_type: None,
537 },
538 basic_blocks: Vec::new(),
539 complexity: ComplexityMetrics {
540 cognitive_complexity: u32::MAX,
541 ..ComplexityMetrics::default()
542 },
543 loops: Vec::new(),
544 };
545
546 let summary =
547 BinaryAnalyzer::summarize_enhanced_control_flow(vec![graph("first"), graph("second")]);
548
549 assert_eq!(
550 summary
551 .cognitive_complexity_summary
552 .total_cognitive_complexity,
553 u32::MAX
554 );
555 assert_eq!(
556 summary
557 .cognitive_complexity_summary
558 .average_cognitive_complexity,
559 f64::from(u32::MAX)
560 );
561 }
562
563 #[test]
564 fn input_size_limit_is_enforced() {
565 let analyzer = BinaryAnalyzer::with_config(AnalysisConfig {
566 max_analysis_size: 3,
567 ..AnalysisConfig::default()
568 });
569
570 let error = analyzer.analyze(&[0_u8; 4]).unwrap_err();
571 assert!(matches!(
572 error,
573 BinaryError::InputTooLarge {
574 actual: 4,
575 limit: 3
576 }
577 ));
578 }
579
580 #[test]
581 fn input_at_size_limit_is_accepted() {
582 let analyzer = BinaryAnalyzer::with_config(AnalysisConfig {
583 max_analysis_size: 4,
584 ..AnalysisConfig::default()
585 });
586
587 let result = analyzer.analyze(&[0_u8; 4]).unwrap();
588 assert_eq!(result.format, BinaryFormat::Raw);
589 }
590
591 #[test]
592 fn default_configuration_is_valid() {
593 AnalysisConfig::default().validate().unwrap();
594 }
595
596 #[cfg(not(any(feature = "disasm-capstone", feature = "disasm-iced")))]
597 #[test]
598 fn unavailable_disassembly_is_reported() {
599 let config = AnalysisConfig {
600 enable_disassembly: true,
601 ..AnalysisConfig::default()
602 };
603
604 assert!(matches!(
605 config.validate(),
606 Err(BinaryError::FeatureNotAvailable(_))
607 ));
608 }
609}