1use std::fmt;
7use std::net::IpAddr;
8use std::time::SystemTime;
9
10use serde::{Deserialize, Serialize};
11
12#[derive(Debug, Clone, Serialize, Deserialize)]
16pub struct PacketInfo {
17 pub src_ip: IpAddr,
19 pub dst_ip: IpAddr,
21 pub src_port: u16,
23 pub dst_port: u16,
25 pub protocol: u8,
27 pub size: usize,
29 pub timestamp: SystemTime,
31}
32
33impl fmt::Display for PacketInfo {
34 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
35 write!(
36 f,
37 "{}:{} -> {}:{} proto={} size={}",
38 self.src_ip, self.src_port, self.dst_ip, self.dst_port, self.protocol, self.size,
39 )
40 }
41}
42
43#[derive(Debug, Clone, Serialize, Deserialize)]
48pub struct LogEntry {
49 pub source: String,
51 pub timestamp: SystemTime,
53 pub hostname: String,
55 pub process: String,
57 pub message: String,
59 pub severity: Severity,
61 pub fields: Vec<(String, String)>,
63}
64
65impl fmt::Display for LogEntry {
66 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
67 write!(
68 f,
69 "[{}] {} {}: {}",
70 self.severity, self.hostname, self.process, self.message,
71 )
72 }
73}
74
75#[derive(Debug, Clone, Serialize, Deserialize)]
79pub struct Alert {
80 pub id: String,
82 pub title: String,
84 pub description: String,
86 pub severity: Severity,
88 pub rule_name: String,
90 pub source_ip: Option<IpAddr>,
92 pub target_ip: Option<IpAddr>,
94 pub created_at: SystemTime,
96}
97
98impl fmt::Display for Alert {
99 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
100 write!(
101 f,
102 "[{}] {} (rule: {})",
103 self.severity, self.title, self.rule_name,
104 )
105 }
106}
107
108#[derive(
113 Debug, Clone, Copy, Default, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize,
114)]
115pub enum Severity {
116 #[default]
118 Info,
119 Low,
121 Medium,
123 High,
125 Critical,
127}
128
129impl Severity {
130 pub fn from_str_loose(s: &str) -> Option<Self> {
134 match s.to_lowercase().as_str() {
135 "info" | "informational" => Some(Self::Info),
136 "low" => Some(Self::Low),
137 "medium" | "med" => Some(Self::Medium),
138 "high" => Some(Self::High),
139 "critical" | "crit" => Some(Self::Critical),
140 _ => None,
141 }
142 }
143}
144
145impl fmt::Display for Severity {
146 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
147 match self {
148 Self::Info => write!(f, "Info"),
149 Self::Low => write!(f, "Low"),
150 Self::Medium => write!(f, "Medium"),
151 Self::High => write!(f, "High"),
152 Self::Critical => write!(f, "Critical"),
153 }
154 }
155}
156
157#[derive(Debug, Clone, Serialize, Deserialize)]
161pub struct ContainerInfo {
162 pub id: String,
164 pub name: String,
166 pub image: String,
168 pub status: String,
170 pub created_at: SystemTime,
172}
173
174impl fmt::Display for ContainerInfo {
175 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
176 write!(
177 f,
178 "{} ({}) image={} status={}",
179 self.name,
180 &self.id[..12.min(self.id.len())],
181 self.image,
182 self.status,
183 )
184 }
185}
186
187#[derive(Debug, Clone, Serialize, Deserialize)]
191pub struct Vulnerability {
192 pub cve_id: String,
194 pub package: String,
196 pub affected_version: String,
198 pub fixed_version: Option<String>,
200 pub severity: Severity,
202 pub description: String,
204}
205
206impl fmt::Display for Vulnerability {
207 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
208 write!(
209 f,
210 "{} [{}] {} {} (fixed: {})",
211 self.cve_id,
212 self.severity,
213 self.package,
214 self.affected_version,
215 self.fixed_version.as_deref().unwrap_or("N/A"),
216 )
217 }
218}
219
220#[cfg(test)]
221mod tests {
222 use super::*;
223
224 #[test]
225 fn severity_ordering() {
226 assert!(Severity::Info < Severity::Low);
227 assert!(Severity::Low < Severity::Medium);
228 assert!(Severity::Medium < Severity::High);
229 assert!(Severity::High < Severity::Critical);
230 }
231
232 #[test]
233 fn severity_default_is_info() {
234 assert_eq!(Severity::default(), Severity::Info);
235 }
236
237 #[test]
238 fn severity_display() {
239 assert_eq!(Severity::Info.to_string(), "Info");
240 assert_eq!(Severity::Low.to_string(), "Low");
241 assert_eq!(Severity::Medium.to_string(), "Medium");
242 assert_eq!(Severity::High.to_string(), "High");
243 assert_eq!(Severity::Critical.to_string(), "Critical");
244 }
245
246 #[test]
247 fn severity_from_str_loose() {
248 assert_eq!(Severity::from_str_loose("info"), Some(Severity::Info));
249 assert_eq!(
250 Severity::from_str_loose("CRITICAL"),
251 Some(Severity::Critical)
252 );
253 assert_eq!(Severity::from_str_loose("Med"), Some(Severity::Medium));
254 assert_eq!(
255 Severity::from_str_loose("informational"),
256 Some(Severity::Info)
257 );
258 assert_eq!(Severity::from_str_loose("crit"), Some(Severity::Critical));
259 assert_eq!(Severity::from_str_loose("unknown"), None);
260 }
261
262 #[test]
263 fn severity_serialize_deserialize() {
264 let severity = Severity::High;
265 let json = serde_json::to_string(&severity).unwrap();
266 let deserialized: Severity = serde_json::from_str(&json).unwrap();
267 assert_eq!(severity, deserialized);
268 }
269
270 #[test]
271 fn packet_info_display() {
272 let info = PacketInfo {
273 src_ip: "192.168.1.1".parse().unwrap(),
274 dst_ip: "10.0.0.1".parse().unwrap(),
275 src_port: 12345,
276 dst_port: 80,
277 protocol: 6,
278 size: 1500,
279 timestamp: SystemTime::now(),
280 };
281 let display = info.to_string();
282 assert!(display.contains("192.168.1.1:12345"));
283 assert!(display.contains("10.0.0.1:80"));
284 }
285
286 #[test]
287 fn log_entry_display() {
288 let entry = LogEntry {
289 source: "syslog".to_owned(),
290 timestamp: SystemTime::now(),
291 hostname: "server-01".to_owned(),
292 process: "sshd".to_owned(),
293 message: "session opened".to_owned(),
294 severity: Severity::Info,
295 fields: vec![],
296 };
297 let display = entry.to_string();
298 assert!(display.contains("Info"));
299 assert!(display.contains("server-01"));
300 assert!(display.contains("sshd"));
301 }
302
303 #[test]
304 fn alert_display() {
305 let alert = Alert {
306 id: "alert-001".to_owned(),
307 title: "Brute force".to_owned(),
308 description: "desc".to_owned(),
309 severity: Severity::High,
310 rule_name: "ssh_brute".to_owned(),
311 source_ip: None,
312 target_ip: None,
313 created_at: SystemTime::now(),
314 };
315 let display = alert.to_string();
316 assert!(display.contains("High"));
317 assert!(display.contains("Brute force"));
318 assert!(display.contains("ssh_brute"));
319 }
320
321 #[test]
322 fn container_info_display() {
323 let info = ContainerInfo {
324 id: "abc123def456".to_owned(),
325 name: "web-server".to_owned(),
326 image: "nginx:latest".to_owned(),
327 status: "running".to_owned(),
328 created_at: SystemTime::now(),
329 };
330 let display = info.to_string();
331 assert!(display.contains("web-server"));
332 assert!(display.contains("nginx:latest"));
333 }
334
335 #[test]
336 fn vulnerability_display() {
337 let vuln = Vulnerability {
338 cve_id: "CVE-2024-1234".to_owned(),
339 package: "openssl".to_owned(),
340 affected_version: "1.1.1".to_owned(),
341 fixed_version: Some("1.1.1t".to_owned()),
342 severity: Severity::Critical,
343 description: "Buffer overflow".to_owned(),
344 };
345 let display = vuln.to_string();
346 assert!(display.contains("CVE-2024-1234"));
347 assert!(display.contains("Critical"));
348 assert!(display.contains("1.1.1t"));
349 }
350
351 #[test]
352 fn vulnerability_display_no_fix() {
353 let vuln = Vulnerability {
354 cve_id: "CVE-2024-5678".to_owned(),
355 package: "libxml2".to_owned(),
356 affected_version: "2.9.0".to_owned(),
357 fixed_version: None,
358 severity: Severity::Medium,
359 description: "XXE vulnerability".to_owned(),
360 };
361 assert!(vuln.to_string().contains("N/A"));
362 }
363
364 #[test]
365 fn packet_info_serialize_roundtrip() {
366 let info = PacketInfo {
367 src_ip: "::1".parse().unwrap(),
368 dst_ip: "::1".parse().unwrap(),
369 src_port: 443,
370 dst_port: 54321,
371 protocol: 6,
372 size: 64,
373 timestamp: SystemTime::now(),
374 };
375 let json = serde_json::to_string(&info).unwrap();
376 let deserialized: PacketInfo = serde_json::from_str(&json).unwrap();
377 assert_eq!(info.src_ip, deserialized.src_ip);
378 assert_eq!(info.dst_port, deserialized.dst_port);
379 }
380}