Skip to main content

threatflux_binary_analysis/
lib.rs

1//! Structural binary parsing and opt-in static-analysis primitives.
2//!
3//! [`BinaryFile`] detects and parses enabled ELF, PE, Mach-O, Java, and
4//! WebAssembly formats into a shared representation. [`BinaryAnalyzer`] adds
5//! feature-gated disassembly, control-flow, entropy, and symbol-demangling
6//! passes when explicitly enabled through [`AnalysisConfig`]. Unrecognized,
7//! non-empty input is represented as [`BinaryFormat::Raw`].
8//!
9//! Results are best-effort structural facts and heuristics. They are not a
10//! malware verdict, an execution sandbox, or proof that an input is safe.
11//!
12//! # Quick start
13//!
14//! ```rust
15//! use threatflux_binary_analysis::{BinaryAnalyzer, BinaryFormat, Result};
16//!
17//! fn inspect(bytes: &[u8]) -> Result<BinaryFormat> {
18//!     let result = BinaryAnalyzer::new().analyze(bytes)?;
19//!     Ok(result.format)
20//! }
21//!
22//! assert_eq!(inspect(b"unrecognized bytes")?, BinaryFormat::Raw);
23//! # Ok::<(), threatflux_binary_analysis::BinaryError>(())
24//! ```
25//!
26//! # Untrusted input
27//!
28//! The analyzer rejects input larger than [`AnalysisConfig::max_analysis_size`],
29//! which defaults to 100 MiB. Callers must still bound reads before allocating
30//! the input and should isolate hostile-file analysis with operating-system CPU,
31//! memory, privilege, and wall-time limits.
32
33pub 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// Re-export main types
44#[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
55/// Main entry point for binary analysis
56pub struct BinaryAnalyzer {
57    config: AnalysisConfig,
58}
59
60/// Configuration for binary analysis
61#[derive(Debug, Clone)]
62pub struct AnalysisConfig {
63    /// Enable disassembly analysis. Disabled by default.
64    pub enable_disassembly: bool,
65    /// Preferred disassembly engine
66    #[cfg(any(feature = "disasm-capstone", feature = "disasm-iced"))]
67    pub disassembly_engine: DisassemblyEngine,
68    /// Enable control flow analysis. Disabled by default.
69    pub enable_control_flow: bool,
70    /// Enable call graph analysis. Disabled by default.
71    pub enable_call_graph: bool,
72    /// Enable cognitive complexity calculation. Disabled by default.
73    pub enable_cognitive_complexity: bool,
74    /// Enable advanced loop analysis. Disabled by default.
75    pub enable_advanced_loops: bool,
76    /// Enable entropy analysis. Disabled by default.
77    pub enable_entropy: bool,
78    /// Enable symbol demangling. Disabled by default.
79    pub enable_symbols: bool,
80    /// Maximum accepted input size and high-level disassembly byte budget.
81    ///
82    /// [`BinaryAnalyzer::analyze`] and [`BinaryAnalyzer::analyze_binary`] return
83    /// [`BinaryError::InputTooLarge`] when the input exceeds this value.
84    pub max_analysis_size: usize,
85    /// Maximum number of instructions returned by high-level disassembly.
86    ///
87    /// This is independent of [`Self::max_analysis_size`] so an allowed input
88    /// cannot expand into an unbounded instruction/result allocation.
89    pub max_disassembly_instructions: usize,
90    /// Architecture hint (None for auto-detection)
91    pub architecture_hint: Option<Architecture>,
92    /// Call graph configuration
93    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, // 100MB
109            max_disassembly_instructions: 10_000,
110            architecture_hint: None,
111            call_graph_config: None,
112        }
113    }
114}
115
116impl AnalysisConfig {
117    /// Validate that every requested runtime analysis was compiled into the crate.
118    ///
119    /// Cargo features are compile-time capabilities; setting a runtime boolean
120    /// cannot enable a missing implementation. Analysis entry points call this
121    /// method automatically and return [`BinaryError::FeatureNotAvailable`] for
122    /// an unavailable request.
123    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    /// Create a new analyzer with default configuration
156    pub fn new() -> Self {
157        Self::with_config(AnalysisConfig::default())
158    }
159
160    /// Create a new analyzer with custom configuration
161    pub fn with_config(config: AnalysisConfig) -> Self {
162        Self { config }
163    }
164
165    /// Get a reference to the analysis configuration
166    pub fn config(&self) -> &AnalysisConfig {
167        &self.config
168    }
169
170    /// Analyze a binary file from raw data
171    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    /// Analyze a parsed binary file
179    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)] // mut needed when optional analysis features are enabled
184        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        // Perform optional analyses based on configuration
197        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        // Compute summary statistics
297        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            // Cognitive complexity stats
313            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            // Loop stats
322            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
390/// Parsed binary file representation
391pub struct BinaryFile {
392    data: Vec<u8>,
393    parsed: Box<dyn BinaryFormatTrait>,
394}
395
396impl BinaryFile {
397    /// Parse binary data and detect format
398    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    /// Get the binary format type
409    pub fn format(&self) -> BinaryFormat {
410        self.parsed.format_type()
411    }
412
413    /// Get the target architecture
414    pub fn architecture(&self) -> Architecture {
415        self.parsed.architecture()
416    }
417
418    /// Get the entry point address
419    pub fn entry_point(&self) -> Option<u64> {
420        self.parsed.entry_point()
421    }
422
423    /// Get binary sections
424    pub fn sections(&self) -> &[Section] {
425        self.parsed.sections()
426    }
427
428    /// Get symbol table
429    pub fn symbols(&self) -> &[Symbol] {
430        self.parsed.symbols()
431    }
432
433    /// Get imports
434    pub fn imports(&self) -> &[Import] {
435        self.parsed.imports()
436    }
437
438    /// Get exports
439    pub fn exports(&self) -> &[Export] {
440        self.parsed.exports()
441    }
442
443    /// Get binary metadata
444    pub fn metadata(&self) -> &BinaryMetadata {
445        self.parsed.metadata()
446    }
447
448    /// Get raw binary data
449    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}