1use ricecoder_providers::audit_log::{AuditLogger, AuditLogEntry, AuditEventType};
8use std::path::PathBuf;
9use std::sync::Arc;
10use tracing::{debug, info, warn};
11
12pub struct ImageAuditLogger {
20 audit_logger: Arc<AuditLogger>,
21}
22
23impl ImageAuditLogger {
24 pub fn new(log_path: PathBuf) -> Self {
30 Self {
31 audit_logger: Arc::new(AuditLogger::new(log_path)),
32 }
33 }
34
35 pub fn log_analysis_request(
45 &self,
46 provider: &str,
47 model: &str,
48 image_count: usize,
49 total_size: u64,
50 image_hashes: Vec<String>,
51 ) -> Result<(), Box<dyn std::error::Error>> {
52 let details = format!(
53 "Image analysis request: {} images, {} bytes total, hashes: {}",
54 image_count,
55 total_size,
56 image_hashes.join(",")
57 );
58
59 let entry = AuditLogEntry::new(
60 AuditEventType::FileAccessed,
61 "ricecoder-images",
62 "system",
63 &format!("{}/{}", provider, model),
64 "request_sent",
65 &details,
66 );
67
68 self.audit_logger.log(&entry)?;
69
70 info!(
71 provider = provider,
72 model = model,
73 image_count = image_count,
74 total_size = total_size,
75 "Image analysis request logged"
76 );
77
78 Ok(())
79 }
80
81 pub fn log_analysis_success(
91 &self,
92 provider: &str,
93 model: &str,
94 image_count: usize,
95 tokens_used: u32,
96 image_hashes: Vec<String>,
97 ) -> Result<(), Box<dyn std::error::Error>> {
98 let details = format!(
99 "Image analysis successful: {} images, {} tokens used, hashes: {}",
100 image_count,
101 tokens_used,
102 image_hashes.join(",")
103 );
104
105 let entry = AuditLogEntry::new(
106 AuditEventType::FileAccessed,
107 "ricecoder-images",
108 "system",
109 &format!("{}/{}", provider, model),
110 "analysis_success",
111 &details,
112 );
113
114 self.audit_logger.log(&entry)?;
115
116 info!(
117 provider = provider,
118 model = model,
119 image_count = image_count,
120 tokens_used = tokens_used,
121 "Image analysis success logged"
122 );
123
124 Ok(())
125 }
126
127 pub fn log_analysis_failure(
137 &self,
138 provider: &str,
139 model: &str,
140 image_count: usize,
141 error: &str,
142 image_hashes: Vec<String>,
143 ) -> Result<(), Box<dyn std::error::Error>> {
144 let details = format!(
145 "Image analysis failed: {} images, error: {}, hashes: {}",
146 image_count,
147 error,
148 image_hashes.join(",")
149 );
150
151 let entry = AuditLogEntry::new(
152 AuditEventType::SecurityError,
153 "ricecoder-images",
154 "system",
155 &format!("{}/{}", provider, model),
156 "analysis_failure",
157 &details,
158 );
159
160 self.audit_logger.log(&entry)?;
161
162 warn!(
163 provider = provider,
164 model = model,
165 image_count = image_count,
166 error = error,
167 "Image analysis failure logged"
168 );
169
170 Ok(())
171 }
172
173 pub fn log_cache_hit(
181 &self,
182 image_hash: &str,
183 provider: &str,
184 age_seconds: u64,
185 ) -> Result<(), Box<dyn std::error::Error>> {
186 let details = format!(
187 "Cache hit for image {}: provider={}, age={}s",
188 image_hash, provider, age_seconds
189 );
190
191 let entry = AuditLogEntry::new(
192 AuditEventType::FileAccessed,
193 "ricecoder-images",
194 "system",
195 &format!("cache/{}", image_hash),
196 "cache_hit",
197 &details,
198 );
199
200 self.audit_logger.log(&entry)?;
201
202 debug!(
203 image_hash = image_hash,
204 provider = provider,
205 age_seconds = age_seconds,
206 "Cache hit logged"
207 );
208
209 Ok(())
210 }
211
212 pub fn log_cache_miss(
218 &self,
219 image_hash: &str,
220 ) -> Result<(), Box<dyn std::error::Error>> {
221 let details = format!("Cache miss for image {}", image_hash);
222
223 let entry = AuditLogEntry::new(
224 AuditEventType::FileAccessed,
225 "ricecoder-images",
226 "system",
227 &format!("cache/{}", image_hash),
228 "cache_miss",
229 &details,
230 );
231
232 self.audit_logger.log(&entry)?;
233
234 debug!(
235 image_hash = image_hash,
236 "Cache miss logged"
237 );
238
239 Ok(())
240 }
241
242 pub fn log_cache_eviction(
249 &self,
250 image_hash: &str,
251 reason: &str,
252 ) -> Result<(), Box<dyn std::error::Error>> {
253 let details = format!("Cache eviction for image {}: reason={}", image_hash, reason);
254
255 let entry = AuditLogEntry::new(
256 AuditEventType::FileModified,
257 "ricecoder-images",
258 "system",
259 &format!("cache/{}", image_hash),
260 "cache_eviction",
261 &details,
262 );
263
264 self.audit_logger.log(&entry)?;
265
266 info!(
267 image_hash = image_hash,
268 reason = reason,
269 "Cache eviction logged"
270 );
271
272 Ok(())
273 }
274
275 pub fn log_invalid_format(
282 &self,
283 file_path: &str,
284 format: &str,
285 ) -> Result<(), Box<dyn std::error::Error>> {
286 let details = format!(
287 "Invalid image format attempt: file={}, format={}",
288 file_path, format
289 );
290
291 let entry = AuditLogEntry::new(
292 AuditEventType::SecurityError,
293 "ricecoder-images",
294 "system",
295 file_path,
296 "invalid_format",
297 &details,
298 );
299
300 self.audit_logger.log(&entry)?;
301
302 warn!(
303 file_path = file_path,
304 format = format,
305 "Invalid format attempt logged"
306 );
307
308 Ok(())
309 }
310
311 pub fn log_file_size_violation(
319 &self,
320 file_path: &str,
321 size_bytes: u64,
322 max_size_bytes: u64,
323 ) -> Result<(), Box<dyn std::error::Error>> {
324 let details = format!(
325 "File size violation: file={}, size={} bytes, max={} bytes",
326 file_path, size_bytes, max_size_bytes
327 );
328
329 let entry = AuditLogEntry::new(
330 AuditEventType::SecurityError,
331 "ricecoder-images",
332 "system",
333 file_path,
334 "size_violation",
335 &details,
336 );
337
338 self.audit_logger.log(&entry)?;
339
340 warn!(
341 file_path = file_path,
342 size_bytes = size_bytes,
343 max_size_bytes = max_size_bytes,
344 "File size violation logged"
345 );
346
347 Ok(())
348 }
349
350 pub fn log_path_traversal_attempt(
356 &self,
357 attempted_path: &str,
358 ) -> Result<(), Box<dyn std::error::Error>> {
359 let details = format!("Path traversal attempt detected: {}", attempted_path);
360
361 let entry = AuditLogEntry::new(
362 AuditEventType::SecurityError,
363 "ricecoder-images",
364 "system",
365 attempted_path,
366 "path_traversal_attempt",
367 &details,
368 );
369
370 self.audit_logger.log(&entry)?;
371
372 warn!(
373 attempted_path = attempted_path,
374 "Path traversal attempt logged"
375 );
376
377 Ok(())
378 }
379
380 pub fn log_analysis_timeout(
389 &self,
390 provider: &str,
391 model: &str,
392 timeout_seconds: u64,
393 image_hashes: Vec<String>,
394 ) -> Result<(), Box<dyn std::error::Error>> {
395 let details = format!(
396 "Image analysis timeout: provider={}, model={}, timeout={}s, hashes: {}",
397 provider,
398 model,
399 timeout_seconds,
400 image_hashes.join(",")
401 );
402
403 let entry = AuditLogEntry::new(
404 AuditEventType::SecurityError,
405 "ricecoder-images",
406 "system",
407 &format!("{}/{}", provider, model),
408 "analysis_timeout",
409 &details,
410 );
411
412 self.audit_logger.log(&entry)?;
413
414 warn!(
415 provider = provider,
416 model = model,
417 timeout_seconds = timeout_seconds,
418 "Analysis timeout logged"
419 );
420
421 Ok(())
422 }
423
424 pub fn audit_logger(&self) -> &AuditLogger {
426 &self.audit_logger
427 }
428}
429
430#[cfg(test)]
431mod tests {
432 use super::*;
433 use tempfile::TempDir;
434
435 fn create_test_logger() -> (ImageAuditLogger, PathBuf, TempDir) {
436 let temp_dir = TempDir::new().unwrap();
437 let log_path = temp_dir.path().join("audit.log");
438 let logger = ImageAuditLogger::new(log_path.clone());
439 (logger, log_path, temp_dir)
440 }
441
442 #[test]
443 fn test_log_analysis_request() {
444 let (logger, log_path, _temp_dir) = create_test_logger();
445
446 let result = logger.log_analysis_request(
447 "openai",
448 "gpt-4-vision",
449 1,
450 1024,
451 vec!["hash1".to_string()],
452 );
453
454 assert!(result.is_ok());
455
456 let content = std::fs::read_to_string(&log_path).unwrap();
457 assert!(content.contains("Image analysis request"));
458 assert!(content.contains("openai"));
459 }
460
461 #[test]
462 fn test_log_analysis_success() {
463 let (logger, log_path, _temp_dir) = create_test_logger();
464
465 let result = logger.log_analysis_success(
466 "openai",
467 "gpt-4-vision",
468 1,
469 100,
470 vec!["hash1".to_string()],
471 );
472
473 assert!(result.is_ok());
474
475 let content = std::fs::read_to_string(&log_path).unwrap();
476 assert!(content.contains("Image analysis successful"));
477 assert!(content.contains("100 tokens"));
478 }
479
480 #[test]
481 fn test_log_analysis_failure() {
482 let (logger, log_path, _temp_dir) = create_test_logger();
483
484 let result = logger.log_analysis_failure(
485 "openai",
486 "gpt-4-vision",
487 1,
488 "Provider error",
489 vec!["hash1".to_string()],
490 );
491
492 assert!(result.is_ok());
493
494 let content = std::fs::read_to_string(&log_path).unwrap();
495 assert!(content.contains("Image analysis failed"));
496 assert!(content.contains("Provider error"));
497 }
498
499 #[test]
500 fn test_log_cache_hit() {
501 let (logger, log_path, _temp_dir) = create_test_logger();
502
503 let result = logger.log_cache_hit("hash1", "openai", 3600);
504
505 assert!(result.is_ok());
506
507 let content = std::fs::read_to_string(&log_path).unwrap();
508 assert!(content.contains("Cache hit"));
509 assert!(content.contains("hash1"));
510 }
511
512 #[test]
513 fn test_log_cache_miss() {
514 let (logger, log_path, _temp_dir) = create_test_logger();
515
516 let result = logger.log_cache_miss("hash1");
517
518 assert!(result.is_ok());
519
520 let content = std::fs::read_to_string(&log_path).unwrap();
521 assert!(content.contains("Cache miss"));
522 assert!(content.contains("hash1"));
523 }
524
525 #[test]
526 fn test_log_cache_eviction() {
527 let (logger, log_path, _temp_dir) = create_test_logger();
528
529 let result = logger.log_cache_eviction("hash1", "LRU");
530
531 assert!(result.is_ok());
532
533 let content = std::fs::read_to_string(&log_path).unwrap();
534 assert!(content.contains("Cache eviction"));
535 assert!(content.contains("LRU"));
536 }
537
538 #[test]
539 fn test_log_invalid_format() {
540 let (logger, log_path, _temp_dir) = create_test_logger();
541
542 let result = logger.log_invalid_format("/path/to/file.bmp", "bmp");
543
544 assert!(result.is_ok());
545
546 let content = std::fs::read_to_string(&log_path).unwrap();
547 assert!(content.contains("Invalid image format"));
548 assert!(content.contains("bmp"));
549 }
550
551 #[test]
552 fn test_log_file_size_violation() {
553 let (logger, log_path, _temp_dir) = create_test_logger();
554
555 let result = logger.log_file_size_violation("/path/to/file.png", 20 * 1024 * 1024, 10 * 1024 * 1024);
556
557 assert!(result.is_ok());
558
559 let content = std::fs::read_to_string(&log_path).unwrap();
560 assert!(content.contains("File size violation"));
561 }
562
563 #[test]
564 fn test_log_path_traversal_attempt() {
565 let (logger, log_path, _temp_dir) = create_test_logger();
566
567 let result = logger.log_path_traversal_attempt("../../etc/passwd");
568
569 assert!(result.is_ok());
570
571 let content = std::fs::read_to_string(&log_path).unwrap();
572 assert!(content.contains("Path traversal attempt"));
573 assert!(content.contains("../../etc/passwd"));
574 }
575
576 #[test]
577 fn test_log_analysis_timeout() {
578 let (logger, log_path, _temp_dir) = create_test_logger();
579
580 let result = logger.log_analysis_timeout(
581 "openai",
582 "gpt-4-vision",
583 10,
584 vec!["hash1".to_string()],
585 );
586
587 assert!(result.is_ok());
588
589 let content = std::fs::read_to_string(&log_path).unwrap();
590 assert!(content.contains("Image analysis timeout"));
591 assert!(content.contains("10s"));
592 }
593
594 #[test]
595 fn test_multiple_audit_entries() {
596 let (logger, log_path, _temp_dir) = create_test_logger();
597
598 logger.log_analysis_request("openai", "gpt-4-vision", 1, 1024, vec!["hash1".to_string()]).unwrap();
599 logger.log_cache_hit("hash1", "openai", 3600).unwrap();
600 logger.log_analysis_success("openai", "gpt-4-vision", 1, 100, vec!["hash1".to_string()]).unwrap();
601
602 let content = std::fs::read_to_string(&log_path).unwrap();
603 let lines: Vec<&str> = content.lines().collect();
604 assert_eq!(lines.len(), 3);
605 }
606}