use crate::core::{FileHash, ScanContext, ScanOutcome, ScanReport, ScanResult, ThreatInfo};
use crate::policy::PolicyDecision;
use crate::quarantine::QuarantineRecord;
use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
pub trait AuditEvent: Serialize {
fn event_type(&self) -> &'static str;
fn timestamp(&self) -> DateTime<Utc>;
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ScanAuditEvent {
pub event_type: String,
pub timestamp: DateTime<Utc>,
pub scan_id: String,
pub file_hash_blake3: String,
pub file_hash_sha256: Option<String>,
pub outcome: String,
pub engine: String,
pub duration_ms: u64,
pub tenant_id: Option<String>,
pub user_id: Option<String>,
pub request_id: Option<String>,
pub threats: Vec<ThreatSummary>,
pub cached: bool,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ThreatSummary {
pub name: String,
pub severity: String,
pub engine: String,
}
impl From<&ThreatInfo> for ThreatSummary {
fn from(t: &ThreatInfo) -> Self {
Self {
name: t.name.clone(),
severity: t.severity.to_string(),
engine: t.engine.clone(),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PolicyAuditEvent {
pub event_type: String,
pub timestamp: DateTime<Utc>,
pub file_hash_blake3: String,
pub action: String,
pub matched_rule_id: Option<String>,
pub matched_rule_name: Option<String>,
pub tenant_id: Option<String>,
pub reason: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct QuarantineAuditEvent {
pub event_type: String,
pub timestamp: DateTime<Utc>,
pub quarantine_id: String,
pub file_hash_blake3: String,
pub operation: String,
pub reason: Option<String>,
pub tenant_id: Option<String>,
}
pub fn emit_scan_started(
scan_id: &str,
file_hash: &FileHash,
context: &ScanContext,
) {
tracing::info!(
target: "scanbridge::audit",
event_type = "scan_started",
scan_id = %scan_id,
file_hash_blake3 = %file_hash.blake3,
file_hash_sha256 = ?file_hash.sha256,
tenant_id = ?context.tenant_id,
user_id = ?context.user_id,
request_id = ?context.request_id,
source = ?context.source,
"Scan started"
);
}
pub fn emit_scan_completed(result: &ScanResult) {
let outcome_str = match &result.outcome {
ScanOutcome::Clean => "clean",
ScanOutcome::Infected { .. } => "infected",
ScanOutcome::Suspicious { .. } => "suspicious",
ScanOutcome::Error { .. } => "error",
};
let threats: Vec<ThreatSummary> = result
.threats()
.map(|t| t.iter().map(ThreatSummary::from).collect())
.unwrap_or_default();
tracing::info!(
target: "scanbridge::audit",
event_type = "scan_completed",
scan_id = %result.id,
file_hash_blake3 = %result.file_metadata.hash.blake3,
file_hash_sha256 = ?result.file_metadata.hash.sha256,
outcome = %outcome_str,
engine = %result.engine,
duration_ms = result.duration.as_millis() as u64,
tenant_id = ?result.context.tenant_id,
user_id = ?result.context.user_id,
request_id = ?result.context.request_id,
threats = ?threats,
cached = result.cached,
"Scan completed"
);
}
pub fn emit_scan_report(report: &ScanReport) {
let outcome_str = match &report.aggregated_outcome {
ScanOutcome::Clean => "clean",
ScanOutcome::Infected { .. } => "infected",
ScanOutcome::Suspicious { .. } => "suspicious",
ScanOutcome::Error { .. } => "error",
};
let threats: Vec<ThreatSummary> = report
.all_threats()
.iter()
.map(|&t| ThreatSummary::from(t))
.collect();
let engines: Vec<&str> = report.results.iter().map(|r| r.engine.as_str()).collect();
tracing::info!(
target: "scanbridge::audit",
event_type = "scan_report",
report_id = %report.id,
file_hash_blake3 = %report.file_hash.blake3,
file_hash_sha256 = ?report.file_hash.sha256,
aggregated_outcome = %outcome_str,
engines = ?engines,
engine_count = report.engine_count(),
total_duration_ms = report.total_duration.as_millis() as u64,
tenant_id = ?report.context.tenant_id,
user_id = ?report.context.user_id,
threats = ?threats,
threat_count = threats.len(),
"Scan report generated"
);
}
pub fn emit_policy_decision(decision: &PolicyDecision, file_hash: &FileHash, context: &ScanContext) {
let action_str = match &decision.action {
crate::policy::PolicyAction::Allow => "allow",
crate::policy::PolicyAction::AllowWithWarning { .. } => "allow_with_warning",
crate::policy::PolicyAction::Quarantine { .. } => "quarantine",
crate::policy::PolicyAction::Block { .. } => "block",
crate::policy::PolicyAction::RequireManualReview => "require_manual_review",
};
tracing::info!(
target: "scanbridge::audit",
event_type = "policy_decision",
file_hash_blake3 = %file_hash.blake3,
action = %action_str,
matched_rule_id = ?decision.matched_rule_id,
matched_rule_name = ?decision.matched_rule_name,
reason = ?decision.reason,
tenant_id = ?context.tenant_id,
user_id = ?context.user_id,
"Policy decision made"
);
}
pub fn emit_quarantine_event(record: &QuarantineRecord, operation: &str) {
tracing::info!(
target: "scanbridge::audit",
event_type = "quarantine_operation",
quarantine_id = %record.id,
file_hash_blake3 = %record.file_hash.blake3,
operation = %operation,
reason = %record.reason,
tenant_id = ?record.tenant_id,
original_filename = ?record.original_filename,
file_size = record.file_size,
"Quarantine operation performed"
);
}
#[cfg(test)]
mod tests {
use super::*;
use crate::core::ThreatSeverity;
#[test]
fn test_threat_summary_from() {
let threat = ThreatInfo::new("Test.Malware", ThreatSeverity::High, "test-engine");
let summary = ThreatSummary::from(&threat);
assert_eq!(summary.name, "Test.Malware");
assert_eq!(summary.severity, "high");
assert_eq!(summary.engine, "test-engine");
}
}