1use std::collections::HashMap;
7use std::path::{Path, PathBuf};
8
9use chrono::{DateTime, Utc};
10use serde::{Deserialize, Serialize};
11use torsh_core::error::{Result, TorshError};
12
13#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
15pub enum AuditEventType {
16 PackageDownload,
18 PackageUpload,
20 PackageDelete,
22 PackageYank,
24 PackageUnyank,
26 UserAuthentication,
28 UserAuthorization,
30 AccessGranted,
32 AccessDenied,
34 RoleAssigned,
36 RoleRevoked,
38 PermissionChanged,
40 SecurityViolation,
42 IntegrityCheck,
44 SignatureVerification,
46 ConfigurationChange,
48 SystemEvent,
50}
51
52#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
54pub enum AuditSeverity {
55 Info,
57 Warning,
59 Error,
61 Critical,
63}
64
65#[derive(Debug, Clone, Serialize, Deserialize)]
67pub struct AuditEvent {
68 pub id: String,
70 pub event_type: AuditEventType,
72 pub severity: AuditSeverity,
74 pub timestamp: DateTime<Utc>,
76 pub user_id: Option<String>,
78 pub ip_address: Option<String>,
80 pub user_agent: Option<String>,
82 pub action: String,
84 pub resource: Option<String>,
86 pub result: ActionResult,
88 pub metadata: HashMap<String, String>,
90 pub error: Option<String>,
92}
93
94#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
96pub enum ActionResult {
97 Success,
99 Failure,
101 Denied,
103}
104
105#[derive(Debug, Clone, Serialize, Deserialize)]
107pub struct AuditLogConfig {
108 pub enabled: bool,
110 pub log_path: PathBuf,
112 pub max_file_size: u64,
114 pub max_files: usize,
116 pub format: AuditLogFormat,
118 pub min_severity: AuditSeverity,
120 pub stream_enabled: bool,
122 pub buffer_size: usize,
124}
125
126#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
128pub enum AuditLogFormat {
129 Json,
131 Csv,
133 Text,
135 Syslog,
137}
138
139pub struct AuditLogger {
141 config: AuditLogConfig,
143 buffer: Vec<AuditEvent>,
145 listeners: Vec<Box<dyn AuditListener>>,
147 statistics: AuditStatistics,
149}
150
151pub trait AuditListener: Send + Sync {
153 fn on_event(&mut self, event: &AuditEvent);
155
156 fn on_flush(&mut self);
158}
159
160#[derive(Debug, Clone, Serialize, Deserialize)]
162pub struct AuditStatistics {
163 pub total_events: u64,
165 pub events_by_type: HashMap<String, u64>,
167 pub events_by_severity: HashMap<String, u64>,
169 pub failed_actions: u64,
171 pub security_violations: u64,
173 pub unique_users: u64,
175}
176
177#[derive(Debug, Clone, Default)]
179pub struct AuditQuery {
180 pub event_types: Vec<AuditEventType>,
182 pub min_severity: Option<AuditSeverity>,
184 pub user_id: Option<String>,
186 pub resource: Option<String>,
188 pub result: Option<ActionResult>,
190 pub start_time: Option<DateTime<Utc>>,
192 pub end_time: Option<DateTime<Utc>>,
194 pub limit: Option<usize>,
196}
197
198impl AuditEvent {
199 pub fn new(event_type: AuditEventType, action: String) -> Self {
201 Self {
202 id: uuid::Uuid::new_v4().to_string(),
203 event_type,
204 severity: AuditSeverity::Info,
205 timestamp: Utc::now(),
206 user_id: None,
207 ip_address: None,
208 user_agent: None,
209 action,
210 resource: None,
211 result: ActionResult::Success,
212 metadata: HashMap::new(),
213 error: None,
214 }
215 }
216
217 pub fn with_severity(mut self, severity: AuditSeverity) -> Self {
219 self.severity = severity;
220 self
221 }
222
223 pub fn with_user(mut self, user_id: String) -> Self {
225 self.user_id = Some(user_id);
226 self
227 }
228
229 pub fn with_ip(mut self, ip: String) -> Self {
231 self.ip_address = Some(ip);
232 self
233 }
234
235 pub fn with_resource(mut self, resource: String) -> Self {
237 self.resource = Some(resource);
238 self
239 }
240
241 pub fn with_result(mut self, result: ActionResult) -> Self {
243 self.result = result;
244 self
245 }
246
247 pub fn with_error(mut self, error: String) -> Self {
249 self.error = Some(error);
250 self
251 }
252
253 pub fn add_metadata(mut self, key: String, value: String) -> Self {
255 self.metadata.insert(key, value);
256 self
257 }
258
259 pub fn to_json(&self) -> Result<String> {
261 serde_json::to_string(self)
262 .map_err(|e| TorshError::InvalidArgument(format!("Failed to serialize event: {}", e)))
263 }
264
265 pub fn to_text(&self) -> String {
267 format!(
268 "[{}] {} - {} - {} - {} - User: {:?} - Resource: {:?} - Result: {:?}{}",
269 self.timestamp.format("%Y-%m-%d %H:%M:%S"),
270 self.severity_str(),
271 self.event_type_str(),
272 self.action,
273 self.id,
274 self.user_id,
275 self.resource,
276 self.result,
277 self.error
278 .as_ref()
279 .map_or(String::new(), |e| format!(" - Error: {}", e))
280 )
281 }
282
283 fn severity_str(&self) -> &str {
285 match self.severity {
286 AuditSeverity::Info => "INFO",
287 AuditSeverity::Warning => "WARN",
288 AuditSeverity::Error => "ERROR",
289 AuditSeverity::Critical => "CRIT",
290 }
291 }
292
293 fn event_type_str(&self) -> &str {
295 match self.event_type {
296 AuditEventType::PackageDownload => "DOWNLOAD",
297 AuditEventType::PackageUpload => "UPLOAD",
298 AuditEventType::PackageDelete => "DELETE",
299 AuditEventType::PackageYank => "YANK",
300 AuditEventType::PackageUnyank => "UNYANK",
301 AuditEventType::UserAuthentication => "AUTH",
302 AuditEventType::UserAuthorization => "AUTHZ",
303 AuditEventType::AccessGranted => "ACCESS_GRANTED",
304 AuditEventType::AccessDenied => "ACCESS_DENIED",
305 AuditEventType::RoleAssigned => "ROLE_ASSIGN",
306 AuditEventType::RoleRevoked => "ROLE_REVOKE",
307 AuditEventType::PermissionChanged => "PERM_CHANGE",
308 AuditEventType::SecurityViolation => "SECURITY_VIOLATION",
309 AuditEventType::IntegrityCheck => "INTEGRITY_CHECK",
310 AuditEventType::SignatureVerification => "SIGNATURE_VERIFY",
311 AuditEventType::ConfigurationChange => "CONFIG_CHANGE",
312 AuditEventType::SystemEvent => "SYSTEM",
313 }
314 }
315}
316
317impl Default for AuditLogConfig {
318 fn default() -> Self {
319 Self {
320 enabled: true,
321 log_path: PathBuf::from("/var/log/torsh/audit.log"),
322 max_file_size: 100 * 1024 * 1024, max_files: 10,
324 format: AuditLogFormat::Json,
325 min_severity: AuditSeverity::Info,
326 stream_enabled: false,
327 buffer_size: 1000,
328 }
329 }
330}
331
332impl AuditLogConfig {
333 pub fn new<P: AsRef<Path>>(log_path: P) -> Self {
335 Self {
336 log_path: log_path.as_ref().to_path_buf(),
337 ..Default::default()
338 }
339 }
340
341 pub fn validate(&self) -> Result<()> {
343 if self.max_file_size == 0 {
344 return Err(TorshError::InvalidArgument(
345 "Max file size must be greater than zero".to_string(),
346 ));
347 }
348
349 if self.max_files == 0 {
350 return Err(TorshError::InvalidArgument(
351 "Max files must be greater than zero".to_string(),
352 ));
353 }
354
355 Ok(())
356 }
357}
358
359impl Default for AuditStatistics {
360 fn default() -> Self {
361 Self::new()
362 }
363}
364
365impl AuditStatistics {
366 pub fn new() -> Self {
368 Self {
369 total_events: 0,
370 events_by_type: HashMap::new(),
371 events_by_severity: HashMap::new(),
372 failed_actions: 0,
373 security_violations: 0,
374 unique_users: 0,
375 }
376 }
377
378 pub fn update(&mut self, event: &AuditEvent) {
380 self.total_events += 1;
381
382 let type_key = format!("{:?}", event.event_type);
384 *self.events_by_type.entry(type_key).or_insert(0) += 1;
385
386 let severity_key = format!("{:?}", event.severity);
388 *self.events_by_severity.entry(severity_key).or_insert(0) += 1;
389
390 if event.result == ActionResult::Failure {
392 self.failed_actions += 1;
393 }
394
395 if event.event_type == AuditEventType::SecurityViolation {
397 self.security_violations += 1;
398 }
399 }
400}
401
402impl AuditLogger {
403 pub fn new(config: AuditLogConfig) -> Result<Self> {
405 config.validate()?;
406
407 Ok(Self {
408 config,
409 buffer: Vec::new(),
410 listeners: Vec::new(),
411 statistics: AuditStatistics::new(),
412 })
413 }
414
415 pub fn log(&mut self, event: AuditEvent) -> Result<()> {
417 if !self.config.enabled {
418 return Ok(());
419 }
420
421 if event.severity < self.config.min_severity {
423 return Ok(());
424 }
425
426 self.statistics.update(&event);
428
429 for listener in &mut self.listeners {
431 listener.on_event(&event);
432 }
433
434 self.buffer.push(event);
436
437 if self.buffer.len() >= self.config.buffer_size {
439 self.flush()?;
440 }
441
442 Ok(())
443 }
444
445 pub fn log_download(&mut self, user_id: &str, package: &str, version: &str) -> Result<()> {
447 let event = AuditEvent::new(
448 AuditEventType::PackageDownload,
449 format!("Download package {}", package),
450 )
451 .with_user(user_id.to_string())
452 .with_resource(format!("{}:{}", package, version))
453 .with_severity(AuditSeverity::Info);
454
455 self.log(event)
456 }
457
458 pub fn log_upload(&mut self, user_id: &str, package: &str, version: &str) -> Result<()> {
460 let event = AuditEvent::new(
461 AuditEventType::PackageUpload,
462 format!("Upload package {}", package),
463 )
464 .with_user(user_id.to_string())
465 .with_resource(format!("{}:{}", package, version))
466 .with_severity(AuditSeverity::Info);
467
468 self.log(event)
469 }
470
471 pub fn log_access_denied(&mut self, user_id: &str, resource: &str, reason: &str) -> Result<()> {
473 let event = AuditEvent::new(
474 AuditEventType::AccessDenied,
475 format!("Access denied to {}", resource),
476 )
477 .with_user(user_id.to_string())
478 .with_resource(resource.to_string())
479 .with_result(ActionResult::Denied)
480 .with_severity(AuditSeverity::Warning)
481 .add_metadata("reason".to_string(), reason.to_string());
482
483 self.log(event)
484 }
485
486 pub fn log_security_violation(
488 &mut self,
489 user_id: Option<&str>,
490 violation: &str,
491 details: &str,
492 ) -> Result<()> {
493 let mut event = AuditEvent::new(
494 AuditEventType::SecurityViolation,
495 format!("Security violation: {}", violation),
496 )
497 .with_severity(AuditSeverity::Critical)
498 .with_result(ActionResult::Failure)
499 .add_metadata("details".to_string(), details.to_string());
500
501 if let Some(uid) = user_id {
502 event = event.with_user(uid.to_string());
503 }
504
505 self.log(event)
506 }
507
508 pub fn flush(&mut self) -> Result<()> {
510 if self.buffer.is_empty() {
511 return Ok(());
512 }
513
514 self.buffer.clear();
517
518 for listener in &mut self.listeners {
520 listener.on_flush();
521 }
522
523 Ok(())
524 }
525
526 pub fn add_listener(&mut self, listener: Box<dyn AuditListener>) {
528 self.listeners.push(listener);
529 }
530
531 pub fn query(&self, _query: &AuditQuery) -> Vec<AuditEvent> {
533 self.buffer.clone()
536 }
537
538 pub fn get_statistics(&self) -> &AuditStatistics {
540 &self.statistics
541 }
542
543 pub fn get_event_count(&self, event_type: &AuditEventType) -> u64 {
545 let key = format!("{:?}", event_type);
546 self.statistics
547 .events_by_type
548 .get(&key)
549 .copied()
550 .unwrap_or(0)
551 }
552}
553
554#[cfg(test)]
555mod tests {
556 use super::*;
557
558 #[test]
559 fn test_audit_event_creation() {
560 let event = AuditEvent::new(
561 AuditEventType::PackageDownload,
562 "Download test-package".to_string(),
563 )
564 .with_user("user1".to_string())
565 .with_resource("test-package:1.0.0".to_string())
566 .with_severity(AuditSeverity::Info);
567
568 assert_eq!(event.event_type, AuditEventType::PackageDownload);
569 assert_eq!(event.user_id, Some("user1".to_string()));
570 assert_eq!(event.result, ActionResult::Success);
571 }
572
573 #[test]
574 fn test_audit_event_formatting() {
575 let event = AuditEvent::new(AuditEventType::PackageDownload, "Download test".to_string());
576
577 let json = event.to_json().unwrap();
578 assert!(json.contains("PackageDownload"));
579
580 let text = event.to_text();
581 assert!(text.contains("DOWNLOAD"));
582 assert!(text.contains("INFO"));
583 }
584
585 #[test]
586 fn test_audit_logger() {
587 let config = AuditLogConfig::new(std::env::temp_dir().join("test-audit.log"));
588 let mut logger = AuditLogger::new(config).unwrap();
589
590 let event = AuditEvent::new(AuditEventType::PackageDownload, "Test download".to_string());
591
592 logger.log(event).unwrap();
593 assert_eq!(logger.statistics.total_events, 1);
594 assert_eq!(logger.buffer.len(), 1);
595 }
596
597 #[test]
598 fn test_log_download() {
599 let config = AuditLogConfig::new(std::env::temp_dir().join("test-audit.log"));
600 let mut logger = AuditLogger::new(config).unwrap();
601
602 logger
603 .log_download("user1", "test-package", "1.0.0")
604 .unwrap();
605
606 assert_eq!(logger.get_event_count(&AuditEventType::PackageDownload), 1);
607 }
608
609 #[test]
610 fn test_log_access_denied() {
611 let config = AuditLogConfig::new(std::env::temp_dir().join("test-audit.log"));
612 let mut logger = AuditLogger::new(config).unwrap();
613
614 logger
615 .log_access_denied("user1", "test-package", "Insufficient permissions")
616 .unwrap();
617
618 assert_eq!(logger.get_event_count(&AuditEventType::AccessDenied), 1);
619 }
620
621 #[test]
622 fn test_security_violation_logging() {
623 let config = AuditLogConfig::new(std::env::temp_dir().join("test-audit.log"));
624 let mut logger = AuditLogger::new(config).unwrap();
625
626 logger
627 .log_security_violation(Some("user1"), "Suspicious activity", "Details here")
628 .unwrap();
629
630 assert_eq!(logger.statistics.security_violations, 1);
631 }
632
633 #[test]
634 fn test_statistics_update() {
635 let mut stats = AuditStatistics::new();
636
637 let event1 = AuditEvent::new(AuditEventType::PackageDownload, "Download".to_string());
638 let event2 = AuditEvent::new(AuditEventType::PackageUpload, "Upload".to_string())
639 .with_result(ActionResult::Failure);
640
641 stats.update(&event1);
642 stats.update(&event2);
643
644 assert_eq!(stats.total_events, 2);
645 assert_eq!(stats.failed_actions, 1);
646 }
647
648 #[test]
649 fn test_buffer_flush() {
650 let mut config = AuditLogConfig::new(std::env::temp_dir().join("test-audit.log"));
651 config.buffer_size = 2;
652
653 let mut logger = AuditLogger::new(config).unwrap();
654
655 logger
656 .log(AuditEvent::new(
657 AuditEventType::PackageDownload,
658 "Test1".to_string(),
659 ))
660 .unwrap();
661 assert_eq!(logger.buffer.len(), 1);
662
663 logger
664 .log(AuditEvent::new(
665 AuditEventType::PackageDownload,
666 "Test2".to_string(),
667 ))
668 .unwrap();
669
670 assert_eq!(logger.buffer.len(), 0);
672 }
673}