#![allow(clippy::uninlined_format_args)]
use pretty_assertions::assert_eq;
use threatflux_binary_analysis::{types::*, AnalysisConfig, BinaryAnalyzer};
mod common;
use common::fixtures::*;
#[test]
fn test_binary_metadata_serialization() {
#[cfg_attr(not(feature = "serde-support"), allow(unused_variables))]
let metadata = create_sample_metadata(BinaryFormat::Elf, Architecture::X86_64);
#[cfg(feature = "serde-support")]
{
let json = serde_json::to_string(&metadata).unwrap();
assert!(!json.is_empty(), "JSON serialization should produce output");
assert!(json.contains("\"format\":\"Elf\""));
assert!(json.contains("\"architecture\":\"X86_64\""));
assert!(json.contains("\"security_features\""));
let deserialized: BinaryMetadata = serde_json::from_str(&json).unwrap();
assert_eq!(deserialized.format, metadata.format);
assert_eq!(deserialized.architecture, metadata.architecture);
assert_eq!(
deserialized.security_features.nx_bit,
metadata.security_features.nx_bit
);
}
}
#[test]
fn test_enhanced_section_structure() {
let sections = create_sample_sections();
for section in §ions {
assert!(!section.name.is_empty(), "Section should have name");
assert!(section.size > 0, "Section should have size");
assert!(section.address > 0, "Section should have address");
let perms = §ion.permissions;
match section.section_type {
SectionType::Code => {
assert!(perms.read, "Code sections should be readable");
assert!(perms.execute, "Code sections should be executable");
assert!(!perms.write, "Code sections should not be writable");
}
SectionType::Data => {
assert!(perms.read, "Data sections should be readable");
assert!(perms.write, "Data sections should be writable");
assert!(!perms.execute, "Data sections should not be executable");
}
SectionType::Bss => {
assert!(perms.read, "BSS sections should be readable");
assert!(perms.write, "BSS sections should be writable");
assert!(!perms.execute, "BSS sections should not be executable");
}
_ => {
}
}
match §ion.data {
Some(data) => {
assert!(
!data.is_empty(),
"Section data should not be empty if present"
);
assert!(
data.len() <= section.size as usize,
"Data should not exceed section size"
);
}
None => {
}
}
}
}
#[test]
fn test_enhanced_symbol_structure() {
let symbols = create_sample_symbols();
for symbol in &symbols {
assert!(!symbol.name.is_empty(), "Symbol should have name");
assert!(symbol.address > 0, "Symbol should have address");
match symbol.symbol_type {
SymbolType::Function => {
assert!(symbol.size > 0, "Function symbols should have size");
assert!(
symbol.address % 4 == 0,
"Function addresses should be aligned"
);
}
SymbolType::Object => {
assert!(symbol.address > 0, "Object symbols should have address");
}
SymbolType::Section => {
assert!(symbol.address > 0, "Section symbols should have address");
}
_ => {}
}
match symbol.binding {
SymbolBinding::Global => {
}
SymbolBinding::Local => {
}
SymbolBinding::Weak => {
}
_ => {}
}
match symbol.visibility {
SymbolVisibility::Default => {
}
SymbolVisibility::Hidden => {
}
SymbolVisibility::Protected => {
}
SymbolVisibility::Internal => {
}
}
if let Some(ref demangled) = symbol.demangled_name {
assert!(!demangled.is_empty(), "Demangled name should not be empty");
if symbol.name.starts_with("_Z")
|| symbol.name.contains("@@")
|| symbol.name.contains("?")
{
assert_ne!(
*demangled, symbol.name,
"Demangled name should differ from mangled"
);
}
}
if let Some(section_index) = symbol.section_index {
assert!(section_index > 0, "Section index should be valid");
}
}
}
#[test]
fn test_enhanced_import_export_structures() {
let imports = create_sample_imports();
let exports = create_sample_exports();
for import in &imports {
assert!(!import.name.is_empty(), "Import should have name");
if let Some(ref library) = import.library {
assert!(!library.is_empty(), "Library name should not be empty");
if library.ends_with(".dll")
|| library.ends_with(".so")
|| library.ends_with(".dylib")
|| library.contains(".framework")
{
}
}
assert!(
import.address.is_some() || import.ordinal.is_some(),
"Import should have address or ordinal"
);
if let Some(address) = import.address {
assert!(address > 0, "Import address should be valid");
}
if let Some(ordinal) = import.ordinal {
assert!(ordinal > 0, "Import ordinal should be valid");
}
}
for export in &exports {
assert!(!export.name.is_empty(), "Export should have name");
assert!(export.address > 0, "Export should have address");
if let Some(ordinal) = export.ordinal {
assert!(ordinal > 0, "Export ordinal should be valid");
}
if let Some(ref forwarded) = export.forwarded_name {
assert!(!forwarded.is_empty(), "Forwarded name should not be empty");
assert!(
forwarded.contains('.'),
"Forwarded name should contain module.function"
);
}
}
}
#[test]
fn test_enhanced_security_features() {
let test_cases = vec![
(
"Modern Linux binary",
create_modern_linux_binary(),
true,
true,
true,
false,
true,
true,
true,
false,
),
(
"Windows ASLR binary",
create_windows_aslr_binary(),
true,
true,
false,
false,
false,
false,
false,
false,
),
(
"macOS signed binary",
create_macos_signed_binary(),
true,
true,
false,
false,
false,
true,
false,
true,
),
(
"Legacy binary",
create_legacy_binary(),
false,
false,
false,
false,
false,
false,
false,
false,
),
];
for (
description,
data,
_expected_nx,
_expected_aslr,
_expected_canary,
_expected_cfi,
_expected_fortify,
_expected_pie,
_expected_relro,
_expected_signed,
) in test_cases
{
let format = threatflux_binary_analysis::formats::detect_format(&data).unwrap();
let result = match format {
BinaryFormat::Elf => BinaryAnalyzer::new().analyze(&data),
BinaryFormat::Pe => BinaryAnalyzer::new().analyze(&data),
BinaryFormat::MachO => BinaryAnalyzer::new().analyze(&data),
_ => continue,
};
if let Ok(parsed) = result {
let security = &parsed.metadata.security_features;
let _nx = security.nx_bit;
let _aslr = security.aslr;
let _canary = security.stack_canary;
let _cfi = security.cfi;
let _fortify = security.fortify;
let _pie = security.pie;
let _relro = security.relro;
let _signed = security.signed;
assert!(
format!("{:?}", security).contains("SecurityFeatures"),
"SecurityFeatures should be debuggable for: {}",
description
);
}
}
}
#[test]
fn test_enhanced_instruction_structure() {
let instructions = create_sample_instructions();
for instruction in &instructions {
assert!(instruction.address > 0, "Instruction should have address");
assert!(
!instruction.bytes.is_empty(),
"Instruction should have bytes"
);
assert!(
!instruction.mnemonic.is_empty(),
"Instruction should have mnemonic"
);
assert!(instruction.size > 0, "Instruction should have size");
assert_eq!(
instruction.size,
instruction.bytes.len(),
"Size should match bytes length"
);
match instruction.category {
InstructionCategory::Arithmetic => {
}
InstructionCategory::Memory => {
}
InstructionCategory::Control => {
match instruction.flow {
ControlFlow::Sequential => {
}
ControlFlow::Jump(target) => {
assert!(target > 0, "Jump target should be valid");
}
ControlFlow::Call(target) => {
assert!(target > 0, "Call target should be valid");
}
ControlFlow::Return => {
}
ControlFlow::ConditionalJump(target) => {
assert!(target > 0, "Conditional target should be valid");
}
ControlFlow::Interrupt => {
}
ControlFlow::Unknown => {
}
}
}
InstructionCategory::Crypto => {
}
InstructionCategory::Logic => {
}
InstructionCategory::System => {
}
InstructionCategory::Vector => {
}
InstructionCategory::Float => {
}
InstructionCategory::Unknown => {
}
}
}
}
#[cfg(feature = "elf")]
#[test]
fn test_analysis_result_structure() {
let analyzer = BinaryAnalyzer::new();
let test_data = create_realistic_elf_64();
let result = analyzer.analyze(&test_data).unwrap();
assert_eq!(result.format, BinaryFormat::Elf);
assert_eq!(result.architecture, Architecture::X86_64);
assert!(result.entry_point.is_some());
assert!(!result.sections.is_empty());
assert_eq!(result.metadata.format, BinaryFormat::Elf);
assert_eq!(result.metadata.architecture, Architecture::X86_64);
assert!(result.metadata.size > 0);
#[cfg(any(feature = "disasm-capstone", feature = "disasm-iced"))]
{
if let Some(ref disassembly) = result.disassembly {
assert!(disassembly.is_empty() || !disassembly.is_empty());
}
}
#[cfg(feature = "control-flow")]
{
if let Some(ref control_flow) = result.control_flow {
assert!(control_flow.is_empty() || !control_flow.is_empty());
}
}
#[cfg(feature = "entropy-analysis")]
{
if let Some(ref entropy) = result.entropy {
assert!(
entropy.overall_entropy >= 0.0,
"Entropy should be non-negative"
);
assert!(
entropy.overall_entropy <= 8.0,
"Entropy should not exceed theoretical maximum"
);
}
}
}
#[cfg(feature = "pe")]
#[test]
fn test_backward_compatibility() {
let data = create_realistic_pe_64();
let result = BinaryAnalyzer::new().analyze(&data).unwrap();
assert_eq!(result.format, BinaryFormat::Pe);
assert_eq!(result.architecture, Architecture::X86_64);
assert!(result.entry_point.is_some());
let sections = &result.sections;
assert!(!sections.is_empty());
let _symbols = &result.symbols;
let _imports = &result.imports;
let _exports = &result.exports;
let metadata = &result.metadata;
assert_eq!(metadata.format, BinaryFormat::Pe);
}
#[test]
fn test_enhanced_error_handling() {
let error_cases = vec![
("Empty data", vec![]),
("Invalid magic", vec![0x00, 0x00, 0x00, 0x00]),
("Truncated header", vec![0x7f, 0x45, 0x4c]),
("Corrupted structure", create_corrupted_binary()),
];
for (description, data) in error_cases {
let result = BinaryAnalyzer::new().analyze(&data);
if let Err(error) = result {
let error_msg = format!("{}", error);
assert!(
!error_msg.is_empty(),
"Error message should not be empty for: {}",
description
);
match error {
threatflux_binary_analysis::BinaryError::InvalidData(_) => {
}
threatflux_binary_analysis::BinaryError::ParseError(_) => {
}
threatflux_binary_analysis::BinaryError::UnsupportedFormat(_) => {
}
_ => {
}
}
}
}
}
#[test]
fn test_analysis_config_serialization() {
let config = AnalysisConfig {
enable_disassembly: true,
#[cfg(any(feature = "disasm-capstone", feature = "disasm-iced"))]
disassembly_engine: threatflux_binary_analysis::DisassemblyEngine::Auto,
enable_control_flow: true,
enable_entropy: true,
enable_symbols: true,
max_analysis_size: 50 * 1024 * 1024,
architecture_hint: Some(Architecture::X86_64),
..Default::default()
};
let config_clone = config.clone();
assert_eq!(config.enable_disassembly, config_clone.enable_disassembly);
assert_eq!(config.max_analysis_size, config_clone.max_analysis_size);
let debug_str = format!("{:?}", config);
assert!(!debug_str.is_empty());
assert!(debug_str.contains("enable_disassembly"));
}
#[test]
fn test_enhanced_metadata_fields() {
let test_cases = vec![
("ELF with timestamp", create_elf_with_build_timestamp()),
("PE with timestamp", create_pe_with_link_timestamp()),
(
"Mach-O with build info",
create_macho_with_build_timestamp(),
),
];
for (description, data) in test_cases {
let format = threatflux_binary_analysis::formats::detect_format(&data).unwrap();
let result = match format {
BinaryFormat::Elf => BinaryAnalyzer::new().analyze(&data),
BinaryFormat::Pe => BinaryAnalyzer::new().analyze(&data),
BinaryFormat::MachO => BinaryAnalyzer::new().analyze(&data),
_ => continue,
};
if let Ok(parsed) = result {
let metadata = &parsed.metadata;
if let Some(timestamp) = metadata.timestamp {
assert!(
timestamp > 0,
"Timestamp should be positive for: {}",
description
);
assert!(
timestamp < 2147483647,
"Timestamp should be reasonable for: {}",
description
);
}
if let Some(ref compiler_info) = metadata.compiler_info {
assert!(
!compiler_info.is_empty(),
"Compiler info should not be empty for: {}",
description
);
}
if let Some(base_address) = metadata.base_address {
assert!(
base_address > 0,
"Base address should be positive for: {}",
description
);
}
}
}
}
#[cfg(feature = "elf")]
#[test]
fn test_thread_safety() {
use std::sync::Arc;
use std::thread;
let data = Arc::new(create_realistic_elf_64());
let mut handles = vec![];
for i in 0..8 {
let data_clone = Arc::clone(&data);
let handle = thread::spawn(move || {
let result = BinaryAnalyzer::new().analyze(&data_clone).unwrap();
let metadata = &result.metadata;
assert_eq!(metadata.format, BinaryFormat::Elf);
assert_eq!(metadata.architecture, Architecture::X86_64);
let sections = &result.sections;
assert!(!sections.is_empty());
i });
handles.push(handle);
}
for (expected_id, handle) in handles.into_iter().enumerate() {
let thread_id = handle.join().unwrap();
assert_eq!(thread_id, expected_id);
}
}
#[test]
fn test_memory_efficiency() {
use std::mem;
assert!(
mem::size_of::<BinaryMetadata>() < 1024,
"BinaryMetadata should be reasonably sized"
);
assert!(
mem::size_of::<Section>() < 512,
"Section should be reasonably sized"
);
assert!(
mem::size_of::<Symbol>() < 256,
"Symbol should be reasonably sized"
);
assert!(
mem::size_of::<Import>() < 128,
"Import should be reasonably sized"
);
assert!(
mem::size_of::<Export>() < 128,
"Export should be reasonably sized"
);
assert!(
mem::size_of::<Instruction>() < 256,
"Instruction should be reasonably sized"
);
assert!(
mem::size_of::<BinaryFormat>() <= 8,
"BinaryFormat should be small"
);
assert!(
mem::size_of::<Architecture>() <= 8,
"Architecture should be small"
);
assert!(
mem::size_of::<SectionType>() <= 32,
"SectionType should be reasonably sized"
);
assert!(
mem::size_of::<SymbolType>() <= 32,
"SymbolType should be reasonably sized"
);
}
fn create_modern_linux_binary() -> Vec<u8> {
let mut data = create_realistic_elf_64();
data.resize(16384, 0);
data
}
fn create_windows_aslr_binary() -> Vec<u8> {
let mut data = create_realistic_pe_64();
data.resize(12288, 0);
data
}
fn create_macos_signed_binary() -> Vec<u8> {
let mut data = create_realistic_macho_64();
data.resize(20480, 0);
data
}
fn create_legacy_binary() -> Vec<u8> {
let mut data = create_realistic_elf_64();
data.resize(8192, 0);
data
}
fn create_corrupted_binary() -> Vec<u8> {
vec![
0x7f, 0x45, 0x4c, 0x46, 0xff, 0xff, 0xff, 0xff, 0x00, 0x00, 0x00, 0x00,
]
}
fn create_elf_with_build_timestamp() -> Vec<u8> {
let mut data = create_realistic_elf_64();
data.resize(12288, 0);
data
}
fn create_pe_with_link_timestamp() -> Vec<u8> {
let mut data = create_realistic_pe_64();
let timestamp: u32 = 1640995200; let timestamp_bytes = timestamp.to_le_bytes();
data[0x88..0x8c].copy_from_slice(×tamp_bytes);
data
}
fn create_macho_with_build_timestamp() -> Vec<u8> {
let mut data = create_realistic_macho_64();
data.resize(10240, 0);
data
}