use async_trait::async_trait;
use scanbridge::prelude::*;
#[derive(Debug)]
struct HashBlocklistScanner {
name: String,
blocklist: std::collections::HashSet<String>,
hasher: FileHasher,
}
impl HashBlocklistScanner {
pub fn new(name: impl Into<String>) -> Self {
Self {
name: name.into(),
blocklist: std::collections::HashSet::new(),
hasher: FileHasher::new(),
}
}
pub fn with_blocked_hash(mut self, hash: impl Into<String>) -> Self {
self.blocklist.insert(hash.into());
self
}
#[allow(dead_code)]
pub fn with_blocked_hashes(mut self, hashes: impl IntoIterator<Item = impl Into<String>>) -> Self {
self.blocklist.extend(hashes.into_iter().map(|h| h.into()));
self
}
}
#[async_trait]
impl Scanner for HashBlocklistScanner {
fn name(&self) -> &str {
&self.name
}
async fn scan(&self, input: &FileInput) -> Result<ScanResult, ScanError> {
let start = std::time::Instant::now();
let hash = self.hasher.hash_input(input)?;
tracing::debug!(
scanner = self.name(),
hash = %hash.blake3,
"Checking hash against blocklist"
);
let outcome = if self.blocklist.contains(&hash.blake3) {
tracing::warn!(
scanner = self.name(),
hash = %hash.blake3,
"Hash found in blocklist!"
);
ScanOutcome::Infected {
threats: vec![
ThreatInfo::new("Blocklist.Match", ThreatSeverity::High, &self.name)
.with_signature_id(&hash.blake3)
.with_category("blocklist")
.with_description("File hash matches known malicious hash"),
],
}
} else {
ScanOutcome::Clean
};
let duration = start.elapsed();
let size = input.size_hint().unwrap_or(0);
let metadata = FileMetadata::new(size, hash);
Ok(ScanResult::new(
outcome,
metadata,
self.name(),
duration,
ScanContext::new(),
))
}
async fn health_check(&self) -> Result<(), ScanError> {
Ok(())
}
fn max_file_size(&self) -> Option<u64> {
Some(1024 * 1024 * 1024) }
fn supports_streaming(&self) -> bool {
true }
}
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
tracing_subscriber::fmt()
.with_max_level(tracing::Level::DEBUG)
.init();
println!("=== Custom Backend Example ===\n");
let malicious_content = b"This content is known to be malicious!";
let hasher = FileHasher::new();
let malicious_hash = hasher.hash_bytes(malicious_content);
println!("Malicious file hash: {}", malicious_hash.blake3);
let scanner = HashBlocklistScanner::new("hash-blocklist")
.with_blocked_hash(&malicious_hash.blake3);
println!("Created HashBlocklistScanner with 1 blocked hash\n");
let manager = ScanManager::builder()
.add_scanner(scanner)
.build()?;
println!("=== Test 1: Scanning a clean file ===");
let clean_input = FileInput::from_bytes(b"This is a perfectly safe file.".to_vec())
.with_filename("safe.txt");
let context = ScanContext::new().with_tenant_id("demo");
let report = manager.scan(clean_input, context.clone()).await?;
println!("Result: {}", if report.is_clean() { "✅ CLEAN" } else { "❌ INFECTED" });
println!("Duration: {:?}", report.total_duration);
println!("\n=== Test 2: Scanning the malicious file ===");
let malicious_input = FileInput::from_bytes(malicious_content.to_vec())
.with_filename("malware.bin");
let report = manager.scan(malicious_input, context.clone()).await?;
if report.is_infected() {
println!("Result: ❌ INFECTED");
for threat in report.all_threats() {
println!(" Threat: {}", threat.name);
println!(" Category: {:?}", threat.category);
println!(" Description: {:?}", threat.description);
println!(" Signature: {:?}", threat.signature_id);
}
} else {
println!("Result: ✅ CLEAN (unexpected!)");
}
println!("Duration: {:?}", report.total_duration);
println!("\n=== Test 3: Integrating with policy engine ===");
use scanbridge::policy::PolicyEngine;
let policy = PolicyEngine::default_policy();
let scanner2 = HashBlocklistScanner::new("blocklist-v2")
.with_blocked_hash(&malicious_hash.blake3);
let manager2 = ScanManager::builder()
.add_scanner(scanner2)
.with_policy_engine(policy)
.build()?;
let malicious_input = FileInput::from_bytes(malicious_content.to_vec())
.with_filename("unknown.exe");
let decision = manager2.scan_with_policy(malicious_input, context).await?;
println!("Policy decision: {:?}", decision.action);
println!("Matched rule: {:?}", decision.matched_rule_name);
println!("\n=== Example Complete ===");
Ok(())
}