use std::path::Path;
use serde::{Deserialize, Serialize};
use serde_json::json;
use crate::domain::entity::ThreatLevel;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct VirusScanResult {
pub threat_level: ThreatLevel,
pub threats: Vec<String>,
pub scan_details: serde_json::Value,
}
impl VirusScanResult {
pub fn safe() -> Self {
Self {
threat_level: ThreatLevel::Safe,
threats: vec![],
scan_details: json!({"scan_method": "basic", "status": "clean"}),
}
}
pub fn blocked(reason: String) -> Self {
Self {
threat_level: ThreatLevel::Critical,
threats: vec![reason.clone()],
scan_details: json!({"scan_method": "extension_check", "blocked_reason": reason}),
}
}
pub fn high_threat(reason: String) -> Self {
Self {
threat_level: ThreatLevel::High,
threats: vec![reason.clone()],
scan_details: json!({"scan_method": "magic_bytes", "threat_reason": reason}),
}
}
}
#[derive(Debug, Clone, Default)]
pub struct VirusScannerService {
blocked_extensions: Vec<&'static str>,
}
impl VirusScannerService {
pub fn new() -> Self {
Self {
blocked_extensions: vec![
"exe", "bat", "cmd", "com", "scr", "pif", "msi", "msp",
"vbs", "vbe", "js", "jse", "ws", "wsf", "wsc", "wsh",
"ps1", "ps1xml", "ps2", "ps2xml", "psc1", "psc2",
"dll", "sys", "drv", "ocx", "cpl",
"lnk", "url",
"hta", "reg",
],
}
}
pub fn scan(&self, content: &[u8], filename: &str) -> VirusScanResult {
if let Some(result) = self.check_extension(filename) {
return result;
}
if let Some(result) = self.check_magic_bytes(content) {
return result;
}
if let Some(result) = self.check_heuristics(content, filename) {
return result;
}
VirusScanResult::safe()
}
fn check_extension(&self, filename: &str) -> Option<VirusScanResult> {
let ext = Path::new(filename)
.extension()
.and_then(|e| e.to_str())
.map(|e| e.to_lowercase())
.unwrap_or_default();
if self.blocked_extensions.contains(&ext.as_str()) {
return Some(VirusScanResult::blocked(
format!("Blocked extension: .{}", ext),
));
}
None
}
fn check_magic_bytes(&self, content: &[u8]) -> Option<VirusScanResult> {
if content.len() < 4 {
return None;
}
if content.starts_with(b"MZ") {
return Some(VirusScanResult::high_threat(
"Windows PE executable detected".to_string(),
));
}
if content.starts_with(b"\x7fELF") {
return Some(VirusScanResult::high_threat(
"Linux ELF executable detected".to_string(),
));
}
let macho_magic = [0xfe, 0xed, 0xfa, 0xce]; let macho_magic_64 = [0xfe, 0xed, 0xfa, 0xcf]; let macho_magic_rev = [0xce, 0xfa, 0xed, 0xfe]; let macho_magic_64_rev = [0xcf, 0xfa, 0xed, 0xfe];
if content.starts_with(&macho_magic)
|| content.starts_with(&macho_magic_64)
|| content.starts_with(&macho_magic_rev)
|| content.starts_with(&macho_magic_64_rev)
{
return Some(VirusScanResult::high_threat(
"macOS Mach-O executable detected".to_string(),
));
}
if content.starts_with(b"\xca\xfe\xba\xbe") {
return Some(VirusScanResult {
threat_level: ThreatLevel::Medium,
threats: vec!["Java class file detected".to_string()],
scan_details: json!({"scan_method": "magic_bytes", "file_type": "java_class"}),
});
}
None
}
fn check_heuristics(&self, content: &[u8], filename: &str) -> Option<VirusScanResult> {
let lower_name = filename.to_lowercase();
let suspicious_double_ext = [
".pdf.exe", ".doc.exe", ".xls.exe", ".jpg.exe", ".png.exe",
".pdf.scr", ".doc.scr", ".xls.scr", ".jpg.scr", ".png.scr",
];
for ext in &suspicious_double_ext {
if lower_name.ends_with(ext) {
return Some(VirusScanResult::blocked(
format!("Suspicious double extension: {}", ext),
));
}
}
if lower_name.ends_with(".svg") || lower_name.ends_with(".html") || lower_name.ends_with(".htm") {
if let Ok(text) = std::str::from_utf8(content) {
let lower_text = text.to_lowercase();
if lower_text.contains("<script") || lower_text.contains("javascript:") {
return Some(VirusScanResult {
threat_level: ThreatLevel::Medium,
threats: vec!["Embedded script detected".to_string()],
scan_details: json!({"scan_method": "heuristics", "issue": "embedded_script"}),
});
}
}
}
None
}
pub fn should_block(&self, result: &VirusScanResult) -> bool {
matches!(result.threat_level, ThreatLevel::Critical | ThreatLevel::High)
}
pub fn should_quarantine(&self, result: &VirusScanResult) -> bool {
matches!(result.threat_level, ThreatLevel::Medium)
}
pub fn add_blocked_extension(&mut self, ext: &'static str) {
if !self.blocked_extensions.contains(&ext) {
self.blocked_extensions.push(ext);
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_blocks_exe_extension() {
let scanner = VirusScannerService::new();
let result = scanner.scan(b"some content", "malware.exe");
assert!(scanner.should_block(&result));
assert_eq!(result.threat_level, ThreatLevel::Critical);
}
#[test]
fn test_blocks_bat_extension() {
let scanner = VirusScannerService::new();
let result = scanner.scan(b"@echo off", "script.bat");
assert!(scanner.should_block(&result));
}
#[test]
fn test_detects_pe_magic_bytes() {
let scanner = VirusScannerService::new();
let pe_content = b"MZ\x90\x00\x03\x00\x00\x00";
let result = scanner.scan(pe_content, "program.bin");
assert!(scanner.should_block(&result));
assert_eq!(result.threat_level, ThreatLevel::High);
}
#[test]
fn test_detects_elf_magic_bytes() {
let scanner = VirusScannerService::new();
let elf_content = b"\x7fELF\x02\x01\x01\x00";
let result = scanner.scan(elf_content, "program");
assert!(scanner.should_block(&result));
}
#[test]
fn test_allows_safe_files() {
let scanner = VirusScannerService::new();
let result = scanner.scan(b"Hello, World!", "document.txt");
assert!(!scanner.should_block(&result));
assert!(!scanner.should_quarantine(&result));
assert_eq!(result.threat_level, ThreatLevel::Safe);
}
#[test]
fn test_detects_double_extension() {
let scanner = VirusScannerService::new();
let result = scanner.scan(b"fake pdf content", "document.pdf.exe");
assert!(scanner.should_block(&result));
}
#[test]
fn test_flags_svg_with_script() {
let scanner = VirusScannerService::new();
let svg = b"<svg><script>alert('xss')</script></svg>";
let result = scanner.scan(svg, "image.svg");
assert!(scanner.should_quarantine(&result));
assert_eq!(result.threat_level, ThreatLevel::Medium);
}
}