use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AuditEvent {
pub id: u64,
pub timestamp: DateTime<Utc>,
pub session_id: String,
pub user: String,
pub operation: OperationType,
pub target: Option<String>,
pub query: String,
pub affected_rows: u64,
pub success: bool,
pub error: Option<String>,
pub metadata: AuditMetadata,
pub checksum: String,
}
impl AuditEvent {
pub fn new(
id: u64,
session_id: String,
user: String,
operation: OperationType,
target: Option<String>,
query: String,
affected_rows: u64,
success: bool,
error: Option<String>,
metadata: AuditMetadata,
) -> Self {
let timestamp = Utc::now();
let mut event = Self {
id,
timestamp,
session_id,
user,
operation,
target,
query,
affected_rows,
success,
error,
metadata,
checksum: String::new(),
};
event.checksum = event.calculate_checksum();
event
}
pub fn calculate_checksum(&self) -> String {
use sha2::{Sha256, Digest};
let mut hasher = Sha256::new();
hasher.update(self.id.to_le_bytes());
hasher.update(self.timestamp.to_rfc3339().as_bytes());
hasher.update(self.session_id.as_bytes());
hasher.update(self.user.as_bytes());
hasher.update(self.operation.to_string().as_bytes());
if let Some(target) = &self.target {
hasher.update(target.as_bytes());
}
hasher.update(self.query.as_bytes());
hasher.update(self.affected_rows.to_le_bytes());
hasher.update([if self.success { 1u8 } else { 0u8 }]);
if let Some(error) = &self.error {
hasher.update(error.as_bytes());
}
let result = hasher.finalize();
format!("{:x}", result)
}
pub fn verify_checksum(&self) -> bool {
let calculated = self.calculate_checksum();
calculated == self.checksum
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub enum OperationType {
CreateTable,
DropTable,
AlterTable,
CreateIndex,
DropIndex,
Insert,
Update,
Delete,
Select,
Begin,
Commit,
Rollback,
Login,
Logout,
GrantPermission,
RevokePermission,
Backup,
Restore,
Vacuum,
Other(String),
}
impl OperationType {
pub fn is_ddl(&self) -> bool {
matches!(
self,
Self::CreateTable
| Self::DropTable
| Self::AlterTable
| Self::CreateIndex
| Self::DropIndex
)
}
pub fn is_dml(&self) -> bool {
matches!(
self,
Self::Insert | Self::Update | Self::Delete | Self::Select
)
}
pub fn is_transaction(&self) -> bool {
matches!(self, Self::Begin | Self::Commit | Self::Rollback)
}
pub fn is_auth(&self) -> bool {
matches!(
self,
Self::Login | Self::Logout | Self::GrantPermission | Self::RevokePermission
)
}
pub fn from_sql_statement(sql: &str) -> Self {
let sql_upper = sql.trim().to_uppercase();
if sql_upper.starts_with("CREATE TABLE") {
Self::CreateTable
} else if sql_upper.starts_with("DROP TABLE") {
Self::DropTable
} else if sql_upper.starts_with("ALTER TABLE") {
Self::AlterTable
} else if sql_upper.starts_with("CREATE INDEX") {
Self::CreateIndex
} else if sql_upper.starts_with("DROP INDEX") {
Self::DropIndex
} else if sql_upper.starts_with("INSERT") {
Self::Insert
} else if sql_upper.starts_with("UPDATE") {
Self::Update
} else if sql_upper.starts_with("DELETE") {
Self::Delete
} else if sql_upper.starts_with("SELECT") {
Self::Select
} else if sql_upper.starts_with("BEGIN") {
Self::Begin
} else if sql_upper.starts_with("COMMIT") {
Self::Commit
} else if sql_upper.starts_with("ROLLBACK") {
Self::Rollback
} else {
Self::Other(sql_upper.split_whitespace().next().unwrap_or("UNKNOWN").to_string())
}
}
}
impl std::fmt::Display for OperationType {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::CreateTable => write!(f, "CREATE_TABLE"),
Self::DropTable => write!(f, "DROP_TABLE"),
Self::AlterTable => write!(f, "ALTER_TABLE"),
Self::CreateIndex => write!(f, "CREATE_INDEX"),
Self::DropIndex => write!(f, "DROP_INDEX"),
Self::Insert => write!(f, "INSERT"),
Self::Update => write!(f, "UPDATE"),
Self::Delete => write!(f, "DELETE"),
Self::Select => write!(f, "SELECT"),
Self::Begin => write!(f, "BEGIN"),
Self::Commit => write!(f, "COMMIT"),
Self::Rollback => write!(f, "ROLLBACK"),
Self::Login => write!(f, "LOGIN"),
Self::Logout => write!(f, "LOGOUT"),
Self::GrantPermission => write!(f, "GRANT_PERMISSION"),
Self::RevokePermission => write!(f, "REVOKE_PERMISSION"),
Self::Backup => write!(f, "BACKUP"),
Self::Restore => write!(f, "RESTORE"),
Self::Vacuum => write!(f, "VACUUM"),
Self::Other(s) => write!(f, "OTHER_{}", s),
}
}
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct AuditMetadata {
pub client_ip: Option<String>,
pub application_name: Option<String>,
pub database_name: Option<String>,
pub execution_time_ms: Option<u64>,
pub custom_fields: std::collections::HashMap<String, String>,
}
#[cfg(test)]
#[allow(clippy::unwrap_used, clippy::expect_used)]
mod tests {
use super::*;
#[test]
fn test_audit_event_checksum() {
let metadata = AuditMetadata::default();
let event = AuditEvent::new(
1,
"session-123".to_string(),
"alice".to_string(),
OperationType::Insert,
Some("users".to_string()),
"INSERT INTO users VALUES (1, 'Alice')".to_string(),
1,
true,
None,
metadata,
);
assert!(!event.checksum.is_empty());
assert!(event.verify_checksum());
}
#[test]
fn test_operation_type_from_sql() {
assert_eq!(
OperationType::from_sql_statement("CREATE TABLE users (id INT)"),
OperationType::CreateTable
);
assert_eq!(
OperationType::from_sql_statement("INSERT INTO users VALUES (1)"),
OperationType::Insert
);
assert_eq!(
OperationType::from_sql_statement("SELECT * FROM users"),
OperationType::Select
);
}
#[test]
fn test_operation_type_classification() {
assert!(OperationType::CreateTable.is_ddl());
assert!(OperationType::Insert.is_dml());
assert!(OperationType::Begin.is_transaction());
assert!(OperationType::Login.is_auth());
}
}