use crate::audit::{AuditEvent, AuditEventType, AuditSeverity};
use std::io::Write;
use std::net::{SocketAddr, TcpStream, UdpSocket};
use torsh_core::error::{Result, TorshError};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum SyslogProtocol {
Rfc3164,
Rfc5424,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum SyslogTransport {
Udp,
Tcp,
Unix,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[repr(u8)]
pub enum SyslogFacility {
Kern = 0,
User = 1,
Mail = 2,
Daemon = 3,
Auth = 4,
Syslog = 5,
Lpr = 6,
News = 7,
Uucp = 8,
Cron = 9,
AuthPriv = 10,
Ftp = 11,
Ntp = 12,
Audit = 13,
Alert = 14,
Clock = 15,
Local0 = 16,
Local1 = 17,
Local2 = 18,
Local3 = 19,
Local4 = 20,
Local5 = 21,
Local6 = 22,
Local7 = 23,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
#[repr(u8)]
pub enum SyslogSeverity {
Emergency = 0,
Alert = 1,
Critical = 2,
Error = 3,
Warning = 4,
Notice = 5,
Info = 6,
Debug = 7,
}
impl From<&AuditSeverity> for SyslogSeverity {
fn from(severity: &AuditSeverity) -> Self {
match severity {
AuditSeverity::Info => SyslogSeverity::Info,
AuditSeverity::Warning => SyslogSeverity::Warning,
AuditSeverity::Error => SyslogSeverity::Error,
AuditSeverity::Critical => SyslogSeverity::Critical,
}
}
}
#[derive(Debug, Clone)]
pub struct SyslogConfig {
pub server_addr: String,
pub server_port: u16,
pub transport: SyslogTransport,
pub protocol: SyslogProtocol,
pub facility: SyslogFacility,
pub app_name: String,
pub hostname: String,
pub process_id: u32,
pub enable_tls: bool,
}
impl SyslogConfig {
pub fn new(server_addr: String, server_port: u16) -> Self {
Self {
server_addr,
server_port,
transport: SyslogTransport::Udp,
protocol: SyslogProtocol::Rfc5424,
facility: SyslogFacility::Local0,
app_name: "torsh-package".to_string(),
hostname: hostname::get()
.ok()
.and_then(|h| h.into_string().ok())
.unwrap_or_else(|| "unknown".to_string()),
process_id: std::process::id(),
enable_tls: false,
}
}
pub fn with_transport(mut self, transport: SyslogTransport) -> Self {
self.transport = transport;
self
}
pub fn with_protocol(mut self, protocol: SyslogProtocol) -> Self {
self.protocol = protocol;
self
}
pub fn with_facility(mut self, facility: SyslogFacility) -> Self {
self.facility = facility;
self
}
pub fn with_app_name(mut self, app_name: String) -> Self {
self.app_name = app_name;
self
}
pub fn with_tls(mut self, enable: bool) -> Self {
self.enable_tls = enable;
self
}
pub fn socket_addr(&self) -> Result<SocketAddr> {
let addr_str = format!("{}:{}", self.server_addr, self.server_port);
addr_str
.parse()
.map_err(|e| TorshError::InvalidArgument(format!("Invalid socket address: {}", e)))
}
}
#[derive(Debug)]
pub struct SyslogClient {
config: SyslogConfig,
message_count: u64,
}
impl SyslogClient {
pub fn new(config: SyslogConfig) -> Result<Self> {
Ok(Self {
config,
message_count: 0,
})
}
pub fn send_event(&mut self, event: &AuditEvent) -> Result<()> {
let severity = SyslogSeverity::from(&event.severity);
let message = self.format_message(event, severity)?;
match self.config.transport {
SyslogTransport::Udp => self.send_udp(&message),
SyslogTransport::Tcp => self.send_tcp(&message),
SyslogTransport::Unix => self.send_unix(&message),
}?;
self.message_count += 1;
Ok(())
}
fn format_message(&self, event: &AuditEvent, severity: SyslogSeverity) -> Result<String> {
match self.config.protocol {
SyslogProtocol::Rfc3164 => self.format_rfc3164(event, severity),
SyslogProtocol::Rfc5424 => self.format_rfc5424(event, severity),
}
}
fn format_rfc3164(&self, event: &AuditEvent, severity: SyslogSeverity) -> Result<String> {
let priority = self.calculate_priority(severity);
let timestamp = self.format_rfc3164_timestamp(&event.timestamp);
let tag = format!("{}[{}]:", self.config.app_name, self.config.process_id);
Ok(format!(
"<{}>{} {} {} {}",
priority, timestamp, self.config.hostname, tag, event.action
))
}
fn format_rfc5424(&self, event: &AuditEvent, severity: SyslogSeverity) -> Result<String> {
let priority = self.calculate_priority(severity);
let timestamp = event.timestamp.to_rfc3339();
let msgid = self.format_msgid(&event.event_type);
let structured_data = self.format_structured_data(event);
Ok(format!(
"<{}>1 {} {} {} {} {} {} {}",
priority,
timestamp,
self.config.hostname,
self.config.app_name,
self.config.process_id,
msgid,
structured_data,
event.action
))
}
fn format_rfc3164_timestamp(&self, timestamp: &chrono::DateTime<chrono::Utc>) -> String {
timestamp.format("%b %d %H:%M:%S").to_string()
}
fn format_msgid(&self, event_type: &AuditEventType) -> String {
match event_type {
AuditEventType::PackageDownload => "PKG-DOWNLOAD",
AuditEventType::PackageUpload => "PKG-UPLOAD",
AuditEventType::PackageDelete => "PKG-DELETE",
AuditEventType::PackageYank => "PKG-YANK",
AuditEventType::PackageUnyank => "PKG-UNYANK",
AuditEventType::UserAuthentication => "USER-AUTH",
AuditEventType::UserAuthorization => "USER-AUTHZ",
AuditEventType::AccessGranted => "ACCESS-GRANTED",
AuditEventType::AccessDenied => "ACCESS-DENIED",
AuditEventType::RoleAssigned => "ROLE-ASSIGNED",
AuditEventType::RoleRevoked => "ROLE-REVOKED",
AuditEventType::PermissionChanged => "PERM-CHANGED",
AuditEventType::SecurityViolation => "SECURITY-VIOLATION",
AuditEventType::IntegrityCheck => "INTEGRITY-CHECK",
AuditEventType::SignatureVerification => "SIGNATURE-VERIFY",
AuditEventType::ConfigurationChange => "CONFIG-CHANGE",
AuditEventType::SystemEvent => "SYSTEM-EVENT",
}
.to_string()
}
fn format_structured_data(&self, event: &AuditEvent) -> String {
let mut sd = String::from("[torsh@32473");
if let Some(user_id) = &event.user_id {
sd.push_str(&format!(" user=\"{}\"", self.escape_sd_param(user_id)));
}
if let Some(ip) = &event.ip_address {
sd.push_str(&format!(" ip=\"{}\"", self.escape_sd_param(ip)));
}
if let Some(resource) = &event.resource {
sd.push_str(&format!(" resource=\"{}\"", self.escape_sd_param(resource)));
}
sd.push_str(&format!(" severity=\"{:?}\"", event.severity));
sd.push_str(&format!(" event_type=\"{:?}\"", event.event_type));
sd.push(']');
if sd == "[torsh@32473]" {
return "-".to_string(); }
sd
}
fn escape_sd_param(&self, value: &str) -> String {
value
.replace('\\', "\\\\")
.replace('"', "\\\"")
.replace(']', "\\]")
}
fn calculate_priority(&self, severity: SyslogSeverity) -> u8 {
(self.config.facility as u8) * 8 + (severity as u8)
}
fn send_udp(&self, message: &str) -> Result<()> {
let socket = UdpSocket::bind("0.0.0.0:0")
.map_err(|e| TorshError::InvalidArgument(format!("UDP bind error: {}", e)))?;
let addr = self.config.socket_addr()?;
socket
.send_to(message.as_bytes(), addr)
.map_err(|e| TorshError::InvalidArgument(format!("UDP send error: {}", e)))?;
Ok(())
}
fn send_tcp(&self, message: &str) -> Result<()> {
let addr = self.config.socket_addr()?;
let mut stream = TcpStream::connect(addr)
.map_err(|e| TorshError::InvalidArgument(format!("TCP connect error: {}", e)))?;
let message_with_newline = format!("{}\n", message);
stream
.write_all(message_with_newline.as_bytes())
.map_err(|e| TorshError::InvalidArgument(format!("TCP write error: {}", e)))?;
stream
.flush()
.map_err(|e| TorshError::InvalidArgument(format!("TCP flush error: {}", e)))?;
Ok(())
}
fn send_unix(&self, _message: &str) -> Result<()> {
#[cfg(unix)]
{
}
#[cfg(not(unix))]
{
return Err(TorshError::InvalidArgument(
"Unix sockets not supported on this platform".to_string(),
));
}
Ok(())
}
pub fn message_count(&self) -> u64 {
self.message_count
}
pub fn get_statistics(&self) -> SyslogStatistics {
SyslogStatistics {
messages_sent: self.message_count,
messages_failed: 0,
bytes_sent: 0,
connection_errors: 0,
}
}
}
#[derive(Debug, Clone)]
pub struct SyslogStatistics {
pub messages_sent: u64,
pub messages_failed: u64,
pub bytes_sent: u64,
pub connection_errors: u64,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_syslog_config() {
let config = SyslogConfig::new("localhost".to_string(), 514)
.with_transport(SyslogTransport::Udp)
.with_protocol(SyslogProtocol::Rfc5424)
.with_facility(SyslogFacility::Local0)
.with_app_name("test-app".to_string());
assert_eq!(config.server_addr, "localhost");
assert_eq!(config.server_port, 514);
assert_eq!(config.transport, SyslogTransport::Udp);
assert_eq!(config.protocol, SyslogProtocol::Rfc5424);
assert_eq!(config.app_name, "test-app");
}
#[test]
fn test_priority_calculation() {
let config =
SyslogConfig::new("localhost".to_string(), 514).with_facility(SyslogFacility::Local0);
let client = SyslogClient::new(config).unwrap();
assert_eq!(client.calculate_priority(SyslogSeverity::Info), 134);
assert_eq!(client.calculate_priority(SyslogSeverity::Error), 131);
}
#[test]
fn test_severity_conversion() {
assert_eq!(
SyslogSeverity::from(&AuditSeverity::Info),
SyslogSeverity::Info
);
assert_eq!(
SyslogSeverity::from(&AuditSeverity::Warning),
SyslogSeverity::Warning
);
assert_eq!(
SyslogSeverity::from(&AuditSeverity::Error),
SyslogSeverity::Error
);
assert_eq!(
SyslogSeverity::from(&AuditSeverity::Critical),
SyslogSeverity::Critical
);
}
#[test]
fn test_msgid_formatting() {
let config = SyslogConfig::new("localhost".to_string(), 514);
let client = SyslogClient::new(config).unwrap();
assert_eq!(
client.format_msgid(&AuditEventType::PackageDownload),
"PKG-DOWNLOAD"
);
assert_eq!(
client.format_msgid(&AuditEventType::SecurityViolation),
"SECURITY-VIOLATION"
);
assert_eq!(
client.format_msgid(&AuditEventType::AccessDenied),
"ACCESS-DENIED"
);
assert_eq!(
client.format_msgid(&AuditEventType::PermissionChanged),
"PERM-CHANGED"
);
assert_eq!(
client.format_msgid(&AuditEventType::IntegrityCheck),
"INTEGRITY-CHECK"
);
}
#[test]
fn test_structured_data_escaping() {
let config = SyslogConfig::new("localhost".to_string(), 514);
let client = SyslogClient::new(config).unwrap();
let escaped = client.escape_sd_param("test\\value\"with]special");
assert_eq!(escaped, "test\\\\value\\\"with\\]special");
}
#[test]
fn test_rfc5424_formatting() {
let config = SyslogConfig::new("localhost".to_string(), 514)
.with_facility(SyslogFacility::Local0)
.with_app_name("test".to_string());
let client = SyslogClient::new(config).unwrap();
let event = AuditEvent::new(AuditEventType::PackageDownload, "Test message".to_string());
let message = client.format_rfc5424(&event, SyslogSeverity::Info).unwrap();
assert!(message.contains("<134>1")); assert!(message.contains("PKG-DOWNLOAD"));
assert!(message.contains("test"));
assert!(message.contains("Test message"));
}
}