use super::alerting::{AlertManager, ConsoleAlertHandler, FileAlertHandler};
use super::audit_trail::{AuditError, AuditTrail};
use super::events::SecurityEvent;
use super::intrusion_detection::{DetectionError, IntrusionDetector};
use super::metrics::{MetricsCollector, SecurityMetrics, TimeWindow};
use std::path::PathBuf;
use std::sync::Arc;
use thiserror::Error;
use tracing::{error, info, warn};
#[derive(Debug, Error)]
pub enum LoggingError {
#[error("Audit error: {0}")]
AuditError(#[from] AuditError),
#[error("Detection error: {0}")]
DetectionError(#[from] DetectionError),
#[error("Alert error: {0}")]
AlertError(#[from] super::alerting::AlertError),
#[error("Metrics error: {0}")]
MetricsError(#[from] super::metrics::MetricsError),
#[error("IO error: {0}")]
IoError(#[from] std::io::Error),
}
#[allow(clippy::struct_excessive_bools)] #[derive(Debug, Clone, Copy)]
pub struct SecurityFeatures {
pub enable_audit: bool,
pub enable_intrusion_detection: bool,
pub enable_metrics: bool,
pub enable_alerting: bool,
}
impl Default for SecurityFeatures {
fn default() -> Self {
Self {
enable_audit: true,
enable_intrusion_detection: true,
enable_metrics: true,
enable_alerting: true,
}
}
}
#[derive(Debug, Clone)]
pub struct SecurityLoggerConfig {
pub features: SecurityFeatures,
pub audit_repo_path: Option<PathBuf>,
pub alert_log_path: Option<PathBuf>,
}
impl Default for SecurityLoggerConfig {
fn default() -> Self {
Self {
features: SecurityFeatures::default(),
audit_repo_path: None,
alert_log_path: None,
}
}
}
pub struct SecurityLogger {
config: SecurityLoggerConfig,
audit_trail: Option<AuditTrail>,
intrusion_detector: Option<IntrusionDetector>,
metrics: Option<MetricsCollector>,
alert_manager: Option<AlertManager>,
}
impl SecurityLogger {
pub fn new() -> Result<Self, LoggingError> {
Self::with_config(SecurityLoggerConfig::default())
}
pub fn get_config(&self) -> &SecurityLoggerConfig {
&self.config
}
pub fn with_config(config: SecurityLoggerConfig) -> Result<Self, LoggingError> {
let audit_trail = if config.features.enable_audit {
Some(if let Some(ref path) = config.audit_repo_path {
AuditTrail::with_repository(path.clone())?
} else {
AuditTrail::new()
})
} else {
None
};
let intrusion_detector = if config.features.enable_intrusion_detection {
Some(IntrusionDetector::new()?)
} else {
None
};
let metrics = if config.features.enable_metrics {
Some(MetricsCollector::new())
} else {
None
};
let alert_manager = if config.features.enable_alerting {
let mut manager = AlertManager::new();
manager.register_handler(Arc::new(ConsoleAlertHandler));
if let Some(ref path) = config.alert_log_path {
manager.register_handler(Arc::new(FileAlertHandler::new(
path.to_string_lossy().to_string(),
)));
}
Some(manager)
} else {
None
};
Ok(Self {
config,
audit_trail,
intrusion_detector,
metrics,
alert_manager,
})
}
pub fn with_repository(repo_path: PathBuf) -> Result<Self, LoggingError> {
let config = SecurityLoggerConfig {
features: SecurityFeatures {
enable_audit: true,
..Default::default()
},
audit_repo_path: Some(repo_path),
..Default::default()
};
Self::with_config(config)
}
pub fn log(&mut self, event: SecurityEvent) -> Result<(), LoggingError> {
let event_id = event.id.clone();
let severity = event.severity;
let category = event.category.clone();
match severity {
super::events::SecuritySeverity::Critical | super::events::SecuritySeverity::High => {
error!(
event_id = %event_id,
severity = ?severity,
category = ?category,
message = %event.message,
"Security event"
);
}
super::events::SecuritySeverity::Medium => {
warn!(
event_id = %event_id,
severity = ?severity,
category = ?category,
message = %event.message,
"Security event"
);
}
_ => {
info!(
event_id = %event_id,
severity = ?severity,
category = ?category,
message = %event.message,
"Security event"
);
}
}
if let Some(ref mut audit) = self.audit_trail {
audit.append(event.clone())?;
}
if let Some(ref mut metrics) = self.metrics {
metrics.record(event.clone());
}
if let Some(ref mut alerter) = self.alert_manager {
alerter.send_from_event(&event)?;
}
Ok(())
}
pub fn analyze_input(&mut self, input: &str) -> Result<Option<SecurityEvent>, LoggingError> {
if let Some(ref detector) = self.intrusion_detector {
if let Some(event) = detector.analyze_input(input) {
self.log(event.clone())?;
return Ok(Some(event));
}
}
Ok(None)
}
pub fn check_auth_rate(&mut self, source: &str) -> Result<(), LoggingError> {
if let Some(ref mut detector) = self.intrusion_detector {
if let Some(event) = detector.check_auth_rate(source) {
self.log(event)?;
}
}
Ok(())
}
pub fn check_request_rate(&mut self, source: &str) -> Result<(), LoggingError> {
if let Some(ref mut detector) = self.intrusion_detector {
if let Some(event) = detector.check_request_rate(source) {
self.log(event)?;
}
}
Ok(())
}
pub fn get_metrics(&self, window: TimeWindow) -> Option<SecurityMetrics> {
self.metrics.as_ref().map(|m| m.get_metrics(window))
}
pub fn get_metrics_for_last_hour(&self) -> Option<SecurityMetrics> {
self.get_metrics(TimeWindow::Hour)
}
pub fn get_metrics_for_last_day(&self) -> Option<SecurityMetrics> {
self.get_metrics(TimeWindow::Day)
}
pub fn verify_audit_trail(&self) -> Result<bool, LoggingError> {
if let Some(ref audit) = self.audit_trail {
Ok(audit.verify_chain()?)
} else {
Ok(true) }
}
pub fn export_audit_trail(&self) -> Result<String, LoggingError> {
if let Some(ref audit) = self.audit_trail {
Ok(audit.export_json()?)
} else {
Ok("[]".to_string())
}
}
pub fn export_metrics(&self, window: TimeWindow) -> Result<String, LoggingError> {
if let Some(ref metrics) = self.metrics {
Ok(metrics.export_json(window)?)
} else {
Ok("{}".to_string())
}
}
pub fn audit_entry_count(&self) -> usize {
self.audit_trail.as_ref().map(|a| a.len()).unwrap_or(0)
}
pub fn total_events(&self) -> usize {
self.metrics.as_ref().map(|m| m.total_events()).unwrap_or(0)
}
}
impl Default for SecurityLogger {
fn default() -> Self {
Self::new().unwrap_or_else(|e| {
log::error!("Failed to initialize default security logger: {}", e);
Self {
config: SecurityLoggerConfig::default(),
audit_trail: None,
intrusion_detector: None,
metrics: None,
alert_manager: None,
}
})
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::security::events::{EventCategory, SecuritySeverity};
use crate::security::AttackPattern;
use std::net::{IpAddr, Ipv4Addr};
#[test]
fn test_security_logger_creation() {
let result = SecurityLogger::new();
assert!(result.is_ok());
}
#[test]
fn test_log_event() {
let mut logger = SecurityLogger::new().unwrap();
let event = SecurityEvent::new(
SecuritySeverity::High,
EventCategory::Authentication,
"Test event",
);
let result = logger.log(event);
assert!(result.is_ok());
assert_eq!(logger.audit_entry_count(), 1);
assert_eq!(logger.total_events(), 1);
}
#[test]
fn test_analyze_input_attack_detected() {
let mut logger = SecurityLogger::new().unwrap();
let result = logger.analyze_input("SELECT * FROM users WHERE id = 1");
assert!(result.is_ok());
let event = result.unwrap();
assert!(event.is_some());
assert_eq!(event.unwrap().attack_pattern, AttackPattern::SqlInjection);
}
#[test]
fn test_analyze_input_no_attack() {
let mut logger = SecurityLogger::new().unwrap();
let result = logger.analyze_input("normal user input");
assert!(result.is_ok());
assert!(result.unwrap().is_none());
}
#[test]
fn test_check_auth_rate() {
let mut logger = SecurityLogger::new().unwrap();
for _ in 0..6 {
let result = logger.check_auth_rate("192.168.1.1");
assert!(result.is_ok());
}
assert!(logger.total_events() > 0);
}
#[test]
fn test_get_metrics() {
let mut logger = SecurityLogger::new().unwrap();
logger
.log(SecurityEvent::new(
SecuritySeverity::High,
EventCategory::Authentication,
"Event 1",
))
.unwrap();
logger
.log(SecurityEvent::new(
SecuritySeverity::Medium,
EventCategory::Authorization,
"Event 2",
))
.unwrap();
let metrics = logger.get_metrics(TimeWindow::AllTime);
assert!(metrics.is_some());
let m = metrics.unwrap();
assert_eq!(m.total_events, 2);
}
#[test]
fn test_verify_audit_trail() {
let mut logger = SecurityLogger::new().unwrap();
logger
.log(SecurityEvent::new(
SecuritySeverity::Info,
EventCategory::DataAccess,
"Access event",
))
.unwrap();
let result = logger.verify_audit_trail();
assert!(result.is_ok());
assert!(result.unwrap());
}
#[test]
fn test_export_audit_trail() {
let mut logger = SecurityLogger::new().unwrap();
logger
.log(SecurityEvent::new(
SecuritySeverity::Critical,
EventCategory::Integrity,
"Breach detected",
))
.unwrap();
let result = logger.export_audit_trail();
assert!(result.is_ok());
let json = result.unwrap();
assert!(json.contains("Breach detected"));
}
#[test]
fn test_export_metrics() {
let mut logger = SecurityLogger::new().unwrap();
logger
.log(SecurityEvent::new(
SecuritySeverity::High,
EventCategory::Authentication,
"Login failed",
))
.unwrap();
let result = logger.export_metrics(TimeWindow::AllTime);
assert!(result.is_ok());
let json = result.unwrap();
assert!(json.contains("total_events"));
}
#[test]
fn test_disabled_features() {
let config = SecurityLoggerConfig {
features: SecurityFeatures {
enable_audit: false,
enable_intrusion_detection: false,
enable_metrics: false,
enable_alerting: false,
..Default::default()
},
..Default::default()
};
let logger = SecurityLogger::with_config(config).unwrap();
assert!(logger.audit_trail.is_none());
assert!(logger.intrusion_detector.is_none());
assert!(logger.metrics.is_none());
assert!(logger.alert_manager.is_none());
}
#[test]
fn test_with_repository() {
let temp_dir = tempfile::tempdir().unwrap();
let repo_path = temp_dir.path().to_path_buf();
let result = SecurityLogger::with_repository(repo_path);
assert!(result.is_ok());
}
#[test]
fn test_multiple_events_logging() {
let mut logger = SecurityLogger::new().unwrap();
let ip = IpAddr::V4(Ipv4Addr::new(192, 168, 1, 1));
logger
.log(SecurityEvent::authentication_failed("user1", ip))
.unwrap();
logger
.log(SecurityEvent::authorization_failed(
"user2", "/admin", "read",
))
.unwrap();
logger
.log(SecurityEvent::input_validation_failed(
"' OR '1'='1",
AttackPattern::SqlInjection,
))
.unwrap();
assert_eq!(logger.audit_entry_count(), 3);
assert_eq!(logger.total_events(), 3);
let metrics = logger.get_metrics(TimeWindow::AllTime).unwrap();
assert_eq!(metrics.total_events, 3);
assert_eq!(metrics.total_attacks, 1);
assert_eq!(metrics.failed_auth_count, 1);
assert_eq!(metrics.failed_authz_count, 1);
}
#[test]
fn test_logger_with_custom_config() {
let temp_dir = tempfile::tempdir().unwrap();
let config = SecurityLoggerConfig {
features: SecurityFeatures {
enable_audit: true,
enable_intrusion_detection: true,
enable_metrics: true,
enable_alerting: true,
},
audit_repo_path: Some(temp_dir.path().to_path_buf()),
alert_log_path: Some(temp_dir.path().join("alerts.log")),
};
let logger = SecurityLogger::with_config(config);
assert!(logger.is_ok());
}
}