Skip to main content

torsh_package/
syslog_integration.rs

1//! Syslog Integration for Centralized Logging
2//!
3//! Provides syslog integration for sending audit events to centralized
4//! logging systems following RFC 5424 and RFC 3164 standards.
5
6use crate::audit::{AuditEvent, AuditEventType, AuditSeverity};
7use std::io::Write;
8use std::net::{SocketAddr, TcpStream, UdpSocket};
9use torsh_core::error::{Result, TorshError};
10
11/// Syslog protocol version
12#[derive(Debug, Clone, Copy, PartialEq, Eq)]
13pub enum SyslogProtocol {
14    /// RFC 3164 (BSD syslog)
15    Rfc3164,
16    /// RFC 5424 (modern syslog)
17    Rfc5424,
18}
19
20/// Syslog transport protocol
21#[derive(Debug, Clone, Copy, PartialEq, Eq)]
22pub enum SyslogTransport {
23    /// UDP transport (connectionless, fast but unreliable)
24    Udp,
25    /// TCP transport (connection-oriented, reliable)
26    Tcp,
27    /// Unix domain socket (local only, fastest and most reliable)
28    Unix,
29}
30
31/// Syslog facility codes (RFC 5424)
32#[derive(Debug, Clone, Copy, PartialEq, Eq)]
33#[repr(u8)]
34pub enum SyslogFacility {
35    /// Kernel messages
36    Kern = 0,
37    /// User-level messages
38    User = 1,
39    /// Mail system
40    Mail = 2,
41    /// System daemons
42    Daemon = 3,
43    /// Security/authorization messages
44    Auth = 4,
45    /// Internal syslog messages
46    Syslog = 5,
47    /// Line printer subsystem
48    Lpr = 6,
49    /// Network news subsystem
50    News = 7,
51    /// UUCP subsystem
52    Uucp = 8,
53    /// Clock daemon
54    Cron = 9,
55    /// Security/authorization messages (private)
56    AuthPriv = 10,
57    /// FTP daemon
58    Ftp = 11,
59    /// NTP subsystem
60    Ntp = 12,
61    /// Log audit
62    Audit = 13,
63    /// Log alert
64    Alert = 14,
65    /// Clock daemon (note 2)
66    Clock = 15,
67    /// Local use 0
68    Local0 = 16,
69    /// Local use 1
70    Local1 = 17,
71    /// Local use 2
72    Local2 = 18,
73    /// Local use 3
74    Local3 = 19,
75    /// Local use 4
76    Local4 = 20,
77    /// Local use 5
78    Local5 = 21,
79    /// Local use 6
80    Local6 = 22,
81    /// Local use 7
82    Local7 = 23,
83}
84
85/// Syslog severity level (RFC 5424)
86#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
87#[repr(u8)]
88pub enum SyslogSeverity {
89    /// Emergency: system is unusable
90    Emergency = 0,
91    /// Alert: action must be taken immediately
92    Alert = 1,
93    /// Critical: critical conditions
94    Critical = 2,
95    /// Error: error conditions
96    Error = 3,
97    /// Warning: warning conditions
98    Warning = 4,
99    /// Notice: normal but significant condition
100    Notice = 5,
101    /// Informational: informational messages
102    Info = 6,
103    /// Debug: debug-level messages
104    Debug = 7,
105}
106
107impl From<&AuditSeverity> for SyslogSeverity {
108    fn from(severity: &AuditSeverity) -> Self {
109        match severity {
110            AuditSeverity::Info => SyslogSeverity::Info,
111            AuditSeverity::Warning => SyslogSeverity::Warning,
112            AuditSeverity::Error => SyslogSeverity::Error,
113            AuditSeverity::Critical => SyslogSeverity::Critical,
114        }
115    }
116}
117
118/// Syslog client configuration
119#[derive(Debug, Clone)]
120pub struct SyslogConfig {
121    /// Syslog server address
122    pub server_addr: String,
123    /// Syslog server port
124    pub server_port: u16,
125    /// Transport protocol
126    pub transport: SyslogTransport,
127    /// Protocol version
128    pub protocol: SyslogProtocol,
129    /// Facility code
130    pub facility: SyslogFacility,
131    /// Application name
132    pub app_name: String,
133    /// Hostname
134    pub hostname: String,
135    /// Process ID
136    pub process_id: u32,
137    /// Enable TLS for TCP connections
138    pub enable_tls: bool,
139}
140
141impl SyslogConfig {
142    /// Create a new syslog configuration
143    pub fn new(server_addr: String, server_port: u16) -> Self {
144        Self {
145            server_addr,
146            server_port,
147            transport: SyslogTransport::Udp,
148            protocol: SyslogProtocol::Rfc5424,
149            facility: SyslogFacility::Local0,
150            app_name: "torsh-package".to_string(),
151            hostname: hostname::get()
152                .ok()
153                .and_then(|h| h.into_string().ok())
154                .unwrap_or_else(|| "unknown".to_string()),
155            process_id: std::process::id(),
156            enable_tls: false,
157        }
158    }
159
160    /// Set transport protocol
161    pub fn with_transport(mut self, transport: SyslogTransport) -> Self {
162        self.transport = transport;
163        self
164    }
165
166    /// Set protocol version
167    pub fn with_protocol(mut self, protocol: SyslogProtocol) -> Self {
168        self.protocol = protocol;
169        self
170    }
171
172    /// Set facility code
173    pub fn with_facility(mut self, facility: SyslogFacility) -> Self {
174        self.facility = facility;
175        self
176    }
177
178    /// Set application name
179    pub fn with_app_name(mut self, app_name: String) -> Self {
180        self.app_name = app_name;
181        self
182    }
183
184    /// Enable TLS for TCP connections
185    pub fn with_tls(mut self, enable: bool) -> Self {
186        self.enable_tls = enable;
187        self
188    }
189
190    /// Get socket address
191    pub fn socket_addr(&self) -> Result<SocketAddr> {
192        let addr_str = format!("{}:{}", self.server_addr, self.server_port);
193        addr_str
194            .parse()
195            .map_err(|e| TorshError::InvalidArgument(format!("Invalid socket address: {}", e)))
196    }
197}
198
199/// Syslog client for sending audit events
200#[derive(Debug)]
201pub struct SyslogClient {
202    config: SyslogConfig,
203    // In production, maintain persistent TCP connections
204    // tcp_connection: Option<TcpStream>,
205    message_count: u64,
206}
207
208impl SyslogClient {
209    /// Create a new syslog client
210    pub fn new(config: SyslogConfig) -> Result<Self> {
211        Ok(Self {
212            config,
213            message_count: 0,
214        })
215    }
216
217    /// Send an audit event to syslog
218    pub fn send_event(&mut self, event: &AuditEvent) -> Result<()> {
219        let severity = SyslogSeverity::from(&event.severity);
220        let message = self.format_message(event, severity)?;
221
222        match self.config.transport {
223            SyslogTransport::Udp => self.send_udp(&message),
224            SyslogTransport::Tcp => self.send_tcp(&message),
225            SyslogTransport::Unix => self.send_unix(&message),
226        }?;
227
228        self.message_count += 1;
229        Ok(())
230    }
231
232    /// Format message according to protocol
233    fn format_message(&self, event: &AuditEvent, severity: SyslogSeverity) -> Result<String> {
234        match self.config.protocol {
235            SyslogProtocol::Rfc3164 => self.format_rfc3164(event, severity),
236            SyslogProtocol::Rfc5424 => self.format_rfc5424(event, severity),
237        }
238    }
239
240    /// Format message according to RFC 3164 (BSD syslog)
241    fn format_rfc3164(&self, event: &AuditEvent, severity: SyslogSeverity) -> Result<String> {
242        let priority = self.calculate_priority(severity);
243        let timestamp = self.format_rfc3164_timestamp(&event.timestamp);
244        let tag = format!("{}[{}]:", self.config.app_name, self.config.process_id);
245
246        // RFC 3164 format: <PRI>TIMESTAMP HOSTNAME TAG MSG
247        Ok(format!(
248            "<{}>{} {} {} {}",
249            priority, timestamp, self.config.hostname, tag, event.action
250        ))
251    }
252
253    /// Format message according to RFC 5424 (modern syslog)
254    fn format_rfc5424(&self, event: &AuditEvent, severity: SyslogSeverity) -> Result<String> {
255        let priority = self.calculate_priority(severity);
256        let timestamp = event.timestamp.to_rfc3339();
257        let msgid = self.format_msgid(&event.event_type);
258
259        // Structured data
260        let structured_data = self.format_structured_data(event);
261
262        // RFC 5424 format: <PRI>VERSION TIMESTAMP HOSTNAME APP-NAME PROCID MSGID STRUCTURED-DATA MSG
263        Ok(format!(
264            "<{}>1 {} {} {} {} {} {} {}",
265            priority,
266            timestamp,
267            self.config.hostname,
268            self.config.app_name,
269            self.config.process_id,
270            msgid,
271            structured_data,
272            event.action
273        ))
274    }
275
276    /// Format RFC 3164 timestamp
277    fn format_rfc3164_timestamp(&self, timestamp: &chrono::DateTime<chrono::Utc>) -> String {
278        // RFC 3164 uses "Mmm dd HH:MM:SS" format
279        timestamp.format("%b %d %H:%M:%S").to_string()
280    }
281
282    /// Format message ID from event type
283    fn format_msgid(&self, event_type: &AuditEventType) -> String {
284        match event_type {
285            AuditEventType::PackageDownload => "PKG-DOWNLOAD",
286            AuditEventType::PackageUpload => "PKG-UPLOAD",
287            AuditEventType::PackageDelete => "PKG-DELETE",
288            AuditEventType::PackageYank => "PKG-YANK",
289            AuditEventType::PackageUnyank => "PKG-UNYANK",
290            AuditEventType::UserAuthentication => "USER-AUTH",
291            AuditEventType::UserAuthorization => "USER-AUTHZ",
292            AuditEventType::AccessGranted => "ACCESS-GRANTED",
293            AuditEventType::AccessDenied => "ACCESS-DENIED",
294            AuditEventType::RoleAssigned => "ROLE-ASSIGNED",
295            AuditEventType::RoleRevoked => "ROLE-REVOKED",
296            AuditEventType::PermissionChanged => "PERM-CHANGED",
297            AuditEventType::SecurityViolation => "SECURITY-VIOLATION",
298            AuditEventType::IntegrityCheck => "INTEGRITY-CHECK",
299            AuditEventType::SignatureVerification => "SIGNATURE-VERIFY",
300            AuditEventType::ConfigurationChange => "CONFIG-CHANGE",
301            AuditEventType::SystemEvent => "SYSTEM-EVENT",
302        }
303        .to_string()
304    }
305
306    /// Format structured data for RFC 5424
307    fn format_structured_data(&self, event: &AuditEvent) -> String {
308        let mut sd = String::from("[torsh@32473");
309
310        if let Some(user_id) = &event.user_id {
311            sd.push_str(&format!(" user=\"{}\"", self.escape_sd_param(user_id)));
312        }
313
314        if let Some(ip) = &event.ip_address {
315            sd.push_str(&format!(" ip=\"{}\"", self.escape_sd_param(ip)));
316        }
317
318        if let Some(resource) = &event.resource {
319            sd.push_str(&format!(" resource=\"{}\"", self.escape_sd_param(resource)));
320        }
321
322        sd.push_str(&format!(" severity=\"{:?}\"", event.severity));
323        sd.push_str(&format!(" event_type=\"{:?}\"", event.event_type));
324
325        sd.push(']');
326
327        if sd == "[torsh@32473]" {
328            return "-".to_string(); // No structured data
329        }
330
331        sd
332    }
333
334    /// Escape structured data parameter value
335    fn escape_sd_param(&self, value: &str) -> String {
336        value
337            .replace('\\', "\\\\")
338            .replace('"', "\\\"")
339            .replace(']', "\\]")
340    }
341
342    /// Calculate priority value from facility and severity
343    fn calculate_priority(&self, severity: SyslogSeverity) -> u8 {
344        (self.config.facility as u8) * 8 + (severity as u8)
345    }
346
347    /// Send message via UDP
348    fn send_udp(&self, message: &str) -> Result<()> {
349        let socket = UdpSocket::bind("0.0.0.0:0")
350            .map_err(|e| TorshError::InvalidArgument(format!("UDP bind error: {}", e)))?;
351
352        let addr = self.config.socket_addr()?;
353        socket
354            .send_to(message.as_bytes(), addr)
355            .map_err(|e| TorshError::InvalidArgument(format!("UDP send error: {}", e)))?;
356
357        Ok(())
358    }
359
360    /// Send message via TCP
361    fn send_tcp(&self, message: &str) -> Result<()> {
362        let addr = self.config.socket_addr()?;
363
364        // In production, maintain persistent connection
365        let mut stream = TcpStream::connect(addr)
366            .map_err(|e| TorshError::InvalidArgument(format!("TCP connect error: {}", e)))?;
367
368        // TCP syslog uses newline-delimited messages
369        let message_with_newline = format!("{}\n", message);
370
371        stream
372            .write_all(message_with_newline.as_bytes())
373            .map_err(|e| TorshError::InvalidArgument(format!("TCP write error: {}", e)))?;
374
375        stream
376            .flush()
377            .map_err(|e| TorshError::InvalidArgument(format!("TCP flush error: {}", e)))?;
378
379        Ok(())
380    }
381
382    /// Send message via Unix domain socket
383    fn send_unix(&self, _message: &str) -> Result<()> {
384        // Unix domain socket support would use std::os::unix::net::UnixDatagram
385        // This is platform-specific
386        #[cfg(unix)]
387        {
388            // In production:
389            // let socket = UnixDatagram::unbound()?;
390            // socket.send_to(message.as_bytes(), "/dev/log")?;
391        }
392
393        #[cfg(not(unix))]
394        {
395            return Err(TorshError::InvalidArgument(
396                "Unix sockets not supported on this platform".to_string(),
397            ));
398        }
399
400        Ok(())
401    }
402
403    /// Get message count
404    pub fn message_count(&self) -> u64 {
405        self.message_count
406    }
407
408    /// Get statistics
409    pub fn get_statistics(&self) -> SyslogStatistics {
410        SyslogStatistics {
411            messages_sent: self.message_count,
412            messages_failed: 0,
413            bytes_sent: 0,
414            connection_errors: 0,
415        }
416    }
417}
418
419/// Syslog client statistics
420#[derive(Debug, Clone)]
421pub struct SyslogStatistics {
422    /// Total messages sent
423    pub messages_sent: u64,
424    /// Messages that failed to send
425    pub messages_failed: u64,
426    /// Total bytes sent
427    pub bytes_sent: u64,
428    /// Connection errors encountered
429    pub connection_errors: u64,
430}
431
432#[cfg(test)]
433mod tests {
434    use super::*;
435
436    #[test]
437    fn test_syslog_config() {
438        let config = SyslogConfig::new("localhost".to_string(), 514)
439            .with_transport(SyslogTransport::Udp)
440            .with_protocol(SyslogProtocol::Rfc5424)
441            .with_facility(SyslogFacility::Local0)
442            .with_app_name("test-app".to_string());
443
444        assert_eq!(config.server_addr, "localhost");
445        assert_eq!(config.server_port, 514);
446        assert_eq!(config.transport, SyslogTransport::Udp);
447        assert_eq!(config.protocol, SyslogProtocol::Rfc5424);
448        assert_eq!(config.app_name, "test-app");
449    }
450
451    #[test]
452    fn test_priority_calculation() {
453        let config =
454            SyslogConfig::new("localhost".to_string(), 514).with_facility(SyslogFacility::Local0);
455
456        let client = SyslogClient::new(config).unwrap();
457
458        // Local0 = 16, Info = 6 => 16*8 + 6 = 134
459        assert_eq!(client.calculate_priority(SyslogSeverity::Info), 134);
460
461        // Local0 = 16, Error = 3 => 16*8 + 3 = 131
462        assert_eq!(client.calculate_priority(SyslogSeverity::Error), 131);
463    }
464
465    #[test]
466    fn test_severity_conversion() {
467        assert_eq!(
468            SyslogSeverity::from(&AuditSeverity::Info),
469            SyslogSeverity::Info
470        );
471        assert_eq!(
472            SyslogSeverity::from(&AuditSeverity::Warning),
473            SyslogSeverity::Warning
474        );
475        assert_eq!(
476            SyslogSeverity::from(&AuditSeverity::Error),
477            SyslogSeverity::Error
478        );
479        assert_eq!(
480            SyslogSeverity::from(&AuditSeverity::Critical),
481            SyslogSeverity::Critical
482        );
483    }
484
485    #[test]
486    fn test_msgid_formatting() {
487        let config = SyslogConfig::new("localhost".to_string(), 514);
488        let client = SyslogClient::new(config).unwrap();
489
490        assert_eq!(
491            client.format_msgid(&AuditEventType::PackageDownload),
492            "PKG-DOWNLOAD"
493        );
494        assert_eq!(
495            client.format_msgid(&AuditEventType::SecurityViolation),
496            "SECURITY-VIOLATION"
497        );
498        assert_eq!(
499            client.format_msgid(&AuditEventType::AccessDenied),
500            "ACCESS-DENIED"
501        );
502        assert_eq!(
503            client.format_msgid(&AuditEventType::PermissionChanged),
504            "PERM-CHANGED"
505        );
506        assert_eq!(
507            client.format_msgid(&AuditEventType::IntegrityCheck),
508            "INTEGRITY-CHECK"
509        );
510    }
511
512    #[test]
513    fn test_structured_data_escaping() {
514        let config = SyslogConfig::new("localhost".to_string(), 514);
515        let client = SyslogClient::new(config).unwrap();
516
517        let escaped = client.escape_sd_param("test\\value\"with]special");
518        assert_eq!(escaped, "test\\\\value\\\"with\\]special");
519    }
520
521    #[test]
522    fn test_rfc5424_formatting() {
523        let config = SyslogConfig::new("localhost".to_string(), 514)
524            .with_facility(SyslogFacility::Local0)
525            .with_app_name("test".to_string());
526
527        let client = SyslogClient::new(config).unwrap();
528
529        let event = AuditEvent::new(AuditEventType::PackageDownload, "Test message".to_string());
530
531        let message = client.format_rfc5424(&event, SyslogSeverity::Info).unwrap();
532
533        assert!(message.contains("<134>1")); // Priority: Local0 + Info
534        assert!(message.contains("PKG-DOWNLOAD"));
535        assert!(message.contains("test"));
536        assert!(message.contains("Test message"));
537    }
538}