use crate::config::audit::AuditConfig;
use crate::dashboard::error::DashboardError;
use serde::{Deserialize, Serialize};
use std::io::Write;
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct AuditRecord {
pub timestamp: String,
pub method: String,
pub initiator_hash: String,
pub correlation_id: Option<String>,
pub allowed: bool,
pub denial_code: Option<String>,
pub denial_control_point: Option<String>,
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum AuditFailureStrategy {
FailClosed,
DeferBounded,
}
impl AuditFailureStrategy {
pub fn from_config(value: &str) -> Self {
match value {
"defer_bounded" => Self::DeferBounded,
_ => Self::FailClosed,
}
}
}
pub enum AuditBackend {
Memory {
buffer: Vec<AuditRecord>,
position: usize,
},
File {
path: String,
file: std::fs::File,
},
}
impl AuditBackend {
pub fn is_healthy(&self) -> bool {
match self {
Self::Memory { .. } => true,
Self::File { file, .. } => {
file.metadata().is_ok()
}
}
}
pub fn new_memory(capacity: usize) -> Self {
Self::Memory {
buffer: Vec::with_capacity(capacity),
position: 0,
}
}
pub fn new_file(path: String) -> Result<Self, DashboardError> {
let file = std::fs::OpenOptions::new()
.create(true)
.append(true)
.open(&path)
.map_err(|error| {
DashboardError::audit_write_failed(format!(
"failed to open audit file {path}: {error}"
))
})?;
Ok(Self::File { path, file })
}
pub fn from_config(config: &AuditConfig) -> Self {
match config.backend.as_str() {
"file" => {
let path = config.file_path.trim();
if path.is_empty() {
AuditBackend::new_memory(4096)
} else {
match AuditBackend::new_file(path.to_owned()) {
Ok(backend) => backend,
Err(error) => {
tracing::error!(
target: "rust_supervisor::ipc::security::audit",
path = %path,
?error,
"failed to open file audit backend, falling back to memory"
);
AuditBackend::new_memory(4096)
}
}
}
}
_ => AuditBackend::new_memory(4096),
}
}
pub fn write(&mut self, record: &AuditRecord) -> Result<(), DashboardError> {
match self {
Self::Memory { buffer, position } => {
if buffer.len() < buffer.capacity() {
buffer.push(record.clone());
} else {
buffer[*position] = record.clone();
*position = (*position + 1) % buffer.capacity();
}
Ok(())
}
Self::File { path: _, file } => {
let line = serde_json::to_string(record).map_err(|error| {
DashboardError::audit_write_failed(format!(
"audit serialization failed: {error}"
))
})?;
writeln!(file, "{line}").map_err(|error| {
DashboardError::audit_write_failed(format!("file audit write failed: {error}"))
})
}
}
}
pub fn recent(&self, count: usize) -> Vec<AuditRecord> {
match self {
Self::Memory {
buffer,
position: _,
} => {
let start = if buffer.len() > count {
buffer.len() - count
} else {
0
};
buffer[start..].iter().rev().take(count).cloned().collect()
}
Self::File { path, file: _ } => {
let _ = path;
vec![]
}
}
}
}
pub mod alerts {
use std::sync::atomic::{AtomicU64, Ordering};
static AUDIT_WRITE_FAILURES: AtomicU64 = AtomicU64::new(0);
pub fn increment_failure_count() -> u64 {
AUDIT_WRITE_FAILURES.fetch_add(1, Ordering::Relaxed)
}
pub fn failure_count() -> u64 {
AUDIT_WRITE_FAILURES.load(Ordering::Relaxed)
}
}