1use crate::audit::{AuditEvent, AuditEventType, AuditSeverity};
7use std::io::Write;
8use std::net::{SocketAddr, TcpStream, UdpSocket};
9use torsh_core::error::{Result, TorshError};
10
11#[derive(Debug, Clone, Copy, PartialEq, Eq)]
13pub enum SyslogProtocol {
14 Rfc3164,
16 Rfc5424,
18}
19
20#[derive(Debug, Clone, Copy, PartialEq, Eq)]
22pub enum SyslogTransport {
23 Udp,
25 Tcp,
27 Unix,
29}
30
31#[derive(Debug, Clone, Copy, PartialEq, Eq)]
33#[repr(u8)]
34pub enum SyslogFacility {
35 Kern = 0,
37 User = 1,
39 Mail = 2,
41 Daemon = 3,
43 Auth = 4,
45 Syslog = 5,
47 Lpr = 6,
49 News = 7,
51 Uucp = 8,
53 Cron = 9,
55 AuthPriv = 10,
57 Ftp = 11,
59 Ntp = 12,
61 Audit = 13,
63 Alert = 14,
65 Clock = 15,
67 Local0 = 16,
69 Local1 = 17,
71 Local2 = 18,
73 Local3 = 19,
75 Local4 = 20,
77 Local5 = 21,
79 Local6 = 22,
81 Local7 = 23,
83}
84
85#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
87#[repr(u8)]
88pub enum SyslogSeverity {
89 Emergency = 0,
91 Alert = 1,
93 Critical = 2,
95 Error = 3,
97 Warning = 4,
99 Notice = 5,
101 Info = 6,
103 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#[derive(Debug, Clone)]
120pub struct SyslogConfig {
121 pub server_addr: String,
123 pub server_port: u16,
125 pub transport: SyslogTransport,
127 pub protocol: SyslogProtocol,
129 pub facility: SyslogFacility,
131 pub app_name: String,
133 pub hostname: String,
135 pub process_id: u32,
137 pub enable_tls: bool,
139}
140
141impl SyslogConfig {
142 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 pub fn with_transport(mut self, transport: SyslogTransport) -> Self {
162 self.transport = transport;
163 self
164 }
165
166 pub fn with_protocol(mut self, protocol: SyslogProtocol) -> Self {
168 self.protocol = protocol;
169 self
170 }
171
172 pub fn with_facility(mut self, facility: SyslogFacility) -> Self {
174 self.facility = facility;
175 self
176 }
177
178 pub fn with_app_name(mut self, app_name: String) -> Self {
180 self.app_name = app_name;
181 self
182 }
183
184 pub fn with_tls(mut self, enable: bool) -> Self {
186 self.enable_tls = enable;
187 self
188 }
189
190 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#[derive(Debug)]
201pub struct SyslogClient {
202 config: SyslogConfig,
203 message_count: u64,
206}
207
208impl SyslogClient {
209 pub fn new(config: SyslogConfig) -> Result<Self> {
211 Ok(Self {
212 config,
213 message_count: 0,
214 })
215 }
216
217 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 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 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 Ok(format!(
248 "<{}>{} {} {} {}",
249 priority, timestamp, self.config.hostname, tag, event.action
250 ))
251 }
252
253 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 let structured_data = self.format_structured_data(event);
261
262 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 fn format_rfc3164_timestamp(&self, timestamp: &chrono::DateTime<chrono::Utc>) -> String {
278 timestamp.format("%b %d %H:%M:%S").to_string()
280 }
281
282 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 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(); }
330
331 sd
332 }
333
334 fn escape_sd_param(&self, value: &str) -> String {
336 value
337 .replace('\\', "\\\\")
338 .replace('"', "\\\"")
339 .replace(']', "\\]")
340 }
341
342 fn calculate_priority(&self, severity: SyslogSeverity) -> u8 {
344 (self.config.facility as u8) * 8 + (severity as u8)
345 }
346
347 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 fn send_tcp(&self, message: &str) -> Result<()> {
362 let addr = self.config.socket_addr()?;
363
364 let mut stream = TcpStream::connect(addr)
366 .map_err(|e| TorshError::InvalidArgument(format!("TCP connect error: {}", e)))?;
367
368 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 fn send_unix(&self, _message: &str) -> Result<()> {
384 #[cfg(unix)]
387 {
388 }
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 pub fn message_count(&self) -> u64 {
405 self.message_count
406 }
407
408 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#[derive(Debug, Clone)]
421pub struct SyslogStatistics {
422 pub messages_sent: u64,
424 pub messages_failed: u64,
426 pub bytes_sent: u64,
428 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 assert_eq!(client.calculate_priority(SyslogSeverity::Info), 134);
460
461 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")); assert!(message.contains("PKG-DOWNLOAD"));
535 assert!(message.contains("test"));
536 assert!(message.contains("Test message"));
537 }
538}