use std::fs::{self, OpenOptions};
use std::io::Write;
use std::path::PathBuf;
use anyhow::{Context, Result};
use chrono::{DateTime, Local, Utc};
use lettre::message::header::ContentType;
use lettre::transport::smtp::authentication::Credentials;
use lettre::{Message, SmtpTransport, Transport};
use rusqlite::{params, Connection};
use serde::{Deserialize, Serialize};
#[derive(Debug, Serialize, Deserialize)]
pub struct AuditConfig {
log_file: PathBuf,
database_file: PathBuf,
max_log_size: u64,
rotation_count: u32,
alert_email: String,
smtp_server: String,
smtp_port: u16,
smtp_username: String,
smtp_password: String,
critical_events: Vec<String>,
}
impl Default for AuditConfig {
fn default() -> Self {
let os_specific_path = match std::env::consts::OS {
"macos" => PathBuf::from("/var/log/bell"),
"linux" => PathBuf::from("/var/log/bell"),
"bellandeos" => PathBuf::from("/bell/log"),
_ => PathBuf::from("./log"),
};
Self {
log_file: os_specific_path.join("audit.log"),
database_file: os_specific_path.join("audit.db"),
max_log_size: 10 * 1024 * 1024, rotation_count: 5,
alert_email: "admin@bellande-architecture-mechanism-research-innovation-center.org"
.to_string(),
smtp_server: "smtp.bellande-architecture-mechanism-research-innovation-center.org"
.to_string(),
smtp_port: 587,
smtp_username: "alerts@bellande-architecture-mechanism.org".to_string(),
smtp_password: "your_secure_password".to_string(),
critical_events: vec![
"AUTHENTICATION_FAILURE".to_string(),
"PERMISSION_DENIED".to_string(),
"SUSPICIOUS_ACTIVITY".to_string(),
"SECURITY_BREACH".to_string(),
"SYSTEM_MODIFICATION".to_string(),
],
}
}
}
#[derive(Debug, Serialize, Deserialize)]
pub struct AuditEvent {
timestamp: DateTime<Utc>,
user: String,
event: String,
details: String,
system: String,
process_id: u32,
severity: EventSeverity,
}
#[derive(Debug, Serialize, Deserialize, PartialEq)]
pub enum EventSeverity {
Info,
Warning,
Critical,
Emergency,
}
pub async fn log_audit_event(event: &str, user: &str, details: &str) -> Result<()> {
let config = AuditConfig::default();
let audit_event = create_audit_event(event, user, details);
if let Some(parent) = config.log_file.parent() {
fs::create_dir_all(parent).context("Failed to create log directory")?;
}
check_and_rotate_logs(&config).await?;
write_to_log_file(&config, &audit_event).await?;
log_to_database(&config, &audit_event).await?;
if is_critical_event(&config, event) {
send_alert(&config, &audit_event).await?;
}
Ok(())
}
fn create_audit_event(event: &str, user: &str, details: &str) -> AuditEvent {
AuditEvent {
timestamp: Utc::now(),
user: user.to_string(),
event: event.to_string(),
details: details.to_string(),
system: std::env::consts::OS.to_string(),
process_id: std::process::id(),
severity: determine_severity(event),
}
}
async fn write_to_log_file(config: &AuditConfig, event: &AuditEvent) -> Result<()> {
let mut file = OpenOptions::new()
.append(true)
.create(true)
.open(&config.log_file)
.context("Failed to open audit log file")?;
let log_entry = format!(
"{} - User: {} - Event: {} - Details: {} - System: {} - PID: {} - Severity: {:?}\n",
event.timestamp.with_timezone(&Local),
event.user,
event.event,
event.details,
event.system,
event.process_id,
event.severity
);
file.write_all(log_entry.as_bytes())
.context("Failed to write to audit log")?;
Ok(())
}
async fn log_to_database(config: &AuditConfig, event: &AuditEvent) -> Result<()> {
let conn = Connection::open(&config.database_file).context("Failed to open database")?;
conn.execute(
"CREATE TABLE IF NOT EXISTS audit_log (
id INTEGER PRIMARY KEY,
timestamp TEXT NOT NULL,
user TEXT NOT NULL,
event TEXT NOT NULL,
details TEXT NOT NULL,
system TEXT NOT NULL,
process_id INTEGER NOT NULL,
severity TEXT NOT NULL
)",
[],
)
.context("Failed to create audit_log table")?;
conn.execute(
"INSERT INTO audit_log (timestamp, user, event, details, system, process_id, severity)
VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7)",
params![
event.timestamp.to_rfc3339(),
event.user,
event.event,
event.details,
event.system,
event.process_id,
format!("{:?}", event.severity)
],
)
.context("Failed to insert log entry into database")?;
Ok(())
}
fn is_critical_event(config: &AuditConfig, event: &str) -> bool {
config.critical_events.contains(&event.to_string())
}
fn determine_severity(event: &str) -> EventSeverity {
match event {
"AUTHENTICATION_FAILURE" | "PERMISSION_DENIED" => EventSeverity::Warning,
"SUSPICIOUS_ACTIVITY" | "SECURITY_BREACH" => EventSeverity::Critical,
"SYSTEM_MODIFICATION" => EventSeverity::Emergency,
_ => EventSeverity::Info,
}
}
async fn send_alert(config: &AuditConfig, event: &AuditEvent) -> Result<()> {
let email = Message::builder()
.from(config.smtp_username.parse().context("Invalid from address")?)
.to(config.alert_email.parse().context("Invalid to address")?)
.subject(format!("Critical Security Alert: {}", event.event))
.header(ContentType::TEXT_PLAIN)
.body(format!(
"Critical security event detected:\n\nTimestamp: {}\nUser: {}\nEvent: {}\nDetails: {}\nSystem: {}\nProcess ID: {}\nSeverity: {:?}",
event.timestamp.with_timezone(&Local),
event.user,
event.event,
event.details,
event.system,
event.process_id,
event.severity
))
.context("Failed to build email")?;
let creds = Credentials::new(config.smtp_username.clone(), config.smtp_password.clone());
let mailer = SmtpTransport::relay(&config.smtp_server)
.context("Failed to create SMTP transport")?
.credentials(creds)
.port(config.smtp_port)
.build();
mailer.send(&email).context("Failed to send email")?;
Ok(())
}
async fn check_and_rotate_logs(config: &AuditConfig) -> Result<()> {
let metadata = fs::metadata(&config.log_file)?;
if metadata.len() >= config.max_log_size {
for i in (1..config.rotation_count).rev() {
let current = config.log_file.with_extension(format!("log.{}", i));
let next = config.log_file.with_extension(format!("log.{}", i + 1));
if current.exists() {
fs::rename(current, next)?;
}
}
let backup = config.log_file.with_extension("log.1");
fs::rename(&config.log_file, backup)?;
fs::File::create(&config.log_file)?;
}
Ok(())
}