1use crate::config::models::defaults::default_true;
4use serde::{Deserialize, Serialize};
5use std::path::PathBuf;
6
7use super::types::LogLevel;
8
9#[derive(Debug, Clone, Serialize, Deserialize)]
11pub struct AuditConfig {
12 #[serde(default)]
14 pub enabled: bool,
15
16 #[serde(default)]
18 pub min_level: LogLevel,
19
20 #[serde(default = "default_true")]
22 pub log_requests: bool,
23
24 #[serde(default = "default_true")]
26 pub log_responses: bool,
27
28 #[serde(default = "default_true")]
30 pub log_user_actions: bool,
31
32 #[serde(default)]
34 pub log_request_body: bool,
35
36 #[serde(default)]
38 pub log_response_body: bool,
39
40 #[serde(default = "default_max_body_size")]
42 pub max_body_size: usize,
43
44 #[serde(default)]
46 pub include_headers: Vec<String>,
47
48 #[serde(default = "default_excluded_headers")]
50 pub exclude_headers: Vec<String>,
51
52 #[serde(default = "default_excluded_paths")]
54 pub exclude_paths: Vec<String>,
55
56 #[serde(default)]
58 pub file_output: Option<FileOutputConfig>,
59
60 #[serde(default = "default_buffer_size")]
62 pub buffer_size: usize,
63
64 #[serde(default = "default_flush_interval")]
66 pub flush_interval_ms: u64,
67
68 #[serde(default)]
70 pub retention_days: u32,
71
72 #[serde(default = "default_true")]
74 pub redact_sensitive: bool,
75
76 #[serde(default = "default_redact_patterns")]
78 pub redact_patterns: Vec<String>,
79}
80
81fn default_max_body_size() -> usize {
82 10 * 1024 }
84
85fn default_excluded_headers() -> Vec<String> {
86 vec![
87 "authorization".to_string(),
88 "x-api-key".to_string(),
89 "cookie".to_string(),
90 "set-cookie".to_string(),
91 ]
92}
93
94fn default_excluded_paths() -> Vec<String> {
95 vec![
96 "/health".to_string(),
97 "/metrics".to_string(),
98 "/ready".to_string(),
99 "/live".to_string(),
100 ]
101}
102
103fn default_buffer_size() -> usize {
104 1000
105}
106
107fn default_flush_interval() -> u64 {
108 1000 }
110
111fn default_redact_patterns() -> Vec<String> {
112 vec![
113 r#"sk-[a-zA-Z0-9_-]{20,}"#.to_string(),
114 r#"sk-ant-[a-zA-Z0-9_-]{20,}"#.to_string(),
115 r#"gw-[a-zA-Z0-9_-]{20,}"#.to_string(),
116 r#"AKIA[0-9A-Z]{16}"#.to_string(),
117 r#"(?i)bearer\s+[a-z0-9._~+/=-]{10,}"#.to_string(),
118 r#"eyJ[a-zA-Z0-9_-]+\.[a-zA-Z0-9_-]+\.[a-zA-Z0-9_-]+"#.to_string(),
119 r#"(?i)authorization["']?\s*[:=]\s*["']?[^"',\s}]+"#.to_string(),
120 r#"api[_-]?key["']?\s*[:=]\s*["']?[a-zA-Z0-9-_]+"#.to_string(),
121 ]
122}
123
124impl Default for AuditConfig {
125 fn default() -> Self {
126 Self {
127 enabled: false,
128 min_level: LogLevel::Info,
129 log_requests: true,
130 log_responses: true,
131 log_user_actions: true,
132 log_request_body: false,
133 log_response_body: false,
134 max_body_size: default_max_body_size(),
135 include_headers: Vec::new(),
136 exclude_headers: default_excluded_headers(),
137 exclude_paths: default_excluded_paths(),
138 file_output: None,
139 buffer_size: default_buffer_size(),
140 flush_interval_ms: default_flush_interval(),
141 retention_days: 0,
142 redact_sensitive: true,
143 redact_patterns: default_redact_patterns(),
144 }
145 }
146}
147
148impl AuditConfig {
149 pub fn new() -> Self {
151 Self::default()
152 }
153
154 pub fn enable(mut self) -> Self {
156 self.enabled = true;
157 self
158 }
159
160 pub fn with_min_level(mut self, level: LogLevel) -> Self {
162 self.min_level = level;
163 self
164 }
165
166 pub fn with_file_output(mut self, path: impl Into<PathBuf>) -> Self {
168 self.file_output = Some(FileOutputConfig {
169 path: path.into(),
170 ..Default::default()
171 });
172 self
173 }
174
175 pub fn with_request_body(mut self, enabled: bool) -> Self {
177 self.log_request_body = enabled;
178 self
179 }
180
181 pub fn with_response_body(mut self, enabled: bool) -> Self {
183 self.log_response_body = enabled;
184 self
185 }
186
187 pub fn with_max_body_size(mut self, size: usize) -> Self {
189 self.max_body_size = size;
190 self
191 }
192
193 pub fn exclude_path(mut self, path: impl Into<String>) -> Self {
195 self.exclude_paths.push(path.into());
196 self
197 }
198
199 pub fn with_retention_days(mut self, days: u32) -> Self {
201 self.retention_days = days;
202 self
203 }
204
205 pub fn is_path_excluded(&self, path: &str) -> bool {
207 self.exclude_paths.iter().any(|p| path.starts_with(p))
208 }
209
210 pub fn is_header_excluded(&self, header: &str) -> bool {
212 let header_lower = header.to_lowercase();
213 self.exclude_headers
214 .iter()
215 .any(|h| h.to_lowercase() == header_lower)
216 }
217
218 pub fn validate(&self) -> Result<(), String> {
220 if self.buffer_size == 0 {
221 return Err("audit buffer_size must be greater than zero".to_string());
222 }
223 if self.flush_interval_ms == 0 {
224 return Err("audit flush_interval_ms must be greater than zero".to_string());
225 }
226 Ok(())
227 }
228}
229
230#[derive(Debug, Clone, Serialize, Deserialize)]
232pub struct FileOutputConfig {
233 pub path: PathBuf,
235
236 #[serde(default = "default_true")]
238 pub rotate: bool,
239
240 #[serde(default = "default_max_file_size")]
242 pub max_file_size: u64,
243
244 #[serde(default = "default_max_backups")]
246 pub max_backups: u32,
247
248 #[serde(default)]
250 pub compress: bool,
251}
252
253fn default_max_file_size() -> u64 {
254 100 * 1024 * 1024 }
256
257fn default_max_backups() -> u32 {
258 10
259}
260
261impl Default for FileOutputConfig {
262 fn default() -> Self {
263 Self {
264 path: PathBuf::from(r"./logs/audit.log"),
265 rotate: true,
266 max_file_size: default_max_file_size(),
267 max_backups: default_max_backups(),
268 compress: false,
269 }
270 }
271}
272
273impl FileOutputConfig {
274 pub fn new(path: impl Into<PathBuf>) -> Self {
276 Self {
277 path: path.into(),
278 ..Default::default()
279 }
280 }
281
282 pub fn with_max_size(mut self, size: u64) -> Self {
284 self.max_file_size = size;
285 self
286 }
287
288 pub fn with_max_backups(mut self, count: u32) -> Self {
290 self.max_backups = count;
291 self
292 }
293
294 pub fn with_compression(mut self, enabled: bool) -> Self {
296 self.compress = enabled;
297 self
298 }
299}
300
301#[cfg(test)]
302mod tests {
303 use super::*;
304
305 #[test]
306 fn test_audit_config_default() {
307 let config = AuditConfig::default();
308 assert!(!config.enabled);
309 assert!(config.log_requests);
310 assert!(config.log_responses);
311 assert!(!config.log_request_body);
312 assert!(config.redact_sensitive);
313 }
314
315 #[test]
316 fn test_audit_config_builder() {
317 let config = AuditConfig::new()
318 .enable()
319 .with_min_level(LogLevel::Debug)
320 .with_file_output(r"./logs/test.log")
321 .with_request_body(true)
322 .with_response_body(true)
323 .with_max_body_size(1024)
324 .with_retention_days(30)
325 .exclude_path(r"/internal");
326
327 assert!(config.enabled);
328 assert_eq!(config.min_level, LogLevel::Debug);
329 assert!(config.file_output.is_some());
330 assert!(config.log_request_body);
331 assert!(config.log_response_body);
332 assert_eq!(config.max_body_size, 1024);
333 assert_eq!(config.retention_days, 30);
334 }
335
336 #[test]
337 fn test_path_exclusion() {
338 let config = AuditConfig::default();
339 assert!(config.is_path_excluded(r"/health"));
340 assert!(config.is_path_excluded(r"/health/live"));
341 assert!(config.is_path_excluded(r"/metrics"));
342 assert!(!config.is_path_excluded(r"/v1/chat/completions"));
343 }
344
345 #[test]
346 fn test_header_exclusion() {
347 let config = AuditConfig::default();
348 assert!(config.is_header_excluded("Authorization"));
349 assert!(config.is_header_excluded("authorization"));
350 assert!(config.is_header_excluded("X-API-Key"));
351 assert!(!config.is_header_excluded("Content-Type"));
352 }
353
354 #[test]
355 fn test_default_redact_patterns_compile() {
356 for pattern in default_redact_patterns() {
357 regex::Regex::new(&pattern).unwrap_or_else(|error| {
358 panic!("default redact pattern should compile: {pattern}: {error}")
359 });
360 }
361 }
362
363 #[test]
364 fn test_file_output_config() {
365 let config = FileOutputConfig::new(r"./logs/audit.log")
366 .with_max_size(50 * 1024 * 1024)
367 .with_max_backups(5)
368 .with_compression(true);
369
370 assert_eq!(config.path, PathBuf::from(r"./logs/audit.log"));
371 assert_eq!(config.max_file_size, 50 * 1024 * 1024);
372 assert_eq!(config.max_backups, 5);
373 assert!(config.compress);
374 }
375
376 #[test]
377 fn test_config_validation() {
378 let config = AuditConfig::default();
379 assert!(config.validate().is_ok());
380
381 let enabled_config = AuditConfig::new().enable();
382 assert!(enabled_config.validate().is_ok());
383
384 let mut invalid = AuditConfig::new().enable();
385 invalid.buffer_size = 1;
386 assert!(invalid.validate().is_ok());
387 invalid.buffer_size = 0;
388 assert!(invalid.validate().is_err());
389 invalid.buffer_size = 1;
390 invalid.flush_interval_ms = 0;
391 assert!(invalid.validate().is_err());
392 }
393
394 #[test]
395 fn test_config_serialization() {
396 let config = AuditConfig::new()
397 .enable()
398 .with_file_output(r"./logs/test.log");
399
400 let json = serde_json::to_string(&config).unwrap();
401 let deserialized: AuditConfig = serde_json::from_str(&json).unwrap();
402
403 assert_eq!(config.enabled, deserialized.enabled);
404 assert!(deserialized.file_output.is_some());
405 }
406}