use crate::core::{Finding, Severity};
use crate::plugins::traits::{PluginError, PluginReport, ScanContext, ScanPhase, SecurityPlugin};
use async_trait::async_trait;
use std::collections::HashMap;
use std::time::Instant;
pub struct EntropyScanner {
threshold: f64,
}
impl Default for EntropyScanner {
fn default() -> Self {
Self::new()
}
}
impl EntropyScanner {
pub fn new() -> Self {
Self { threshold: 4.5 }
}
fn calculate_entropy(&self, data: &[u8]) -> f64 {
if data.is_empty() {
return 0.0;
}
let mut frequency: HashMap<u8, usize> = HashMap::new();
for &byte in data {
*frequency.entry(byte).or_insert(0) += 1;
}
let len = data.len() as f64;
let mut entropy = 0.0;
for count in frequency.values() {
let probability = *count as f64 / len;
entropy -= probability * probability.log2();
}
entropy
}
}
#[async_trait]
impl SecurityPlugin for EntropyScanner {
fn name(&self) -> &str {
"entropy"
}
fn version(&self) -> &str {
"0.1.0"
}
fn description(&self) -> &str {
"Detect high-entropy data (encrypted/obfuscated content)"
}
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 !content.is_empty() && content.contains(&0) {
return Ok(report);
}
let entropy = self.calculate_entropy(content);
if entropy > self.threshold {
let finding = Finding::new(
"ENT-001".to_string(),
"High entropy content detected".to_string(),
if entropy > 7.0 {
Severity::High
} else {
Severity::Medium
},
)
.with_file(context.path.to_path_buf())
.with_description(format!(
"File has entropy of {:.2} (threshold: {:.2}). \
May contain encrypted, compressed, or obfuscated data.",
entropy, self.threshold
))
.with_evidence(format!("Entropy: {:.2}", entropy));
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_high_entropy_detection() {
let scanner = EntropyScanner::new();
let content = b"aB3$kL9#mN2@pQ5&rS8*tU1!vX4^yZ7aB3$kL9#mN2@pQ5&rS8*tU1!vX4^yZ7aB3$kL9#mN2@pQ5&rS8*tU1!vX4^yZ7";
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!(!report.findings.is_empty());
}
#[tokio::test]
async fn test_low_entropy_passes() {
let scanner = EntropyScanner::new();
let content = b"aaaaaaaaaa";
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);
}
#[tokio::test]
async fn test_empty_content() {
let scanner = EntropyScanner::new();
let content = b"";
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);
}
}