use crate::policy::PolicySummary; use serde::{Deserialize, Serialize}; use std::collections::{BTreeMap, HashMap}; use std::net::IpAddr; use wtfnet_platform::{FlowOwner, FlowOwnerConfidence, FlowProtocol}; #[derive(Debug, Clone, Copy, Serialize, Deserialize)] #[serde(rename_all = "lowercase")] pub enum LeakTransport { Udp53, Tcp53, Dot, Doh, Unknown, } #[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord, Hash)] #[serde(rename_all = "lowercase")] pub enum LeakType { A, B, C, D, } #[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)] #[serde(rename_all = "lowercase")] pub enum RouteClass { Loopback, Tunnel, Physical, Unknown, } #[derive(Debug, Clone, Copy, Serialize, Deserialize)] #[serde(rename_all = "lowercase")] pub enum Severity { P0, P1, P2, P3, } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct EnrichedEvent { pub timestamp_ms: u128, pub proto: FlowProtocol, pub src_ip: IpAddr, pub src_port: u16, pub dst_ip: IpAddr, pub dst_port: u16, pub iface_name: Option, pub transport: LeakTransport, pub qname: Option, pub qtype: Option, pub rcode: Option, pub is_response: bool, pub answer_ips: Vec, pub route_class: RouteClass, pub owner: Option, pub owner_confidence: FlowOwnerConfidence, pub owner_failure: Option, } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct LeakEvent { pub timestamp_ms: u128, pub transport: LeakTransport, pub qname: Option, pub qtype: Option, pub rcode: Option, pub iface_name: Option, pub route_class: RouteClass, pub dst_ip: String, pub dst_port: u16, pub pid: Option, pub ppid: Option, pub process_name: Option, pub process_path: Option, pub attribution_confidence: FlowOwnerConfidence, pub attribution_failure: Option, pub leak_type: LeakType, pub severity: Severity, pub policy_rule_id: String, } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct LeakTypeCount { pub leak_type: LeakType, pub count: usize, } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct SummaryItem { pub key: String, pub count: usize, } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct LeakSummary { pub total: usize, pub by_type: Vec, pub top_processes: Vec, pub top_destinations: Vec, } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct LeakReport { pub duration_ms: u64, pub policy: PolicySummary, pub summary: LeakSummary, pub events: Vec, } impl LeakEvent { pub fn from_decision(event: EnrichedEvent, decision: crate::rules::LeakDecision) -> Self { let (pid, ppid, process_name, process_path) = event .owner .as_ref() .map(|owner| { ( owner.pid, owner.ppid, owner.process_name.clone(), owner.process_path.clone(), ) }) .unwrap_or((None, None, None, None)); LeakEvent { timestamp_ms: event.timestamp_ms, transport: event.transport, qname: event.qname, qtype: event.qtype, rcode: event.rcode, iface_name: event.iface_name, route_class: event.route_class, dst_ip: event.dst_ip.to_string(), dst_port: event.dst_port, pid, ppid, process_name, process_path, attribution_confidence: event.owner_confidence, attribution_failure: event.owner_failure, leak_type: decision.leak_type, severity: decision.severity, policy_rule_id: decision.policy_rule_id, } } } impl LeakSummary { pub fn from_events(events: &[LeakEvent]) -> Self { let total = events.len(); let mut by_type_map: HashMap = HashMap::new(); let mut process_map: BTreeMap = BTreeMap::new(); let mut dest_map: BTreeMap = BTreeMap::new(); for event in events { *by_type_map.entry(event.leak_type).or_insert(0) += 1; if let Some(name) = event.process_name.as_ref() { *process_map.entry(name.clone()).or_insert(0) += 1; } let dst_key = format!("{}:{}", event.dst_ip, event.dst_port); *dest_map.entry(dst_key).or_insert(0) += 1; } let mut by_type = by_type_map .into_iter() .map(|(leak_type, count)| LeakTypeCount { leak_type, count }) .collect::>(); by_type.sort_by(|a, b| a.leak_type.cmp(&b.leak_type)); let top_processes = top_items(process_map, 5); let top_destinations = top_items(dest_map, 5); LeakSummary { total, by_type, top_processes, top_destinations, } } } fn top_items(map: BTreeMap, limit: usize) -> Vec { let mut items = map .into_iter() .map(|(key, count)| SummaryItem { key, count }) .collect::>(); items.sort_by(|a, b| b.count.cmp(&a.count).then_with(|| a.key.cmp(&b.key))); items.truncate(limit); items }