1#![deny(unsafe_code)]
14#![allow(dead_code)]
15
16pub mod authenticity;
17pub mod blocking;
18pub mod chain_of_custody;
19pub mod clone_detection;
20pub mod compression;
21pub mod compression_history;
22pub mod copy_detect;
23pub mod edit_history;
24pub mod ela;
25pub mod ela_analysis;
26pub mod file_integrity;
27pub mod fingerprint;
28pub mod flat_array2;
29pub mod format_forensics;
30pub mod frame_forensics;
31pub mod frequency_forensics;
32pub mod geometric;
33pub mod hash_registry;
34pub mod lighting;
35pub mod metadata;
36pub mod metadata_forensics;
37pub mod noise;
38pub mod noise_analysis;
39pub mod pattern;
40pub mod provenance;
41pub mod report;
42pub mod shadow_analysis;
43pub mod source_camera;
44pub mod splicing;
45pub mod steganalysis;
46pub mod tampering;
47pub mod time_forensics;
48pub mod watermark_detect;
49
50use rayon::prelude::*;
51use serde::{Deserialize, Serialize};
52use std::collections::HashMap;
53use std::path::PathBuf;
54use thiserror::Error;
55
56#[derive(Error, Debug)]
58pub enum ForensicsError {
59 #[error("Invalid image data: {0}")]
60 InvalidImage(String),
61
62 #[error("Analysis failed: {0}")]
63 AnalysisFailed(String),
64
65 #[error("Unsupported format: {0}")]
66 UnsupportedFormat(String),
67
68 #[error("IO error: {0}")]
69 IoError(#[from] std::io::Error),
70
71 #[error("Image processing error: {0}")]
72 ImageError(#[from] image::ImageError),
73}
74
75pub type ForensicsResult<T> = Result<T, ForensicsError>;
77
78#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
80pub enum ConfidenceLevel {
81 VeryLow,
83 Low,
85 Medium,
87 High,
89 VeryHigh,
91}
92
93impl ConfidenceLevel {
94 pub fn from_score(score: f64) -> Self {
96 match score {
97 s if s < 0.2 => ConfidenceLevel::VeryLow,
98 s if s < 0.4 => ConfidenceLevel::Low,
99 s if s < 0.6 => ConfidenceLevel::Medium,
100 s if s < 0.8 => ConfidenceLevel::High,
101 _ => ConfidenceLevel::VeryHigh,
102 }
103 }
104
105 pub fn to_score(&self) -> f64 {
107 match self {
108 ConfidenceLevel::VeryLow => 0.1,
109 ConfidenceLevel::Low => 0.3,
110 ConfidenceLevel::Medium => 0.5,
111 ConfidenceLevel::High => 0.7,
112 ConfidenceLevel::VeryHigh => 0.9,
113 }
114 }
115}
116
117#[derive(Debug, Clone, Serialize, Deserialize)]
119pub struct ForensicTest {
120 pub name: String,
122 pub tampering_detected: bool,
124 pub confidence: f64,
126 pub findings: Vec<String>,
128 #[serde(skip)]
130 pub anomaly_map: Option<flat_array2::FlatArray2<f64>>,
131}
132
133impl ForensicTest {
134 pub fn new(name: &str) -> Self {
136 Self {
137 name: name.to_string(),
138 tampering_detected: false,
139 confidence: 0.0,
140 findings: Vec::new(),
141 anomaly_map: None,
142 }
143 }
144
145 pub fn add_finding(&mut self, finding: String) {
147 self.findings.push(finding);
148 }
149
150 pub fn set_confidence(&mut self, confidence: f64) {
152 self.confidence = confidence.clamp(0.0, 1.0);
153 }
154
155 pub fn confidence_level(&self) -> ConfidenceLevel {
157 ConfidenceLevel::from_score(self.confidence)
158 }
159}
160
161#[derive(Debug, Clone, Serialize, Deserialize)]
163pub struct TamperingReport {
164 pub tampering_detected: bool,
166 pub overall_confidence: f64,
168 pub tests: HashMap<String, ForensicTest>,
170 pub summary: String,
172 pub recommendations: Vec<String>,
174}
175
176impl TamperingReport {
177 pub fn new() -> Self {
179 Self {
180 tampering_detected: false,
181 overall_confidence: 0.0,
182 tests: HashMap::new(),
183 summary: String::new(),
184 recommendations: Vec::new(),
185 }
186 }
187
188 pub fn add_test(&mut self, test: ForensicTest) {
190 self.tests.insert(test.name.clone(), test);
191 }
192
193 pub fn calculate_overall_confidence(&mut self) {
207 if self.tests.is_empty() {
208 self.overall_confidence = 0.0;
209 return;
210 }
211
212 let mut weighted_sum = 0.0_f64;
213 let mut weight_total = 0.0_f64;
214
215 for test in self.tests.values() {
216 let base_weight = test_reliability_weight(&test.name);
217 let effective_weight = if test.tampering_detected {
221 base_weight * 1.2
222 } else {
223 base_weight
224 };
225
226 weighted_sum += test.confidence * effective_weight;
227 weight_total += effective_weight;
228 }
229
230 self.overall_confidence = if weight_total > 0.0 {
231 (weighted_sum / weight_total).clamp(0.0, 1.0)
232 } else {
233 0.0
234 };
235
236 self.tampering_detected = self.overall_confidence > 0.5;
238 }
239
240 pub fn calculate_overall_confidence_unweighted(&mut self) {
245 if self.tests.is_empty() {
246 self.overall_confidence = 0.0;
247 return;
248 }
249
250 let total: f64 = self.tests.values().map(|t| t.confidence).sum();
251 self.overall_confidence = total / self.tests.len() as f64;
252 self.tampering_detected = self.overall_confidence > 0.5;
253 }
254
255 pub fn to_json(&self) -> String {
260 serde_json::to_string_pretty(self).unwrap_or_else(|_| "{}".to_string())
261 }
262
263 pub fn generate_summary(&mut self) {
265 let num_tests = self.tests.len();
266 let num_positive = self.tests.values().filter(|t| t.tampering_detected).count();
267
268 if self.tampering_detected {
269 self.summary = format!(
270 "Tampering detected with {:.1}% confidence. {} out of {} tests indicated manipulation.",
271 self.overall_confidence * 100.0,
272 num_positive,
273 num_tests
274 );
275 } else {
276 self.summary = format!(
277 "No significant tampering detected. {} out of {} tests passed.",
278 num_tests - num_positive,
279 num_tests
280 );
281 }
282 }
283}
284
285impl Default for TamperingReport {
286 fn default() -> Self {
287 Self::new()
288 }
289}
290
291#[derive(Debug, Clone, Serialize, Deserialize)]
293pub struct ForensicsConfig {
294 pub enable_compression_analysis: bool,
296 pub enable_ela: bool,
298 pub enable_noise_analysis: bool,
300 pub enable_metadata_analysis: bool,
302 pub enable_geometric_analysis: bool,
304 pub enable_lighting_analysis: bool,
306 pub min_confidence_threshold: f64,
308}
309
310impl Default for ForensicsConfig {
311 fn default() -> Self {
312 Self {
313 enable_compression_analysis: true,
314 enable_ela: true,
315 enable_noise_analysis: true,
316 enable_metadata_analysis: true,
317 enable_geometric_analysis: true,
318 enable_lighting_analysis: true,
319 min_confidence_threshold: 0.5,
320 }
321 }
322}
323
324#[derive(Debug, Clone, Serialize, Deserialize)]
333pub struct TestWeight {
334 pub ela: f64,
336 pub prnu: f64,
338 pub copy_move: f64,
340 pub compression: f64,
342 pub lighting: f64,
344 pub metadata: f64,
346 pub default_weight: f64,
348}
349
350impl Default for TestWeight {
351 fn default() -> Self {
352 Self {
353 ela: 1.3,
354 prnu: 1.0,
355 copy_move: 1.5,
356 compression: 1.2,
357 lighting: 0.9,
358 metadata: 0.7,
359 default_weight: 1.0,
360 }
361 }
362}
363
364impl TestWeight {
365 #[must_use]
367 pub fn weight_for(&self, test_name: &str) -> f64 {
368 let lower = test_name.to_lowercase();
369 if lower.contains("copy") || lower.contains("geometric") || lower.contains("clone") {
370 self.copy_move
371 } else if lower.contains("ela") || lower.contains("error level") {
372 self.ela
373 } else if lower.contains("compress") || lower.contains("dct") || lower.contains("jpeg") {
374 self.compression
375 } else if lower.contains("noise") || lower.contains("prnu") {
376 self.prnu
377 } else if lower.contains("light") || lower.contains("shadow") || lower.contains("illuminat")
378 {
379 self.lighting
380 } else if lower.contains("metadata") || lower.contains("exif") || lower.contains("software")
381 {
382 self.metadata
383 } else {
384 self.default_weight
385 }
386 }
387}
388
389pub struct ForensicsAnalyzer {
391 config: ForensicsConfig,
392}
393
394impl ForensicsAnalyzer {
395 pub fn new() -> Self {
397 Self {
398 config: ForensicsConfig::default(),
399 }
400 }
401
402 pub fn with_config(config: ForensicsConfig) -> Self {
404 Self { config }
405 }
406
407 pub fn analyze(&self, image_data: &[u8]) -> ForensicsResult<TamperingReport> {
414 let image = image::load_from_memory(image_data)?;
416 let rgb_image = image.to_rgb8();
417
418 type TaskFn<'a> = Box<dyn Fn() -> ForensicsResult<ForensicTest> + Send + Sync + 'a>;
421
422 let mut tasks: Vec<TaskFn<'_>> = Vec::new();
423
424 if self.config.enable_compression_analysis {
425 tasks.push(Box::new(|| compression::analyze_compression(&rgb_image)));
426 }
427 if self.config.enable_ela {
428 tasks.push(Box::new(|| ela::perform_ela(&rgb_image)));
429 }
430 if self.config.enable_noise_analysis {
431 tasks.push(Box::new(|| noise::analyze_noise(&rgb_image)));
432 }
433 if self.config.enable_geometric_analysis {
434 tasks.push(Box::new(|| geometric::detect_copy_move(&rgb_image)));
435 }
436 if self.config.enable_lighting_analysis {
437 tasks.push(Box::new(|| lighting::analyze_lighting(&rgb_image)));
438 }
439
440 let pixel_results: Vec<ForensicsResult<ForensicTest>> =
442 tasks.par_iter().map(|f| f()).collect();
443
444 let metadata_result: Option<ForensicsResult<ForensicTest>> =
447 if self.config.enable_metadata_analysis {
448 Some(metadata::analyze_metadata(image_data))
449 } else {
450 None
451 };
452
453 let mut report = TamperingReport::new();
455
456 for result in pixel_results {
457 report.add_test(result?);
458 }
459 if let Some(result) = metadata_result {
460 report.add_test(result?);
461 }
462
463 report.calculate_overall_confidence();
465 report.generate_summary();
466
467 Ok(report)
468 }
469
470 pub fn analyze_batch(&self, paths: &[PathBuf]) -> Vec<TamperingReport> {
481 paths
482 .par_iter()
483 .map(|path| {
484 let data = match std::fs::read(path) {
485 Ok(d) => d,
486 Err(e) => {
487 let mut report = TamperingReport::new();
488 report.summary = format!("Failed to read {}: {}", path.display(), e);
489 return report;
490 }
491 };
492 match self.analyze(&data) {
493 Ok(report) => report,
494 Err(e) => {
495 let mut report = TamperingReport::new();
496 report.summary = format!("Analysis failed for {}: {}", path.display(), e);
497 report
498 }
499 }
500 })
501 .collect()
502 }
503
504 pub fn config(&self) -> &ForensicsConfig {
506 &self.config
507 }
508
509 pub fn set_config(&mut self, config: ForensicsConfig) {
511 self.config = config;
512 }
513}
514
515impl Default for ForensicsAnalyzer {
516 fn default() -> Self {
517 Self::new()
518 }
519}
520
521#[must_use]
537pub fn test_reliability_weight(test_name: &str) -> f64 {
538 let lower = test_name.to_lowercase();
539 if lower.contains("copy") || lower.contains("geometric") || lower.contains("clone") {
540 1.5
541 } else if lower.contains("ela") || lower.contains("error level") {
542 1.3
543 } else if lower.contains("compress") || lower.contains("dct") || lower.contains("jpeg") {
544 1.2
545 } else if lower.contains("noise") || lower.contains("prnu") {
546 1.0
547 } else if lower.contains("light") || lower.contains("shadow") || lower.contains("illuminat") {
548 0.9
549 } else if lower.contains("metadata") || lower.contains("exif") || lower.contains("software") {
550 0.7
551 } else {
552 1.0
553 }
554}
555
556#[cfg(test)]
557mod tests {
558 use super::*;
559
560 #[test]
561 fn test_confidence_level_conversion() {
562 assert_eq!(ConfidenceLevel::from_score(0.1), ConfidenceLevel::VeryLow);
563 assert_eq!(ConfidenceLevel::from_score(0.3), ConfidenceLevel::Low);
564 assert_eq!(ConfidenceLevel::from_score(0.5), ConfidenceLevel::Medium);
565 assert_eq!(ConfidenceLevel::from_score(0.7), ConfidenceLevel::High);
566 assert_eq!(ConfidenceLevel::from_score(0.9), ConfidenceLevel::VeryHigh);
567 }
568
569 #[test]
570 fn test_forensic_test_creation() {
571 let mut test = ForensicTest::new("Test");
572 assert_eq!(test.name, "Test");
573 assert!(!test.tampering_detected);
574 assert_eq!(test.confidence, 0.0);
575
576 test.add_finding("Finding 1".to_string());
577 test.set_confidence(0.75);
578 assert_eq!(test.findings.len(), 1);
579 assert_eq!(test.confidence, 0.75);
580 }
581
582 #[test]
583 fn test_tampering_report_unweighted() {
584 let mut report = TamperingReport::new();
585
586 let mut test1 = ForensicTest::new("Test1");
587 test1.set_confidence(0.8);
588 test1.tampering_detected = true;
589
590 let mut test2 = ForensicTest::new("Test2");
591 test2.set_confidence(0.6);
592 test2.tampering_detected = true;
593
594 report.add_test(test1);
595 report.add_test(test2);
596 report.calculate_overall_confidence_unweighted();
597
598 assert_eq!(report.tests.len(), 2);
599 assert!((report.overall_confidence - 0.7).abs() < 1e-10);
600 assert!(report.tampering_detected);
601 }
602
603 #[test]
604 fn test_tampering_report_weighted_confidence_empty() {
605 let mut report = TamperingReport::new();
606 report.calculate_overall_confidence();
607 assert!((report.overall_confidence).abs() < 1e-10);
608 assert!(!report.tampering_detected);
609 }
610
611 #[test]
612 fn test_tampering_report_weighted_confidence_single_test() {
613 let mut report = TamperingReport::new();
614 let mut test = ForensicTest::new("ELA Analysis");
615 test.set_confidence(0.8);
616 test.tampering_detected = true;
617 report.add_test(test);
618 report.calculate_overall_confidence();
619 assert!((report.overall_confidence - 0.8).abs() < 1e-10);
621 }
622
623 #[test]
624 fn test_weighted_confidence_strong_test_dominates() {
625 let mut report = TamperingReport::new();
626
627 let mut copy_move = ForensicTest::new("Copy-Move Detection");
629 copy_move.set_confidence(0.9);
630 copy_move.tampering_detected = true;
631
632 let mut metadata = ForensicTest::new("Metadata Analysis");
634 metadata.set_confidence(0.1);
635 metadata.tampering_detected = false;
636
637 report.add_test(copy_move);
638 report.add_test(metadata);
639 report.calculate_overall_confidence();
640
641 assert!(report.overall_confidence > 0.5);
643
644 let mut report2 = TamperingReport::new();
646 let mut cm2 = ForensicTest::new("Copy-Move Detection");
647 cm2.set_confidence(0.9);
648 cm2.tampering_detected = true;
649 let mut md2 = ForensicTest::new("Metadata Analysis");
650 md2.set_confidence(0.1);
651 md2.tampering_detected = false;
652 report2.add_test(cm2);
653 report2.add_test(md2);
654 report2.calculate_overall_confidence_unweighted();
655
656 assert!(report.overall_confidence > report2.overall_confidence);
658 }
659
660 #[test]
661 fn test_weighted_confidence_detection_boost() {
662 let mut report = TamperingReport::new();
664
665 let mut t1 = ForensicTest::new("Noise Analysis");
666 t1.set_confidence(0.6);
667 t1.tampering_detected = true;
668
669 let mut t2 = ForensicTest::new("Noise Check");
670 t2.set_confidence(0.6);
671 t2.tampering_detected = false;
672
673 report.add_test(t1);
674 report.add_test(t2);
675 report.calculate_overall_confidence();
676
677 assert!(report.overall_confidence > 0.59);
680 assert!(report.overall_confidence < 0.65);
681 }
682
683 #[test]
684 fn test_reliability_weight_categories() {
685 assert!((test_reliability_weight("Copy-Move Detection") - 1.5).abs() < 1e-10);
686 assert!((test_reliability_weight("ELA Analysis") - 1.3).abs() < 1e-10);
687 assert!((test_reliability_weight("JPEG Compression Analysis") - 1.2).abs() < 1e-10);
688 assert!((test_reliability_weight("Noise Analysis") - 1.0).abs() < 1e-10);
689 assert!((test_reliability_weight("Lighting Analysis") - 0.9).abs() < 1e-10);
690 assert!((test_reliability_weight("Metadata Analysis") - 0.7).abs() < 1e-10);
691 assert!((test_reliability_weight("Unknown Test") - 1.0).abs() < 1e-10);
692 }
693
694 #[test]
695 fn test_weighted_confidence_all_below_threshold() {
696 let mut report = TamperingReport::new();
697
698 let mut t1 = ForensicTest::new("ELA");
699 t1.set_confidence(0.2);
700 t1.tampering_detected = false;
701
702 let mut t2 = ForensicTest::new("Metadata");
703 t2.set_confidence(0.3);
704 t2.tampering_detected = false;
705
706 report.add_test(t1);
707 report.add_test(t2);
708 report.calculate_overall_confidence();
709
710 assert!(!report.tampering_detected);
711 assert!(report.overall_confidence < 0.5);
712 }
713
714 #[test]
715 fn test_weighted_confidence_clamped_to_unit() {
716 let mut report = TamperingReport::new();
717 let mut t = ForensicTest::new("ELA");
718 t.set_confidence(1.0);
719 t.tampering_detected = true;
720 report.add_test(t);
721 report.calculate_overall_confidence();
722 assert!(report.overall_confidence <= 1.0);
723 }
724
725 #[test]
728 fn test_tampering_report_to_json_empty() {
729 let report = TamperingReport::new();
730 let json = report.to_json();
731 assert!(json.contains("overall_confidence"));
732 assert!(json.contains("tampering_detected"));
733 assert!(json.contains("tests"));
734 }
735
736 #[test]
737 fn test_tampering_report_to_json_with_tests() {
738 let mut report = TamperingReport::new();
739 let mut test = ForensicTest::new("ELA Analysis");
740 test.set_confidence(0.75);
741 test.tampering_detected = true;
742 test.add_finding("Suspicious region detected".to_string());
743 report.add_test(test);
744 report.calculate_overall_confidence();
745 report.generate_summary();
746
747 let json = report.to_json();
748 assert!(json.contains("ELA Analysis"));
749 assert!(json.contains("0.75"));
750 let parsed: serde_json::Value = serde_json::from_str(&json).expect("should be valid JSON");
752 assert!(parsed.get("tampering_detected").is_some());
753 }
754
755 #[test]
756 fn test_tampering_report_to_json_is_valid_json() {
757 let mut report = TamperingReport::new();
758 let mut t1 = ForensicTest::new("Copy-Move Detection");
759 t1.set_confidence(0.9);
760 t1.tampering_detected = true;
761 let mut t2 = ForensicTest::new("Metadata Analysis");
762 t2.set_confidence(0.1);
763 report.add_test(t1);
764 report.add_test(t2);
765 report.calculate_overall_confidence();
766
767 let json = report.to_json();
768 let _parsed: serde_json::Value =
769 serde_json::from_str(&json).expect("to_json must produce valid JSON");
770 }
771
772 #[test]
775 fn test_analyze_batch_empty_paths() {
776 let analyzer = ForensicsAnalyzer::new();
777 let results = analyzer.analyze_batch(&[]);
778 assert!(results.is_empty());
779 }
780
781 #[test]
782 fn test_analyze_batch_nonexistent_path() {
783 let analyzer = ForensicsAnalyzer::new();
784 let path = std::path::PathBuf::from("/nonexistent/path/image.jpg");
785 let results = analyzer.analyze_batch(&[path]);
786 assert_eq!(results.len(), 1);
787 assert!(!results[0].summary.is_empty());
789 }
790
791 #[test]
792 fn test_analyze_batch_valid_images() {
793 use std::io::Cursor;
794 use std::io::Write;
795
796 let img = image::RgbImage::new(128, 128);
799 let dyn_img = image::DynamicImage::ImageRgb8(img);
800 let mut buf = Cursor::new(Vec::new());
801 dyn_img
802 .write_to(&mut buf, image::ImageFormat::Png)
803 .expect("PNG encoding should work");
804 let png_bytes = buf.into_inner();
805
806 let mut tmp = std::env::temp_dir();
808 tmp.push("oximedia_forensics_batch_test.png");
809 {
810 let mut f = std::fs::File::create(&tmp).expect("temp file creation");
811 f.write_all(&png_bytes).expect("write PNG bytes");
812 }
813
814 let analyzer = ForensicsAnalyzer::new();
815 let results = analyzer.analyze_batch(&[tmp.clone()]);
816 assert_eq!(results.len(), 1);
817 assert!(!results[0].summary.is_empty());
819
820 let _ = std::fs::remove_file(&tmp);
822 }
823
824 #[test]
827 fn test_test_weight_defaults() {
828 let tw = TestWeight::default();
829 assert!((tw.ela - 1.3).abs() < 1e-10);
830 assert!((tw.prnu - 1.0).abs() < 1e-10);
831 assert!((tw.copy_move - 1.5).abs() < 1e-10);
832 assert!((tw.compression - 1.2).abs() < 1e-10);
833 assert!((tw.lighting - 0.9).abs() < 1e-10);
834 assert!((tw.metadata - 0.7).abs() < 1e-10);
835 assert!((tw.default_weight - 1.0).abs() < 1e-10);
836 }
837
838 #[test]
839 fn test_test_weight_lookup() {
840 let tw = TestWeight::default();
841 assert!((tw.weight_for("ELA Analysis") - 1.3).abs() < 1e-10);
842 assert!((tw.weight_for("Copy-Move Detection") - 1.5).abs() < 1e-10);
843 assert!((tw.weight_for("JPEG Compression Analysis") - 1.2).abs() < 1e-10);
844 assert!((tw.weight_for("Noise Analysis PRNU") - 1.0).abs() < 1e-10);
845 assert!((tw.weight_for("Lighting Analysis") - 0.9).abs() < 1e-10);
846 assert!((tw.weight_for("Metadata Analysis") - 0.7).abs() < 1e-10);
847 assert!((tw.weight_for("Unknown Custom Test") - 1.0).abs() < 1e-10);
848 }
849
850 #[test]
851 fn test_test_weight_custom_values() {
852 let tw = TestWeight {
853 ela: 2.0,
854 prnu: 0.5,
855 copy_move: 3.0,
856 compression: 1.0,
857 lighting: 0.5,
858 metadata: 0.3,
859 default_weight: 1.5,
860 };
861 assert!((tw.weight_for("ELA") - 2.0).abs() < 1e-10);
862 assert!((tw.weight_for("Clone Detection") - 3.0).abs() < 1e-10);
863 assert!((tw.weight_for("PRNU Extraction") - 0.5).abs() < 1e-10);
864 assert!((tw.weight_for("Shadow Analysis") - 0.5).abs() < 1e-10);
865 assert!((tw.weight_for("EXIF Check") - 0.3).abs() < 1e-10);
866 assert!((tw.weight_for("Anything Else") - 1.5).abs() < 1e-10);
867 }
868}