Skip to main content

Threat Assessment

How Batin combines all detection signals to assess file risk.

Overview

After the four detection stages complete, Batin synthesizes results into a single ThreatLevel:


Threat Level Enum

#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize)]
pub enum ThreatLevel {
Safe, // 0 - No risk indicators
Suspicious, // 1 - Minor concerns
Dangerous, // 2 - High risk indicators
Critical, // 3 - Immediate threat
}

Ordering

Threat levels are ordered, enabling comparisons:

assert!(ThreatLevel::Safe < ThreatLevel::Suspicious);
assert!(ThreatLevel::Dangerous < ThreatLevel::Critical);

// Get maximum threat level
let levels = [ThreatLevel::Safe, ThreatLevel::Dangerous];
let max = levels.iter().max().unwrap(); // Dangerous

Assessment Algorithm

fn assess_threat(
signature: &FileSignature,
entropy_profile: &Option<EntropyProfile>,
detected_formats: &[String],
) -> ThreatLevel {
// Start with category-based default
let mut level = match signature.category {
FileCategory::Executable => ThreatLevel::Suspicious,
_ => ThreatLevel::Safe,
};

// Polyglot detection → Dangerous
if detected_formats.len() > 1 {
level = level.max(ThreatLevel::Dangerous);
}

// Entropy analysis
if let Some(entropy) = entropy_profile {
// Packed → Dangerous
if entropy.is_packed {
level = level.max(ThreatLevel::Dangerous);
}
// Encrypted → Dangerous
if entropy.is_encrypted {
level = level.max(ThreatLevel::Dangerous);
}
}

level
}

Assessment Rules

Rule 1: Category-Based Default

File CategoryDefault LevelRationale
ExecutableSuspiciousCan run code
DocumentSafeGenerally harmless
ImageSafeDisplay only
ArchiveSafeContainer, scan contents
MultimediaSafeNon-executable

Rule 2: Polyglot Escalation

if detected_formats.len() > 1 {
level = level.max(ThreatLevel::Dangerous);
}

Why? Polyglots are never legitimate in uploaded content.

Rule 3: Entropy Escalation

if entropy.is_packed || entropy.is_encrypted {
level = level.max(ThreatLevel::Dangerous);
}

Why? Packing/encryption indicate evasion attempts.

Rule 4: Embedded Threat Escalation

Handled separately after assess_threat:

// In FileType construction
let threat_level = if !embedded_threats.is_empty() {
embedded_threats.iter()
.map(|t| t.severity)
.max()
.unwrap_or(base_level)
.max(base_level)
} else {
base_level
};

Decision Tree


Threat Level Application

Filtering by Level

fn threat_level_value(level: &ThreatLevel) -> u8 {
match level {
ThreatLevel::Safe => 0,
ThreatLevel::Suspicious => 1,
ThreatLevel::Dangerous => 2,
ThreatLevel::Critical => 3,
}
}

// Filter files by minimum threat level
let filtered: Vec<_> = results
.iter()
.filter(|r| threat_level_value(&r.threat_level) >= min_level)
.collect();

CLI Integration

# Show only dangerous and critical
batin scan /uploads -r --min-threat dangerous

Customizing Assessment

Stricter Assessment

fn strict_assess_threat(result: &FileType) -> ThreatLevel {
let base = result.threat_level;

// All executables are Dangerous
if result.extension == "exe" || result.extension == "dll" {
return ThreatLevel::Dangerous;
}

// Any macro presence is Critical
if result.embedded_threats.iter().any(|t|
matches!(t.threat_type, ThreatType::Macro)
) {
return ThreatLevel::Critical;
}

// High entropy in non-archive = Dangerous
if let Some(profile) = &result.entropy_profile {
if profile.global_entropy > 7.0 && result.extension != "zip" {
return ThreatLevel::Dangerous;
}
}

base
}

Lenient Assessment (Whitelisting)

fn lenient_assess_threat(result: &FileType, allowed_types: &[&str]) -> ThreatLevel {
// Known types are always Safe
if allowed_types.contains(&result.extension.as_str()) {
// But still check for embedded threats
if result.embedded_threats.is_empty() {
return ThreatLevel::Safe;
}
}

result.threat_level
}

Risk Scoring (Extended)

For more granular assessment, convert to numeric score:

fn calculate_risk_score(result: &FileType) -> f64 {
let mut score = 0.0;

// Base score by category
match result.threat_level {
ThreatLevel::Safe => score += 0.0,
ThreatLevel::Suspicious => score += 25.0,
ThreatLevel::Dangerous => score += 50.0,
ThreatLevel::Critical => score += 100.0,
}

// Add points for concerning indicators
if let Some(profile) = &result.entropy_profile {
// High entropy adds risk
if profile.global_entropy > 7.0 {
score += (profile.global_entropy - 7.0) * 20.0;
}
if profile.is_packed { score += 15.0; }
if profile.is_encrypted { score += 20.0; }
}

// Polyglot adds significant risk
if result.detected_formats.len() > 1 {
score += 30.0;
}

// Embedded threats
for threat in &result.embedded_threats {
score += match threat.severity {
ThreatLevel::Suspicious => 10.0,
ThreatLevel::Dangerous => 25.0,
ThreatLevel::Critical => 50.0,
_ => 0.0,
};
}

// Cap at 100
score.min(100.0)
}

Score Interpretation

Score RangeMeaning
0-10Safe, no concerns
11-30Low risk, log only
31-50Medium risk, review
51-75High risk, quarantine
76-100Critical, block immediately

Integration Example

use batin::{FileType, DetectionConfig, ThreatLevel};

async fn security_gate(data: &[u8]) -> Result<(), String> {
let config = DetectionConfig::default();
let result = FileType::from_bytes(data, &config)
.map_err(|e| format!("Detection failed: {}", e))?;

match result.threat_level {
ThreatLevel::Safe => {
log::info!("File passed: {}", result.extension);
Ok(())
}
ThreatLevel::Suspicious => {
log::warn!("Suspicious file: {} - logging", result.extension);
// Log but allow
Ok(())
}
ThreatLevel::Dangerous => {
log::error!("Dangerous file blocked: {:?}", result);
Err("File blocked: dangerous content detected".into())
}
ThreatLevel::Critical => {
log::error!("CRITICAL THREAT: {:?}", result);
// Alert security team
notify_security_team(&result);
Err("File blocked: critical threat detected".into())
}
}
}

Design Philosophy

The threat assessment is conservative by design. It is better to over-warn than miss a real threat.

Legitimate files can be whitelisted through:

  • Custom assessment functions
  • Extension allowlists
  • Environment-specific rules

But the default should always err on the side of security.