//! Tests for [`super`]. use super::*; #[test] fn layer_verdict_serializes_lowercase() { assert_eq!( serde_json::to_string(&LayerVerdict::Pass).unwrap(), "\"pass\"" ); assert_eq!( serde_json::to_string(&LayerVerdict::Fail).unwrap(), "\"fail\"" ); assert_eq!( serde_json::to_string(&LayerVerdict::Skip).unwrap(), "\"skip\"" ); assert_eq!( serde_json::to_string(&LayerVerdict::Error).unwrap(), "\"error\"" ); } #[test] fn scan_result_quarantined_on_any_fail() { let layers = [ LayerResult { layer: "test1", verdict: LayerVerdict::Pass, detail: None, }, LayerResult { layer: "test2", verdict: LayerVerdict::Fail, detail: Some("bad".to_string()), }, ]; let has_fail = layers.iter().any(|l| l.verdict == LayerVerdict::Fail); assert!(has_fail); } #[test] fn panicked_sync_layer_is_held_for_review_not_clean() { // A file that panics a CPU parser must never come back Clean, it is // held for admin review, and the panic is contained in a returnable // result (not an `.expect` that would unwind the scan worker). let r = panicked_sync_result(4096); assert_eq!(r.status, FileScanStatus::HeldForReview); assert_eq!(r.file_size, 4096); assert!(r.sha256.is_empty()); assert_eq!(r.layers.len(), 1); assert_eq!(r.layers[0].verdict, LayerVerdict::Error); // The synthetic layer must resolve to a FailClosed policy (the default // for unregistered names), which is what makes final_status hold it. assert_eq!(error_policy_for(r.layers[0].layer), ErrorPolicy::FailClosed); } #[test] fn oversize_file_is_held_for_review_not_failed() { // A file above the spool ceiling is held for admin review (explicit, // documented policy) rather than failing the job and stranding the // upload in Pending. Verdict resolves to FailClosed -> HeldForReview. let huge = crate::constants::SCAN_SPOOL_MAX_BYTES + 1; let r = too_large_to_scan(huge); assert_eq!(r.status, FileScanStatus::HeldForReview); assert_eq!(r.file_size, huge); assert!(r.sha256.is_empty()); assert_eq!(r.layers[0].layer, "scan_size_limit"); assert_eq!(error_policy_for(r.layers[0].layer), ErrorPolicy::FailClosed); } #[test] fn sha256_computation() { let mut hasher = Sha256::new(); hasher.update(b"hello"); let hash = hex::encode(hasher.finalize()); assert_eq!( hash, "2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824" ); } // Pipeline integration tests /// Create a minimal ScanPipeline with no external deps (no YARA, no ClamAV, no MalwareBazaar). /// Wrapped in `Arc` because `scan` consumes `Arc` (see `pub async fn scan`). fn make_pipeline() -> std::sync::Arc { std::sync::Arc::new(ScanPipeline { yara_rules: None, yara_rule_count: 0, yara_min_rule_files: 0, clamav_socket: None, clamav_max_scan_bytes: None, malwarebazaar_enabled: false, urlhaus_enabled: false, abuse_ch_auth_key: None, metadefender_api_key: None, }) } /// SEC-S2: a corpus that compiled fewer rule files than the configured floor /// is degraded coverage masquerading as a live layer, boot must fail closed. #[tokio::test] async fn assert_live_refuses_degraded_yara_corpus() { let mut compiler = yara_x::Compiler::new(); compiler .add_source(r#"rule r { strings: $a = "x" condition: $a }"#) .unwrap(); let pipeline = std::sync::Arc::new(ScanPipeline { yara_rules: Some(compiler.build()), yara_rule_count: 1, yara_min_rule_files: 2, clamav_socket: None, clamav_max_scan_bytes: None, malwarebazaar_enabled: false, urlhaus_enabled: false, abuse_ch_auth_key: None, metadefender_api_key: None, }); let err = pipeline .assert_live() .await .expect_err("degraded corpus must refuse boot"); assert!( err.contains("YARA corpus degraded"), "unexpected error: {err}" ); } /// The complement: a corpus meeting the floor boots, with YARA counted live. #[tokio::test] async fn assert_live_accepts_corpus_meeting_floor() { let mut compiler = yara_x::Compiler::new(); compiler .add_source(r#"rule r { strings: $a = "x" condition: $a }"#) .unwrap(); let pipeline = std::sync::Arc::new(ScanPipeline { yara_rules: Some(compiler.build()), yara_rule_count: 3, yara_min_rule_files: 3, clamav_socket: None, clamav_max_scan_bytes: None, malwarebazaar_enabled: false, urlhaus_enabled: false, abuse_ch_auth_key: None, metadefender_api_key: None, }); pipeline .assert_live() .await .expect("a corpus meeting the floor must boot"); } #[tokio::test] async fn pipeline_clean_download_passes() { let pipeline = make_pipeline(); let result = pipeline .clone() .scan(b"just some file content".to_vec(), FileType::Download) .await; assert_eq!(result.status, FileScanStatus::Clean); assert_eq!(result.file_size, 22); assert!(!result.sha256.is_empty()); assert_eq!(result.layers.len(), 12); } #[tokio::test] async fn pipeline_unrecognized_audio_quarantined() { let pipeline = make_pipeline(); // Unrecognized data claimed as audio should be rejected by content_type layer let result = pipeline .clone() .scan(b"audio data here".to_vec(), FileType::Audio) .await; assert_eq!(result.status, FileScanStatus::Quarantined); } #[tokio::test] async fn pipeline_clean_cover_passes() { let pipeline = make_pipeline(); // PNG magic bytes let png = [0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A]; let result = pipeline.clone().scan(png.to_vec(), FileType::Cover).await; assert_eq!(result.status, FileScanStatus::Clean); } #[tokio::test] async fn pipeline_pe_as_audio_quarantined() { let pipeline = make_pipeline(); // PE magic bytes, content-type layer should detect application/* and fail let pe_header = b"MZ\x90\x00\x03\x00\x00\x00"; let result = pipeline .clone() .scan(pe_header.to_vec(), FileType::Audio) .await; assert_eq!(result.status, FileScanStatus::Quarantined); // Verify content_type layer produced the fail let content_type_layer = result .layers .iter() .find(|l| l.layer == "content_type") .unwrap(); assert_eq!(content_type_layer.verdict, LayerVerdict::Fail); } #[tokio::test] async fn pipeline_pe_as_cover_quarantined() { let pipeline = make_pipeline(); let pe_header = b"MZ\x90\x00\x03\x00\x00\x00"; let result = pipeline .clone() .scan(pe_header.to_vec(), FileType::Cover) .await; assert_eq!(result.status, FileScanStatus::Quarantined); } #[tokio::test] async fn pipeline_sha256_is_deterministic() { let pipeline = make_pipeline(); let data = b"deterministic hash test"; let r1 = pipeline .clone() .scan(data.to_vec(), FileType::Download) .await; let r2 = pipeline .clone() .scan(data.to_vec(), FileType::Download) .await; assert_eq!(r1.sha256, r2.sha256); } #[tokio::test] async fn pipeline_skips_optional_layers_when_unconfigured() { let pipeline = make_pipeline(); let result = pipeline .clone() .scan(b"test".to_vec(), FileType::Download) .await; let yara = result.layers.iter().find(|l| l.layer == "yara").unwrap(); assert_eq!(yara.verdict, LayerVerdict::Skip); let clamav = result.layers.iter().find(|l| l.layer == "clamav").unwrap(); assert_eq!(clamav.verdict, LayerVerdict::Skip); let mb = result .layers .iter() .find(|l| l.layer == "malwarebazaar") .unwrap(); assert_eq!(mb.verdict, LayerVerdict::Skip); let uh = result.layers.iter().find(|l| l.layer == "urlhaus").unwrap(); assert_eq!(uh.verdict, LayerVerdict::Skip); } #[tokio::test] async fn pipeline_always_produces_12_layers() { let pipeline = make_pipeline(); for file_type in [FileType::Audio, FileType::Cover, FileType::Download] { let result = pipeline.clone().scan(b"data".to_vec(), file_type).await; // 11 base layers + the recursive archive-interior layer (archive_nested). assert_eq!( result.layers.len(), 12, "Expected 12 layers for {file_type:?}" ); } } #[test] fn suspicion_present_on_fail() { let layers = vec![pass("content_type"), fail("yara")]; assert!(suspicion_present(&layers)); } #[test] fn suspicion_present_on_fail_closed_error() { let layers = vec![pass("content_type"), err("archive")]; assert!(suspicion_present(&layers)); } #[test] fn no_suspicion_when_fail_open_error_only() { // External-layer errors are operational noise, not malware signals; // they must not invoke MetaDefender. let layers = vec![pass("content_type"), err("malwarebazaar"), err("urlhaus")]; assert!(!suspicion_present(&layers)); } #[test] fn no_suspicion_when_all_clean() { let layers = vec![pass("content_type"), skip("yara"), pass("structural")]; assert!(!suspicion_present(&layers)); } #[tokio::test] async fn pipeline_errors_held_for_review() { // Errors from fail-closed layers (archive is in-process deterministic) // should hold the file for admin review. let pipeline = make_pipeline(); // Corrupted ZIP magic bytes, archive layer returns Error let mut data = vec![0x50, 0x4B, 0x03, 0x04]; data.extend_from_slice(&[0xFF; 100]); let result = pipeline.clone().scan(data, FileType::Download).await; let archive = result.layers.iter().find(|l| l.layer == "archive").unwrap(); assert_eq!(archive.verdict, LayerVerdict::Error); assert_eq!(result.status, FileScanStatus::HeldForReview); } // Per-layer fail policy tests fn err(layer: &'static str) -> LayerResult { LayerResult { layer, verdict: LayerVerdict::Error, detail: None, } } fn pass(layer: &'static str) -> LayerResult { LayerResult { layer, verdict: LayerVerdict::Pass, detail: None, } } fn skip(layer: &'static str) -> LayerResult { LayerResult { layer, verdict: LayerVerdict::Skip, detail: None, } } fn fail(layer: &'static str) -> LayerResult { LayerResult { layer, verdict: LayerVerdict::Fail, detail: None, } } #[test] fn yara_tail_unscanned_only_without_backstop() { use crate::constants::SCAN_YARA_MAX_BYTES; let over = SCAN_YARA_MAX_BYTES + 1; // Over the cap, no declared ClamAV coverage → tail unscanned, must hold. assert!(yara_tail_unscanned(over, None)); // Over the cap, ClamAV coverage present but SHORT of the file (Run #24 // MODERATE: clamd reachable but MaxScanSize doesn't reach) → still hold. assert!(yara_tail_unscanned(over, Some(over as u64 - 1))); // Over the cap, declared coverage reaches the file → ClamAV is a real // full-file backstop, don't hold. assert!(!yara_tail_unscanned(over, Some(over as u64))); assert!(!yara_tail_unscanned(over, Some(u64::MAX))); // Within the YARA cap → whole file scanned by YARA regardless of ClamAV. assert!(!yara_tail_unscanned(SCAN_YARA_MAX_BYTES, None)); assert!(!yara_tail_unscanned(1024, None)); } #[test] fn final_status_clean_when_all_pass() { let layers = vec![ pass("content_type"), pass("structural"), pass("archive"), skip("yara"), skip("clamav"), skip("malwarebazaar"), ]; assert_eq!(final_status(&layers), FileScanStatus::Clean); } #[test] fn final_status_quarantined_on_any_fail() { let layers = vec![pass("content_type"), fail("yara"), skip("clamav")]; assert_eq!(final_status(&layers), FileScanStatus::Quarantined); } #[test] fn final_status_fail_beats_error() { // A Fail anywhere supersedes any Error, regardless of policy. let layers = vec![err("malwarebazaar"), fail("yara")]; assert_eq!(final_status(&layers), FileScanStatus::Quarantined); } #[test] fn final_status_held_on_fail_closed_error() { // archive is FailClosed, its Error must hold the file. let layers = vec![pass("content_type"), err("archive"), skip("clamav")]; assert_eq!(final_status(&layers), FileScanStatus::HeldForReview); } #[test] fn final_status_clean_on_fail_open_error_only() { // malwarebazaar is FailOpen, its Error must NOT hold the file. // This is the regression of 2026-05-10 that motivated the audit. let layers = vec![ pass("content_type"), pass("structural"), pass("archive"), skip("yara"), skip("clamav"), err("malwarebazaar"), ]; assert_eq!(final_status(&layers), FileScanStatus::Clean); } #[test] fn final_status_clean_when_all_external_layers_error() { // Worst-case external-services outage: every network/daemon layer // erroring at once. As long as the in-process layers pass, the file // is Clean. Health is surfaced separately via per-layer monitoring. let layers = vec![ pass("content_type"), pass("structural"), pass("archive"), skip("yara"), err("clamav"), err("malwarebazaar"), ]; assert_eq!(final_status(&layers), FileScanStatus::Clean); } #[test] fn clamav_incomplete_is_fail_closed_and_holds() { // CHRONIC S1: a reachable-but-incomplete clamav scan is emitted under the // `clamav_incomplete` layer, which must be FailClosed → HeldForReview, // distinct from the FailOpen `clamav` layer used for an unreachable daemon. assert_eq!( error_policy_for("clamav_incomplete"), ErrorPolicy::FailClosed ); assert_eq!(error_policy_for("clamav"), ErrorPolicy::FailOpen); let layers = vec![ pass("content_type"), pass("structural"), pass("archive"), skip("yara"), err("clamav_incomplete"), ]; assert_eq!(final_status(&layers), FileScanStatus::HeldForReview); } #[test] fn final_status_held_on_unknown_layer_error() { // Defensive default: an unknown layer name that errors falls through // to FailClosed. This is what catches a new layer added without // wiring its policy into `error_policy_for`. let layers = vec![ pass("content_type"), err("brand_new_layer_someone_forgot_to_register"), ]; assert_eq!(final_status(&layers), FileScanStatus::HeldForReview); } #[test] fn error_policy_for_all_known_layers() { // Every layer name produced by the pipeline must have an explicit // declaration in `error_policy_for`. The default branch is reserved // for genuine programmer error (new layer, forgot to register). for name in [ "content_type", "structural", "archive", "yara", "clamav", "malwarebazaar", ] { let policy = error_policy_for(name); // Both values are valid; we just want this to not hit the default. // If a layer is renamed without updating `error_policy_for`, this // test still passes (the rename produces a new unknown name), but // the per-layer name tests below catch that. let _ = policy; } } #[test] fn content_type_is_fail_closed() { assert_eq!(error_policy_for("content_type"), ErrorPolicy::FailClosed); } #[test] fn structural_is_fail_closed() { assert_eq!(error_policy_for("structural"), ErrorPolicy::FailClosed); } #[test] fn archive_is_fail_closed() { assert_eq!(error_policy_for("archive"), ErrorPolicy::FailClosed); } #[test] fn yara_is_fail_closed() { assert_eq!(error_policy_for("yara"), ErrorPolicy::FailClosed); } #[test] fn clamav_is_fail_open() { assert_eq!(error_policy_for("clamav"), ErrorPolicy::FailOpen); } #[test] fn malwarebazaar_is_fail_open() { assert_eq!(error_policy_for("malwarebazaar"), ErrorPolicy::FailOpen); } #[test] fn urlhaus_is_fail_open() { assert_eq!(error_policy_for("urlhaus"), ErrorPolicy::FailOpen); } #[test] fn signing_macos_is_fail_open() { assert_eq!(error_policy_for("signing_macos"), ErrorPolicy::FailOpen); } #[test] fn metadefender_is_fail_open() { assert_eq!(error_policy_for("metadefender"), ErrorPolicy::FailOpen); } #[test] fn signing_windows_is_fail_open() { assert_eq!(error_policy_for("signing_windows"), ErrorPolicy::FailOpen); } #[test] fn signing_linux_is_fail_open() { assert_eq!(error_policy_for("signing_linux"), ErrorPolicy::FailOpen); }