Skip to main content

torsh_package/
audit.rs

1//! Audit logging for security compliance and tracking
2//!
3//! This module provides comprehensive audit logging for all package operations
4//! including downloads, uploads, access control changes, and security events.
5
6use 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/// Audit event type
14#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
15pub enum AuditEventType {
16    /// Package downloaded
17    PackageDownload,
18    /// Package uploaded/published
19    PackageUpload,
20    /// Package deleted
21    PackageDelete,
22    /// Package version yanked
23    PackageYank,
24    /// Package version unyanked
25    PackageUnyank,
26    /// User authentication
27    UserAuthentication,
28    /// User authorization check
29    UserAuthorization,
30    /// Access granted
31    AccessGranted,
32    /// Access denied
33    AccessDenied,
34    /// Role assigned
35    RoleAssigned,
36    /// Role revoked
37    RoleRevoked,
38    /// Permission changed
39    PermissionChanged,
40    /// Security violation detected
41    SecurityViolation,
42    /// Package integrity check
43    IntegrityCheck,
44    /// Package signature verification
45    SignatureVerification,
46    /// Configuration change
47    ConfigurationChange,
48    /// System event
49    SystemEvent,
50}
51
52/// Audit event severity
53#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
54pub enum AuditSeverity {
55    /// Informational event
56    Info,
57    /// Warning level
58    Warning,
59    /// Error level
60    Error,
61    /// Critical security event
62    Critical,
63}
64
65/// Audit event record
66#[derive(Debug, Clone, Serialize, Deserialize)]
67pub struct AuditEvent {
68    /// Event ID (unique)
69    pub id: String,
70    /// Event type
71    pub event_type: AuditEventType,
72    /// Severity level
73    pub severity: AuditSeverity,
74    /// Timestamp
75    pub timestamp: DateTime<Utc>,
76    /// User ID who performed the action
77    pub user_id: Option<String>,
78    /// IP address of the client
79    pub ip_address: Option<String>,
80    /// User agent string
81    pub user_agent: Option<String>,
82    /// Action performed
83    pub action: String,
84    /// Resource affected (e.g., package name)
85    pub resource: Option<String>,
86    /// Result of the action (success/failure)
87    pub result: ActionResult,
88    /// Additional metadata
89    pub metadata: HashMap<String, String>,
90    /// Error message if action failed
91    pub error: Option<String>,
92}
93
94/// Action result
95#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
96pub enum ActionResult {
97    /// Action succeeded
98    Success,
99    /// Action failed
100    Failure,
101    /// Action was denied
102    Denied,
103}
104
105/// Audit log configuration
106#[derive(Debug, Clone, Serialize, Deserialize)]
107pub struct AuditLogConfig {
108    /// Enable audit logging
109    pub enabled: bool,
110    /// Log file path
111    pub log_path: PathBuf,
112    /// Maximum log file size in bytes before rotation
113    pub max_file_size: u64,
114    /// Number of rotated files to keep
115    pub max_files: usize,
116    /// Log format
117    pub format: AuditLogFormat,
118    /// Minimum severity to log
119    pub min_severity: AuditSeverity,
120    /// Enable real-time log streaming
121    pub stream_enabled: bool,
122    /// Buffer size for log entries
123    pub buffer_size: usize,
124}
125
126/// Audit log format
127#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
128pub enum AuditLogFormat {
129    /// JSON format (one event per line)
130    Json,
131    /// CSV format
132    Csv,
133    /// Plain text format
134    Text,
135    /// Syslog format
136    Syslog,
137}
138
139/// Audit logger
140pub struct AuditLogger {
141    /// Configuration
142    config: AuditLogConfig,
143    /// Event buffer
144    buffer: Vec<AuditEvent>,
145    /// Event listeners for real-time streaming
146    listeners: Vec<Box<dyn AuditListener>>,
147    /// Statistics
148    statistics: AuditStatistics,
149}
150
151/// Audit listener for real-time event streaming
152pub trait AuditListener: Send + Sync {
153    /// Called when an event is logged
154    fn on_event(&mut self, event: &AuditEvent);
155
156    /// Called on flush
157    fn on_flush(&mut self);
158}
159
160/// Audit statistics
161#[derive(Debug, Clone, Serialize, Deserialize)]
162pub struct AuditStatistics {
163    /// Total events logged
164    pub total_events: u64,
165    /// Events by type
166    pub events_by_type: HashMap<String, u64>,
167    /// Events by severity
168    pub events_by_severity: HashMap<String, u64>,
169    /// Failed actions
170    pub failed_actions: u64,
171    /// Security violations
172    pub security_violations: u64,
173    /// Unique users
174    pub unique_users: u64,
175}
176
177/// Audit query filter
178#[derive(Debug, Clone, Default)]
179pub struct AuditQuery {
180    /// Filter by event type
181    pub event_types: Vec<AuditEventType>,
182    /// Filter by severity
183    pub min_severity: Option<AuditSeverity>,
184    /// Filter by user ID
185    pub user_id: Option<String>,
186    /// Filter by resource
187    pub resource: Option<String>,
188    /// Filter by result
189    pub result: Option<ActionResult>,
190    /// Start time for query range
191    pub start_time: Option<DateTime<Utc>>,
192    /// End time for query range
193    pub end_time: Option<DateTime<Utc>>,
194    /// Maximum number of results
195    pub limit: Option<usize>,
196}
197
198impl AuditEvent {
199    /// Create a new audit event
200    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    /// Set severity
218    pub fn with_severity(mut self, severity: AuditSeverity) -> Self {
219        self.severity = severity;
220        self
221    }
222
223    /// Set user ID
224    pub fn with_user(mut self, user_id: String) -> Self {
225        self.user_id = Some(user_id);
226        self
227    }
228
229    /// Set IP address
230    pub fn with_ip(mut self, ip: String) -> Self {
231        self.ip_address = Some(ip);
232        self
233    }
234
235    /// Set resource
236    pub fn with_resource(mut self, resource: String) -> Self {
237        self.resource = Some(resource);
238        self
239    }
240
241    /// Set result
242    pub fn with_result(mut self, result: ActionResult) -> Self {
243        self.result = result;
244        self
245    }
246
247    /// Set error message
248    pub fn with_error(mut self, error: String) -> Self {
249        self.error = Some(error);
250        self
251    }
252
253    /// Add metadata
254    pub fn add_metadata(mut self, key: String, value: String) -> Self {
255        self.metadata.insert(key, value);
256        self
257    }
258
259    /// Format as JSON
260    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    /// Format as text
266    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    /// Get severity as string
284    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    /// Get event type as string
294    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, // 100 MB
323            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    /// Create new configuration
334    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    /// Validate configuration
342    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    /// Create new statistics
367    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    /// Update statistics with an event
379    pub fn update(&mut self, event: &AuditEvent) {
380        self.total_events += 1;
381
382        // Count by type
383        let type_key = format!("{:?}", event.event_type);
384        *self.events_by_type.entry(type_key).or_insert(0) += 1;
385
386        // Count by severity
387        let severity_key = format!("{:?}", event.severity);
388        *self.events_by_severity.entry(severity_key).or_insert(0) += 1;
389
390        // Count failures
391        if event.result == ActionResult::Failure {
392            self.failed_actions += 1;
393        }
394
395        // Count security violations
396        if event.event_type == AuditEventType::SecurityViolation {
397            self.security_violations += 1;
398        }
399    }
400}
401
402impl AuditLogger {
403    /// Create a new audit logger
404    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    /// Log an event
416    pub fn log(&mut self, event: AuditEvent) -> Result<()> {
417        if !self.config.enabled {
418            return Ok(());
419        }
420
421        // Check severity filter
422        if event.severity < self.config.min_severity {
423            return Ok(());
424        }
425
426        // Update statistics
427        self.statistics.update(&event);
428
429        // Notify listeners
430        for listener in &mut self.listeners {
431            listener.on_event(&event);
432        }
433
434        // Add to buffer
435        self.buffer.push(event);
436
437        // Flush if buffer is full
438        if self.buffer.len() >= self.config.buffer_size {
439            self.flush()?;
440        }
441
442        Ok(())
443    }
444
445    /// Log package download
446    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    /// Log package upload
459    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    /// Log access denial
472    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    /// Log security violation
487    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    /// Flush buffered events to disk
509    pub fn flush(&mut self) -> Result<()> {
510        if self.buffer.is_empty() {
511            return Ok(());
512        }
513
514        // In production, would write to file
515        // For now, just clear the buffer
516        self.buffer.clear();
517
518        // Notify listeners
519        for listener in &mut self.listeners {
520            listener.on_flush();
521        }
522
523        Ok(())
524    }
525
526    /// Add an event listener
527    pub fn add_listener(&mut self, listener: Box<dyn AuditListener>) {
528        self.listeners.push(listener);
529    }
530
531    /// Query events (simplified - in production would query from persistent storage)
532    pub fn query(&self, _query: &AuditQuery) -> Vec<AuditEvent> {
533        // In production, would query from log files or database
534        // For now, return buffered events
535        self.buffer.clone()
536    }
537
538    /// Get statistics
539    pub fn get_statistics(&self) -> &AuditStatistics {
540        &self.statistics
541    }
542
543    /// Get event count by type
544    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        // Buffer should be flushed after reaching buffer_size
671        assert_eq!(logger.buffer.len(), 0);
672    }
673}