use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::path::PathBuf;
use std::sync::Arc;
use tokio::sync::RwLock;
use uuid::Uuid;
use crate::error::{BitcoinError, Result};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum AuditSeverity {
Info,
Warning,
Critical,
Security,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub enum AuditEventType {
TransactionCreated,
TransactionSigned,
TransactionBroadcasted,
TransactionConfirmed,
WithdrawalRequested,
WithdrawalApproved,
WithdrawalRejected,
WithdrawalCompleted,
AddressGenerated,
KeyAccessed,
ConfigurationChanged,
AdminAction,
SecurityAlert,
LimitExceeded,
Authentication,
Authorization,
PsbtCreated,
PsbtSigned,
MultisigOperation,
HardwareWalletOperation,
Custom(String),
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AuditEvent {
pub id: Uuid,
pub timestamp: DateTime<Utc>,
pub event_type: AuditEventType,
pub severity: AuditSeverity,
pub actor: String,
pub resource: Option<String>,
pub action: String,
pub metadata: HashMap<String, String>,
pub ip_address: Option<String>,
pub session_id: Option<String>,
pub success: bool,
pub error: Option<String>,
}
impl AuditEvent {
pub fn new(
event_type: AuditEventType,
severity: AuditSeverity,
actor: String,
action: String,
) -> Self {
Self {
id: Uuid::new_v4(),
timestamp: Utc::now(),
event_type,
severity,
actor,
resource: None,
action,
metadata: HashMap::new(),
ip_address: None,
session_id: None,
success: true,
error: None,
}
}
pub fn with_resource(mut self, resource: String) -> Self {
self.resource = Some(resource);
self
}
pub fn with_metadata(mut self, key: String, value: String) -> Self {
self.metadata.insert(key, value);
self
}
pub fn with_ip_address(mut self, ip: String) -> Self {
self.ip_address = Some(ip);
self
}
pub fn with_session_id(mut self, session: String) -> Self {
self.session_id = Some(session);
self
}
pub fn with_error(mut self, error: String) -> Self {
self.success = false;
self.error = Some(error);
self
}
}
#[allow(dead_code)]
#[async_trait::async_trait]
pub trait AuditStorage: Send + Sync {
async fn store(&self, event: &AuditEvent) -> Result<()>;
async fn query(&self, criteria: &AuditQueryCriteria) -> Result<Vec<AuditEvent>>;
async fn count(&self, criteria: &AuditQueryCriteria) -> Result<usize>;
}
#[derive(Debug, Clone, Default)]
pub struct AuditQueryCriteria {
pub event_type: Option<AuditEventType>,
pub severity: Option<AuditSeverity>,
pub actor: Option<String>,
pub resource: Option<String>,
pub time_from: Option<DateTime<Utc>>,
pub time_to: Option<DateTime<Utc>>,
pub success: Option<bool>,
pub limit: Option<usize>,
pub offset: Option<usize>,
}
#[derive(Clone)]
pub struct InMemoryAuditStorage {
events: Arc<RwLock<Vec<AuditEvent>>>,
}
impl InMemoryAuditStorage {
pub fn new() -> Self {
Self {
events: Arc::new(RwLock::new(Vec::new())),
}
}
pub async fn all_events(&self) -> Vec<AuditEvent> {
self.events.read().await.clone()
}
pub async fn clear(&self) {
self.events.write().await.clear();
}
}
impl Default for InMemoryAuditStorage {
fn default() -> Self {
Self::new()
}
}
#[async_trait::async_trait]
impl AuditStorage for InMemoryAuditStorage {
async fn store(&self, event: &AuditEvent) -> Result<()> {
self.events.write().await.push(event.clone());
Ok(())
}
async fn query(&self, criteria: &AuditQueryCriteria) -> Result<Vec<AuditEvent>> {
let events = self.events.read().await;
let mut filtered: Vec<AuditEvent> = events
.iter()
.filter(|e| {
if let Some(ref event_type) = criteria.event_type {
if &e.event_type != event_type {
return false;
}
}
if let Some(ref severity) = criteria.severity {
if &e.severity != severity {
return false;
}
}
if let Some(ref actor) = criteria.actor {
if &e.actor != actor {
return false;
}
}
if let Some(ref resource) = criteria.resource {
if e.resource.as_ref() != Some(resource) {
return false;
}
}
if let Some(time_from) = criteria.time_from {
if e.timestamp < time_from {
return false;
}
}
if let Some(time_to) = criteria.time_to {
if e.timestamp > time_to {
return false;
}
}
if let Some(success) = criteria.success {
if e.success != success {
return false;
}
}
true
})
.cloned()
.collect();
if let Some(offset) = criteria.offset {
filtered = filtered.into_iter().skip(offset).collect();
}
if let Some(limit) = criteria.limit {
filtered.truncate(limit);
}
Ok(filtered)
}
async fn count(&self, criteria: &AuditQueryCriteria) -> Result<usize> {
let events = self.query(criteria).await?;
Ok(events.len())
}
}
pub struct FileAuditStorage {
file_path: PathBuf,
}
impl FileAuditStorage {
pub fn new(file_path: PathBuf) -> Result<Self> {
if let Some(parent) = file_path.parent() {
std::fs::create_dir_all(parent).map_err(|e| {
BitcoinError::Validation(format!("Failed to create audit log directory: {}", e))
})?;
}
Ok(Self { file_path })
}
}
#[async_trait::async_trait]
impl AuditStorage for FileAuditStorage {
async fn store(&self, event: &AuditEvent) -> Result<()> {
let json = serde_json::to_string(event).map_err(|e| {
BitcoinError::Validation(format!("Failed to serialize audit event: {}", e))
})?;
use std::io::Write;
let mut file = std::fs::OpenOptions::new()
.create(true)
.append(true)
.open(&self.file_path)
.map_err(|e| {
BitcoinError::Validation(format!("Failed to open audit log file: {}", e))
})?;
writeln!(file, "{}", json).map_err(|e| {
BitcoinError::Validation(format!("Failed to write to audit log: {}", e))
})?;
Ok(())
}
async fn query(&self, criteria: &AuditQueryCriteria) -> Result<Vec<AuditEvent>> {
use std::io::{BufRead, BufReader};
let file = std::fs::File::open(&self.file_path).map_err(|e| {
BitcoinError::Validation(format!("Failed to open audit log file: {}", e))
})?;
let reader = BufReader::new(file);
let mut filtered = Vec::new();
for line in reader.lines() {
let line = line.map_err(|e| {
BitcoinError::Validation(format!("Failed to read audit log line: {}", e))
})?;
let event: AuditEvent = serde_json::from_str(&line).map_err(|e| {
BitcoinError::Validation(format!("Failed to parse audit event: {}", e))
})?;
if let Some(ref event_type) = criteria.event_type {
if &event.event_type != event_type {
continue;
}
}
if let Some(ref severity) = criteria.severity {
if &event.severity != severity {
continue;
}
}
if let Some(ref actor) = criteria.actor {
if &event.actor != actor {
continue;
}
}
if let Some(ref resource) = criteria.resource {
if event.resource.as_ref() != Some(resource) {
continue;
}
}
if let Some(time_from) = criteria.time_from {
if event.timestamp < time_from {
continue;
}
}
if let Some(time_to) = criteria.time_to {
if event.timestamp > time_to {
continue;
}
}
if let Some(success) = criteria.success {
if event.success != success {
continue;
}
}
filtered.push(event);
}
if let Some(offset) = criteria.offset {
filtered = filtered.into_iter().skip(offset).collect();
}
if let Some(limit) = criteria.limit {
filtered.truncate(limit);
}
Ok(filtered)
}
async fn count(&self, criteria: &AuditQueryCriteria) -> Result<usize> {
let events = self.query(criteria).await?;
Ok(events.len())
}
}
pub struct AuditLogger<S: AuditStorage> {
storage: Arc<S>,
enabled: bool,
}
impl<S: AuditStorage> AuditLogger<S> {
pub fn new(storage: S) -> Self {
Self {
storage: Arc::new(storage),
enabled: true,
}
}
pub async fn log(&self, event: AuditEvent) -> Result<()> {
if !self.enabled {
return Ok(());
}
tracing::info!(
event_id = %event.id,
event_type = ?event.event_type,
severity = ?event.severity,
actor = %event.actor,
"Audit event logged"
);
self.storage.store(&event).await
}
pub async fn query(&self, criteria: &AuditQueryCriteria) -> Result<Vec<AuditEvent>> {
self.storage.query(criteria).await
}
pub async fn count(&self, criteria: &AuditQueryCriteria) -> Result<usize> {
self.storage.count(criteria).await
}
pub fn set_enabled(&mut self, enabled: bool) {
self.enabled = enabled;
}
}
pub struct AuditEventBuilder {
event: AuditEvent,
}
impl AuditEventBuilder {
pub fn new(event_type: AuditEventType, actor: String, action: String) -> Self {
Self {
event: AuditEvent::new(event_type, AuditSeverity::Info, actor, action),
}
}
pub fn severity(mut self, severity: AuditSeverity) -> Self {
self.event.severity = severity;
self
}
pub fn resource(mut self, resource: String) -> Self {
self.event.resource = Some(resource);
self
}
pub fn metadata(mut self, key: String, value: String) -> Self {
self.event.metadata.insert(key, value);
self
}
pub fn ip_address(mut self, ip: String) -> Self {
self.event.ip_address = Some(ip);
self
}
pub fn session_id(mut self, session: String) -> Self {
self.event.session_id = Some(session);
self
}
pub fn failed(mut self, error: String) -> Self {
self.event.success = false;
self.event.error = Some(error);
self
}
pub fn build(self) -> AuditEvent {
self.event
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_audit_event_creation() {
let event = AuditEvent::new(
AuditEventType::TransactionCreated,
AuditSeverity::Info,
"system".to_string(),
"Created new transaction".to_string(),
);
assert_eq!(event.event_type, AuditEventType::TransactionCreated);
assert_eq!(event.severity, AuditSeverity::Info);
assert_eq!(event.actor, "system");
assert!(event.success);
assert!(event.error.is_none());
}
#[test]
fn test_audit_event_builder() {
let event = AuditEventBuilder::new(
AuditEventType::WithdrawalRequested,
"user123".to_string(),
"Requested withdrawal of 1 BTC".to_string(),
)
.severity(AuditSeverity::Critical)
.resource("tx_abc123".to_string())
.metadata("amount".to_string(), "100000000".to_string())
.build();
assert_eq!(event.severity, AuditSeverity::Critical);
assert_eq!(event.resource, Some("tx_abc123".to_string()));
assert_eq!(event.metadata.get("amount"), Some(&"100000000".to_string()));
}
#[tokio::test]
async fn test_in_memory_storage() {
let storage = InMemoryAuditStorage::new();
let event = AuditEvent::new(
AuditEventType::TransactionCreated,
AuditSeverity::Info,
"test".to_string(),
"test action".to_string(),
);
storage.store(&event).await.unwrap();
let events = storage.all_events().await;
assert_eq!(events.len(), 1);
assert_eq!(events[0].actor, "test");
}
#[tokio::test]
async fn test_audit_query() {
let storage = InMemoryAuditStorage::new();
for i in 0..5 {
let event = AuditEvent::new(
AuditEventType::TransactionCreated,
AuditSeverity::Info,
format!("user{}", i),
"test".to_string(),
);
storage.store(&event).await.unwrap();
}
let criteria = AuditQueryCriteria {
actor: Some("user2".to_string()),
..Default::default()
};
let results = storage.query(&criteria).await.unwrap();
assert_eq!(results.len(), 1);
assert_eq!(results[0].actor, "user2");
}
#[tokio::test]
async fn test_audit_logger() {
let storage = InMemoryAuditStorage::new();
let logger = AuditLogger::new(storage.clone());
let event = AuditEvent::new(
AuditEventType::SecurityAlert,
AuditSeverity::Security,
"system".to_string(),
"Suspicious activity detected".to_string(),
);
logger.log(event).await.unwrap();
let events = storage.all_events().await;
assert_eq!(events.len(), 1);
assert_eq!(events[0].severity, AuditSeverity::Security);
}
}