use crate::core::{Finding, Severity};
use crate::plugins::traits::{PluginError, PluginReport, ScanContext, ScanPhase, SecurityPlugin};
use async_trait::async_trait;
use std::time::Instant;
pub struct BinaryScanner;
impl Default for BinaryScanner {
fn default() -> Self {
Self::new()
}
}
impl BinaryScanner {
pub fn new() -> Self {
Self
}
fn is_binary(&self, data: &[u8]) -> bool {
if data.is_empty() {
return false;
}
if data.contains(&0) {
return true;
}
if data.len() >= 4 {
let header = &data[0..4];
if header == b"\x7fELF" {
return true;
}
if header[0..2] == *b"MZ" {
return true;
}
if header == b"\xfe\xed\xfa\xce" || header == b"\xfe\xed\xfa\xcf" {
return true;
}
}
let non_printable = data
.iter()
.filter(|&&b| b < 32 && b != b'\n' && b != b'\r' && b != b'\t')
.count();
let ratio = non_printable as f64 / data.len() as f64;
ratio > 0.3
}
fn detect_executable_type(&self, data: &[u8]) -> Option<&'static str> {
if data.len() < 4 {
return None;
}
let header = &data[0..4];
if header == b"\x7fELF" {
return Some("ELF");
}
if header[0..2] == *b"MZ" {
return Some("PE/Windows");
}
if header == b"\xfe\xed\xfa\xce" || header == b"\xfe\xed\xfa\xcf" {
return Some("Mach-O/macOS");
}
None
}
}
#[async_trait]
impl SecurityPlugin for BinaryScanner {
fn name(&self) -> &str {
"binary"
}
fn version(&self) -> &str {
"0.1.0"
}
fn description(&self) -> &str {
"Detect unexpected binary/executable files"
}
fn scan_phase(&self) -> ScanPhase {
ScanPhase::PostExtract
}
async fn initialize(&mut self) -> Result<(), PluginError> {
Ok(())
}
async fn scan(&self, context: &ScanContext<'_>) -> Result<PluginReport, PluginError> {
let start = Instant::now();
let mut report = PluginReport::new(self.name().to_string());
if let Some(content) = context.file_content {
if self.is_binary(content) {
let exe_type = self.detect_executable_type(content);
let (severity, description) = if let Some(ref etype) = exe_type {
(
Severity::High,
format!("Executable binary found: {:?}", etype),
)
} else {
(
Severity::Medium,
"Binary file found in source repository".to_string(),
)
};
let finding = Finding::new("BIN-001".to_string(), description.clone(), severity)
.with_file(context.path.to_path_buf())
.with_description(format!(
"{}. Binary files in source repositories can be a security risk. \
Verify this file is legitimate and expected.",
description
));
report.findings.push(finding);
}
report.scanned_files = 1;
}
report.duration_ms = start.elapsed().as_millis() as u64;
Ok(report)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::plugins::traits::ScanContext;
#[tokio::test]
async fn test_elf_detection() {
let scanner = BinaryScanner::new();
let content = b"\x7fELF\x00\x00\x00\x00";
let context = ScanContext {
path: std::path::Path::new("test.bin"),
scan_phase: ScanPhase::PostExtract,
file_content: Some(content),
metadata: std::collections::HashMap::new(),
};
let report = scanner.scan(&context).await.unwrap();
assert!(!report.findings.is_empty());
assert!(report.findings[0].severity >= Severity::High);
}
#[tokio::test]
async fn test_pe_detection() {
let scanner = BinaryScanner::new();
let content = b"MZ\x00\x00\x00\x00\x00\x00";
let context = ScanContext {
path: std::path::Path::new("test.exe"),
scan_phase: ScanPhase::PostExtract,
file_content: Some(content),
metadata: std::collections::HashMap::new(),
};
let report = scanner.scan(&context).await.unwrap();
assert!(!report.findings.is_empty());
}
#[tokio::test]
async fn test_text_file_passes() {
let scanner = BinaryScanner::new();
let content = b"hello world\n";
let context = ScanContext {
path: std::path::Path::new("test.txt"),
scan_phase: ScanPhase::PostExtract,
file_content: Some(content),
metadata: std::collections::HashMap::new(),
};
let report = scanner.scan(&context).await.unwrap();
assert_eq!(report.findings.len(), 0);
}
}