1use async_trait::async_trait;
7use chrono::{DateTime, Utc};
8use serde::{Deserialize, Serialize};
9use std::path::PathBuf;
10use std::sync::Arc;
11use thiserror::Error;
12use tokio::fs::OpenOptions;
13use tokio::io::AsyncWriteExt;
14
15#[derive(Debug, Error, Clone, Serialize, Deserialize)]
17pub enum AuditError {
18 #[error("Audit IO error: {message}")]
20 IoError { message: String },
21
22 #[error("Audit serialization error: {message}")]
24 SerializationError { message: String },
25
26 #[error("Audit configuration error: {message}")]
28 ConfigurationError { message: String },
29
30 #[error("Audit permission error: {message}")]
32 PermissionError { message: String },
33}
34
35#[derive(Debug, Clone, Serialize, Deserialize)]
37pub struct SecretAuditEvent {
38 pub timestamp: DateTime<Utc>,
40 pub agent_id: String,
42 pub operation: String,
44 pub secret_key: Option<String>,
46 pub outcome: AuditOutcome,
48 pub error_message: Option<String>,
50 pub metadata: Option<serde_json::Value>,
52}
53
54#[derive(Debug, Clone, Serialize, Deserialize)]
56#[serde(rename_all = "lowercase")]
57pub enum AuditOutcome {
58 Success,
60 Failure,
62}
63
64impl SecretAuditEvent {
65 pub fn success(agent_id: String, operation: String, secret_key: Option<String>) -> Self {
67 Self {
68 timestamp: Utc::now(),
69 agent_id,
70 operation,
71 secret_key,
72 outcome: AuditOutcome::Success,
73 error_message: None,
74 metadata: None,
75 }
76 }
77
78 pub fn failure(
80 agent_id: String,
81 operation: String,
82 secret_key: Option<String>,
83 error_message: String,
84 ) -> Self {
85 Self {
86 timestamp: Utc::now(),
87 agent_id,
88 operation,
89 secret_key,
90 outcome: AuditOutcome::Failure,
91 error_message: Some(error_message),
92 metadata: None,
93 }
94 }
95
96 pub fn with_metadata(mut self, metadata: serde_json::Value) -> Self {
98 self.metadata = Some(metadata);
99 self
100 }
101}
102
103#[async_trait]
105pub trait SecretAuditSink: Send + Sync {
106 async fn log_event(&self, event: SecretAuditEvent) -> Result<(), AuditError>;
115}
116
117pub struct JsonFileAuditSink {
119 file_path: PathBuf,
121}
122
123impl JsonFileAuditSink {
124 pub fn new(file_path: PathBuf) -> Self {
132 Self { file_path }
133 }
134
135 async fn ensure_directory_exists(&self) -> Result<(), AuditError> {
137 if let Some(parent) = self.file_path.parent() {
138 tokio::fs::create_dir_all(parent)
139 .await
140 .map_err(|e| AuditError::IoError {
141 message: format!("Failed to create audit log directory: {}", e),
142 })?;
143 }
144 Ok(())
145 }
146}
147
148#[async_trait]
149impl SecretAuditSink for JsonFileAuditSink {
150 async fn log_event(&self, event: SecretAuditEvent) -> Result<(), AuditError> {
151 self.ensure_directory_exists().await?;
153
154 let json_line =
156 serde_json::to_string(&event).map_err(|e| AuditError::SerializationError {
157 message: format!("Failed to serialize audit event: {}", e),
158 })?;
159
160 let mut file = OpenOptions::new()
162 .create(true)
163 .append(true)
164 .open(&self.file_path)
165 .await
166 .map_err(|e| AuditError::IoError {
167 message: format!("Failed to open audit log file: {}", e),
168 })?;
169
170 file.write_all(json_line.as_bytes())
172 .await
173 .map_err(|e| AuditError::IoError {
174 message: format!("Failed to write to audit log: {}", e),
175 })?;
176
177 file.write_all(b"\n")
178 .await
179 .map_err(|e| AuditError::IoError {
180 message: format!("Failed to write newline to audit log: {}", e),
181 })?;
182
183 file.flush().await.map_err(|e| AuditError::IoError {
185 message: format!("Failed to flush audit log: {}", e),
186 })?;
187
188 Ok(())
189 }
190}
191
192pub type BoxedAuditSink = Arc<dyn SecretAuditSink + Send + Sync>;
194
195pub fn create_audit_sink(audit_config: &Option<AuditConfig>) -> Option<BoxedAuditSink> {
197 audit_config.as_ref().map(|config| match config {
198 AuditConfig::JsonFile { file_path } => {
199 Arc::new(JsonFileAuditSink::new(file_path.clone())) as BoxedAuditSink
200 }
201 })
202}
203
204#[derive(Debug, Clone, Serialize, Deserialize)]
206#[serde(tag = "type", rename_all = "lowercase")]
207pub enum AuditConfig {
208 JsonFile {
210 file_path: PathBuf,
212 },
213}
214
215#[cfg(test)]
216mod tests {
217 use super::*;
218 use tempfile::NamedTempFile;
219 use tokio::fs;
220
221 #[tokio::test]
222 async fn test_secret_audit_event_creation() {
223 let event = SecretAuditEvent::success(
224 "agent-123".to_string(),
225 "get_secret".to_string(),
226 Some("api-key".to_string()),
227 );
228
229 assert_eq!(event.agent_id, "agent-123");
230 assert_eq!(event.operation, "get_secret");
231 assert_eq!(event.secret_key, Some("api-key".to_string()));
232 assert!(matches!(event.outcome, AuditOutcome::Success));
233 assert!(event.error_message.is_none());
234 }
235
236 #[tokio::test]
237 async fn test_failure_audit_event() {
238 let event = SecretAuditEvent::failure(
239 "agent-456".to_string(),
240 "get_secret".to_string(),
241 Some("missing-key".to_string()),
242 "Secret not found".to_string(),
243 );
244
245 assert_eq!(event.agent_id, "agent-456");
246 assert!(matches!(event.outcome, AuditOutcome::Failure));
247 assert_eq!(event.error_message, Some("Secret not found".to_string()));
248 }
249
250 #[tokio::test]
251 async fn test_json_file_audit_sink() {
252 let temp_file = NamedTempFile::new().unwrap();
253 let sink = JsonFileAuditSink::new(temp_file.path().to_path_buf());
254
255 let event = SecretAuditEvent::success(
256 "test-agent".to_string(),
257 "get_secret".to_string(),
258 Some("test-key".to_string()),
259 );
260
261 let result = sink.log_event(event.clone()).await;
262 assert!(result.is_ok());
263
264 let content = fs::read_to_string(temp_file.path()).await.unwrap();
266 let lines: Vec<&str> = content.trim().split('\n').collect();
267 assert_eq!(lines.len(), 1);
268
269 let parsed_event: SecretAuditEvent = serde_json::from_str(lines[0]).unwrap();
271 assert_eq!(parsed_event.agent_id, "test-agent");
272 assert_eq!(parsed_event.operation, "get_secret");
273 }
274
275 #[tokio::test]
276 async fn test_multiple_audit_events() {
277 let temp_file = NamedTempFile::new().unwrap();
278 let sink = JsonFileAuditSink::new(temp_file.path().to_path_buf());
279
280 for i in 0..3 {
282 let event =
283 SecretAuditEvent::success(format!("agent-{}", i), "list_secrets".to_string(), None);
284 sink.log_event(event).await.unwrap();
285 }
286
287 let content = fs::read_to_string(temp_file.path()).await.unwrap();
289 let lines: Vec<&str> = content.trim().split('\n').collect();
290 assert_eq!(lines.len(), 3);
291
292 for (i, line) in lines.iter().enumerate() {
294 let parsed_event: SecretAuditEvent = serde_json::from_str(line).unwrap();
295 assert_eq!(parsed_event.agent_id, format!("agent-{}", i));
296 assert_eq!(parsed_event.operation, "list_secrets");
297 }
298 }
299}