diff --git a/ares-cli/src/dedup/tests.rs b/ares-cli/src/dedup/tests.rs index a7e9eea8..37741985 100644 --- a/ares-cli/src/dedup/tests.rs +++ b/ares-cli/src/dedup/tests.rs @@ -49,7 +49,7 @@ fn make_hash(domain: &str, username: &str, hash_type: &str, hash_value: &str) -> } #[test] -fn test_dedup_users_basic() { +fn dedup_users_basic() { let nb = HashMap::new(); let users = vec![ make_user("contoso.local", "admin"), @@ -61,7 +61,7 @@ fn test_dedup_users_basic() { } #[test] -fn test_dedup_users_case_insensitive() { +fn dedup_users_case_insensitive() { let nb = HashMap::new(); let users = vec![ make_user("CONTOSO.LOCAL", "Admin"), @@ -72,7 +72,7 @@ fn test_dedup_users_case_insensitive() { } #[test] -fn test_dedup_users_different_domains() { +fn dedup_users_different_domains() { let nb = HashMap::new(); let users = vec![ make_user("contoso.local", "admin"), @@ -83,7 +83,7 @@ fn test_dedup_users_different_domains() { } #[test] -fn test_dedup_credentials_basic() { +fn dedup_credentials_basic() { let creds = vec![ make_cred("contoso.local", "admin", "P@ss1"), make_cred("contoso.local", "admin", "P@ss1"), // dup @@ -94,7 +94,7 @@ fn test_dedup_credentials_basic() { } #[test] -fn test_dedup_credentials_case_insensitive_username() { +fn dedup_credentials_case_insensitive_username() { let creds = vec![ make_cred("contoso.local", "Admin", "P@ss1"), make_cred("CONTOSO.LOCAL", "admin", "P@ss1"), @@ -104,7 +104,7 @@ fn test_dedup_credentials_case_insensitive_username() { } #[test] -fn test_dedup_hashes_basic() { +fn dedup_hashes_basic() { let hashes = vec![ make_hash("contoso.local", "admin", "ntlm", "aabbccdd"), make_hash("contoso.local", "admin", "ntlm", "aabbccdd"), // dup @@ -115,7 +115,7 @@ fn test_dedup_hashes_basic() { } #[test] -fn test_dedup_hashes_case_insensitive() { +fn dedup_hashes_case_insensitive() { let hashes = vec![ make_hash("contoso.local", "Admin", "NTLM", "AABBCCDD"), make_hash("CONTOSO.LOCAL", "admin", "ntlm", "aabbccdd"), @@ -125,12 +125,12 @@ fn test_dedup_hashes_case_insensitive() { } #[test] -fn test_normalize_source_label_empty() { +fn normalize_source_label_empty() { assert_eq!(normalize_source_label(""), "Unknown"); } #[test] -fn test_normalize_source_label_exact_match() { +fn normalize_source_label_exact_match() { assert_eq!(normalize_source_label("recon"), "Reconnaissance"); assert_eq!(normalize_source_label("privesc"), "Privilege Escalation"); assert_eq!(normalize_source_label("bloodhound"), "BloodHound"); @@ -138,18 +138,18 @@ fn test_normalize_source_label_exact_match() { } #[test] -fn test_normalize_source_label_case_insensitive() { +fn normalize_source_label_case_insensitive() { assert_eq!(normalize_source_label("RECON"), "Reconnaissance"); assert_eq!(normalize_source_label("BloodHound"), "BloodHound"); } #[test] -fn test_normalize_source_label_dedup_colon() { +fn normalize_source_label_dedup_colon() { assert_eq!(normalize_source_label("recon:recon"), "Reconnaissance"); } #[test] -fn test_normalize_source_label_prefix_match() { +fn normalize_source_label_prefix_match() { assert_eq!( normalize_source_label("privesc_enumeration"), "Privesc Enumeration" @@ -161,7 +161,7 @@ fn test_normalize_source_label_prefix_match() { } #[test] -fn test_normalize_source_label_task_suffix() { +fn normalize_source_label_task_suffix() { assert_eq!( normalize_source_label("recon_abc12345678"), "Reconnaissance" @@ -169,7 +169,7 @@ fn test_normalize_source_label_task_suffix() { } #[test] -fn test_normalize_source_label_fallback() { +fn normalize_source_label_fallback() { assert_eq!( normalize_source_label("some_custom_source"), "Some Custom Source" @@ -177,7 +177,7 @@ fn test_normalize_source_label_fallback() { } #[test] -fn test_normalize_state_domains_corrects_cred_domain() { +fn normalize_state_domains_corrects_cred_domain() { let users = vec![make_user("contoso.local", "admin")]; let mut creds = vec![make_cred("WRONG.local", "admin", "P@ss1")]; let mut hashes = vec![]; @@ -193,7 +193,7 @@ fn test_normalize_state_domains_corrects_cred_domain() { } #[test] -fn test_normalize_state_domains_dedupes_cross_domain_creds() { +fn normalize_state_domains_dedupes_cross_domain_creds() { let users = vec![make_user("contoso.local", "admin")]; let mut creds = vec![ make_cred("contoso.local", "admin", "P@ss1"), @@ -210,7 +210,7 @@ fn test_normalize_state_domains_dedupes_cross_domain_creds() { } #[test] -fn test_normalize_state_domains_preserves_well_known() { +fn normalize_state_domains_preserves_well_known() { let users = vec![ make_user("contoso.local", "administrator"), make_user("child.contoso.local", "administrator"), @@ -233,7 +233,7 @@ fn test_normalize_state_domains_preserves_well_known() { } #[test] -fn test_sanitize_strips_password_prefix() { +fn sanitize_strips_password_prefix() { let mut creds = vec![ make_cred("contoso.local", "jdoe", "Password: jdoe"), make_cred("contoso.local", "admin", "password:secret"), @@ -250,7 +250,7 @@ fn test_sanitize_strips_password_prefix() { } #[test] -fn test_sanitize_removes_password_only() { +fn sanitize_removes_password_only() { let mut creds = vec![ make_cred("contoso.local", "jdoe", "Password"), make_cred("contoso.local", "admin", "password"), @@ -264,7 +264,7 @@ fn test_sanitize_removes_password_only() { } #[test] -fn test_sanitize_strips_trailing_paren_metadata() { +fn sanitize_strips_trailing_paren_metadata() { let mut creds = vec![ make_cred("contoso.local", "svc_test", "svc_test (Guest)"), make_cred("contoso.local", "admin", "P@ss1 (Pwn3d!)"), @@ -278,7 +278,7 @@ fn test_sanitize_strips_trailing_paren_metadata() { } #[test] -fn test_sanitize_normalizes_username_with_at_domain() { +fn sanitize_normalizes_username_with_at_domain() { let mut creds = vec![ make_cred( "fabrikam.local", @@ -300,7 +300,7 @@ fn test_sanitize_normalizes_username_with_at_domain() { } #[test] -fn test_sanitize_preserves_clean_credentials() { +fn sanitize_preserves_clean_credentials() { let mut creds = vec![ make_cred("contoso.local", "admin", "P@ss1"), make_cred("contoso.local", "user1", "Secret123!"), @@ -313,7 +313,7 @@ fn test_sanitize_preserves_clean_credentials() { } #[test] -fn test_sanitize_removes_empty_password_after_strip() { +fn sanitize_removes_empty_password_after_strip() { let mut creds = vec![ make_cred("contoso.local", "jdoe", "Password: "), make_cred("contoso.local", "admin", ""), @@ -323,7 +323,7 @@ fn test_sanitize_removes_empty_password_after_strip() { } #[test] -fn test_sanitize_then_dedup_collapses_variants() { +fn sanitize_then_dedup_collapses_variants() { // jdoe:jdoe is a valid credential; "Password: jdoe" strips to "jdoe" (dup); // "Password" is filtered as noise let mut creds = vec![ @@ -338,7 +338,7 @@ fn test_sanitize_then_dedup_collapses_variants() { } #[test] -fn test_sanitize_keeps_password_equals_username() { +fn sanitize_keeps_password_equals_username() { // password == username is valid (e.g. jdoe:jdoe) let mut creds = vec![ make_cred("contoso.local", "admin", "admin"), @@ -400,8 +400,6 @@ fn make_host(ip: &str, hostname: &str) -> Host { } } -// ==================== normalize_state_domains edge cases ==================== - #[test] fn normalize_state_domains_empty_inputs() { let users: Vec = vec![]; @@ -696,8 +694,6 @@ fn normalize_state_domains_cred_one_domain_no_matching_corrects_best() { assert_eq!(creds[0].domain, "contoso.local"); } -// ==================== dedup_hashes edge cases ==================== - #[test] fn dedup_hashes_normalizes_hash_type() { let hashes = vec![ @@ -814,8 +810,6 @@ fn dedup_hashes_unknown_hash_type_preserved() { assert_eq!(deduped[0].hash_type, "des-cbc-md5"); } -// ==================== normalize_source_label edge cases ==================== - #[test] fn normalize_source_label_task_input_pattern() { assert_eq!( @@ -880,8 +874,6 @@ fn normalize_source_label_mixed_case_prefix_match() { assert_eq!(normalize_source_label("Exploit_something"), "Exploitation"); } -// ==================== dedup_users edge cases ==================== - #[test] fn dedup_users_filters_noise_usernames() { let nb = HashMap::new(); @@ -1025,8 +1017,6 @@ fn dedup_users_empty_source_accepted() { assert_eq!(deduped.len(), 1); } -// ==================== dedup_credentials additional edge cases ==================== - #[test] fn dedup_credentials_strips_trailing_dot_domains() { let creds = vec![ diff --git a/ares-cli/src/detection/markdown.rs b/ares-cli/src/detection/markdown.rs index 3bbbdaa5..d3038890 100644 --- a/ares-cli/src/detection/markdown.rs +++ b/ares-cli/src/detection/markdown.rs @@ -202,19 +202,19 @@ mod tests { } #[test] - fn test_generate_markdown_has_header() { + fn generate_markdown_has_header() { let md = generate_detection_markdown(&make_playbook()); assert!(md.starts_with("# Detection Playbook")); } #[test] - fn test_generate_markdown_has_operation_id() { + fn generate_markdown_has_operation_id() { let md = generate_detection_markdown(&make_playbook()); assert!(md.contains("test-op")); } #[test] - fn test_generate_markdown_has_sections() { + fn generate_markdown_has_sections() { let md = generate_detection_markdown(&make_playbook()); assert!(md.contains("## Executive Summary")); assert!(md.contains("## Attack Statistics")); @@ -224,26 +224,26 @@ mod tests { } #[test] - fn test_generate_markdown_has_ioc_table() { + fn generate_markdown_has_ioc_table() { let md = generate_detection_markdown(&make_playbook()); assert!(md.contains("| Type | Value |")); assert!(md.contains("192.168.58.10")); } #[test] - fn test_generate_markdown_has_footer() { + fn generate_markdown_has_footer() { let md = generate_detection_markdown(&make_playbook()); assert!(md.contains("Generated by Ares Detection Playbook Export")); } #[test] - fn test_generate_markdown_has_logql() { + fn generate_markdown_has_logql() { let md = generate_detection_markdown(&make_playbook()); assert!(md.contains("```logql")); } #[test] - fn test_generate_markdown_domain_admin_no() { + fn generate_markdown_domain_admin_no() { let md = generate_detection_markdown(&make_playbook()); assert!(md.contains("**Domain Admin Achieved:** No")); } diff --git a/ares-cli/src/detection/playbook.rs b/ares-cli/src/detection/playbook.rs index 14fc1a5e..efa8610e 100644 --- a/ares-cli/src/detection/playbook.rs +++ b/ares-cli/src/detection/playbook.rs @@ -193,7 +193,7 @@ mod tests { } #[test] - fn test_generate_playbook_minimal() { + fn generate_playbook_minimal() { let state = make_state(); let playbook = generate_detection_playbook(&state, &[]); assert_eq!(playbook.operation_id, "test-op-001"); @@ -205,7 +205,7 @@ mod tests { } #[test] - fn test_generate_playbook_with_hosts() { + fn generate_playbook_with_hosts() { let mut state = make_state(); state.all_hosts = vec![Host { ip: "192.168.58.10".to_string(), @@ -235,7 +235,7 @@ mod tests { } #[test] - fn test_generate_playbook_with_credentials() { + fn generate_playbook_with_credentials() { let mut state = make_state(); state.all_credentials = vec![Credential { id: String::new(), @@ -259,7 +259,7 @@ mod tests { } #[test] - fn test_generate_playbook_with_hashes() { + fn generate_playbook_with_hashes() { let mut state = make_state(); state.all_hashes = vec![Hash { id: String::new(), @@ -285,7 +285,7 @@ mod tests { } #[test] - fn test_generate_playbook_domain_admin_summary() { + fn generate_playbook_domain_admin_summary() { let mut state = make_state(); state.has_domain_admin = true; state.domain_admin_path = Some("admin -> DA".to_string()); @@ -299,7 +299,7 @@ mod tests { } #[test] - fn test_generate_playbook_technique_detections() { + fn generate_playbook_technique_detections() { let state = make_state(); let techniques = vec!["T1003".to_string(), "T1558.003".to_string()]; let playbook = generate_detection_playbook(&state, &techniques); diff --git a/ares-cli/src/detection/queries.rs b/ares-cli/src/detection/queries.rs index d2bdab98..fdf59c53 100644 --- a/ares-cli/src/detection/queries.rs +++ b/ares-cli/src/detection/queries.rs @@ -172,7 +172,7 @@ mod tests { } #[test] - fn test_build_priority_queries_minimal() { + fn build_priority_queries_minimal() { let state = make_state(); let start = Utc::now() - chrono::Duration::hours(1); let end = Utc::now(); @@ -183,7 +183,7 @@ mod tests { } #[test] - fn test_build_priority_queries_with_domain_admin() { + fn build_priority_queries_with_domain_admin() { let mut state = make_state(); state.has_domain_admin = true; let start = Utc::now() - chrono::Duration::hours(1); @@ -195,7 +195,7 @@ mod tests { } #[test] - fn test_build_priority_queries_with_hashes() { + fn build_priority_queries_with_hashes() { let mut state = make_state(); state.all_hashes = vec![Hash { id: String::new(), @@ -217,7 +217,7 @@ mod tests { } #[test] - fn test_build_priority_queries_with_lateral_movement() { + fn build_priority_queries_with_lateral_movement() { let mut state = make_state(); state.all_hosts = vec![ Host { @@ -246,7 +246,7 @@ mod tests { } #[test] - fn test_build_priority_queries_kerberos_techniques() { + fn build_priority_queries_kerberos_techniques() { let state = make_state(); let start = Utc::now() - chrono::Duration::hours(1); let end = Utc::now(); @@ -256,7 +256,7 @@ mod tests { } #[test] - fn test_build_priority_queries_credential_accounts() { + fn build_priority_queries_credential_accounts() { let mut state = make_state(); state.all_credentials = vec![Credential { id: String::new(), @@ -278,7 +278,7 @@ mod tests { } #[test] - fn test_build_priority_queries_sorted_by_priority() { + fn build_priority_queries_sorted_by_priority() { let mut state = make_state(); state.has_domain_admin = true; state.all_hashes = vec![Hash { diff --git a/ares-cli/src/detection/techniques/tests.rs b/ares-cli/src/detection/techniques/tests.rs index 368a6c38..5821c39b 100644 --- a/ares-cli/src/detection/techniques/tests.rs +++ b/ares-cli/src/detection/techniques/tests.rs @@ -5,7 +5,7 @@ use super::names::{get_technique_name, pyramid_level_name}; use ares_core::models::SharedRedTeamState; #[test] -fn test_get_technique_name_known() { +fn get_technique_name_known() { assert_eq!(get_technique_name("T1046"), "Network Service Discovery"); assert_eq!(get_technique_name("T1003"), "OS Credential Dumping"); assert_eq!(get_technique_name("T1003.006"), "DCSync"); @@ -17,13 +17,13 @@ fn test_get_technique_name_known() { } #[test] -fn test_get_technique_name_unknown() { +fn get_technique_name_unknown() { assert_eq!(get_technique_name("T9999"), ""); assert_eq!(get_technique_name(""), ""); } #[test] -fn test_pyramid_level_name_all_levels() { +fn pyramid_level_name_all_levels() { assert_eq!(pyramid_level_name(1), "Hash Values (L1)"); assert_eq!(pyramid_level_name(2), "IP Addresses (L2)"); assert_eq!(pyramid_level_name(3), "Domain Names (L3)"); @@ -33,14 +33,14 @@ fn test_pyramid_level_name_all_levels() { } #[test] -fn test_pyramid_level_name_unknown() { +fn pyramid_level_name_unknown() { assert_eq!(pyramid_level_name(0), "Unknown"); assert_eq!(pyramid_level_name(7), "Unknown"); assert_eq!(pyramid_level_name(255), "Unknown"); } #[test] -fn test_build_technique_detections_known_techniques() { +fn build_technique_detections_known_techniques() { let state = SharedRedTeamState::new("test-op".to_string()); let start = Utc::now() - chrono::Duration::hours(1); let end = Utc::now(); @@ -57,7 +57,7 @@ fn test_build_technique_detections_known_techniques() { } #[test] -fn test_build_technique_detections_sub_technique() { +fn build_technique_detections_sub_technique() { let state = SharedRedTeamState::new("test-op".to_string()); let start = Utc::now() - chrono::Duration::hours(1); let end = Utc::now(); @@ -68,7 +68,7 @@ fn test_build_technique_detections_sub_technique() { } #[test] -fn test_build_technique_detections_empty() { +fn build_technique_detections_empty() { let state = SharedRedTeamState::new("test-op".to_string()); let start = Utc::now(); let end = Utc::now(); @@ -77,7 +77,7 @@ fn test_build_technique_detections_empty() { } #[test] -fn test_technique_detection_has_event_ids() { +fn technique_detection_has_event_ids() { let state = SharedRedTeamState::new("test-op".to_string()); let start = Utc::now() - chrono::Duration::hours(1); let end = Utc::now(); diff --git a/ares-cli/src/ops/delete.rs b/ares-cli/src/ops/delete.rs index 3ed66577..d584505c 100644 --- a/ares-cli/src/ops/delete.rs +++ b/ares-cli/src/ops/delete.rs @@ -101,7 +101,7 @@ mod tests { use super::*; #[test] - fn test_parse_operation_timestamp_valid() { + fn parse_operation_timestamp_valid() { let ts = parse_operation_timestamp("op-20250128-123456").unwrap(); assert_eq!( ts.format("%Y-%m-%d %H:%M:%S").to_string(), @@ -110,14 +110,14 @@ mod tests { } #[test] - fn test_parse_operation_timestamp_invalid() { + fn parse_operation_timestamp_invalid() { assert!(parse_operation_timestamp("not-an-op-id").is_none()); assert!(parse_operation_timestamp("op-bad").is_none()); assert!(parse_operation_timestamp("").is_none()); } #[test] - fn test_parse_operation_timestamp_with_suffix() { + fn parse_operation_timestamp_with_suffix() { // Some IDs may have extra suffix after the timestamp let ts = parse_operation_timestamp("op-20260407-091000-abc123").unwrap(); assert_eq!( diff --git a/ares-cli/src/ops/loot/format/display.rs b/ares-cli/src/ops/loot/format/display.rs index 6872d1ec..ce6f1fa9 100644 --- a/ares-cli/src/ops/loot/format/display.rs +++ b/ares-cli/src/ops/loot/format/display.rs @@ -709,9 +709,7 @@ mod tests { use super::*; use serde_json::json; - // ----------------------------------------------------------------------- // Helper: build a minimal SharedRedTeamState for testing - // ----------------------------------------------------------------------- fn empty_state() -> SharedRedTeamState { SharedRedTeamState::new("op-test-001".to_string()) @@ -747,42 +745,38 @@ mod tests { } } - // ----------------------------------------------------------------------- // capitalize - // ----------------------------------------------------------------------- #[test] - fn test_capitalize_normal() { + fn capitalize_normal() { assert_eq!(capitalize("hostname"), "Hostname"); } #[test] - fn test_capitalize_already_upper() { + fn capitalize_already_upper() { assert_eq!(capitalize("Domain"), "Domain"); } #[test] - fn test_capitalize_empty() { + fn capitalize_empty() { assert_eq!(capitalize(""), ""); } #[test] - fn test_capitalize_single_char() { + fn capitalize_single_char() { assert_eq!(capitalize("a"), "A"); } - // ----------------------------------------------------------------------- // format_vuln_details - // ----------------------------------------------------------------------- #[test] - fn test_format_vuln_details_empty() { + fn format_vuln_details_empty() { let details = HashMap::new(); assert_eq!(format_vuln_details(&details), ""); } #[test] - fn test_format_vuln_details_priority_keys_order() { + fn format_vuln_details_priority_keys_order() { let mut details = HashMap::new(); details.insert("domain".to_string(), json!("contoso.local")); details.insert("hostname".to_string(), json!("dc01.contoso.local")); @@ -798,7 +792,7 @@ mod tests { } #[test] - fn test_format_vuln_details_skips_null_and_empty() { + fn format_vuln_details_skips_null_and_empty() { let mut details = HashMap::new(); details.insert("hostname".to_string(), json!("dc01.contoso.local")); details.insert("note".to_string(), json!(null)); @@ -811,7 +805,7 @@ mod tests { } #[test] - fn test_format_vuln_details_non_string_values() { + fn format_vuln_details_non_string_values() { let mut details = HashMap::new(); details.insert("hostname".to_string(), json!(42)); @@ -820,7 +814,7 @@ mod tests { } #[test] - fn test_format_vuln_details_remaining_keys_sorted() { + fn format_vuln_details_remaining_keys_sorted() { let mut details = HashMap::new(); details.insert("zebra".to_string(), json!("z")); details.insert("alpha".to_string(), json!("a")); @@ -831,48 +825,44 @@ mod tests { assert!(alpha_pos < zebra_pos); } - // ----------------------------------------------------------------------- // format_timeline_timestamp - // ----------------------------------------------------------------------- #[test] - fn test_format_timeline_timestamp_rfc3339() { + fn format_timeline_timestamp_rfc3339() { let result = format_timeline_timestamp("2026-04-20T14:30:00Z"); assert_eq!(result, "2026-04-20 14:30:00"); } #[test] - fn test_format_timeline_timestamp_rfc3339_with_offset() { + fn format_timeline_timestamp_rfc3339_with_offset() { let result = format_timeline_timestamp("2026-04-20T14:30:00+00:00"); assert_eq!(result, "2026-04-20 14:30:00"); } #[test] - fn test_format_timeline_timestamp_naive_with_fractional() { + fn format_timeline_timestamp_naive_with_fractional() { let result = format_timeline_timestamp("2026-04-20T14:30:00.123456"); assert_eq!(result, "2026-04-20 14:30:00"); } #[test] - fn test_format_timeline_timestamp_unparseable_short() { + fn format_timeline_timestamp_unparseable_short() { let result = format_timeline_timestamp("unknown"); assert_eq!(result, "unknown"); } #[test] - fn test_format_timeline_timestamp_unparseable_long() { + fn format_timeline_timestamp_unparseable_long() { let long = "this-is-a-very-long-timestamp-value-that-exceeds-23-chars"; let result = format_timeline_timestamp(long); assert_eq!(result.len(), 23); assert_eq!(result, &long[..23]); } - // ----------------------------------------------------------------------- // extract_mitre_from_event - // ----------------------------------------------------------------------- #[test] - fn test_extract_mitre_from_event_array() { + fn extract_mitre_from_event_array() { let event = json!({ "mitre_techniques": ["T1003", "T1558.001"] }); @@ -881,7 +871,7 @@ mod tests { } #[test] - fn test_extract_mitre_from_event_string() { + fn extract_mitre_from_event_string() { let event = json!({ "mitre_techniques": "T1003.006" }); @@ -890,7 +880,7 @@ mod tests { } #[test] - fn test_extract_mitre_from_event_missing() { + fn extract_mitre_from_event_missing() { let event = json!({ "description": "some event" }); @@ -899,7 +889,7 @@ mod tests { } #[test] - fn test_extract_mitre_from_event_empty_array() { + fn extract_mitre_from_event_empty_array() { let event = json!({ "mitre_techniques": [] }); @@ -907,12 +897,10 @@ mod tests { assert_eq!(result, ""); } - // ----------------------------------------------------------------------- // mitre_technique_name - // ----------------------------------------------------------------------- #[test] - fn test_mitre_technique_name_known() { + fn mitre_technique_name_known() { assert_eq!(mitre_technique_name("T1003"), "OS Credential Dumping"); assert_eq!(mitre_technique_name("T1558.001"), "Golden Ticket"); assert_eq!(mitre_technique_name("T1003.006"), "DCSync"); @@ -920,73 +908,69 @@ mod tests { } #[test] - fn test_mitre_technique_name_unknown() { + fn mitre_technique_name_unknown() { assert_eq!(mitre_technique_name("T9999"), ""); assert_eq!(mitre_technique_name(""), ""); } - // ----------------------------------------------------------------------- // resolve_domain_fqdn - // ----------------------------------------------------------------------- #[test] - fn test_resolve_domain_fqdn_already_fqdn() { + fn resolve_domain_fqdn_already_fqdn() { let map = HashMap::new(); assert_eq!(resolve_domain_fqdn("contoso.local", &map), "contoso.local"); } #[test] - fn test_resolve_domain_fqdn_trailing_dot() { + fn resolve_domain_fqdn_trailing_dot() { let map = HashMap::new(); assert_eq!(resolve_domain_fqdn("contoso.local.", &map), "contoso.local"); } #[test] - fn test_resolve_domain_fqdn_netbios_lower_lookup() { + fn resolve_domain_fqdn_netbios_lower_lookup() { let mut map = HashMap::new(); map.insert("contoso".to_string(), "contoso.local".to_string()); assert_eq!(resolve_domain_fqdn("contoso", &map), "contoso.local"); } #[test] - fn test_resolve_domain_fqdn_netbios_upper_lookup() { + fn resolve_domain_fqdn_netbios_upper_lookup() { let mut map = HashMap::new(); map.insert("CONTOSO".to_string(), "contoso.local".to_string()); assert_eq!(resolve_domain_fqdn("CONTOSO", &map), "contoso.local"); } #[test] - fn test_resolve_domain_fqdn_empty() { + fn resolve_domain_fqdn_empty() { let map = HashMap::new(); assert_eq!(resolve_domain_fqdn("", &map), ""); } #[test] - fn test_resolve_domain_fqdn_unresolvable_netbios() { + fn resolve_domain_fqdn_unresolvable_netbios() { let map = HashMap::new(); // No match in map, returns as lowercase assert_eq!(resolve_domain_fqdn("CONTOSO", &map), "contoso"); } #[test] - fn test_resolve_domain_fqdn_case_normalization() { + fn resolve_domain_fqdn_case_normalization() { let map = HashMap::new(); assert_eq!(resolve_domain_fqdn("CONTOSO.LOCAL", &map), "contoso.local"); } - // ----------------------------------------------------------------------- // build_domain_achievements - // ----------------------------------------------------------------------- #[test] - fn test_build_domain_achievements_empty() { + fn build_domain_achievements_empty() { let state = empty_state(); let achievements = build_domain_achievements(&state, &[], &[]); assert!(achievements.is_empty()); } #[test] - fn test_build_domain_achievements_krbtgt_hash() { + fn build_domain_achievements_krbtgt_hash() { let state = empty_state(); let hashes = vec![make_hash("krbtgt", "contoso.local", "ntlm")]; @@ -998,7 +982,7 @@ mod tests { } #[test] - fn test_build_domain_achievements_krbtgt_multiple_hash_types() { + fn build_domain_achievements_krbtgt_multiple_hash_types() { let state = empty_state(); let hashes = vec![ make_hash("krbtgt", "contoso.local", "ntlm"), @@ -1014,7 +998,7 @@ mod tests { } #[test] - fn test_build_domain_achievements_administrator_hash() { + fn build_domain_achievements_administrator_hash() { let state = empty_state(); let hashes = vec![make_hash("Administrator", "contoso.local", "ntlm")]; @@ -1025,7 +1009,7 @@ mod tests { } #[test] - fn test_build_domain_achievements_admin_credential() { + fn build_domain_achievements_admin_credential() { let state = empty_state(); let credentials = vec![make_credential("dadmin", "contoso.local", true)]; @@ -1036,7 +1020,7 @@ mod tests { } #[test] - fn test_build_domain_achievements_golden_ticket_vuln() { + fn build_domain_achievements_golden_ticket_vuln() { let mut state = empty_state(); let mut details = HashMap::new(); details.insert("domain".to_string(), json!("contoso.local")); @@ -1060,7 +1044,7 @@ mod tests { } #[test] - fn test_build_domain_achievements_multi_domain() { + fn build_domain_achievements_multi_domain() { let mut state = empty_state(); state .netbios_to_fqdn @@ -1086,7 +1070,7 @@ mod tests { } #[test] - fn test_build_domain_achievements_netbios_resolution() { + fn build_domain_achievements_netbios_resolution() { let mut state = empty_state(); state .netbios_to_fqdn @@ -1102,7 +1086,7 @@ mod tests { } #[test] - fn test_build_domain_achievements_empty_domain_skipped() { + fn build_domain_achievements_empty_domain_skipped() { let state = empty_state(); let hashes = vec![make_hash("krbtgt", "", "ntlm")]; @@ -1110,9 +1094,7 @@ mod tests { assert!(achievements.is_empty()); } - // ----------------------------------------------------------------------- // Domain/forest structure computation (inline in print_loot_human) - // ----------------------------------------------------------------------- /// Extract the domain/forest structure logic into a helper we can test. fn compute_forest_structure( @@ -1146,7 +1128,7 @@ mod tests { } #[test] - fn test_forest_structure_single_domain() { + fn forest_structure_single_domain() { let input = vec!["contoso.local".to_string()]; let (domains, roots, children) = compute_forest_structure(&input); assert_eq!(domains, vec!["contoso.local"]); @@ -1155,7 +1137,7 @@ mod tests { } #[test] - fn test_forest_structure_parent_child() { + fn forest_structure_parent_child() { let input = vec![ "contoso.local".to_string(), "child.contoso.local".to_string(), @@ -1170,7 +1152,7 @@ mod tests { } #[test] - fn test_forest_structure_two_forests() { + fn forest_structure_two_forests() { let input = vec!["contoso.local".to_string(), "fabrikam.local".to_string()]; let (_domains, roots, children) = compute_forest_structure(&input); assert_eq!(roots, vec!["contoso.local", "fabrikam.local"]); @@ -1178,7 +1160,7 @@ mod tests { } #[test] - fn test_forest_structure_dedup_and_normalization() { + fn forest_structure_dedup_and_normalization() { let input = vec![ "CONTOSO.LOCAL.".to_string(), "contoso.local".to_string(), @@ -1190,7 +1172,7 @@ mod tests { } #[test] - fn test_forest_structure_empty_strings_filtered() { + fn forest_structure_empty_strings_filtered() { let input = vec![ "".to_string(), " ".to_string(), @@ -1202,7 +1184,7 @@ mod tests { } #[test] - fn test_forest_structure_orphan_subdomain() { + fn forest_structure_orphan_subdomain() { // subdomain without its parent in the list: treated as a forest root let input = vec!["sub.contoso.local".to_string()]; let (_domains, roots, children) = compute_forest_structure(&input); diff --git a/ares-cli/src/ops/resolve.rs b/ares-cli/src/ops/resolve.rs index 6219272c..e7f9ba8f 100644 --- a/ares-cli/src/ops/resolve.rs +++ b/ares-cli/src/ops/resolve.rs @@ -85,18 +85,18 @@ mod tests { use super::*; #[test] - fn test_looks_like_ip_single() { + fn looks_like_ip_single() { assert!(looks_like_ip("192.168.58.10")); assert!(looks_like_ip("192.168.58.10")); } #[test] - fn test_looks_like_ip_comma_separated() { + fn looks_like_ip_comma_separated() { assert!(looks_like_ip("192.168.58.10,192.168.58.11")); } #[test] - fn test_looks_like_ip_not_ip() { + fn looks_like_ip_not_ip() { assert!(!looks_like_ip("dreadgoad")); assert!(!looks_like_ip("my-server")); assert!(!looks_like_ip("")); diff --git a/ares-cli/src/orchestrator/automation/adcs_exploitation.rs b/ares-cli/src/orchestrator/automation/adcs_exploitation.rs index d5070e2a..124c9c2f 100644 --- a/ares-cli/src/orchestrator/automation/adcs_exploitation.rs +++ b/ares-cli/src/orchestrator/automation/adcs_exploitation.rs @@ -339,7 +339,7 @@ mod tests { } #[test] - fn test_exploitable_esc_types() { + fn exploitable_esc_types() { for &t in EXPLOITABLE_ESC_TYPES { assert!( t.contains("esc"), @@ -348,12 +348,10 @@ mod tests { } } - // ----------------------------------------------------------------------- // is_exploitable_esc_type - // ----------------------------------------------------------------------- #[test] - fn test_is_exploitable_esc_type_positive() { + fn is_exploitable_esc_type_positive() { assert!(is_exploitable_esc_type("esc1")); assert!(is_exploitable_esc_type("esc4")); assert!(is_exploitable_esc_type("esc8")); @@ -363,7 +361,7 @@ mod tests { } #[test] - fn test_is_exploitable_esc_type_case_insensitive() { + fn is_exploitable_esc_type_case_insensitive() { assert!(is_exploitable_esc_type("ESC1")); assert!(is_exploitable_esc_type("ADCS_ESC4")); assert!(is_exploitable_esc_type("Esc8")); @@ -371,7 +369,7 @@ mod tests { } #[test] - fn test_is_exploitable_esc_type_negative() { + fn is_exploitable_esc_type_negative() { assert!(!is_exploitable_esc_type("esc2")); assert!(!is_exploitable_esc_type("esc3")); assert!(!is_exploitable_esc_type("rbcd")); @@ -381,43 +379,39 @@ mod tests { assert!(!is_exploitable_esc_type("adcs_esc2")); } - // ----------------------------------------------------------------------- // normalize_esc_type - // ----------------------------------------------------------------------- #[test] - fn test_normalize_esc_type_strips_prefix() { + fn normalize_esc_type_strips_prefix() { assert_eq!(normalize_esc_type("adcs_esc1"), "esc1"); assert_eq!(normalize_esc_type("adcs_esc4"), "esc4"); assert_eq!(normalize_esc_type("adcs_esc8"), "esc8"); } #[test] - fn test_normalize_esc_type_no_prefix() { + fn normalize_esc_type_no_prefix() { assert_eq!(normalize_esc_type("esc1"), "esc1"); assert_eq!(normalize_esc_type("esc4"), "esc4"); assert_eq!(normalize_esc_type("esc8"), "esc8"); } #[test] - fn test_normalize_esc_type_case_insensitive() { + fn normalize_esc_type_case_insensitive() { assert_eq!(normalize_esc_type("ADCS_ESC1"), "esc1"); assert_eq!(normalize_esc_type("ESC4"), "esc4"); assert_eq!(normalize_esc_type("Adcs_Esc8"), "esc8"); } #[test] - fn test_normalize_esc_type_non_adcs_prefix() { + fn normalize_esc_type_non_adcs_prefix() { // Only "adcs_" prefix is stripped, not other prefixes assert_eq!(normalize_esc_type("vuln_esc1"), "vuln_esc1"); } - // ----------------------------------------------------------------------- // extract_ca_name - // ----------------------------------------------------------------------- #[test] - fn test_extract_ca_name_primary_key() { + fn extract_ca_name_primary_key() { let mut details = HashMap::new(); details.insert( "ca_name".to_string(), @@ -430,7 +424,7 @@ mod tests { } #[test] - fn test_extract_ca_name_fallback_uppercase_ca() { + fn extract_ca_name_fallback_uppercase_ca() { let mut details = HashMap::new(); details.insert( "CA".to_string(), @@ -440,7 +434,7 @@ mod tests { } #[test] - fn test_extract_ca_name_fallback_lowercase_ca() { + fn extract_ca_name_fallback_lowercase_ca() { let mut details = HashMap::new(); details.insert( "ca".to_string(), @@ -450,7 +444,7 @@ mod tests { } #[test] - fn test_extract_ca_name_priority_order() { + fn extract_ca_name_priority_order() { let mut details = HashMap::new(); details.insert( "ca_name".to_string(), @@ -468,27 +462,27 @@ mod tests { } #[test] - fn test_extract_ca_name_empty_details() { + fn extract_ca_name_empty_details() { let details = HashMap::new(); assert_eq!(extract_ca_name(&details), None); } #[test] - fn test_extract_ca_name_non_string_value() { + fn extract_ca_name_non_string_value() { let mut details = HashMap::new(); details.insert("ca_name".to_string(), serde_json::Value::Number(42.into())); assert_eq!(extract_ca_name(&details), None); } #[test] - fn test_extract_ca_name_null_value() { + fn extract_ca_name_null_value() { let mut details = HashMap::new(); details.insert("ca_name".to_string(), serde_json::Value::Null); assert_eq!(extract_ca_name(&details), None); } #[test] - fn test_extract_ca_name_null_falls_through_to_next_key() { + fn extract_ca_name_null_falls_through_to_next_key() { // When "ca_name" key exists but is Null, as_str() returns None, // so the function falls through to try "CA" next. let mut details = HashMap::new(); @@ -500,12 +494,10 @@ mod tests { assert_eq!(extract_ca_name(&details), Some("fallback-CA".to_string())); } - // ----------------------------------------------------------------------- // extract_template_name - // ----------------------------------------------------------------------- #[test] - fn test_extract_template_name_primary_key() { + fn extract_template_name_primary_key() { let mut details = HashMap::new(); details.insert( "template".to_string(), @@ -518,7 +510,7 @@ mod tests { } #[test] - fn test_extract_template_name_fallback_template_name() { + fn extract_template_name_fallback_template_name() { let mut details = HashMap::new(); details.insert( "template_name".to_string(), @@ -528,7 +520,7 @@ mod tests { } #[test] - fn test_extract_template_name_fallback_pascal_case() { + fn extract_template_name_fallback_pascal_case() { let mut details = HashMap::new(); details.insert( "Template".to_string(), @@ -538,7 +530,7 @@ mod tests { } #[test] - fn test_extract_template_name_priority_order() { + fn extract_template_name_priority_order() { let mut details = HashMap::new(); details.insert( "template".to_string(), @@ -556,24 +548,22 @@ mod tests { } #[test] - fn test_extract_template_name_empty_details() { + fn extract_template_name_empty_details() { let details = HashMap::new(); assert_eq!(extract_template_name(&details), None); } #[test] - fn test_extract_template_name_non_string_value() { + fn extract_template_name_non_string_value() { let mut details = HashMap::new(); details.insert("template".to_string(), serde_json::Value::Bool(true)); assert_eq!(extract_template_name(&details), None); } - // ----------------------------------------------------------------------- // extract_ca_host - // ----------------------------------------------------------------------- #[test] - fn test_extract_ca_host_primary_key() { + fn extract_ca_host_primary_key() { let mut details = HashMap::new(); details.insert( "ca_host".to_string(), @@ -586,7 +576,7 @@ mod tests { } #[test] - fn test_extract_ca_host_fallback_ca_ip() { + fn extract_ca_host_fallback_ca_ip() { let mut details = HashMap::new(); details.insert( "ca_ip".to_string(), @@ -599,7 +589,7 @@ mod tests { } #[test] - fn test_extract_ca_host_fallback_to_target() { + fn extract_ca_host_fallback_to_target() { let details = HashMap::new(); assert_eq!( extract_ca_host(&details, "192.168.58.30"), @@ -608,13 +598,13 @@ mod tests { } #[test] - fn test_extract_ca_host_empty_target_returns_none() { + fn extract_ca_host_empty_target_returns_none() { let details = HashMap::new(); assert_eq!(extract_ca_host(&details, ""), None); } #[test] - fn test_extract_ca_host_priority_order() { + fn extract_ca_host_priority_order() { let mut details = HashMap::new(); details.insert( "ca_host".to_string(), @@ -631,7 +621,7 @@ mod tests { } #[test] - fn test_extract_ca_host_non_string_falls_to_target() { + fn extract_ca_host_non_string_falls_to_target() { let mut details = HashMap::new(); details.insert("ca_host".to_string(), serde_json::Value::Number(42.into())); assert_eq!( @@ -641,7 +631,7 @@ mod tests { } #[test] - fn test_extract_ca_host_hostname_value() { + fn extract_ca_host_hostname_value() { let mut details = HashMap::new(); details.insert( "ca_host".to_string(), @@ -653,12 +643,10 @@ mod tests { ); } - // ----------------------------------------------------------------------- // extract_account_name - // ----------------------------------------------------------------------- #[test] - fn test_extract_account_name_primary_key() { + fn extract_account_name_primary_key() { let mut details = HashMap::new(); details.insert( "account_name".to_string(), @@ -668,7 +656,7 @@ mod tests { } #[test] - fn test_extract_account_name_fallback_source() { + fn extract_account_name_fallback_source() { let mut details = HashMap::new(); details.insert( "source".to_string(), @@ -678,7 +666,7 @@ mod tests { } #[test] - fn test_extract_account_name_fallback_enrollee() { + fn extract_account_name_fallback_enrollee() { let mut details = HashMap::new(); details.insert( "enrollee".to_string(), @@ -691,7 +679,7 @@ mod tests { } #[test] - fn test_extract_account_name_priority_order() { + fn extract_account_name_priority_order() { let mut details = HashMap::new(); details.insert( "account_name".to_string(), @@ -709,60 +697,54 @@ mod tests { } #[test] - fn test_extract_account_name_empty_details() { + fn extract_account_name_empty_details() { let details = HashMap::new(); assert_eq!(extract_account_name(&details), None); } - // ----------------------------------------------------------------------- // role_for_esc_type - // ----------------------------------------------------------------------- #[test] - fn test_role_for_esc8_is_coercion() { + fn role_for_esc8_is_coercion() { assert_eq!(role_for_esc_type("esc8"), "coercion"); } #[test] - fn test_role_for_esc1_is_privesc() { + fn role_for_esc1_is_privesc() { assert_eq!(role_for_esc_type("esc1"), "privesc"); } #[test] - fn test_role_for_esc4_is_privesc() { + fn role_for_esc4_is_privesc() { assert_eq!(role_for_esc_type("esc4"), "privesc"); } #[test] - fn test_role_for_unknown_defaults_to_privesc() { + fn role_for_unknown_defaults_to_privesc() { assert_eq!(role_for_esc_type("esc99"), "privesc"); assert_eq!(role_for_esc_type("something_else"), "privesc"); } - // ----------------------------------------------------------------------- // dedup key format - // ----------------------------------------------------------------------- #[test] - fn test_dedup_key_format() { + fn dedup_key_format() { let vuln_id = "vuln-adcs-001"; let dedup_key = format!("{DEDUP_ADCS_EXPLOIT}:{vuln_id}"); assert_eq!(dedup_key, "adcs_exploit:vuln-adcs-001"); } #[test] - fn test_dedup_key_unique_per_vuln() { + fn dedup_key_unique_per_vuln() { let key1 = format!("{DEDUP_ADCS_EXPLOIT}:vuln-001"); let key2 = format!("{DEDUP_ADCS_EXPLOIT}:vuln-002"); assert_ne!(key1, key2); } - // ----------------------------------------------------------------------- // Integration-like: combined extraction from realistic details - // ----------------------------------------------------------------------- #[test] - fn test_esc1_full_extraction() { + fn esc1_full_extraction() { let mut details = HashMap::new(); details.insert( "ca_name".to_string(), @@ -805,7 +787,7 @@ mod tests { } #[test] - fn test_esc8_full_extraction() { + fn esc8_full_extraction() { let mut details = HashMap::new(); details.insert( "CA".to_string(), @@ -833,7 +815,7 @@ mod tests { } #[test] - fn test_minimal_details_uses_target_fallback() { + fn minimal_details_uses_target_fallback() { let mut details = HashMap::new(); details.insert( "domain".to_string(), diff --git a/ares-cli/src/orchestrator/automation/credential_expansion.rs b/ares-cli/src/orchestrator/automation/credential_expansion.rs index 7e851875..773af2d6 100644 --- a/ares-cli/src/orchestrator/automation/credential_expansion.rs +++ b/ares-cli/src/orchestrator/automation/credential_expansion.rs @@ -400,7 +400,7 @@ mod tests { use super::*; #[test] - fn test_lateral_techniques_order() { + fn lateral_techniques_order() { // smbexec first (stealthiest), then wmiexec, then psexec assert_eq!(LATERAL_TECHNIQUES[0], "smbexec"); assert_eq!(LATERAL_TECHNIQUES[1], "wmiexec"); @@ -408,12 +408,12 @@ mod tests { } #[test] - fn test_lateral_techniques_count() { + fn lateral_techniques_count() { assert_eq!(LATERAL_TECHNIQUES.len(), 3); } #[test] - fn test_lateral_techniques_contains() { + fn lateral_techniques_contains() { assert!(LATERAL_TECHNIQUES.contains(&"smbexec")); assert!(LATERAL_TECHNIQUES.contains(&"wmiexec")); assert!(LATERAL_TECHNIQUES.contains(&"psexec")); @@ -421,7 +421,7 @@ mod tests { } #[test] - fn test_netbios_domain_resolution() { + fn netbios_domain_resolution() { // Simulate the NetBIOS→FQDN resolution logic from the automation loop let raw = "NORTH"; let raw_lower = raw.to_lowercase(); @@ -465,7 +465,7 @@ mod tests { } #[test] - fn test_domain_matching_logic() { + fn domain_matching_logic() { // Simulate the host domain matching from credential expansion let cred_dom = "contoso.local"; @@ -503,7 +503,7 @@ mod tests { } #[test] - fn test_host_domain_from_fqdn() { + fn host_domain_from_fqdn() { // Simulate extracting domain from FQDN hostname let hostname = "dc01.contoso.local"; let domain = hostname @@ -536,7 +536,7 @@ mod tests { } #[test] - fn test_hash_expansion_dedup_key() { + fn hash_expansion_dedup_key() { // Test the dedup key format for hash-based expansion let domain = "contoso.local"; let username = "Administrator"; @@ -554,7 +554,7 @@ mod tests { } #[test] - fn test_pth_credential_building() { + fn pth_credential_building() { // Verify that pass-the-hash builds the credential with hash_value as password let hash = ares_core::models::Hash { id: "hash-1".to_string(), @@ -593,7 +593,7 @@ mod tests { } #[test] - fn test_hash_filter_ntlm_only() { + fn hash_filter_ntlm_only() { // Only NTLM hashes pass the filter; aes/des/lm should be excluded let hashes = [ ( @@ -626,7 +626,7 @@ mod tests { } #[test] - fn test_hash_filter_excludes_krbtgt() { + fn hash_filter_excludes_krbtgt() { // krbtgt hashes are excluded from pass-the-hash (used for golden tickets, not PtH) let username = "krbtgt"; let passes = username.to_lowercase() != "krbtgt" && !username.ends_with('$'); @@ -634,7 +634,7 @@ mod tests { } #[test] - fn test_hash_filter_excludes_machine_accounts() { + fn hash_filter_excludes_machine_accounts() { // Machine accounts (ending with $) are excluded from pass-the-hash let usernames = vec!["DC01$", "SQL01$", "WEB01$"]; for u in usernames { @@ -648,7 +648,7 @@ mod tests { } #[test] - fn test_hash_filter_allows_normal_users() { + fn hash_filter_allows_normal_users() { // Normal users should pass the hash filter let usernames = vec!["administrator", "jdoe", "svc_sql"]; for u in usernames { @@ -658,7 +658,7 @@ mod tests { } #[test] - fn test_secretsdump_dedup_key_format() { + fn secretsdump_dedup_key_format() { // secretsdump dedup: dc_ip:domain:username let dc_ip = "192.168.58.10"; let domain = "CONTOSO.LOCAL"; @@ -673,7 +673,7 @@ mod tests { } #[test] - fn test_secretsdump_dedup_different_dcs_are_unique() { + fn secretsdump_dedup_different_dcs_are_unique() { // Same credential against different DCs should produce different dedup keys let domain = "contoso.local"; let username = "admin"; @@ -683,7 +683,7 @@ mod tests { } #[test] - fn test_credential_expansion_dedup_key_format() { + fn credential_expansion_dedup_key_format() { // Expansion dedup: domain:username let domain = "CONTOSO.LOCAL"; let username = "JDoe"; @@ -692,7 +692,7 @@ mod tests { } #[test] - fn test_credential_filter_empty_domain_excluded() { + fn credential_filter_empty_domain_excluded() { // Credentials with empty domain are excluded let creds = [ ("user1", "P@ss", "contoso.local"), @@ -709,7 +709,7 @@ mod tests { } #[test] - fn test_credential_filter_empty_password_excluded() { + fn credential_filter_empty_password_excluded() { // Credentials with empty password are excluded let creds = [ ("user1", "P@ssw0rd!", "contoso.local"), // pragma: allowlist secret @@ -726,7 +726,7 @@ mod tests { } #[test] - fn test_target_filtering_owned_hosts_excluded() { + fn target_filtering_owned_hosts_excluded() { // Only non-owned hosts are targeted for lateral movement let hosts = [ ("192.168.58.10", true), // owned - should be excluded @@ -741,7 +741,7 @@ mod tests { } #[test] - fn test_netbios_resolution_uppercase_fallback() { + fn netbios_resolution_uppercase_fallback() { // When lowercase lookup fails, try uppercase let mut map = std::collections::HashMap::new(); map.insert("CONTOSO".to_string(), "contoso.local".to_string()); @@ -762,7 +762,7 @@ mod tests { } #[test] - fn test_domain_matching_empty_host_domain_rejected() { + fn domain_matching_empty_host_domain_rejected() { // Hosts with empty domain should not match any credential domain let host_domain = ""; let cred_dom = "contoso.local"; @@ -774,7 +774,7 @@ mod tests { } #[test] - fn test_domain_matching_sibling_domains_rejected() { + fn domain_matching_sibling_domains_rejected() { // Sibling child domains should NOT match each other let cred_dom = "child1.contoso.local"; let host_domain = "child2.contoso.local"; @@ -788,7 +788,7 @@ mod tests { } #[test] - fn test_hash_dedup_truncates_to_32_chars() { + fn hash_dedup_truncates_to_32_chars() { // Hash dedup uses first 32 chars of hash_value let short_hash = "aabbccdd"; let long_hash = "aad3b435b51404eeaad3b435b51404ee:31d6cfe0d16ae931b73c59d7e0c089c0"; @@ -801,7 +801,7 @@ mod tests { } #[test] - fn test_host_domain_from_bare_ip_falls_back_to_dc_map() { + fn host_domain_from_bare_ip_falls_back_to_dc_map() { // When hostname has no domain suffix, fall back to domain_controllers map let hostname = "192.168.58.10"; // bare IP, no FQDN let from_hostname = hostname diff --git a/ares-cli/src/orchestrator/automation/gmsa.rs b/ares-cli/src/orchestrator/automation/gmsa.rs index ce1eceed..c6d82727 100644 --- a/ares-cli/src/orchestrator/automation/gmsa.rs +++ b/ares-cli/src/orchestrator/automation/gmsa.rs @@ -246,10 +246,8 @@ struct GmsaWork { mod tests { use super::*; - // ─── is_gmsa_account ──────────────────────────────────────────────────── - #[test] - fn test_is_gmsa_account_managed_service_description() { + fn is_gmsa_account_managed_service_description() { assert!(is_gmsa_account( "svc_web$", "Managed Service Account for web servers" @@ -257,12 +255,12 @@ mod tests { } #[test] - fn test_is_gmsa_account_gmsa_in_username() { + fn is_gmsa_account_gmsa_in_username() { assert!(is_gmsa_account("gmsa_svc$", "some service account")); } #[test] - fn test_is_gmsa_account_case_insensitive_description() { + fn is_gmsa_account_case_insensitive_description() { assert!(is_gmsa_account( "svc_sql$", "MANAGED SERVICE account for SQL" @@ -270,12 +268,12 @@ mod tests { } #[test] - fn test_is_gmsa_account_case_insensitive_username() { + fn is_gmsa_account_case_insensitive_username() { assert!(is_gmsa_account("GMSA_SVC$", "regular account")); } #[test] - fn test_is_gmsa_account_no_dollar_suffix() { + fn is_gmsa_account_no_dollar_suffix() { // Must end with $ assert!(!is_gmsa_account( "svc_web", @@ -284,52 +282,50 @@ mod tests { } #[test] - fn test_is_gmsa_account_dollar_but_no_indicators() { + fn is_gmsa_account_dollar_but_no_indicators() { // Ends with $ but no "managed service" in description and no "gmsa" in name assert!(!is_gmsa_account("svc_sql$", "regular computer account")); } #[test] - fn test_is_gmsa_account_regular_user() { + fn is_gmsa_account_regular_user() { assert!(!is_gmsa_account("administrator", "Built-in admin account")); } #[test] - fn test_is_gmsa_account_empty_description_with_gmsa_name() { + fn is_gmsa_account_empty_description_with_gmsa_name() { assert!(is_gmsa_account("gmsa_backup$", "")); } #[test] - fn test_is_gmsa_account_empty_description_without_gmsa_name() { + fn is_gmsa_account_empty_description_without_gmsa_name() { assert!(!is_gmsa_account("svc_backup$", "")); } - // ─── is_gmsa_vuln_type ────────────────────────────────────────────────── - #[test] - fn test_is_gmsa_vuln_type_gmsa() { + fn is_gmsa_vuln_type_gmsa() { assert!(is_gmsa_vuln_type("gmsa")); } #[test] - fn test_is_gmsa_vuln_type_gmsa_reader() { + fn is_gmsa_vuln_type_gmsa_reader() { assert!(is_gmsa_vuln_type("gmsa_reader")); } #[test] - fn test_is_gmsa_vuln_type_readgmsapassword() { + fn is_gmsa_vuln_type_readgmsapassword() { assert!(is_gmsa_vuln_type("readgmsapassword")); } #[test] - fn test_is_gmsa_vuln_type_case_insensitive() { + fn is_gmsa_vuln_type_case_insensitive() { assert!(is_gmsa_vuln_type("GMSA")); assert!(is_gmsa_vuln_type("GMSA_READER")); assert!(is_gmsa_vuln_type("ReadGMSAPassword")); } #[test] - fn test_is_gmsa_vuln_type_negative() { + fn is_gmsa_vuln_type_negative() { assert!(!is_gmsa_vuln_type("rbcd")); assert!(!is_gmsa_vuln_type("laps")); assert!(!is_gmsa_vuln_type("constrained_delegation")); @@ -338,15 +334,13 @@ mod tests { assert!(!is_gmsa_vuln_type("")); } - // ─── dedup key construction ───────────────────────────────────────────── - #[test] - fn test_dedup_gmsa_accounts_value() { + fn dedup_gmsa_accounts_value() { assert_eq!(DEDUP_GMSA_ACCOUNTS, "gmsa_accounts"); } #[test] - fn test_dedup_key_format() { + fn dedup_key_format() { let domain = "contoso.local"; let username = "gmsa_svc$"; let key = format!("{}:{}", domain.to_lowercase(), username.to_lowercase()); @@ -354,7 +348,7 @@ mod tests { } #[test] - fn test_dedup_key_normalizes_case() { + fn dedup_key_normalizes_case() { let key = format!( "{}:{}", "FABRIKAM.LOCAL".to_lowercase(), diff --git a/ares-cli/src/orchestrator/automation/gpo.rs b/ares-cli/src/orchestrator/automation/gpo.rs index 79d61683..04e6b6bc 100644 --- a/ares-cli/src/orchestrator/automation/gpo.rs +++ b/ares-cli/src/orchestrator/automation/gpo.rs @@ -229,7 +229,7 @@ mod tests { use std::collections::HashMap; #[test] - fn test_is_gpo_candidate() { + fn is_gpo_candidate_basic() { assert!(is_gpo_candidate("gpo_abuse")); assert!(is_gpo_candidate("GPO_ABUSE")); assert!(is_gpo_candidate("gpo_write")); @@ -241,7 +241,7 @@ mod tests { } #[test] - fn test_is_gpo_candidate_all_explicit_types() { + fn is_gpo_candidate_all_explicit_types() { // Verify every explicitly listed GPO vuln type let gpo_types = vec![ "gpo_abuse", @@ -265,7 +265,7 @@ mod tests { } #[test] - fn test_is_gpo_candidate_wildcard_prefix() { + fn is_gpo_candidate_wildcard_prefix() { // Anything starting with gpo_ should match via starts_with assert!(is_gpo_candidate("gpo_custom_edge")); assert!(is_gpo_candidate("GPO_something_new")); @@ -273,7 +273,7 @@ mod tests { } #[test] - fn test_is_gpo_candidate_non_gpo_types() { + fn is_gpo_candidate_non_gpo_types() { // Exhaustive negative cases let non_gpo = vec![ "rbcd", @@ -299,14 +299,14 @@ mod tests { } #[test] - fn test_dedup_key_format() { + fn dedup_key_format() { let vuln_id = "vuln-gpo-001"; let dedup_key = format!("{DEDUP_GPO_ABUSE}:{vuln_id}"); assert_eq!(dedup_key, "gpo_abuse:vuln-gpo-001"); } #[test] - fn test_dedup_key_constant() { + fn dedup_key_constant() { assert_eq!(DEDUP_GPO_ABUSE, "gpo_abuse"); } @@ -340,21 +340,21 @@ mod tests { } #[test] - fn test_extract_source_user_from_source_key() { + fn extract_source_user_from_source_key() { let mut details = HashMap::new(); details.insert("source".to_string(), json!("jdoe")); assert_eq!(extract_gpo_source_user(&details), Some("jdoe".to_string())); } #[test] - fn test_extract_source_user_from_source_user_key() { + fn extract_source_user_from_source_user_key() { let mut details = HashMap::new(); details.insert("source_user".to_string(), json!("admin")); assert_eq!(extract_gpo_source_user(&details), Some("admin".to_string())); } #[test] - fn test_extract_source_user_from_account_name_key() { + fn extract_source_user_from_account_name_key() { let mut details = HashMap::new(); details.insert("account_name".to_string(), json!("svc_gpo")); assert_eq!( @@ -364,7 +364,7 @@ mod tests { } #[test] - fn test_extract_source_user_prefers_source_over_account_name() { + fn extract_source_user_prefers_source_over_account_name() { // "source" takes priority over "account_name" let mut details = HashMap::new(); details.insert("source".to_string(), json!("primary_user")); @@ -376,7 +376,7 @@ mod tests { } #[test] - fn test_extract_source_user_prefers_source_over_source_user() { + fn extract_source_user_prefers_source_over_source_user() { // "source" takes priority over "source_user" let mut details = HashMap::new(); details.insert("source".to_string(), json!("first")); @@ -385,20 +385,20 @@ mod tests { } #[test] - fn test_extract_source_user_none_when_empty() { + fn extract_source_user_none_when_empty() { let details = HashMap::new(); assert_eq!(extract_gpo_source_user(&details), None); } #[test] - fn test_extract_source_user_none_when_non_string() { + fn extract_source_user_none_when_non_string() { let mut details = HashMap::new(); details.insert("source".to_string(), json!(42)); assert_eq!(extract_gpo_source_user(&details), None); } #[test] - fn test_extract_gpo_id_from_gpo_id_key() { + fn extract_gpo_id_from_gpo_id_key() { let mut details = HashMap::new(); details.insert( "gpo_id".to_string(), @@ -411,7 +411,7 @@ mod tests { } #[test] - fn test_extract_gpo_id_from_gpo_guid_key() { + fn extract_gpo_id_from_gpo_guid_key() { let mut details = HashMap::new(); details.insert( "gpo_guid".to_string(), @@ -424,7 +424,7 @@ mod tests { } #[test] - fn test_extract_gpo_id_from_object_id_key() { + fn extract_gpo_id_from_object_id_key() { let mut details = HashMap::new(); details.insert("object_id".to_string(), json!("S-1-5-21-abc-123")); assert_eq!( @@ -434,7 +434,7 @@ mod tests { } #[test] - fn test_extract_gpo_id_prefers_gpo_id_over_gpo_guid() { + fn extract_gpo_id_prefers_gpo_id_over_gpo_guid() { let mut details = HashMap::new(); details.insert("gpo_id".to_string(), json!("primary-gpo")); details.insert("gpo_guid".to_string(), json!("fallback-guid")); @@ -442,13 +442,13 @@ mod tests { } #[test] - fn test_extract_gpo_id_none_when_empty() { + fn extract_gpo_id_none_when_empty() { let details = HashMap::new(); assert_eq!(extract_gpo_id(&details), None); } #[test] - fn test_extract_gpo_name_from_gpo_name_key() { + fn extract_gpo_name_from_gpo_name_key() { let mut details = HashMap::new(); details.insert("gpo_name".to_string(), json!("Default Domain Policy")); assert_eq!( @@ -458,7 +458,7 @@ mod tests { } #[test] - fn test_extract_gpo_name_from_display_name_key() { + fn extract_gpo_name_from_display_name_key() { let mut details = HashMap::new(); details.insert( "gpo_display_name".to_string(), @@ -471,7 +471,7 @@ mod tests { } #[test] - fn test_extract_gpo_name_prefers_gpo_name_over_display_name() { + fn extract_gpo_name_prefers_gpo_name_over_display_name() { let mut details = HashMap::new(); details.insert("gpo_name".to_string(), json!("Primary Name")); details.insert("gpo_display_name".to_string(), json!("Display Name")); @@ -479,20 +479,20 @@ mod tests { } #[test] - fn test_extract_gpo_name_none_when_empty() { + fn extract_gpo_name_none_when_empty() { let details = HashMap::new(); assert_eq!(extract_gpo_name(&details), None); } #[test] - fn test_extract_gpo_name_none_when_non_string() { + fn extract_gpo_name_none_when_non_string() { let mut details = HashMap::new(); details.insert("gpo_name".to_string(), json!(true)); assert_eq!(extract_gpo_name(&details), None); } #[test] - fn test_domain_extraction_from_details() { + fn domain_extraction_from_details() { // Simulate the domain extraction logic from auto_gpo_abuse let mut details = HashMap::new(); details.insert("domain".to_string(), json!("contoso.local")); @@ -505,7 +505,7 @@ mod tests { } #[test] - fn test_domain_extraction_missing_defaults_empty() { + fn domain_extraction_missing_defaults_empty() { let details: HashMap = HashMap::new(); let domain = details .get("domain") diff --git a/ares-cli/src/orchestrator/automation/laps.rs b/ares-cli/src/orchestrator/automation/laps.rs index b85cc967..a807cb57 100644 --- a/ares-cli/src/orchestrator/automation/laps.rs +++ b/ares-cli/src/orchestrator/automation/laps.rs @@ -236,32 +236,30 @@ struct LapsWork { mod tests { use super::*; - // ─── is_laps_candidate ────────────────────────────────────────────────── - #[test] - fn test_is_laps_candidate_laps_abuse() { + fn is_laps_candidate_laps_abuse() { assert!(is_laps_candidate("laps_abuse")); } #[test] - fn test_is_laps_candidate_laps_reader() { + fn is_laps_candidate_laps_reader() { assert!(is_laps_candidate("laps_reader")); } #[test] - fn test_is_laps_candidate_laps_plain() { + fn is_laps_candidate_laps_plain() { assert!(is_laps_candidate("laps")); } #[test] - fn test_is_laps_candidate_case_insensitive() { + fn is_laps_candidate_case_insensitive() { assert!(is_laps_candidate("LAPS_ABUSE")); assert!(is_laps_candidate("Laps_Reader")); assert!(is_laps_candidate("LAPS")); } #[test] - fn test_is_laps_candidate_negative() { + fn is_laps_candidate_negative() { assert!(!is_laps_candidate("rbcd")); assert!(!is_laps_candidate("constrained_delegation")); assert!(!is_laps_candidate("esc1")); @@ -270,24 +268,20 @@ mod tests { assert!(!is_laps_candidate("")); } - // ─── DEDUP_LAPS constant ──────────────────────────────────────────────── - #[test] - fn test_dedup_laps_value() { + fn dedup_laps_value() { assert_eq!(DEDUP_LAPS, "laps_extract"); } - // ─── dedup key construction ───────────────────────────────────────────── - #[test] - fn test_vuln_dedup_key_format() { + fn vuln_dedup_key_format() { let vuln_id = "vuln-laps-dc01"; let dedup_key = format!("{DEDUP_LAPS}:vuln:{vuln_id}"); assert_eq!(dedup_key, "laps_extract:vuln:vuln-laps-dc01"); } #[test] - fn test_sweep_dedup_key_format() { + fn sweep_dedup_key_format() { let domain = "contoso.local"; let username = "svc_admin"; let dedup_key = format!( @@ -299,7 +293,7 @@ mod tests { } #[test] - fn test_sweep_dedup_key_normalizes_case() { + fn sweep_dedup_key_normalizes_case() { let dedup_key = format!( "{DEDUP_LAPS}:sweep:{}:{}", "CONTOSO.LOCAL".to_lowercase(), diff --git a/ares-cli/src/orchestrator/automation/mssql_exploitation.rs b/ares-cli/src/orchestrator/automation/mssql_exploitation.rs index 26c57182..8c2ab558 100644 --- a/ares-cli/src/orchestrator/automation/mssql_exploitation.rs +++ b/ares-cli/src/orchestrator/automation/mssql_exploitation.rs @@ -222,7 +222,7 @@ mod tests { } #[test] - fn test_is_mssql_deep_candidate_positive() { + fn is_mssql_deep_candidate_positive() { assert!(is_mssql_deep_candidate("mssql_access")); assert!(is_mssql_deep_candidate("MSSQL_ACCESS")); assert!(is_mssql_deep_candidate("mssql_linked_server")); @@ -230,7 +230,7 @@ mod tests { } #[test] - fn test_is_mssql_deep_candidate_negative() { + fn is_mssql_deep_candidate_negative() { assert!(!is_mssql_deep_candidate("mssql_impersonation")); assert!(!is_mssql_deep_candidate("rbcd")); assert!(!is_mssql_deep_candidate("esc1")); @@ -239,7 +239,7 @@ mod tests { } #[test] - fn test_resolve_mssql_target_ip_from_target_ip() { + fn resolve_mssql_target_ip_from_target_ip() { let details = make_details(&[("target_ip", "192.168.58.20"), ("target", "192.168.58.30")]); assert_eq!( resolve_mssql_target_ip(&details, "fallback"), @@ -248,7 +248,7 @@ mod tests { } #[test] - fn test_resolve_mssql_target_ip_from_target() { + fn resolve_mssql_target_ip_from_target() { let details = make_details(&[("target", "192.168.58.30")]); assert_eq!( resolve_mssql_target_ip(&details, "fallback"), @@ -257,7 +257,7 @@ mod tests { } #[test] - fn test_resolve_mssql_target_ip_fallback() { + fn resolve_mssql_target_ip_fallback() { let details = make_details(&[("domain", "contoso.local")]); assert_eq!( resolve_mssql_target_ip(&details, "192.168.58.99"), @@ -266,7 +266,7 @@ mod tests { } #[test] - fn test_resolve_mssql_target_ip_empty_details() { + fn resolve_mssql_target_ip_empty_details() { let details: HashMap = HashMap::new(); assert_eq!( resolve_mssql_target_ip(&details, "192.168.58.1"), @@ -275,7 +275,7 @@ mod tests { } #[test] - fn test_dedup_key_format() { + fn dedup_key_format() { let vuln_id = "vuln-789"; let dedup_key = format!("{DEDUP_MSSQL_DEEP}:{vuln_id}"); assert_eq!(dedup_key, "mssql_deep:vuln-789"); diff --git a/ares-cli/src/orchestrator/automation/rbcd.rs b/ares-cli/src/orchestrator/automation/rbcd.rs index 03b1add2..28d3ce6e 100644 --- a/ares-cli/src/orchestrator/automation/rbcd.rs +++ b/ares-cli/src/orchestrator/automation/rbcd.rs @@ -273,7 +273,7 @@ mod tests { use super::*; #[test] - fn test_is_rbcd_candidate_direct_types() { + fn is_rbcd_candidate_direct_types() { assert!(is_rbcd_candidate("rbcd", None)); assert!(is_rbcd_candidate("RBCD", None)); assert!(is_rbcd_candidate("genericall_computer", None)); @@ -281,14 +281,14 @@ mod tests { } #[test] - fn test_is_rbcd_candidate_with_target_type() { + fn is_rbcd_candidate_with_target_type() { assert!(is_rbcd_candidate("genericall", Some("Computer"))); assert!(is_rbcd_candidate("genericwrite", Some("DC01$"))); assert!(is_rbcd_candidate("GenericAll", Some("computer"))); } #[test] - fn test_is_rbcd_candidate_negative() { + fn is_rbcd_candidate_negative() { assert!(!is_rbcd_candidate("genericall", None)); assert!(!is_rbcd_candidate("genericall", Some("User"))); assert!(!is_rbcd_candidate("genericwrite", Some("Group"))); @@ -297,7 +297,7 @@ mod tests { } #[test] - fn test_resolve_computer_ip_exact_match() { + fn resolve_computer_ip_exact_match() { let hosts = vec![ ("dc01", "192.168.58.10"), ("sql01.contoso.local", "192.168.58.20"), @@ -307,7 +307,7 @@ mod tests { } #[test] - fn test_resolve_computer_ip_fqdn_match() { + fn resolve_computer_ip_fqdn_match() { let hosts = vec![ ("dc01.contoso.local", "192.168.58.10"), ("sql01.contoso.local", "192.168.58.20"), @@ -317,21 +317,21 @@ mod tests { } #[test] - fn test_resolve_computer_ip_no_match() { + fn resolve_computer_ip_no_match() { let hosts = vec![("dc01.contoso.local", "192.168.58.10")]; let result = resolve_computer_ip("dc02$", hosts.into_iter()); assert!(result.is_none()); } #[test] - fn test_resolve_computer_ip_no_dollar_suffix() { + fn resolve_computer_ip_no_dollar_suffix() { let hosts = vec![("web01.contoso.local", "192.168.58.30")]; let result = resolve_computer_ip("web01", hosts.into_iter()); assert_eq!(result, Some("192.168.58.30".to_string())); } #[test] - fn test_resolve_computer_ip_partial_no_match() { + fn resolve_computer_ip_partial_no_match() { // "dc01" should not match "dc011.contoso.local" let hosts = vec![("dc011.contoso.local", "192.168.58.11")]; let result = resolve_computer_ip("dc01$", hosts.into_iter()); @@ -339,19 +339,19 @@ mod tests { } #[test] - fn test_dedup_key_format() { + fn dedup_key_format() { let vuln_id = "vuln-123"; let dedup_key = format!("{DEDUP_RBCD}:{vuln_id}"); assert_eq!(dedup_key, "rbcd_exploit:vuln-123"); } #[test] - fn test_dedup_key_constant() { + fn dedup_key_constant() { assert_eq!(DEDUP_RBCD, "rbcd_exploit"); } #[test] - fn test_dedup_key_with_uuid_vuln_id() { + fn dedup_key_with_uuid_vuln_id() { let vuln_id = "550e8400-e29b-41d4-a716-446655440000"; let dedup_key = format!("{DEDUP_RBCD}:{vuln_id}"); assert_eq!( @@ -361,7 +361,7 @@ mod tests { } #[test] - fn test_resolve_computer_ip_empty_hostname() { + fn resolve_computer_ip_empty_hostname() { // Hosts with empty hostname should not match anything let hosts = vec![("", "192.168.58.10")]; let result = resolve_computer_ip("dc01$", hosts.into_iter()); @@ -369,7 +369,7 @@ mod tests { } #[test] - fn test_resolve_computer_ip_empty_target() { + fn resolve_computer_ip_empty_target() { // Empty target should not match any host let hosts = vec![("dc01.contoso.local", "192.168.58.10")]; let result = resolve_computer_ip("", hosts.into_iter()); @@ -377,7 +377,7 @@ mod tests { } #[test] - fn test_resolve_computer_ip_dollar_only_target() { + fn resolve_computer_ip_dollar_only_target() { // A target of just "$" should trim to empty and not match let hosts = vec![("dc01.contoso.local", "192.168.58.10")]; let result = resolve_computer_ip("$", hosts.into_iter()); @@ -385,14 +385,14 @@ mod tests { } #[test] - fn test_resolve_computer_ip_case_insensitive() { + fn resolve_computer_ip_case_insensitive() { let hosts = vec![("DC01.CONTOSO.LOCAL", "192.168.58.10")]; let result = resolve_computer_ip("dc01", hosts.into_iter()); assert_eq!(result, Some("192.168.58.10".to_string())); } #[test] - fn test_resolve_computer_ip_multiple_hosts_first_match() { + fn resolve_computer_ip_multiple_hosts_first_match() { // When multiple hosts could match, returns the first one let hosts = vec![ ("dc01.contoso.local", "192.168.58.10"), @@ -403,14 +403,14 @@ mod tests { } #[test] - fn test_resolve_computer_ip_empty_hosts_list() { + fn resolve_computer_ip_empty_hosts_list() { let hosts: Vec<(&str, &str)> = vec![]; let result = resolve_computer_ip("dc01$", hosts.into_iter()); assert!(result.is_none()); } #[test] - fn test_resolve_computer_ip_machine_account_with_dollar() { + fn resolve_computer_ip_machine_account_with_dollar() { // Verify $ is stripped from machine account names let hosts = vec![("sql01.contoso.local", "192.168.58.20")]; let result = resolve_computer_ip("SQL01$", hosts.into_iter()); @@ -418,7 +418,7 @@ mod tests { } #[test] - fn test_resolve_computer_ip_fqdn_target_no_match() { + fn resolve_computer_ip_fqdn_target_no_match() { // FQDN target should not match since we only compare short name // "dc01.contoso.local" trimmed of $ is "dc01.contoso.local" // which does not equal "dc01" and "dc01" does not start with "dc01.contoso.local." @@ -430,7 +430,7 @@ mod tests { } #[test] - fn test_is_rbcd_candidate_all_vuln_type_strings() { + fn is_rbcd_candidate_all_vuln_type_strings() { // Exhaustive test of all recognized RBCD vuln_type values assert!(is_rbcd_candidate("rbcd", None)); assert!(is_rbcd_candidate("RBCD", None)); @@ -444,7 +444,7 @@ mod tests { } #[test] - fn test_is_rbcd_candidate_generic_with_computer_target() { + fn is_rbcd_candidate_generic_with_computer_target() { // genericall/genericwrite require target_type=Computer to be RBCD candidates assert!(is_rbcd_candidate("genericall", Some("Computer"))); assert!(is_rbcd_candidate("genericall", Some("computer"))); @@ -454,7 +454,7 @@ mod tests { } #[test] - fn test_is_rbcd_candidate_generic_with_machine_account_target() { + fn is_rbcd_candidate_generic_with_machine_account_target() { // Machine accounts ending with $ are treated as computer targets assert!(is_rbcd_candidate("genericall", Some("DC01$"))); assert!(is_rbcd_candidate("genericwrite", Some("SQL01$"))); @@ -462,14 +462,14 @@ mod tests { } #[test] - fn test_is_rbcd_candidate_generic_without_target_type_rejected() { + fn is_rbcd_candidate_generic_without_target_type_rejected() { // genericall/genericwrite without target_type should NOT be RBCD assert!(!is_rbcd_candidate("genericall", None)); assert!(!is_rbcd_candidate("genericwrite", None)); } #[test] - fn test_is_rbcd_candidate_generic_with_non_computer_target() { + fn is_rbcd_candidate_generic_with_non_computer_target() { // genericall/genericwrite on non-computer targets assert!(!is_rbcd_candidate("genericall", Some("User"))); assert!(!is_rbcd_candidate("genericall", Some("Group"))); @@ -479,7 +479,7 @@ mod tests { } #[test] - fn test_is_rbcd_candidate_unrelated_vuln_types() { + fn is_rbcd_candidate_unrelated_vuln_types() { // Non-RBCD vuln types should all return false regardless of target_type let non_rbcd = vec![ "esc1", diff --git a/ares-cli/src/orchestrator/automation/s4u.rs b/ares-cli/src/orchestrator/automation/s4u.rs index 379d979c..008d5e17 100644 --- a/ares-cli/src/orchestrator/automation/s4u.rs +++ b/ares-cli/src/orchestrator/automation/s4u.rs @@ -379,42 +379,38 @@ mod tests { } } - // ─── Constants ────────────────────────────────────────────────────────── - #[test] - fn test_s4u_failure_cooldown_is_five_minutes() { + fn s4u_failure_cooldown_is_five_minutes() { assert_eq!(S4U_FAILURE_COOLDOWN, Duration::from_secs(300)); } #[test] - fn test_s4u_max_failures_value() { + fn s4u_max_failures_value() { assert_eq!(S4U_MAX_FAILURES, 6); } #[test] - fn test_permanent_revocation_patterns_contents() { + fn permanent_revocation_patterns_contents() { assert!(PERMANENT_REVOCATION_PATTERNS.contains(&"STATUS_ACCOUNT_DISABLED")); assert!(PERMANENT_REVOCATION_PATTERNS.contains(&"KDC_ERR_KEY_EXPIRED")); assert_eq!(PERMANENT_REVOCATION_PATTERNS.len(), 2); } #[test] - fn test_lockout_patterns_contents() { + fn lockout_patterns_contents() { assert!(LOCKOUT_PATTERNS.contains(&"KDC_ERR_CLIENT_REVOKED")); assert!(LOCKOUT_PATTERNS.contains(&"STATUS_ACCOUNT_LOCKED_OUT")); assert_eq!(LOCKOUT_PATTERNS.len(), 2); } - // ─── result_matches_patterns ──────────────────────────────────────────── - #[test] - fn test_result_matches_patterns_no_result_returns_false() { + fn result_matches_patterns_no_result_returns_false() { let tr = make_result(None, None); assert!(!result_matches_patterns(&tr, &["STATUS_ACCOUNT_DISABLED"])); } #[test] - fn test_result_matches_patterns_error_field_match() { + fn result_matches_patterns_error_field_match() { let tr = make_result( Some(json!({})), Some("Kerberos error: STATUS_ACCOUNT_DISABLED on dc01.contoso.local".to_string()), @@ -423,7 +419,7 @@ mod tests { } #[test] - fn test_result_matches_patterns_tool_outputs_match() { + fn result_matches_patterns_tool_outputs_match() { let tr = make_result( Some(json!({ "tool_outputs": [ @@ -437,7 +433,7 @@ mod tests { } #[test] - fn test_result_matches_patterns_summary_match() { + fn result_matches_patterns_summary_match() { let tr = make_result( Some(json!({ "summary": "S4U attack failed: STATUS_ACCOUNT_LOCKED_OUT for svc_sql$@contoso.local" @@ -448,7 +444,7 @@ mod tests { } #[test] - fn test_result_matches_patterns_output_key_match() { + fn result_matches_patterns_output_key_match() { let tr = make_result( Some(json!({ "output": "KDC_ERR_KEY_EXPIRED when requesting TGT for svc_web$@contoso.local" @@ -459,7 +455,7 @@ mod tests { } #[test] - fn test_result_matches_patterns_tool_output_key_match() { + fn result_matches_patterns_tool_output_key_match() { let tr = make_result( Some(json!({ "tool_output": "STATUS_ACCOUNT_DISABLED: svc_sql@contoso.local disabled in AD" @@ -470,7 +466,7 @@ mod tests { } #[test] - fn test_result_matches_patterns_no_match() { + fn result_matches_patterns_no_match() { let tr = make_result( Some(json!({ "summary": "S4U attack succeeded, got ticket for Administrator@contoso.local", @@ -486,7 +482,7 @@ mod tests { } #[test] - fn test_result_matches_patterns_tool_outputs_non_string_ignored() { + fn result_matches_patterns_tool_outputs_non_string_ignored() { // tool_outputs with non-string elements should not panic let tr = make_result( Some(json!({ @@ -497,10 +493,8 @@ mod tests { assert!(result_matches_patterns(&tr, &["KDC_ERR_CLIENT_REVOKED"])); } - // ─── has_permanent_revocation ─────────────────────────────────────────── - #[test] - fn test_has_permanent_revocation_status_account_disabled() { + fn has_permanent_revocation_status_account_disabled() { let tr = make_result( Some(json!({ "summary": "STATUS_ACCOUNT_DISABLED for svc_sql$@contoso.local" @@ -511,13 +505,13 @@ mod tests { } #[test] - fn test_has_permanent_revocation_kdc_err_key_expired() { + fn has_permanent_revocation_kdc_err_key_expired() { let tr = make_result(Some(json!({})), Some("KDC_ERR_KEY_EXPIRED".to_string())); assert!(has_permanent_revocation(&tr)); } #[test] - fn test_has_permanent_revocation_false_for_lockout() { + fn has_permanent_revocation_false_for_lockout() { let tr = make_result( Some(json!({ "summary": "KDC_ERR_CLIENT_REVOKED for svc_sql@contoso.local" @@ -527,10 +521,8 @@ mod tests { assert!(!has_permanent_revocation(&tr)); } - // ─── has_lockout_error ────────────────────────────────────────────────── - #[test] - fn test_has_lockout_error_kdc_err_client_revoked() { + fn has_lockout_error_kdc_err_client_revoked() { let tr = make_result( Some(json!({ "output": "KDC_ERR_CLIENT_REVOKED when requesting TGT for svc_sql@contoso.local" @@ -541,7 +533,7 @@ mod tests { } #[test] - fn test_has_lockout_error_status_account_locked_out() { + fn has_lockout_error_status_account_locked_out() { let tr = make_result( Some(json!({})), Some("SMB error: STATUS_ACCOUNT_LOCKED_OUT on 192.168.58.10".to_string()), @@ -550,7 +542,7 @@ mod tests { } #[test] - fn test_has_lockout_error_false_for_permanent() { + fn has_lockout_error_false_for_permanent() { let tr = make_result( Some(json!({ "summary": "STATUS_ACCOUNT_DISABLED for svc_sql$@contoso.local" @@ -561,7 +553,7 @@ mod tests { } #[test] - fn test_has_lockout_error_false_for_success() { + fn has_lockout_error_false_for_success() { let tr = make_result( Some(json!({ "summary": "S4U attack succeeded, ticket for Administrator@contoso.local" diff --git a/ares-cli/src/orchestrator/automation/shadow_credentials.rs b/ares-cli/src/orchestrator/automation/shadow_credentials.rs index ae571b76..4d8759ec 100644 --- a/ares-cli/src/orchestrator/automation/shadow_credentials.rs +++ b/ares-cli/src/orchestrator/automation/shadow_credentials.rs @@ -255,12 +255,10 @@ mod tests { use super::*; use std::collections::HashMap; - // ----------------------------------------------------------------------- // is_shadow_cred_candidate - // ----------------------------------------------------------------------- #[test] - fn test_is_shadow_cred_candidate_positive() { + fn is_shadow_cred_candidate_positive() { assert!(is_shadow_cred_candidate("genericall")); assert!(is_shadow_cred_candidate("GenericAll")); assert!(is_shadow_cred_candidate("genericwrite")); @@ -273,7 +271,7 @@ mod tests { } #[test] - fn test_is_shadow_cred_candidate_negative() { + fn is_shadow_cred_candidate_negative() { assert!(!is_shadow_cred_candidate("rbcd")); assert!(!is_shadow_cred_candidate("esc1")); assert!(!is_shadow_cred_candidate("mssql_access")); @@ -283,14 +281,14 @@ mod tests { } #[test] - fn test_is_shadow_cred_candidate_case_insensitive() { + fn is_shadow_cred_candidate_case_insensitive() { assert!(is_shadow_cred_candidate("GENERICALL")); assert!(is_shadow_cred_candidate("WriteDacl")); assert!(is_shadow_cred_candidate("ACL_GENERICWRITE")); } #[test] - fn test_is_shadow_cred_candidate_partial_match_rejected() { + fn is_shadow_cred_candidate_partial_match_rejected() { // Substrings or superstrings should not match assert!(!is_shadow_cred_candidate("acl_genericall_extra")); assert!(!is_shadow_cred_candidate("not_genericall")); @@ -299,18 +297,16 @@ mod tests { } #[test] - fn test_is_shadow_cred_candidate_whitespace_rejected() { + fn is_shadow_cred_candidate_whitespace_rejected() { assert!(!is_shadow_cred_candidate(" genericall")); assert!(!is_shadow_cred_candidate("genericall ")); assert!(!is_shadow_cred_candidate(" genericall ")); } - // ----------------------------------------------------------------------- // extract_source_user - // ----------------------------------------------------------------------- #[test] - fn test_extract_source_user_primary_key() { + fn extract_source_user_primary_key() { let mut details = HashMap::new(); details.insert( "source".to_string(), @@ -320,7 +316,7 @@ mod tests { } #[test] - fn test_extract_source_user_fallback_source_user() { + fn extract_source_user_fallback_source_user() { let mut details = HashMap::new(); details.insert( "source_user".to_string(), @@ -333,7 +329,7 @@ mod tests { } #[test] - fn test_extract_source_user_fallback_attacker() { + fn extract_source_user_fallback_attacker() { let mut details = HashMap::new(); details.insert( "attacker".to_string(), @@ -343,7 +339,7 @@ mod tests { } #[test] - fn test_extract_source_user_priority_order() { + fn extract_source_user_priority_order() { let mut details = HashMap::new(); details.insert( "source".to_string(), @@ -361,20 +357,20 @@ mod tests { } #[test] - fn test_extract_source_user_empty_details() { + fn extract_source_user_empty_details() { let details = HashMap::new(); assert_eq!(extract_source_user(&details), None); } #[test] - fn test_extract_source_user_non_string_value() { + fn extract_source_user_non_string_value() { let mut details = HashMap::new(); details.insert("source".to_string(), serde_json::Value::Number(123.into())); assert_eq!(extract_source_user(&details), None); } #[test] - fn test_extract_source_user_null_does_not_fall_through() { + fn extract_source_user_null_does_not_fall_through() { // When "source" key exists but is Null, get() returns Some(&Null), // so or_else() does NOT try "attacker". The as_str() on Null returns None. let mut details = HashMap::new(); @@ -386,12 +382,10 @@ mod tests { assert_eq!(extract_source_user(&details), None); } - // ----------------------------------------------------------------------- // extract_target_user - // ----------------------------------------------------------------------- #[test] - fn test_extract_target_user_primary_key() { + fn extract_target_user_primary_key() { let mut details = HashMap::new(); details.insert( "target".to_string(), @@ -401,7 +395,7 @@ mod tests { } #[test] - fn test_extract_target_user_fallback_target_user() { + fn extract_target_user_fallback_target_user() { let mut details = HashMap::new(); details.insert( "target_user".to_string(), @@ -411,7 +405,7 @@ mod tests { } #[test] - fn test_extract_target_user_fallback_victim() { + fn extract_target_user_fallback_victim() { let mut details = HashMap::new(); details.insert( "victim".to_string(), @@ -421,7 +415,7 @@ mod tests { } #[test] - fn test_extract_target_user_fallback_account_name() { + fn extract_target_user_fallback_account_name() { let mut details = HashMap::new(); details.insert( "account_name".to_string(), @@ -431,7 +425,7 @@ mod tests { } #[test] - fn test_extract_target_user_priority_order() { + fn extract_target_user_priority_order() { let mut details = HashMap::new(); details.insert( "target".to_string(), @@ -453,48 +447,44 @@ mod tests { } #[test] - fn test_extract_target_user_empty_details() { + fn extract_target_user_empty_details() { let details = HashMap::new(); assert_eq!(extract_target_user(&details), None); } #[test] - fn test_extract_target_user_non_string_value() { + fn extract_target_user_non_string_value() { let mut details = HashMap::new(); details.insert("target".to_string(), serde_json::Value::Bool(false)); assert_eq!(extract_target_user(&details), None); } - // ----------------------------------------------------------------------- // dedup key format - // ----------------------------------------------------------------------- #[test] - fn test_dedup_key_format() { + fn dedup_key_format() { let vuln_id = "vuln-456"; let dedup_key = format!("{DEDUP_SHADOW_CREDS}:{vuln_id}"); assert_eq!(dedup_key, "shadow_creds:vuln-456"); } #[test] - fn test_dedup_key_unique_per_vuln() { + fn dedup_key_unique_per_vuln() { let key1 = format!("{DEDUP_SHADOW_CREDS}:vuln-001"); let key2 = format!("{DEDUP_SHADOW_CREDS}:vuln-002"); assert_ne!(key1, key2); } #[test] - fn test_dedup_key_contains_prefix() { + fn dedup_key_contains_prefix() { let key = format!("{DEDUP_SHADOW_CREDS}:vuln-123"); assert!(key.starts_with("shadow_creds:")); } - // ----------------------------------------------------------------------- // ShadowCredWork construction patterns - // ----------------------------------------------------------------------- #[test] - fn test_shadow_cred_work_with_credential() { + fn shadow_cred_work_with_credential() { let work = ShadowCredWork { vuln_id: "vuln-sc-001".to_string(), dedup_key: format!("{DEDUP_SHADOW_CREDS}:vuln-sc-001"), @@ -524,7 +514,7 @@ mod tests { } #[test] - fn test_shadow_cred_work_with_hash_fallback() { + fn shadow_cred_work_with_hash_fallback() { let work = ShadowCredWork { vuln_id: "vuln-sc-002".to_string(), dedup_key: format!("{DEDUP_SHADOW_CREDS}:vuln-sc-002"), @@ -550,12 +540,14 @@ mod tests { }; assert!(work.credential.is_none()); - assert!(work.hash.is_some()); - assert_eq!(work.hash.as_ref().unwrap().hash_type, "NTLM"); + assert_eq!( + work.hash.as_ref().expect("hash should be set").hash_type, + "NTLM" + ); } #[test] - fn test_shadow_cred_work_no_dc_ip() { + fn shadow_cred_work_no_dc_ip() { let work = ShadowCredWork { vuln_id: "vuln-sc-003".to_string(), dedup_key: format!("{DEDUP_SHADOW_CREDS}:vuln-sc-003"), @@ -580,12 +572,10 @@ mod tests { assert!(work.dc_ip.is_none()); } - // ----------------------------------------------------------------------- // Integration-like: combined extraction from realistic vuln details - // ----------------------------------------------------------------------- #[test] - fn test_full_shadow_cred_extraction() { + fn full_shadow_cred_extraction() { let mut details = HashMap::new(); details.insert( "source".to_string(), @@ -606,7 +596,7 @@ mod tests { } #[test] - fn test_extraction_with_alternate_keys() { + fn extraction_with_alternate_keys() { let mut details = HashMap::new(); details.insert( "attacker".to_string(), @@ -626,7 +616,7 @@ mod tests { } #[test] - fn test_extraction_missing_source_returns_none() { + fn extraction_missing_source_returns_none() { let mut details = HashMap::new(); // Only target present, no source details.insert( @@ -639,7 +629,7 @@ mod tests { } #[test] - fn test_extraction_missing_target_returns_none() { + fn extraction_missing_target_returns_none() { let mut details = HashMap::new(); // Only source present, no target details.insert( diff --git a/ares-cli/src/orchestrator/automation/unconstrained.rs b/ares-cli/src/orchestrator/automation/unconstrained.rs index ff4166c1..4353a014 100644 --- a/ares-cli/src/orchestrator/automation/unconstrained.rs +++ b/ares-cli/src/orchestrator/automation/unconstrained.rs @@ -455,9 +455,7 @@ mod tests { use std::time::Duration; use tokio::time::Instant; - // ----------------------------------------------------------------------- // hostname resolution logic - // ----------------------------------------------------------------------- /// Simulate the hostname resolution logic from the main function. fn resolve_host_ip(account_name: &str, hosts: &[(String, String)]) -> Option { @@ -473,7 +471,7 @@ mod tests { } #[test] - fn test_hostname_resolution_machine_account() { + fn hostname_resolution_machine_account() { let account = "DC02$"; let prefix = account.trim_end_matches('$').to_lowercase(); assert_eq!(prefix, "dc02"); @@ -484,7 +482,7 @@ mod tests { } #[test] - fn test_hostname_resolution_short_name() { + fn hostname_resolution_short_name() { let account = "DC01$"; let prefix = account.trim_end_matches('$').to_lowercase(); assert_eq!(prefix, "dc01"); @@ -495,7 +493,7 @@ mod tests { } #[test] - fn test_hostname_resolution_fqdn_match() { + fn hostname_resolution_fqdn_match() { let hosts = vec![ ( "dc01.contoso.local".to_string(), @@ -513,7 +511,7 @@ mod tests { } #[test] - fn test_hostname_resolution_short_hostname_match() { + fn hostname_resolution_short_hostname_match() { let hosts = vec![("dc01".to_string(), "192.168.58.10".to_string())]; assert_eq!( resolve_host_ip("DC01$", &hosts), @@ -522,7 +520,7 @@ mod tests { } #[test] - fn test_hostname_resolution_no_match() { + fn hostname_resolution_no_match() { let hosts = vec![ ( "sql01.contoso.local".to_string(), @@ -537,7 +535,7 @@ mod tests { } #[test] - fn test_hostname_resolution_case_insensitive() { + fn hostname_resolution_case_insensitive() { let hosts = vec![( "DC01.CONTOSO.LOCAL".to_string(), "192.168.58.10".to_string(), @@ -549,7 +547,7 @@ mod tests { } #[test] - fn test_hostname_resolution_prefix_not_substring() { + fn hostname_resolution_prefix_not_substring() { // "dc01" should not match "dc011.contoso.local" let hosts = vec![( "dc011.contoso.local".to_string(), @@ -559,7 +557,7 @@ mod tests { } #[test] - fn test_hostname_resolution_multiple_domains() { + fn hostname_resolution_multiple_domains() { let hosts = vec![ ( "dc01.contoso.local".to_string(), @@ -577,12 +575,10 @@ mod tests { ); } - // ----------------------------------------------------------------------- // is_machine_account - // ----------------------------------------------------------------------- #[test] - fn test_is_machine_account() { + fn is_machine_account() { assert!("DC02$".ends_with('$')); assert!("SQL01$".ends_with('$')); assert!("WEB01$".ends_with('$')); @@ -592,54 +588,48 @@ mod tests { } #[test] - fn test_machine_account_prefix_extraction() { + fn machine_account_prefix_extraction() { assert_eq!("DC01$".trim_end_matches('$').to_lowercase(), "dc01"); assert_eq!("SQL01$".trim_end_matches('$').to_lowercase(), "sql01"); assert_eq!("WEB-SRV$".trim_end_matches('$').to_lowercase(), "web-srv"); } - // ----------------------------------------------------------------------- // user account handling - // ----------------------------------------------------------------------- #[test] - fn test_user_account_gets_dc_ip_as_target() { + fn user_account_gets_dc_ip_as_target() { let account = "testuser"; let is_machine = account.ends_with('$'); assert!(!is_machine); } - // ----------------------------------------------------------------------- // dedup key format - // ----------------------------------------------------------------------- #[test] - fn test_dedup_key_format_user_account() { + fn dedup_key_format_user_account() { let account = "testuser"; let dedup_key = format!("uc_user:{}", account.to_lowercase()); assert_eq!(dedup_key, "uc_user:testuser"); } #[test] - fn test_dedup_key_case_normalized() { + fn dedup_key_case_normalized() { let key1 = format!("uc_user:{}", "TestUser".to_lowercase()); let key2 = format!("uc_user:{}", "testuser".to_lowercase()); assert_eq!(key1, key2); } #[test] - fn test_dedup_key_unique_per_user() { + fn dedup_key_unique_per_user() { let key1 = format!("uc_user:{}", "user1".to_lowercase()); let key2 = format!("uc_user:{}", "user2".to_lowercase()); assert_ne!(key1, key2); } - // ----------------------------------------------------------------------- // PhaseState - // ----------------------------------------------------------------------- #[test] - fn test_phase_state_defaults() { + fn phase_state_defaults() { let phase = PhaseState { coercion_dispatched_at: None, dump_attempts: 0, @@ -653,7 +643,7 @@ mod tests { } #[test] - fn test_phase_state_after_coercion() { + fn phase_state_after_coercion() { let phase = PhaseState { coercion_dispatched_at: Some(Instant::now()), dump_attempts: 0, @@ -666,7 +656,7 @@ mod tests { } #[test] - fn test_phase_state_after_first_dump() { + fn phase_state_after_first_dump() { let phase = PhaseState { coercion_dispatched_at: Some(Instant::now()), dump_attempts: 1, @@ -679,7 +669,7 @@ mod tests { } #[test] - fn test_phase_state_max_attempts_reached() { + fn phase_state_max_attempts_reached() { let phase = PhaseState { coercion_dispatched_at: Some(Instant::now()), dump_attempts: MAX_DUMP_ATTEMPTS, @@ -691,7 +681,7 @@ mod tests { } #[test] - fn test_phase_state_under_max_attempts() { + fn phase_state_under_max_attempts() { let phase = PhaseState { coercion_dispatched_at: Some(Instant::now()), dump_attempts: MAX_DUMP_ATTEMPTS - 1, @@ -702,12 +692,10 @@ mod tests { assert!(!phase.completed); } - // ----------------------------------------------------------------------- // Coercion timing logic - // ----------------------------------------------------------------------- #[test] - fn test_coerce_to_dump_delay_not_elapsed() { + fn coerce_to_dump_delay_not_elapsed() { let phase = PhaseState { coercion_dispatched_at: Some(Instant::now()), dump_attempts: 0, @@ -719,12 +707,10 @@ mod tests { assert!(elapsed < COERCE_TO_DUMP_DELAY); } - // ----------------------------------------------------------------------- // Dump retry timing logic - // ----------------------------------------------------------------------- #[test] - fn test_dump_retry_eligible_no_last_dump() { + fn dump_retry_eligible_no_last_dump() { let phase = PhaseState { coercion_dispatched_at: Some(Instant::now()), dump_attempts: 1, @@ -738,7 +724,7 @@ mod tests { } #[test] - fn test_dump_retry_not_yet_eligible() { + fn dump_retry_not_yet_eligible() { let phase = PhaseState { coercion_dispatched_at: Some(Instant::now()), dump_attempts: 1, @@ -750,42 +736,36 @@ mod tests { assert!(elapsed < DUMP_RETRY_DELAY); } - // ----------------------------------------------------------------------- // Constants - // ----------------------------------------------------------------------- #[test] - fn test_max_dump_attempts_constant() { + fn max_dump_attempts_constant() { assert_eq!(MAX_DUMP_ATTEMPTS, 3); } #[test] - fn test_coerce_to_dump_delay() { + fn coerce_to_dump_delay() { assert_eq!(COERCE_TO_DUMP_DELAY, Duration::from_secs(15)); } #[test] - fn test_dump_retry_delay() { + fn dump_retry_delay() { assert_eq!(DUMP_RETRY_DELAY, Duration::from_secs(60)); } - // ----------------------------------------------------------------------- // Action enum - // ----------------------------------------------------------------------- #[test] - fn test_action_debug_format() { + fn action_debug_format() { assert_eq!(format!("{:?}", Action::Coerce), "Coerce"); assert_eq!(format!("{:?}", Action::Dump), "Dump"); assert_eq!(format!("{:?}", Action::LlmExploit), "LlmExploit"); } - // ----------------------------------------------------------------------- // UnconstrainedWork construction patterns - // ----------------------------------------------------------------------- #[test] - fn test_unconstrained_work_machine_coerce() { + fn unconstrained_work_machine_coerce() { let work = UnconstrainedWork { vuln_id: "vuln-uc-001".to_string(), account_name: "DC02$".to_string(), @@ -815,7 +795,7 @@ mod tests { } #[test] - fn test_unconstrained_work_machine_dump() { + fn unconstrained_work_machine_dump() { let work = UnconstrainedWork { vuln_id: "vuln-uc-002".to_string(), account_name: "SQL01$".to_string(), @@ -842,7 +822,7 @@ mod tests { } #[test] - fn test_unconstrained_work_user_llm_exploit() { + fn unconstrained_work_user_llm_exploit() { let work = UnconstrainedWork { vuln_id: "vuln-uc-003".to_string(), account_name: "svc_admin".to_string(), @@ -866,18 +846,18 @@ mod tests { assert!(!work.account_name.ends_with('$')); assert!(matches!(work.action, Action::LlmExploit)); - assert!(work._dedup_key.is_some()); - assert_eq!(work._dedup_key.as_ref().unwrap(), "uc_user:svc_admin"); + assert_eq!( + work._dedup_key.as_ref().expect("dedup key should be set"), + "uc_user:svc_admin" + ); // For user accounts, host_ip matches dc_ip assert_eq!(work.host_ip, work.dc_ip.as_ref().unwrap().as_str()); } - // ----------------------------------------------------------------------- // Phase state machine transitions - // ----------------------------------------------------------------------- #[test] - fn test_phase_transition_none_to_coerce() { + fn phase_transition_none_to_coerce() { // When no phase exists and DC is available, action should be Coerce let mut phases: HashMap = HashMap::new(); let vuln_id = "vuln-001"; @@ -907,7 +887,7 @@ mod tests { } #[test] - fn test_phase_transition_already_coerced_skips_to_dump() { + fn phase_transition_already_coerced_skips_to_dump() { let phases: HashMap = HashMap::new(); let vuln_id = "vuln-002"; let dc_ip = Some("192.168.58.10".to_string()); @@ -924,7 +904,7 @@ mod tests { } #[test] - fn test_phase_dump_increments_attempts() { + fn phase_dump_increments_attempts() { let mut phase = PhaseState { coercion_dispatched_at: Some(Instant::now()), dump_attempts: 0, @@ -953,7 +933,7 @@ mod tests { } #[test] - fn test_phase_llm_exploit_immediately_completed() { + fn phase_llm_exploit_immediately_completed() { let phase = PhaseState { coercion_dispatched_at: None, dump_attempts: 0, diff --git a/ares-cli/src/orchestrator/blue/callbacks.rs b/ares-cli/src/orchestrator/blue/callbacks.rs index da93be0f..bf092974 100644 --- a/ares-cli/src/orchestrator/blue/callbacks.rs +++ b/ares-cli/src/orchestrator/blue/callbacks.rs @@ -514,7 +514,7 @@ mod tests { use serde_json::json; #[test] - fn test_is_callback() { + fn checks_callback() { let handler = BlueCallbackHandler { provider: Arc::new(MockProvider), dispatcher: Arc::new(MockDispatcher), @@ -536,7 +536,7 @@ mod tests { } #[test] - fn test_triage_complete_callback() { + fn triage_complete_callback() { let call = ToolCall { id: "c1".into(), name: "triage_complete".into(), @@ -557,7 +557,7 @@ mod tests { } #[test] - fn test_escalate_investigation_not_in_lifecycle_callbacks() { + fn escalate_investigation_not_in_lifecycle_callbacks() { // escalate_investigation is now handled async via dispatch_escalation_triage, // not the static handle_lifecycle_callback let call = ToolCall { @@ -572,7 +572,7 @@ mod tests { } #[test] - fn test_complete_investigation_callback() { + fn complete_investigation_callback() { let call = ToolCall { id: "c3".into(), name: "complete_investigation".into(), @@ -590,7 +590,7 @@ mod tests { } #[test] - fn test_unknown_callback() { + fn unknown_callback() { let call = ToolCall { id: "c4".into(), name: "nmap_scan".into(), diff --git a/ares-cli/src/orchestrator/blue/chaining.rs b/ares-cli/src/orchestrator/blue/chaining.rs index 98138642..efa29228 100644 --- a/ares-cli/src/orchestrator/blue/chaining.rs +++ b/ares-cli/src/orchestrator/blue/chaining.rs @@ -395,7 +395,7 @@ mod tests { use serde_json::json; #[test] - fn test_extract_evidence_types_from_evidence_types_array() { + fn extract_evidence_types_from_evidence_types_array() { let payload = json!({ "evidence_types": ["suspicious_ip", "lateral_movement"] }); @@ -404,7 +404,7 @@ mod tests { } #[test] - fn test_extract_evidence_types_from_evidence_objects() { + fn extract_evidence_types_from_evidence_objects() { let payload = json!({ "evidence": [ { "type": "Credential_Access", "value": "hash123" }, @@ -416,7 +416,7 @@ mod tests { } #[test] - fn test_extract_evidence_types_from_techniques() { + fn extract_evidence_types_from_techniques() { let payload = json!({ "techniques_found": ["T1558.003", "T1021.002"] }); @@ -425,7 +425,7 @@ mod tests { } #[test] - fn test_extract_evidence_types_dedup() { + fn extract_evidence_types_dedup() { let payload = json!({ "evidence_types": ["lateral_movement"], "techniques_found": ["T1550.002"] @@ -436,7 +436,7 @@ mod tests { } #[test] - fn test_should_escalate_critical_user_in_users_investigated() { + fn should_escalate_critical_user_in_users_investigated() { let result = BlueTaskResult { task_id: "t1".into(), investigation_id: "inv1".into(), @@ -454,7 +454,7 @@ mod tests { } #[test] - fn test_should_escalate_critical_user_in_highlights() { + fn should_escalate_critical_user_in_highlights() { let result = BlueTaskResult { task_id: "t2".into(), investigation_id: "inv1".into(), @@ -472,7 +472,7 @@ mod tests { } #[test] - fn test_should_escalate_high_severity() { + fn should_escalate_high_severity() { let result = BlueTaskResult { task_id: "t3".into(), investigation_id: "inv1".into(), @@ -491,7 +491,7 @@ mod tests { } #[test] - fn test_should_escalate_schema_admins() { + fn should_escalate_schema_admins() { let result = BlueTaskResult { task_id: "t4".into(), investigation_id: "inv1".into(), @@ -509,7 +509,7 @@ mod tests { } #[test] - fn test_should_not_escalate_normal_result() { + fn should_not_escalate_normal_result() { let result = BlueTaskResult { task_id: "t5".into(), investigation_id: "inv1".into(), @@ -526,7 +526,7 @@ mod tests { } #[test] - fn test_should_not_escalate_failed_result() { + fn should_not_escalate_failed_result() { let result = BlueTaskResult { task_id: "t6".into(), investigation_id: "inv1".into(), @@ -540,7 +540,7 @@ mod tests { } #[test] - fn test_should_escalate_findings_mention() { + fn should_escalate_findings_mention() { let result = BlueTaskResult { task_id: "t7".into(), investigation_id: "inv1".into(), @@ -558,7 +558,7 @@ mod tests { } #[test] - fn test_chain_map_coverage() { + fn chain_map_coverage() { // Verify all expected evidence types are present in the map let expected = [ "suspicious_ip", @@ -578,7 +578,7 @@ mod tests { } #[test] - fn test_privilege_escalation_dispatches_two_actions() { + fn privilege_escalation_dispatches_two_actions() { let actions = EVIDENCE_CHAIN_MAP.get("privilege_escalation").unwrap(); assert_eq!(actions.len(), 2); let task_types: Vec<&str> = actions.iter().map(|a| a.task_type).collect(); @@ -587,7 +587,7 @@ mod tests { } #[test] - fn test_critical_users_set() { + fn critical_users_set() { assert!(CRITICAL_USERS.contains("krbtgt")); assert!(CRITICAL_USERS.contains("administrator")); assert!(CRITICAL_USERS.contains("domain admins")); diff --git a/ares-cli/src/orchestrator/blue/investigation.rs b/ares-cli/src/orchestrator/blue/investigation.rs index 3138a2b8..5bd75041 100644 --- a/ares-cli/src/orchestrator/blue/investigation.rs +++ b/ares-cli/src/orchestrator/blue/investigation.rs @@ -528,7 +528,7 @@ mod tests { use super::*; #[test] - fn test_extract_verdict() { + fn extracts_verdict() { assert_eq!(extract_verdict("This is a true positive"), "true_positive"); assert_eq!( extract_verdict("Determined to be a false positive"), @@ -540,7 +540,7 @@ mod tests { } #[test] - fn test_process_outcome_completed() { + fn process_outcome_completed() { let outcome = AgentLoopOutcome { reason: LoopEndReason::TaskComplete { task_id: "inv1".into(), @@ -562,7 +562,7 @@ mod tests { } #[test] - fn test_process_outcome_escalated() { + fn process_outcome_escalated() { let outcome = AgentLoopOutcome { reason: LoopEndReason::RequestAssistance { issue: "Critical: active data exfiltration".into(), diff --git a/ares-cli/src/orchestrator/callback_handler/tests.rs b/ares-cli/src/orchestrator/callback_handler/tests.rs index 492df619..7a96212a 100644 --- a/ares-cli/src/orchestrator/callback_handler/tests.rs +++ b/ares-cli/src/orchestrator/callback_handler/tests.rs @@ -54,7 +54,7 @@ fn make_handler() -> OrchestratorCallbackHandler { } #[tokio::test] -async fn test_credential_summary_empty() { +async fn credential_summary_empty() { let handler = make_handler(); let call = ToolCall { id: "c1".into(), @@ -72,7 +72,7 @@ async fn test_credential_summary_empty() { } #[tokio::test] -async fn test_credential_summary_with_data() { +async fn credential_summary_with_data() { let handler = make_handler(); { let mut s = handler.state.write().await; @@ -98,7 +98,7 @@ async fn test_credential_summary_with_data() { } #[tokio::test] -async fn test_hash_summary_empty() { +async fn hash_summary_empty() { let handler = make_handler(); let call = ToolCall { id: "c3".into(), @@ -116,7 +116,7 @@ async fn test_hash_summary_empty() { } #[tokio::test] -async fn test_hash_value_lookup() { +async fn hash_value_lookup() { let handler = make_handler(); { let mut s = handler.state.write().await; @@ -145,7 +145,7 @@ async fn test_hash_value_lookup() { } #[tokio::test] -async fn test_hash_value_not_found() { +async fn hash_value_not_found() { let handler = make_handler(); let call = ToolCall { id: "c5".into(), @@ -160,7 +160,7 @@ async fn test_hash_value_not_found() { } #[tokio::test] -async fn test_pending_tasks_empty() { +async fn pending_tasks_empty() { let handler = make_handler(); let call = ToolCall { id: "c6".into(), @@ -178,7 +178,7 @@ async fn test_pending_tasks_empty() { } #[tokio::test] -async fn test_unknown_tool_returns_none() { +async fn unknown_tool_returns_none() { let handler = make_handler(); let call = ToolCall { id: "c7".into(), @@ -189,7 +189,7 @@ async fn test_unknown_tool_returns_none() { } #[tokio::test] -async fn test_dispatch_without_dispatcher() { +async fn dispatch_without_dispatcher() { let handler = make_handler(); let call = ToolCall { id: "c8".into(), @@ -201,7 +201,7 @@ async fn test_dispatch_without_dispatcher() { } #[tokio::test] -async fn test_operation_summary() { +async fn operation_summary() { let handler = make_handler(); { let mut s = handler.state.write().await; @@ -236,7 +236,7 @@ async fn test_operation_summary() { } #[tokio::test] -async fn test_dispatch_crack_without_dispatcher() { +async fn dispatch_crack_without_dispatcher() { let handler = make_handler(); let call = ToolCall { id: "c11".into(), @@ -248,7 +248,7 @@ async fn test_dispatch_crack_without_dispatcher() { } #[tokio::test] -async fn test_all_credentials_pagination() { +async fn all_credentials_pagination() { let handler = make_handler(); { let mut s = handler.state.write().await; @@ -280,7 +280,7 @@ async fn test_all_credentials_pagination() { } #[tokio::test] -async fn test_full_summary_with_populated_state() { +async fn full_summary_with_populated_state() { let handler = make_handler(); { let mut s = handler.state.write().await; @@ -342,7 +342,7 @@ async fn test_full_summary_with_populated_state() { } #[tokio::test] -async fn test_credential_summary_multi_domain() { +async fn credential_summary_multi_domain() { let handler = make_handler(); { let mut s = handler.state.write().await; @@ -372,7 +372,7 @@ async fn test_credential_summary_multi_domain() { } #[tokio::test] -async fn test_hash_value_case_insensitive_lookup() { +async fn hash_value_case_insensitive_lookup() { let handler = make_handler(); { let mut s = handler.state.write().await; @@ -398,7 +398,7 @@ async fn test_hash_value_case_insensitive_lookup() { } #[tokio::test] -async fn test_hash_value_filter_by_type() { +async fn hash_value_filter_by_type() { let handler = make_handler(); { let mut s = handler.state.write().await; @@ -434,7 +434,7 @@ async fn test_hash_value_filter_by_type() { } #[tokio::test] -async fn test_all_dispatch_tools_fail_without_dispatcher() { +async fn all_dispatch_tools_fail_without_dispatcher() { let handler = make_handler(); let dispatch_tools = [ ("dispatch_recon", json!({"target_ip": "192.168.58.10"})), @@ -473,7 +473,7 @@ async fn test_all_dispatch_tools_fail_without_dispatcher() { } #[tokio::test] -async fn test_all_callback_tools_recognized() { +async fn all_callback_tools_recognized() { let handler = make_handler(); let tools = [ "get_credential_summary", @@ -515,7 +515,7 @@ async fn test_all_callback_tools_recognized() { } #[tokio::test] -async fn test_all_hashes_pagination_large() { +async fn all_hashes_pagination_large() { let handler = make_handler(); { let mut s = handler.state.write().await; @@ -546,10 +546,8 @@ async fn test_all_hashes_pagination_large() { } } -// --- Disabled tool handler tests (dispatch.rs coverage) --- - #[tokio::test] -async fn test_record_credential_disabled() { +async fn record_credential_disabled() { let handler = make_handler(); let call = ToolCall { id: "dis-1".into(), @@ -567,7 +565,7 @@ async fn test_record_credential_disabled() { } #[tokio::test] -async fn test_record_timeline_event_disabled() { +async fn record_timeline_event_disabled() { let handler = make_handler(); let call = ToolCall { id: "dis-2".into(), @@ -585,7 +583,7 @@ async fn test_record_timeline_event_disabled() { } #[tokio::test] -async fn test_report_cracked_credential_disabled() { +async fn report_cracked_credential_disabled() { let handler = make_handler(); let call = ToolCall { id: "dis-3".into(), @@ -603,7 +601,7 @@ async fn test_report_cracked_credential_disabled() { } #[tokio::test] -async fn test_list_credentials_delegates_to_get_all() { +async fn list_credentials_delegates_to_get_all() { let handler = make_handler(); { let mut s = handler.state.write().await; @@ -630,7 +628,7 @@ async fn test_list_credentials_delegates_to_get_all() { } #[tokio::test] -async fn test_dispatch_coercion_without_dispatcher() { +async fn dispatch_coercion_without_dispatcher() { let handler = make_handler(); let call = ToolCall { id: "co-1".into(), @@ -642,7 +640,7 @@ async fn test_dispatch_coercion_without_dispatcher() { } #[tokio::test] -async fn test_dispatch_exploit_without_dispatcher() { +async fn dispatch_exploit_without_dispatcher() { let handler = make_handler(); let call = ToolCall { id: "ex-1".into(), @@ -654,7 +652,7 @@ async fn test_dispatch_exploit_without_dispatcher() { } #[tokio::test] -async fn test_get_agent_status_without_task_queue() { +async fn get_agent_status_without_task_queue() { let handler = make_handler(); let call = ToolCall { id: "as-1".into(), @@ -667,7 +665,7 @@ async fn test_get_agent_status_without_task_queue() { } #[tokio::test] -async fn test_hash_summary_with_mixed_types() { +async fn hash_summary_with_mixed_types() { let handler = make_handler(); { let mut s = handler.state.write().await; @@ -708,7 +706,7 @@ async fn test_hash_summary_with_mixed_types() { } #[tokio::test] -async fn test_all_credentials_zero_offset_default_limit() { +async fn all_credentials_zero_offset_default_limit() { let handler = make_handler(); { let mut s = handler.state.write().await; @@ -742,7 +740,7 @@ async fn test_all_credentials_zero_offset_default_limit() { } #[tokio::test] -async fn test_all_hashes_default_params() { +async fn all_hashes_default_params() { let handler = make_handler(); { let mut s = handler.state.write().await; @@ -774,7 +772,7 @@ async fn test_all_hashes_default_params() { } #[tokio::test] -async fn test_operation_summary_empty_state() { +async fn operation_summary_empty_state() { let handler = make_handler(); let call = ToolCall { id: "os-empty".into(), @@ -796,7 +794,7 @@ async fn test_operation_summary_empty_state() { } #[tokio::test] -async fn test_hash_value_empty_domain_filter() { +async fn hash_value_empty_domain_filter() { let handler = make_handler(); { let mut s = handler.state.write().await; diff --git a/ares-cli/src/orchestrator/completion.rs b/ares-cli/src/orchestrator/completion.rs index d71cbb08..64cd1cc3 100644 --- a/ares-cli/src/orchestrator/completion.rs +++ b/ares-cli/src/orchestrator/completion.rs @@ -499,17 +499,17 @@ mod tests { use super::*; #[test] - fn test_forest_root_of_simple() { + fn forest_root_of_simple() { assert_eq!(forest_root_of("contoso.local"), "contoso.local"); } #[test] - fn test_forest_root_of_child() { + fn forest_root_of_child() { assert_eq!(forest_root_of("north.contoso.local"), "contoso.local"); } #[test] - fn test_forest_root_of_deep_child() { + fn forest_root_of_deep_child() { assert_eq!(forest_root_of("sub.north.contoso.local"), "contoso.local"); } @@ -524,7 +524,7 @@ mod tests { } #[test] - fn test_undominated_single_domain_no_trusts() { + fn undominated_single_domain_no_trusts() { let trusted = std::collections::HashMap::new(); let dcs = std::collections::HashMap::new(); let mut dominated = HashSet::new(); @@ -551,7 +551,7 @@ mod tests { } #[test] - fn test_undominated_cross_forest_trust() { + fn undominated_cross_forest_trust() { let mut trusted = std::collections::HashMap::new(); trusted.insert( "fabrikam.local".to_string(), @@ -573,7 +573,7 @@ mod tests { } #[test] - fn test_undominated_all_forests_dominated() { + fn undominated_all_forests_dominated() { let mut trusted = std::collections::HashMap::new(); trusted.insert( "fabrikam.local".to_string(), @@ -595,7 +595,7 @@ mod tests { } #[test] - fn test_undominated_child_domain_not_separate_forest() { + fn undominated_child_domain_not_separate_forest() { // parent_child trust should NOT add a separate required forest let mut trusted = std::collections::HashMap::new(); trusted.insert( @@ -618,7 +618,7 @@ mod tests { } #[test] - fn test_undominated_child_domain_does_not_cover_forest() { + fn undominated_child_domain_does_not_cover_forest() { // Dominating a child domain does NOT cover the forest root — the // forest root DC has its own krbtgt and must be secretsdumped via // trust escalation (ExtraSid / trust key). @@ -638,7 +638,7 @@ mod tests { } #[test] - fn test_undominated_forest_root_dominated_directly() { + fn undominated_forest_root_dominated_directly() { // Dominating the forest root itself should satisfy the requirement let trusted = std::collections::HashMap::new(); let mut dominated = HashSet::new(); @@ -655,7 +655,7 @@ mod tests { } #[test] - fn test_undominated_dc_discovered_before_trust_enum() { + fn undominated_dc_discovered_before_trust_enum() { // fabrikam.local DC discovered via recon but trust not yet enumerated. // The DC should be included in required_forests to prevent premature // completion. @@ -676,27 +676,25 @@ mod tests { assert_eq!(result, vec!["fabrikam.local"]); } - // --- Additional coverage tests --- - #[test] - fn test_forest_root_of_case_insensitive() { + fn forest_root_of_case_insensitive() { assert_eq!(forest_root_of("CONTOSO.LOCAL"), "contoso.local"); assert_eq!(forest_root_of("North.Contoso.Local"), "contoso.local"); } #[test] - fn test_forest_root_of_single_label() { + fn forest_root_of_single_label() { // Single-label domain (unusual but should not panic) assert_eq!(forest_root_of("localhost"), "localhost"); } #[test] - fn test_forest_root_of_empty() { + fn forest_root_of_empty() { assert_eq!(forest_root_of(""), ""); } #[test] - fn test_undominated_no_target_no_first_domain() { + fn undominated_no_target_no_first_domain() { // Both target_domain and first_domain are None let trusted = std::collections::HashMap::new(); let dominated = HashSet::new(); @@ -706,7 +704,7 @@ mod tests { } #[test] - fn test_undominated_empty_target_domain() { + fn undominated_empty_target_domain() { // target_domain is Some("") — should be treated as missing let trusted = std::collections::HashMap::new(); let dominated = HashSet::new(); @@ -716,7 +714,7 @@ mod tests { } #[test] - fn test_undominated_only_first_domain() { + fn undominated_only_first_domain() { // target_domain is None but first_domain is set let trusted = std::collections::HashMap::new(); let dominated = HashSet::new(); @@ -727,7 +725,7 @@ mod tests { } #[test] - fn test_undominated_external_trust_is_cross_forest() { + fn undominated_external_trust_is_cross_forest() { // "external" trust type should be treated as cross-forest let mut trusted = std::collections::HashMap::new(); trusted.insert( @@ -748,7 +746,7 @@ mod tests { } #[test] - fn test_undominated_unknown_trust_not_cross_forest() { + fn undominated_unknown_trust_not_cross_forest() { // "unknown" trust type should NOT be treated as cross-forest let mut trusted = std::collections::HashMap::new(); trusted.insert( @@ -770,7 +768,7 @@ mod tests { } #[test] - fn test_undominated_multiple_cross_forest_trusts() { + fn undominated_multiple_cross_forest_trusts() { let mut trusted = std::collections::HashMap::new(); trusted.insert( "fabrikam.local".to_string(), @@ -797,7 +795,7 @@ mod tests { } #[test] - fn test_undominated_child_trust_domain_maps_to_parent_forest() { + fn undominated_child_trust_domain_maps_to_parent_forest() { // Cross-forest trust with a child domain like "north.fabrikam.local" // should map to forest root "fabrikam.local" let mut trusted = std::collections::HashMap::new(); @@ -820,7 +818,7 @@ mod tests { } #[test] - fn test_undominated_empty_dc_key_ignored() { + fn undominated_empty_dc_key_ignored() { // Empty string DC key should be ignored let trusted = std::collections::HashMap::new(); let mut dominated = HashSet::new(); @@ -838,7 +836,7 @@ mod tests { } #[test] - fn test_undominated_case_insensitive_dominated() { + fn undominated_case_insensitive_dominated() { // forest_root_of lowercases, so dominated domains with mixed case should still match let trusted = std::collections::HashMap::new(); let mut dominated = HashSet::new(); @@ -851,7 +849,7 @@ mod tests { } #[test] - fn test_undominated_target_and_first_same_forest() { + fn undominated_target_and_first_same_forest() { // target and first_domain in the same forest should only produce one entry let trusted = std::collections::HashMap::new(); let dominated = HashSet::new(); @@ -868,7 +866,7 @@ mod tests { } #[test] - fn test_undominated_target_and_first_different_forests() { + fn undominated_target_and_first_different_forests() { let trusted = std::collections::HashMap::new(); let dominated = HashSet::new(); let dcs = std::collections::HashMap::new(); @@ -886,7 +884,7 @@ mod tests { } #[test] - fn test_make_trust_helper() { + fn make_trust_helper() { let trust = make_trust("fabrikam.local", "forest"); assert_eq!(trust.domain, "fabrikam.local"); assert_eq!(trust.flat_name, "FABRIKAM"); diff --git a/ares-cli/src/orchestrator/deferred.rs b/ares-cli/src/orchestrator/deferred.rs index 8ae1e573..48b1b111 100644 --- a/ares-cli/src/orchestrator/deferred.rs +++ b/ares-cli/src/orchestrator/deferred.rs @@ -393,8 +393,6 @@ mod tests { assert!((t.enqueue_time - t2.enqueue_time).abs() < f64::EPSILON); } - // --- Additional coverage tests --- - #[test] fn score_zero_priority() { let t = make_task(0, 1000.0); diff --git a/ares-cli/src/orchestrator/llm_runner.rs b/ares-cli/src/orchestrator/llm_runner.rs index 7c053acd..8563ec2f 100644 --- a/ares-cli/src/orchestrator/llm_runner.rs +++ b/ares-cli/src/orchestrator/llm_runner.rs @@ -317,7 +317,7 @@ mod tests { use super::*; #[test] - fn test_role_for_task_type_recon_variants() { + fn role_for_task_type_recon_variants() { for tt in &[ "recon", "nmap", @@ -334,7 +334,7 @@ mod tests { } #[test] - fn test_role_for_task_type_credential_access_variants() { + fn role_for_task_type_credential_access_variants() { for tt in &[ "credential_access", "secretsdump", @@ -352,7 +352,7 @@ mod tests { } #[test] - fn test_role_for_task_type_other_roles() { + fn role_for_task_type_other_roles() { assert_eq!(role_for_task_type("crack"), Some(AgentRole::Cracker)); assert_eq!(role_for_task_type("lateral"), Some(AgentRole::Lateral)); assert_eq!( @@ -369,14 +369,14 @@ mod tests { } #[test] - fn test_role_for_task_type_unmapped() { + fn role_for_task_type_unmapped() { assert_eq!(role_for_task_type("command"), None); assert_eq!(role_for_task_type("unknown"), None); assert_eq!(role_for_task_type(""), None); } #[test] - fn test_build_system_prompt_all_roles() { + fn build_system_prompt_all_roles() { let snapshot = StateSnapshot::default(); for role in &[ AgentRole::Recon, @@ -396,7 +396,7 @@ mod tests { } #[test] - fn test_build_task_prompt_known_types() { + fn build_task_prompt_known_types() { let snapshot = StateSnapshot::default(); let payload = serde_json::json!({ "target_ip": "192.168.58.10", @@ -410,7 +410,7 @@ mod tests { } #[test] - fn test_build_task_prompt_unknown_type_falls_back() { + fn build_task_prompt_unknown_type_falls_back() { let snapshot = StateSnapshot::default(); let payload = serde_json::json!({"foo": "bar"}); diff --git a/ares-cli/src/orchestrator/monitoring.rs b/ares-cli/src/orchestrator/monitoring.rs index e54c38ec..45e47232 100644 --- a/ares-cli/src/orchestrator/monitoring.rs +++ b/ares-cli/src/orchestrator/monitoring.rs @@ -469,8 +469,6 @@ mod tests { assert_eq!(agents.get("a1").unwrap().role, "lateral"); } - // --- Additional coverage tests --- - #[tokio::test] async fn agent_names_empty_initially() { let r = AgentRegistry::new(); diff --git a/ares-cli/src/orchestrator/output_extraction/tests.rs b/ares-cli/src/orchestrator/output_extraction/tests.rs index 003dc25c..1842b256 100644 --- a/ares-cli/src/orchestrator/output_extraction/tests.rs +++ b/ares-cli/src/orchestrator/output_extraction/tests.rs @@ -1,7 +1,7 @@ use super::*; #[test] -fn test_extract_ntlm_with_domain() { +fn extract_ntlm_with_domain() { let output = "CONTOSO\\Administrator:500:aad3b435b51404eeaad3b435b51404ee:e19ccf75ee54e06b06a5907af13cef42:::"; let hashes = extract_hashes(output, "contoso.local"); @@ -15,7 +15,7 @@ fn test_extract_ntlm_with_domain() { } #[test] -fn test_extract_ntlm_without_domain() { +fn extract_ntlm_without_domain() { let output = "Administrator:500:aad3b435b51404eeaad3b435b51404ee:e19ccf75ee54e06b06a5907af13cef42:::"; let hashes = extract_hashes(output, "contoso.local"); @@ -25,7 +25,7 @@ fn test_extract_ntlm_without_domain() { } #[test] -fn test_extract_tgs_hash() { +fn extract_tgs_hash() { let output = "$krb5tgs$23$*svc_sql$CONTOSO.LOCAL$contoso.local/svc_sql*$abc123def456"; let hashes = extract_hashes(output, "contoso.local"); assert_eq!(hashes.len(), 1); @@ -35,7 +35,7 @@ fn test_extract_tgs_hash() { } #[test] -fn test_extract_asrep_hash() { +fn extract_asrep_hash() { let output = "$krb5asrep$23$jdoe@CONTOSO.LOCAL:abc123def456789012345678901234567890abcdef"; let hashes = extract_hashes(output, "contoso.local"); assert_eq!(hashes.len(), 1); @@ -45,7 +45,7 @@ fn test_extract_asrep_hash() { } #[test] -fn test_extract_line_wrapped_ntlm() { +fn extract_line_wrapped_ntlm() { let output = "Administrator:500:aad3b435b51404eeaad3b435b51404ee:e19ccf75\nee54e06b06a5907af13cef42:::"; let hashes = extract_hashes(output, "contoso.local"); @@ -54,7 +54,7 @@ fn test_extract_line_wrapped_ntlm() { } #[test] -fn test_extract_hashes_dedup() { +fn extract_hashes_dedup() { let output = "\ CONTOSO\\admin:500:aad3b435b51404eeaad3b435b51404ee:e19ccf75ee54e06b06a5907af13cef42:::\n\ CONTOSO\\admin:500:aad3b435b51404eeaad3b435b51404ee:e19ccf75ee54e06b06a5907af13cef42:::"; @@ -63,7 +63,7 @@ CONTOSO\\admin:500:aad3b435b51404eeaad3b435b51404ee:e19ccf75ee54e06b06a5907af13c } #[test] -fn test_extract_hosts_banner() { +fn extract_hosts_banner() { let output = "SMB 192.168.58.10 445 DC01 [*] Windows Server 2019 (name:DC01) (domain:contoso.local) (signing:True)"; let hosts = extract_hosts(output); assert_eq!(hosts.len(), 1); @@ -73,7 +73,7 @@ fn test_extract_hosts_banner() { } #[test] -fn test_extract_hosts_banner_fqdn_construction() { +fn extract_hosts_banner_fqdn_construction() { // Verify FQDN is built from (name:X)(domain:Y) → x.y let output = "SMB 192.168.58.11 445 DC02 [*] Windows Server 2019 (name:DC02) (domain:child.contoso.local) (signing:True)"; let hosts = extract_hosts(output); @@ -83,7 +83,7 @@ fn test_extract_hosts_banner_fqdn_construction() { } #[test] -fn test_extract_hosts_banner_domain_trailing_zero() { +fn extract_hosts_banner_domain_trailing_zero() { // netexec sometimes appends "0." to domain — verify it's stripped let output = "SMB 192.168.58.11 445 DC02 [*] Windows Server 2019 (name:DC02) (domain:contoso.local0.) (signing:True)"; let hosts = extract_hosts(output); @@ -92,7 +92,7 @@ fn test_extract_hosts_banner_domain_trailing_zero() { } #[test] -fn test_extract_hosts_simple() { +fn extract_hosts_simple() { let output = "SMB 192.168.58.20 445 SRV01 some output"; let hosts = extract_hosts(output); assert_eq!(hosts.len(), 1); @@ -101,7 +101,7 @@ fn test_extract_hosts_simple() { } #[test] -fn test_extract_hosts_dedup() { +fn extract_hosts_dedup() { let output = "\ SMB 192.168.58.10 445 DC01 [*] Windows (name:DC01) (domain:contoso.local)\n\ SMB 192.168.58.10 445 DC01 something else"; @@ -111,7 +111,7 @@ SMB 192.168.58.10 445 DC01 something else"; } #[test] -fn test_extract_users_domain_backslash() { +fn extract_users_domain_backslash() { let output = "CONTOSO\\alice.johnson (SidTypeUser)"; let users = extract_users(output, "contoso.local"); assert_eq!(users.len(), 1); @@ -120,7 +120,7 @@ fn test_extract_users_domain_backslash() { } #[test] -fn test_extract_users_upn() { +fn extract_users_upn() { let output = "Found user: bob@contoso.local"; let users = extract_users(output, "contoso.local"); assert_eq!(users.len(), 1); @@ -129,7 +129,7 @@ fn test_extract_users_upn() { } #[test] -fn test_extract_users_rpc_format() { +fn extract_users_rpc_format() { let output = "user:[admin] rid:[0x1f4]"; let users = extract_users(output, "contoso.local"); assert_eq!(users.len(), 1); @@ -138,7 +138,7 @@ fn test_extract_users_rpc_format() { } #[test] -fn test_extract_users_samaccountname() { +fn extract_users_samaccountname() { let output = "sAMAccountName: svc_sql"; let users = extract_users(output, "contoso.local"); assert_eq!(users.len(), 1); @@ -146,7 +146,7 @@ fn test_extract_users_samaccountname() { } #[test] -fn test_extract_users_skip_machine_accounts() { +fn extract_users_skip_machine_accounts() { let output = "CONTOSO\\DC01$ (SidTypeUser)"; let users = extract_users(output, "contoso.local"); assert!( @@ -156,21 +156,21 @@ fn test_extract_users_skip_machine_accounts() { } #[test] -fn test_extract_users_skip_anonymous() { +fn extract_users_skip_anonymous() { let output = "user:[anonymous] rid:[0x1f5]"; let users = extract_users(output, "contoso.local"); assert!(users.is_empty()); } #[test] -fn test_extract_users_smb_timestamp() { +fn extract_users_smb_timestamp() { let output = "SMB 192.168.58.10 445 DC01 alice.johnson 2026-03-25 23:21:09 0 Alice"; let users = extract_users(output, "contoso.local"); assert!(users.iter().any(|u| u.username == "alice.johnson")); } #[test] -fn test_extract_users_domain_context_propagation() { +fn extract_users_domain_context_propagation() { let output = "\ [*] Windows (name:DC01) (domain:north.contoso.local)\n\ user:[alice] rid:[0x1f4]"; @@ -180,7 +180,7 @@ user:[alice] rid:[0x1f4]"; } #[test] -fn test_extract_password_from_description() { +fn extract_password_from_description() { let output = "SMB 192.168.58.10 445 DC01 dave.miller 2026-03-25 23:22:25 0 Dave Miller (Password : Summer2026!)"; let creds = extract_plaintext_passwords(output, "contoso.local"); @@ -190,7 +190,7 @@ fn test_extract_password_from_description() { } #[test] -fn test_extract_default_password() { +fn extract_default_password() { let output = "\ [*] DefaultPassword\n\ CONTOSO\\svc_backup:BackupPass123!"; @@ -202,7 +202,7 @@ CONTOSO\\svc_backup:BackupPass123!"; } #[test] -fn test_extract_password_rejects_paths() { +fn extract_password_rejects_paths() { let output = "Password : /tmp/users.txt"; let creds = extract_plaintext_passwords(output, "contoso.local"); assert!(creds.is_empty()); @@ -216,7 +216,7 @@ fn test_extract_password_rejects_paths() { /// Fix: password lines without a same-line username are skipped entirely. /// Per-tool parsers handle structured extraction (LDIF, nxc table format). #[test] -fn test_stale_context_does_not_leak_across_passwords() { +fn stale_context_does_not_leak_across_passwords() { // Simulate secretsdump output followed by LDAP description output let output = "\ CHILD\\john.smith:1103:aad3b435b51404eeaad3b435b51404ee:abc123def456abc123def456abc123de:::\n\ @@ -235,7 +235,7 @@ Password: Summer2025"; /// extract_plaintext_passwords must never misattribute passwords from /// a previous entry's username context. #[test] -fn test_ldif_attribute_order_no_misattribution() { +fn ldif_attribute_order_no_misattribution() { // ldapsearch output where description comes BEFORE sAMAccountName // and john.smith's entry appears before sam.wilson's let output = "\ @@ -263,7 +263,7 @@ userPrincipalName: sam.wilson@child.contoso.local"; /// nxc SMB lines without timestamps should still extract via RE_SMB_LINE_PASSWORD. #[test] -fn test_smb_line_without_timestamp() { +fn smb_line_without_timestamp() { let output = "SMB 192.168.58.10 445 DC01 svc_test 0 Service Account (Password : TestPass!)"; let creds = extract_plaintext_passwords(output, "contoso.local"); @@ -275,7 +275,7 @@ fn test_smb_line_without_timestamp() { /// Ensure that two separate tool outputs processed independently don't /// cross-contaminate username context. #[test] -fn test_separate_outputs_no_cross_contamination() { +fn separate_outputs_no_cross_contamination() { // Tool output 1: secretsdump mentions john.smith let output1 = "CHILD\\john.smith:1103:aad3b435b51404eeaad3b435b51404ee:abc123:::\n"; // Tool output 2: LDAP description with password for sam.wilson @@ -295,7 +295,7 @@ fn test_separate_outputs_no_cross_contamination() { } #[test] -fn test_extract_shares() { +fn extracts_shares() { let output = "\ SMB 192.168.58.10 445 DC01 Share Permissions Remark\n\ SMB 192.168.58.10 445 DC01 ----- ----------- ------\n\ @@ -312,7 +312,7 @@ SMB 192.168.58.10 445 DC01 [*] Enumerated 2 shares"; } #[test] -fn test_full_extraction() { +fn full_extraction() { let output = "\ SMB 192.168.58.10 445 DC01 [*] Windows Server 2019 (name:DC01) (domain:contoso.local) (signing:True)\n\ SMB 192.168.58.10 445 DC01 [+] contoso.local\\:\n\ @@ -329,13 +329,13 @@ CONTOSO\\krbtgt:502:aad3b435b51404eeaad3b435b51404ee:313b6f423a71d74c0a1b8a2f43b } #[test] -fn test_empty_output() { +fn empty_output() { let result = extract_from_output_text("", "contoso.local"); assert!(result.is_empty()); } #[test] -fn test_extract_netexec_success_credential() { +fn extract_netexec_success_credential() { let output = "\ SMB 192.168.58.11 445 DC02 [*] Windows 10 / Server 2019 Build 17763 x64 (name:DC02) (domain:child.contoso.local) (signing:True)\n\ SMB 192.168.58.11 445 DC02 [-] child.contoso.local\\admin:admin STATUS_LOGON_FAILURE\n\ @@ -350,7 +350,7 @@ SMB 192.168.58.11 445 DC02 [+] child.contoso.local\\jdoe:jdoe"; } #[test] -fn test_extract_netexec_success_with_pwned() { +fn extract_netexec_success_with_pwned() { let output = "SMB 192.168.58.11 445 DC01 [+] contoso.local\\Administrator:P@ssw0rd(Pwn3d!)"; let result = extract_from_output_text(output, "contoso.local"); @@ -360,7 +360,7 @@ fn test_extract_netexec_success_with_pwned() { } #[test] -fn test_extract_netexec_guest_filtered() { +fn extract_netexec_guest_filtered() { let output = "\ SMB 192.168.58.11 445 DC02 [+] child.contoso.local\\admin:admin (Guest)\n\ SMB 192.168.58.11 445 DC02 [+] child.contoso.local\\jdoe:jdoe (Guest)\n\ @@ -377,7 +377,7 @@ SMB 192.168.58.11 445 DC02 [+] child.contoso.local\\realuser:realpass"; } #[test] -fn test_valid_credential_rejects_null_usernames() { +fn valid_credential_rejects_null_usernames() { assert!(!is_valid_credential("(none)", "pass")); assert!(!is_valid_credential("none", "pass")); assert!(!is_valid_credential("null", "pass")); @@ -386,7 +386,7 @@ fn test_valid_credential_rejects_null_usernames() { } #[test] -fn test_valid_credential_rejects_evil_artifacts() { +fn valid_credential_rejects_evil_artifacts() { assert!(!is_valid_credential("EVIL625686$", "pass")); assert!(!is_valid_credential("evil12345$", "pass")); // Non-numeric middle should pass @@ -394,7 +394,7 @@ fn test_valid_credential_rejects_evil_artifacts() { } #[test] -fn test_valid_credential_rejects_noise_passwords() { +fn valid_credential_rejects_noise_passwords() { assert!(!is_valid_credential("user", "(null)")); assert!(!is_valid_credential("user", "*BLANK*")); assert!(!is_valid_credential("user", "")); @@ -405,14 +405,14 @@ fn test_valid_credential_rejects_noise_passwords() { } #[test] -fn test_valid_credential_accepts_real_passwords() { +fn valid_credential_accepts_real_passwords() { assert!(is_valid_credential("admin", "P@ss1")); assert!(is_valid_credential("jdoe", "jdoe")); assert!(is_valid_credential("svc_test", "svc_test")); } #[test] -fn test_extract_cracked_tgs_hashcat() { +fn extract_cracked_tgs_hashcat() { let output = "$krb5tgs$23$*svc_sql$CONTOSO.LOCAL$contoso.local/svc_sql*$abc123def456:Summer2024!"; let creds = extract_cracked_passwords(output, "contoso.local"); @@ -424,7 +424,7 @@ fn test_extract_cracked_tgs_hashcat() { } #[test] -fn test_extract_cracked_asrep_hashcat() { +fn extract_cracked_asrep_hashcat() { let output = "$krb5asrep$23$jdoe@CONTOSO.LOCAL:abc123def456:Winter2024!"; let creds = extract_cracked_passwords(output, "contoso.local"); assert_eq!(creds.len(), 1); @@ -435,7 +435,7 @@ fn test_extract_cracked_asrep_hashcat() { } #[test] -fn test_extract_cracked_john_show() { +fn extract_cracked_john_show() { let output = "svc_sql:Summer2024!::::::::\n1 password hash cracked, 0 left"; let creds = extract_cracked_passwords(output, "contoso.local"); assert_eq!(creds.len(), 1); @@ -445,7 +445,7 @@ fn test_extract_cracked_john_show() { } #[test] -fn test_extract_cracked_dedup() { +fn extract_cracked_dedup() { let output = "\ $krb5tgs$23$*svc_sql$CONTOSO.LOCAL$contoso.local/svc_sql*$abc:Summer2024!\n\ $krb5tgs$23$*svc_sql$CONTOSO.LOCAL$contoso.local/svc_sql*$def:Summer2024!"; @@ -454,7 +454,7 @@ $krb5tgs$23$*svc_sql$CONTOSO.LOCAL$contoso.local/svc_sql*$def:Summer2024!"; } #[test] -fn test_extract_cracked_no_false_positives_on_uncracked() { +fn extract_cracked_no_false_positives_on_uncracked() { // Uncracked TGS hash should NOT produce a cracked credential let output = "$krb5tgs$23$*svc_sql$CONTOSO.LOCAL$contoso.local/svc_sql*$abc123def456"; let creds = extract_cracked_passwords(output, "contoso.local"); @@ -465,7 +465,7 @@ fn test_extract_cracked_no_false_positives_on_uncracked() { } #[test] -fn test_extract_cracked_john_not_triggered_without_context() { +fn extract_cracked_john_not_triggered_without_context() { // john --show format should only match if "password hash cracked" context is present let output = "svc_sql:Summer2024!::::::::"; let creds = extract_cracked_passwords(output, "contoso.local"); @@ -476,7 +476,7 @@ fn test_extract_cracked_john_not_triggered_without_context() { } #[test] -fn test_extract_cracked_asrep_john_show_no_hex() { +fn extract_cracked_asrep_john_show_no_hex() { // John --show for AS-REP omits the hex hash section let output = "--- john --show ---\n\ $krb5asrep$23$brian.davis@CHILD.CONTOSO.LOCAL:letmein2025\n\n\ @@ -489,7 +489,7 @@ fn test_extract_cracked_asrep_john_show_no_hex() { } #[test] -fn test_extract_cracked_tgs_john_show_unknown_user() { +fn extract_cracked_tgs_john_show_unknown_user() { // John --show for TGS shows ?:password — extract user from TGS hash in same output let output = "Loaded 1 password hash (krb5tgs)\n\ $krb5tgs$23$*john.smith$CHILD.CONTOSO.LOCAL$CIFS/filesvr01*$abcdef$123456\n\ @@ -505,7 +505,7 @@ fn test_extract_cracked_tgs_john_show_unknown_user() { } #[test] -fn test_extract_cracked_tgs_john_unknown_user_no_hash_context() { +fn extract_cracked_tgs_john_unknown_user_no_hash_context() { // Without a TGS hash line in the output, ?:password is skipped let output = "--- john --show ---\n\ ?:iknownothing\n\n\ @@ -515,7 +515,7 @@ fn test_extract_cracked_tgs_john_unknown_user_no_hash_context() { } #[test] -fn test_extract_cracked_no_false_positive_on_raw_asrep_hash() { +fn extract_cracked_no_false_positive_on_raw_asrep_hash() { // Raw GetNPUsers AS-REP hash should NOT produce a cracked credential. // The hash body is long hex+$ which is_valid_credential must reject. let output = "$krb5asrep$23$brian.davis@CHILD.CONTOSO.LOCAL:7dae198e2c2fd940e1cbb59d7817c755$ef0c20c7d3abaaf411eb7c9bfe28c6aeae8410170fd08daf198b9269344aa64b9ad78f3f5b807dee0e8573e3bdec9fd90d0b46fa56baba08708f716d9b43a9f9bb2481ab56453d7a340f60ac478f6114f4fb0db7a424fd075f4cef9061954bf53ac6ac6dc3b0cc153b1bc909cac6cdcad9337022bf24ad2069d1991e9ca6eced54eb31f0016f3d9a2983c7f95c7f92261a8a1c435300576a98943a34046f4c08ecc4c6e81d9ca7aa3ae9a4baeb0e4071cd27c82203a225e741f4867afd15405552a47145ec3d79f1d5d19a90109b24ea593c26169fbccc54816f288a30c08ff34dc11bc105366685769b3edf9027be1dbad2f770edfa3ccd3f9524e93de40033464f07cdefb0"; @@ -527,7 +527,7 @@ fn test_extract_cracked_no_false_positive_on_raw_asrep_hash() { } #[test] -fn test_valid_credential_rejects_hash_body_password() { +fn valid_credential_rejects_hash_body_password() { // Long hex+$ strings should be rejected as hash fragments assert!(!is_valid_credential( "brian.davis", diff --git a/ares-cli/src/orchestrator/recovery/dedup.rs b/ares-cli/src/orchestrator/recovery/dedup.rs index 22da9a39..fb5d5c89 100644 --- a/ares-cli/src/orchestrator/recovery/dedup.rs +++ b/ares-cli/src/orchestrator/recovery/dedup.rs @@ -112,7 +112,7 @@ mod tests { // --- extract_kerberoast_spn_key --- #[test] - fn test_extract_spn_key_valid() { + fn extract_spn_key_valid() { let hash = "$krb5tgs$23$*svc_sql$CONTOSO.LOCAL$MSSQLSvc/db01.contoso.local*$aabb$ccdd"; let key = extract_kerberoast_spn_key(hash); assert!(key.is_some()); @@ -122,19 +122,19 @@ mod tests { } #[test] - fn test_extract_spn_key_not_krb5tgs() { + fn extract_spn_key_not_krb5tgs() { assert_eq!(extract_kerberoast_spn_key("$krb5asrep$23$user"), None); } #[test] - fn test_extract_spn_key_too_short() { + fn extract_spn_key_too_short() { assert_eq!(extract_kerberoast_spn_key("$krb5tgs$"), None); } // --- dedupe_hashes --- #[test] - fn test_dedupe_ntlm_by_hash_value() { + fn dedupe_ntlm_by_hash_value() { let hashes = vec![ make_hash( "admin", @@ -160,7 +160,7 @@ mod tests { } #[test] - fn test_dedupe_asrep_by_domain_user() { + fn dedupe_asrep_by_domain_user() { let hashes = vec![ make_hash( "svc_web", @@ -180,7 +180,7 @@ mod tests { } #[test] - fn test_dedupe_asrep_different_users() { + fn dedupe_asrep_different_users() { let hashes = vec![ make_hash( "svc_web", @@ -200,7 +200,7 @@ mod tests { } #[test] - fn test_dedupe_kerberoast_by_spn() { + fn dedupe_kerberoast_by_spn() { let hashes = vec![ make_hash( "svc_sql", @@ -220,7 +220,7 @@ mod tests { } #[test] - fn test_dedupe_mixed_types() { + fn dedupe_mixed_types() { let hashes = vec![ make_hash( "admin", @@ -246,13 +246,13 @@ mod tests { } #[test] - fn test_dedupe_empty() { + fn dedupe_empty() { let deduped = dedupe_hashes(vec![]); assert!(deduped.is_empty()); } #[test] - fn test_dedupe_case_insensitive() { + fn dedupe_case_insensitive() { let hashes = vec![ make_hash( "Admin", diff --git a/ares-cli/src/orchestrator/recovery/mod.rs b/ares-cli/src/orchestrator/recovery/mod.rs index f9ea6fd5..654107a5 100644 --- a/ares-cli/src/orchestrator/recovery/mod.rs +++ b/ares-cli/src/orchestrator/recovery/mod.rs @@ -73,7 +73,7 @@ mod tests { // --- Hash dedup tests --- #[test] - fn test_dedupe_asrep_by_domain_username() { + fn dedupe_asrep_by_domain_username() { let hashes = vec![ make_hash( "edavis", @@ -107,7 +107,7 @@ mod tests { } #[test] - fn test_dedupe_asrep_different_users_kept() { + fn dedupe_asrep_different_users_kept() { let hashes = vec![ make_hash( "edavis", @@ -127,7 +127,7 @@ mod tests { } #[test] - fn test_dedupe_kerberoast_by_spn() { + fn dedupe_kerberoast_by_spn() { let hashes = vec![ make_hash( "svc_sql", @@ -147,7 +147,7 @@ mod tests { } #[test] - fn test_dedupe_kerberoast_different_spn_kept() { + fn dedupe_kerberoast_different_spn_kept() { let hashes = vec![ make_hash( "svc_sql", @@ -167,7 +167,7 @@ mod tests { } #[test] - fn test_dedupe_ntlm_by_exact_value() { + fn dedupe_ntlm_by_exact_value() { let hashes = vec![ make_hash( "admin", @@ -197,7 +197,7 @@ mod tests { } #[test] - fn test_dedupe_mixed_types() { + fn dedupe_mixed_types() { let hashes = vec![ // 2 AS-REP for same user -> 1 make_hash( @@ -231,13 +231,13 @@ mod tests { } #[test] - fn test_dedupe_empty() { + fn dedupe_empty() { let result = dedupe_hashes(vec![]); assert!(result.is_empty()); } #[test] - fn test_dedupe_case_insensitive() { + fn dedupe_case_insensitive() { let hashes = vec![ make_hash( "EDavis", @@ -259,7 +259,7 @@ mod tests { // --- Retry limit tests --- #[test] - fn test_retry_limit_not_exceeded() { + fn retry_limit_not_exceeded() { let task = TaskInfo { task_id: "test_1".to_string(), task_type: "recon".to_string(), @@ -283,7 +283,7 @@ mod tests { } #[test] - fn test_retry_limit_exceeded() { + fn retry_limit_exceeded() { let task = TaskInfo { task_id: "test_2".to_string(), task_type: "recon".to_string(), @@ -309,7 +309,7 @@ mod tests { // --- State normalization tests --- #[test] - fn test_normalize_credential_domains_netbios_to_fqdn() { + fn normalize_credential_domains_netbios_to_fqdn() { let mut creds = vec![ Credential { id: "1".to_string(), @@ -345,7 +345,7 @@ mod tests { } #[test] - fn test_normalize_hash_domains() { + fn normalizes_hash_domains() { let mut hashes = vec![make_hash("admin", "FABRIKAM", "NTLM", "hash123")]; let mut netbios_map = HashMap::new(); @@ -357,7 +357,7 @@ mod tests { } #[test] - fn test_normalize_no_changes_when_fqdn() { + fn normalize_no_changes_when_fqdn() { let mut creds = vec![Credential { id: "1".to_string(), username: "admin".to_string(), @@ -376,7 +376,7 @@ mod tests { } #[test] - fn test_resolve_domain_empty_and_dotted() { + fn resolve_domain_empty_and_dotted() { let map = HashMap::new(); assert!(resolve_domain("", &map).is_none(), "Empty domain -> None"); assert!( @@ -386,7 +386,7 @@ mod tests { } #[test] - fn test_resolve_domain_case_insensitive_lookup() { + fn resolve_domain_case_insensitive_lookup() { let mut map = HashMap::new(); map.insert("CONTOSO".to_string(), "contoso.local".to_string()); @@ -408,14 +408,14 @@ mod tests { // --- Kerberoast SPN extraction --- #[test] - fn test_extract_kerberoast_spn_key_valid() { + fn extract_kerberoast_spn_key_valid() { let hash = "$krb5tgs$23$*svc_sql$CONTOSO.LOCAL$MSSQLSvc/db01.contoso.local*$chk$enc"; let result = extract_kerberoast_spn_key(hash); assert_eq!(result, Some("23:MSSQLSvc/db01.contoso.local".to_string())); } #[test] - fn test_extract_kerberoast_spn_key_invalid() { + fn extract_kerberoast_spn_key_invalid() { assert!(extract_kerberoast_spn_key("not_a_krb_hash").is_none()); assert!(extract_kerberoast_spn_key("$krb5tgs$").is_none()); assert!(extract_kerberoast_spn_key("$krb5tgs$23$nope").is_none()); @@ -424,7 +424,7 @@ mod tests { // --- Connection error detection --- #[test] - fn test_is_connection_error() { + fn connection_error_detection() { let conn_err = anyhow::anyhow!("Connection reset by peer"); assert!(is_connection_error(&conn_err)); diff --git a/ares-cli/src/orchestrator/recovery/normalize.rs b/ares-cli/src/orchestrator/recovery/normalize.rs index 5271bfa3..ce8cea6b 100644 --- a/ares-cli/src/orchestrator/recovery/normalize.rs +++ b/ares-cli/src/orchestrator/recovery/normalize.rs @@ -73,7 +73,7 @@ mod tests { } #[test] - fn test_resolve_domain_netbios_to_fqdn() { + fn resolve_domain_netbios_to_fqdn() { let map = make_map(); assert_eq!( resolve_domain("CONTOSO", &map), @@ -82,7 +82,7 @@ mod tests { } #[test] - fn test_resolve_domain_case_insensitive() { + fn resolve_domain_case_insensitive() { let map = make_map(); assert_eq!( resolve_domain("contoso", &map), @@ -91,25 +91,25 @@ mod tests { } #[test] - fn test_resolve_domain_already_fqdn() { + fn resolve_domain_already_fqdn() { let map = make_map(); assert_eq!(resolve_domain("contoso.local", &map), None); } #[test] - fn test_resolve_domain_empty() { + fn resolve_domain_empty() { let map = make_map(); assert_eq!(resolve_domain("", &map), None); } #[test] - fn test_resolve_domain_unknown_netbios() { + fn resolve_domain_unknown_netbios() { let map = make_map(); assert_eq!(resolve_domain("UNKNOWN", &map), None); } #[test] - fn test_normalize_credential_domains() { + fn normalizes_credential_domains() { let map = make_map(); let mut creds = vec![ Credential { @@ -142,7 +142,7 @@ mod tests { } #[test] - fn test_normalize_hash_domains() { + fn normalizes_hash_domains() { let map = make_map(); let mut hashes = vec![Hash { id: String::new(), @@ -163,7 +163,7 @@ mod tests { } #[test] - fn test_normalize_empty_slice() { + fn normalize_empty_slice() { let map = make_map(); let mut creds: Vec = vec![]; assert_eq!(normalize_credential_domains(&mut creds, &map), 0); diff --git a/ares-cli/src/orchestrator/recovery/types.rs b/ares-cli/src/orchestrator/recovery/types.rs index cc68ebce..00857ff5 100644 --- a/ares-cli/src/orchestrator/recovery/types.rs +++ b/ares-cli/src/orchestrator/recovery/types.rs @@ -71,55 +71,55 @@ mod tests { use super::*; #[test] - fn test_is_connection_error_connection() { + fn is_connection_error_connection() { let err = anyhow::anyhow!("Redis connection refused"); assert!(is_connection_error(&err)); } #[test] - fn test_is_connection_error_timeout() { + fn is_connection_error_timeout() { let err = anyhow::anyhow!("Operation timeout after 30s"); assert!(is_connection_error(&err)); } #[test] - fn test_is_connection_error_broken_pipe() { + fn is_connection_error_broken_pipe() { let err = anyhow::anyhow!("Broken pipe while writing"); assert!(is_connection_error(&err)); } #[test] - fn test_is_connection_error_reset() { + fn is_connection_error_reset() { let err = anyhow::anyhow!("Connection reset by peer"); assert!(is_connection_error(&err)); } #[test] - fn test_is_connection_error_closed() { + fn is_connection_error_closed() { let err = anyhow::anyhow!("Socket closed unexpectedly"); assert!(is_connection_error(&err)); } #[test] - fn test_is_connection_error_case_insensitive() { + fn is_connection_error_case_insensitive() { let err = anyhow::anyhow!("TIMEOUT waiting for response"); assert!(is_connection_error(&err)); } #[test] - fn test_is_not_connection_error() { + fn is_not_connection_error() { let err = anyhow::anyhow!("Key not found in Redis"); assert!(!is_connection_error(&err)); } #[test] - fn test_is_not_connection_error_parse() { + fn is_not_connection_error_parse() { let err = anyhow::anyhow!("Failed to parse JSON response"); assert!(!is_connection_error(&err)); } #[test] - fn test_constants() { + fn constants() { assert_eq!(MAX_RETRIES, 3); assert_eq!(MAX_CONNECTION_RETRIES, 3); assert_eq!(INTERRUPTED_STATUSES.len(), 3); diff --git a/ares-cli/src/orchestrator/result_processing/tests.rs b/ares-cli/src/orchestrator/result_processing/tests.rs index ae556adc..eb328b6e 100644 --- a/ares-cli/src/orchestrator/result_processing/tests.rs +++ b/ares-cli/src/orchestrator/result_processing/tests.rs @@ -3,7 +3,7 @@ use ares_core::models::{Credential, Hash}; use serde_json::json; #[test] -fn test_parse_credentials_array() { +fn parse_credentials_array() { let payload = json!({ "credentials": [ {"id": "c1", "username": "admin", "password": "P@ss1", @@ -19,7 +19,7 @@ fn test_parse_credentials_array() { } #[test] -fn test_parse_single_credential() { +fn parse_single_credential() { let payload = json!({ "credential": { "id": "c1", "username": "admin", "password": "P@ss1", @@ -32,7 +32,7 @@ fn test_parse_single_credential() { } #[test] -fn test_parse_cracked_password() { +fn parse_cracked_password() { let payload = json!({"cracked_password": "Summer2024!", "username": "jdoe", "domain": "contoso.local"}); let parsed = parse_discoveries(&payload); @@ -43,14 +43,14 @@ fn test_parse_cracked_password() { } #[test] -fn test_parse_cracked_password_without_username_ignored() { +fn parse_cracked_password_without_username_ignored() { let payload = json!({"cracked_password": "Summer2024!"}); let parsed = parse_discoveries(&payload); assert!(parsed.credentials.is_empty()); } #[test] -fn test_parse_hashes() { +fn parse_hashes() { let payload = json!({ "hashes": [{"id": "h1", "username": "Administrator", "hash_value": "aad3b435:abcdef123456", "hash_type": "NTLM", "domain": "contoso.local", "source": "secretsdump", @@ -63,7 +63,7 @@ fn test_parse_hashes() { } #[test] -fn test_parse_hosts() { +fn parse_hosts() { let payload = json!({ "hosts": [{"ip": "192.168.58.10", "hostname": "dc01.contoso.local", "os": "Windows Server 2019", "is_dc": true, "open_ports": [88, 389, 445]}] @@ -75,7 +75,7 @@ fn test_parse_hosts() { } #[test] -fn test_parse_users_with_trusted_source() { +fn parse_users_with_trusted_source() { let payload = json!({ "discovered_users": [{"username": "jdoe", "domain": "contoso.local", "source": "kerberos_enum", "is_admin": false}] @@ -86,7 +86,7 @@ fn test_parse_users_with_trusted_source() { } #[test] -fn test_parse_users_rejects_untrusted_source() { +fn parse_users_rejects_untrusted_source() { let payload = json!({ "discovered_users": [ {"username": "fake_admin", "domain": "contoso.local", "is_admin": false}, @@ -99,7 +99,7 @@ fn test_parse_users_rejects_untrusted_source() { } #[test] -fn test_parse_vulnerabilities() { +fn parse_vulnerabilities() { let payload = json!({ "vulnerabilities": [{"vuln_id": "vuln-001", "vuln_type": "constrained_delegation", "target": "192.168.58.20", "discovered_by": "recon", @@ -115,7 +115,7 @@ fn test_parse_vulnerabilities() { } #[test] -fn test_parse_shares() { +fn parse_shares() { let payload = json!({ "shares": [ {"host": "192.168.58.10", "name": "SYSVOL", "permissions": "READ", "comment": "Logon server share"}, @@ -129,7 +129,7 @@ fn test_parse_shares() { } #[test] -fn test_parse_empty_payload() { +fn parse_empty_payload() { let payload = json!({}); let parsed = parse_discoveries(&payload); assert!(parsed.credentials.is_empty()); @@ -141,7 +141,7 @@ fn test_parse_empty_payload() { } #[test] -fn test_parse_malformed_entries_skipped() { +fn parse_malformed_entries_skipped() { let payload = json!({ "credentials": [ {"username": "valid", "id": "c1", "password": "x", "domain": "d", @@ -156,7 +156,7 @@ fn test_parse_malformed_entries_skipped() { } #[test] -fn test_parse_mixed_payload() { +fn parse_mixed_payload() { let payload = json!({ "credentials": [{"id": "c1", "username": "admin", "password": "P@ss", "domain": "contoso.local", "source": "test", "is_admin": true, "attack_step": 0}], @@ -172,49 +172,47 @@ fn test_parse_mixed_payload() { } #[test] -fn test_da_indicator_explicit_flag() { +fn da_indicator_explicit_flag() { assert!(has_domain_admin_indicator( &json!({"has_domain_admin": true}) )); } #[test] -fn test_da_indicator_false_flag() { +fn da_indicator_false_flag() { assert!(!has_domain_admin_indicator( &json!({"has_domain_admin": false}) )); } #[test] -fn test_da_indicator_krbtgt_hash() { +fn da_indicator_krbtgt_hash() { assert!(has_domain_admin_indicator( &json!({"hashes": [{"username": "krbtgt", "hash_value": "abc"}]}) )); } #[test] -fn test_da_indicator_krbtgt_case_insensitive() { +fn da_indicator_krbtgt_case_insensitive() { assert!(has_domain_admin_indicator( &json!({"hashes": [{"username": "KRBTGT", "hash_value": "abc"}]}) )); } #[test] -fn test_da_indicator_non_krbtgt_hash() { +fn da_indicator_non_krbtgt_hash() { assert!(!has_domain_admin_indicator( &json!({"hashes": [{"username": "Administrator", "hash_value": "abc"}]}) )); } #[test] -fn test_da_indicator_empty_payload() { +fn da_indicator_empty_payload() { assert!(!has_domain_admin_indicator(&json!({}))); } -// ==================== has_domain_admin_indicator edge cases ==================== - #[test] -fn test_da_indicator_multiple_hashes_one_krbtgt() { +fn da_indicator_multiple_hashes_one_krbtgt() { assert!(has_domain_admin_indicator(&json!({"hashes": [ {"username": "Administrator", "hash_value": "abc"}, {"username": "krbtgt", "hash_value": "def"}, @@ -223,12 +221,12 @@ fn test_da_indicator_multiple_hashes_one_krbtgt() { } #[test] -fn test_da_indicator_empty_hashes_array() { +fn da_indicator_empty_hashes_array() { assert!(!has_domain_admin_indicator(&json!({"hashes": []}))); } #[test] -fn test_da_indicator_non_bool_value() { +fn da_indicator_non_bool_value() { // has_domain_admin is a string "true" instead of bool true -- should NOT trigger assert!(!has_domain_admin_indicator( &json!({"has_domain_admin": "true"}) @@ -236,14 +234,14 @@ fn test_da_indicator_non_bool_value() { } #[test] -fn test_da_indicator_null_value() { +fn da_indicator_null_value() { assert!(!has_domain_admin_indicator( &json!({"has_domain_admin": null}) )); } #[test] -fn test_da_indicator_hashes_missing_username() { +fn da_indicator_hashes_missing_username() { // Hash entry without a username field should not cause a panic assert!(!has_domain_admin_indicator( &json!({"hashes": [{"hash_value": "abc"}]}) @@ -251,15 +249,13 @@ fn test_da_indicator_hashes_missing_username() { } #[test] -fn test_da_indicator_hashes_not_array() { +fn da_indicator_hashes_not_array() { // hashes is not an array -- should be safely ignored assert!(!has_domain_admin_indicator( &json!({"hashes": "not_an_array"}) )); } -// ==================== resolve_parent_id tests ==================== - fn make_test_credential(id: &str, username: &str, domain: &str, attack_step: i32) -> Credential { Credential { id: id.to_string(), @@ -291,7 +287,7 @@ fn make_test_hash(id: &str, username: &str, domain: &str, attack_step: i32) -> H } #[test] -fn test_resolve_parent_cracked_source_finds_hash() { +fn resolve_parent_cracked_source_finds_hash() { let creds: Vec = vec![]; let hashes = vec![make_test_hash("h1", "jdoe", "contoso.local", 1)]; @@ -310,7 +306,7 @@ fn test_resolve_parent_cracked_source_finds_hash() { } #[test] -fn test_resolve_parent_cracked_source_case_insensitive() { +fn resolve_parent_cracked_source_case_insensitive() { let creds: Vec = vec![]; let hashes = vec![make_test_hash("h1", "JDoe", "CONTOSO.LOCAL", 0)]; @@ -329,7 +325,7 @@ fn test_resolve_parent_cracked_source_case_insensitive() { } #[test] -fn test_resolve_parent_cracked_source_empty_domain_matches() { +fn resolve_parent_cracked_source_empty_domain_matches() { let creds: Vec = vec![]; let hashes = vec![make_test_hash("h1", "jdoe", "contoso.local", 2)]; @@ -341,7 +337,7 @@ fn test_resolve_parent_cracked_source_empty_domain_matches() { } #[test] -fn test_resolve_parent_cracked_source_no_matching_hash() { +fn resolve_parent_cracked_source_no_matching_hash() { let creds: Vec = vec![]; let hashes = vec![make_test_hash("h1", "other_user", "contoso.local", 0)]; @@ -360,7 +356,7 @@ fn test_resolve_parent_cracked_source_no_matching_hash() { } #[test] -fn test_resolve_parent_cracked_picks_last_matching_hash() { +fn resolve_parent_cracked_picks_last_matching_hash() { let creds: Vec = vec![]; let hashes = vec![ make_test_hash("h1", "jdoe", "contoso.local", 0), @@ -382,7 +378,7 @@ fn test_resolve_parent_cracked_picks_last_matching_hash() { } #[test] -fn test_resolve_parent_input_username_differs_finds_credential() { +fn resolve_parent_input_username_differs_finds_credential() { let creds = vec![make_test_credential("c1", "svc_sql", "contoso.local", 0)]; let hashes: Vec = vec![]; @@ -402,7 +398,7 @@ fn test_resolve_parent_input_username_differs_finds_credential() { } #[test] -fn test_resolve_parent_input_username_differs_finds_hash_when_no_cred() { +fn resolve_parent_input_username_differs_finds_hash_when_no_cred() { let creds: Vec = vec![]; let hashes = vec![make_test_hash("h1", "svc_sql", "contoso.local", 1)]; @@ -422,7 +418,7 @@ fn test_resolve_parent_input_username_differs_finds_hash_when_no_cred() { } #[test] -fn test_resolve_parent_input_username_same_as_discovered_returns_none() { +fn resolve_parent_input_username_same_as_discovered_returns_none() { let creds = vec![make_test_credential("c1", "jdoe", "contoso.local", 0)]; let hashes: Vec = vec![]; @@ -442,7 +438,7 @@ fn test_resolve_parent_input_username_same_as_discovered_returns_none() { } #[test] -fn test_resolve_parent_no_parent_returns_none_zero() { +fn resolve_parent_no_parent_returns_none_zero() { let creds: Vec = vec![]; let hashes: Vec = vec![]; @@ -461,7 +457,7 @@ fn test_resolve_parent_no_parent_returns_none_zero() { } #[test] -fn test_resolve_parent_empty_input_username_skipped() { +fn resolve_parent_empty_input_username_skipped() { let creds = vec![make_test_credential("c1", "", "contoso.local", 0)]; let hashes: Vec = vec![]; @@ -481,7 +477,7 @@ fn test_resolve_parent_empty_input_username_skipped() { } #[test] -fn test_resolve_parent_input_username_case_insensitive() { +fn resolve_parent_input_username_case_insensitive() { let creds = vec![make_test_credential("c1", "SVC_SQL", "contoso.local", 0)]; let hashes: Vec = vec![]; @@ -500,7 +496,7 @@ fn test_resolve_parent_input_username_case_insensitive() { } #[test] -fn test_resolve_parent_input_domain_empty_still_matches() { +fn resolve_parent_input_domain_empty_still_matches() { let creds = vec![make_test_credential("c1", "svc_sql", "contoso.local", 0)]; let hashes: Vec = vec![]; @@ -520,7 +516,7 @@ fn test_resolve_parent_input_domain_empty_still_matches() { } #[test] -fn test_resolve_parent_non_cracked_source_with_input_username() { +fn resolve_parent_non_cracked_source_with_input_username() { let creds = vec![make_test_credential("c1", "svc_web", "fabrikam.local", 2)]; let hashes: Vec = vec![]; @@ -539,7 +535,7 @@ fn test_resolve_parent_non_cracked_source_with_input_username() { } #[test] -fn test_resolve_parent_prefers_credential_over_hash() { +fn resolve_parent_prefers_credential_over_hash() { // When both a credential and hash match, credential should be found first let creds = vec![make_test_credential("c1", "svc_sql", "contoso.local", 1)]; let hashes = vec![make_test_hash("h1", "svc_sql", "contoso.local", 0)]; @@ -559,10 +555,8 @@ fn test_resolve_parent_prefers_credential_over_hash() { assert_eq!(step, 2); } -// ==================== parse_discoveries additional edge cases ==================== - #[test] -fn test_parse_single_vulnerability() { +fn parse_single_vulnerability() { // Test the singular "vulnerability" key (fallback when "vulnerabilities" is empty) let payload = json!({ "vulnerability": { @@ -584,7 +578,7 @@ fn test_parse_single_vulnerability() { } #[test] -fn test_parse_singular_vulnerability_not_used_when_array_present() { +fn parse_singular_vulnerability_not_used_when_array_present() { // When "vulnerabilities" array is present, "vulnerability" singular should be ignored let payload = json!({ "vulnerabilities": [{ @@ -612,7 +606,7 @@ fn test_parse_singular_vulnerability_not_used_when_array_present() { } #[test] -fn test_parse_users_with_netexec_source() { +fn parse_users_with_netexec_source() { let payload = json!({ "discovered_users": [ {"username": "jdoe", "domain": "contoso.local", "source": "netexec_user_enum", "is_admin": false} @@ -623,7 +617,7 @@ fn test_parse_users_with_netexec_source() { } #[test] -fn test_parse_cracked_password_with_domain() { +fn parse_cracked_password_with_domain() { let payload = json!({ "cracked_password": "Winter2025!", "username": "svc_sql", @@ -636,7 +630,7 @@ fn test_parse_cracked_password_with_domain() { } #[test] -fn test_parse_cracked_password_without_domain_defaults_empty() { +fn parse_cracked_password_without_domain_defaults_empty() { let payload = json!({ "cracked_password": "Winter2025!", "username": "svc_sql" @@ -647,7 +641,7 @@ fn test_parse_cracked_password_without_domain_defaults_empty() { } #[test] -fn test_parse_hashes_malformed_skipped() { +fn parse_hashes_malformed_skipped() { let payload = json!({ "hashes": [ {"id": "h1", "username": "admin", "hash_value": "aabb", "hash_type": "NTLM", @@ -660,7 +654,7 @@ fn test_parse_hashes_malformed_skipped() { } #[test] -fn test_parse_shares_with_comment() { +fn parse_shares_with_comment() { let payload = json!({ "shares": [ {"host": "192.168.58.10", "name": "NETLOGON", "permissions": "READ", "comment": "Logon server share"} diff --git a/ares-cli/src/orchestrator/state/inner.rs b/ares-cli/src/orchestrator/state/inner.rs index 4c2e7887..552c0aec 100644 --- a/ares-cli/src/orchestrator/state/inner.rs +++ b/ares-cli/src/orchestrator/state/inner.rs @@ -200,7 +200,7 @@ mod tests { use crate::orchestrator::state::*; #[test] - fn test_state_inner_new_initializes_all_dedup_sets() { + fn state_inner_new_initializes_all_dedup_sets() { let state = StateInner::new("op-test".into()); assert_eq!(state.operation_id, "op-test"); assert!(!state.has_domain_admin); @@ -216,13 +216,13 @@ mod tests { } #[test] - fn test_is_processed_returns_false_for_unknown_set() { + fn is_processed_returns_false_for_unknown_set() { let state = StateInner::new("op-1".into()); assert!(!state.is_processed("nonexistent_set", "key1")); } #[test] - fn test_mark_processed_and_is_processed() { + fn mark_processed_and_is_processed() { let mut state = StateInner::new("op-1".into()); assert!(!state.is_processed(DEDUP_CRACK_REQUESTS, "hash1")); @@ -232,14 +232,14 @@ mod tests { } #[test] - fn test_mark_processed_creates_new_set_if_needed() { + fn mark_processed_creates_new_set_if_needed() { let mut state = StateInner::new("op-1".into()); state.mark_processed("custom_set", "key1".into()); assert!(state.is_processed("custom_set", "key1")); } #[test] - fn test_mark_processed_idempotent() { + fn mark_processed_idempotent() { let mut state = StateInner::new("op-1".into()); state.mark_processed(DEDUP_SECRETSDUMP, "192.168.58.10".into()); state.mark_processed(DEDUP_SECRETSDUMP, "192.168.58.10".into()); @@ -247,7 +247,7 @@ mod tests { } #[test] - fn test_dedup_sets_are_independent() { + fn dedup_sets_are_independent() { let mut state = StateInner::new("op-1".into()); state.mark_processed(DEDUP_CRACK_REQUESTS, "hash1".into()); state.mark_processed(DEDUP_SECRETSDUMP, "192.168.58.10".into()); @@ -259,7 +259,7 @@ mod tests { } #[test] - fn test_exploited_vulnerabilities_tracking() { + fn exploited_vulnerabilities_tracking() { let mut state = StateInner::new("op-1".into()); assert!(state.exploited_vulnerabilities.is_empty()); @@ -271,7 +271,7 @@ mod tests { } #[test] - fn test_mssql_enum_dispatched_tracking() { + fn mssql_enum_dispatched_tracking() { let mut state = StateInner::new("op-1".into()); assert!(!state.mssql_enum_dispatched.contains("192.168.58.20")); @@ -282,7 +282,7 @@ mod tests { } #[test] - fn test_domain_controller_map() { + fn domain_controller_map() { let mut state = StateInner::new("op-1".into()); state .domain_controllers @@ -303,7 +303,7 @@ mod tests { } #[test] - fn test_all_known_dedup_set_constants() { + fn all_known_dedup_set_constants() { // Verify constants are accessible and match expected names let expected = vec![ DEDUP_CRACK_REQUESTS, @@ -342,7 +342,7 @@ mod tests { } #[test] - fn test_is_delegation_account() { + fn checks_delegation_account() { let mut state = StateInner::new("op-1".into()); assert!(!state.is_delegation_account("john.smith")); @@ -369,7 +369,7 @@ mod tests { } #[test] - fn test_credential_quarantine() { + fn credential_quarantine() { let mut state = StateInner::new("op-1".into()); // Not quarantined initially @@ -385,14 +385,14 @@ mod tests { } #[test] - fn test_all_forests_dominated_no_forests() { + fn all_forests_dominated_no_forests() { let state = StateInner::new("op-1".into()); // No domains, no DCs, no trusts → vacuously true assert!(state.all_forests_dominated()); } #[test] - fn test_all_forests_dominated_single_forest() { + fn all_forests_dominated_single_forest() { let mut state = StateInner::new("op-1".into()); state .domain_controllers @@ -406,7 +406,7 @@ mod tests { } #[test] - fn test_all_forests_dominated_multi_forest() { + fn all_forests_dominated_multi_forest() { let mut state = StateInner::new("op-1".into()); state .domain_controllers @@ -431,7 +431,7 @@ mod tests { } #[test] - fn test_credential_quarantine_expired() { + fn credential_quarantine_expired() { let mut state = StateInner::new("op-1".into()); // Insert with an already-expired time diff --git a/ares-cli/src/orchestrator/state/shared.rs b/ares-cli/src/orchestrator/state/shared.rs index c9653f45..ea805d49 100644 --- a/ares-cli/src/orchestrator/state/shared.rs +++ b/ares-cli/src/orchestrator/state/shared.rs @@ -105,13 +105,13 @@ mod tests { use std::collections::HashMap; #[tokio::test] - async fn test_shared_state_new() { + async fn shared_state_new() { let state = SharedState::new("op-test".into()); assert_eq!(state.operation_id().await, "op-test"); } #[tokio::test] - async fn test_snapshot_empty_state() { + async fn snapshot_empty_state() { let state = SharedState::new("op-1".into()); let snap = state.snapshot().await; assert!(snap.credentials.is_empty()); @@ -127,7 +127,7 @@ mod tests { } #[tokio::test] - async fn test_snapshot_reflects_state_mutations() { + async fn snapshot_reflects_state_mutations() { let state = SharedState::new("op-1".into()); // Mutate state directly @@ -163,7 +163,7 @@ mod tests { } #[tokio::test] - async fn test_snapshot_is_independent_copy() { + async fn snapshot_is_independent_copy() { let state = SharedState::new("op-1".into()); { let mut inner = state.write().await; @@ -188,7 +188,7 @@ mod tests { } #[tokio::test] - async fn test_vuln_queue_key() { + async fn returns_vuln_queue_key() { let state = SharedState::new("op-abc".into()); let key = state.vuln_queue_key().await; assert!(key.contains("op-abc")); @@ -196,7 +196,7 @@ mod tests { } #[tokio::test] - async fn test_discovery_key() { + async fn returns_discovery_key() { let state = SharedState::new("op-xyz".into()); let key = state.discovery_key().await; assert!(key.contains("op-xyz")); @@ -204,7 +204,7 @@ mod tests { } #[tokio::test] - async fn test_snapshot_with_vulnerabilities() { + async fn snapshot_with_vulnerabilities() { let state = SharedState::new("op-1".into()); { let mut inner = state.write().await; diff --git a/ares-cli/src/orchestrator/strategy.rs b/ares-cli/src/orchestrator/strategy.rs index eb57ab33..09a5aec8 100644 --- a/ares-cli/src/orchestrator/strategy.rs +++ b/ares-cli/src/orchestrator/strategy.rs @@ -417,27 +417,27 @@ mod tests { use super::*; #[test] - fn test_default_strategy_is_fast() { + fn default_strategy_is_fast() { let s = Strategy::default(); assert_eq!(s.preset, StrategyPreset::Fast); assert!(!s.continue_after_da); } #[test] - fn test_comprehensive_implies_continue_after_da() { + fn comprehensive_implies_continue_after_da() { let s = Strategy::from_preset(StrategyPreset::Comprehensive); assert!(s.continue_after_da); } #[test] - fn test_technique_allowed_no_filters() { + fn technique_allowed_no_filters() { let s = Strategy::default(); assert!(s.is_technique_allowed("secretsdump")); assert!(s.is_technique_allowed("esc1")); } #[test] - fn test_technique_excluded() { + fn technique_excluded() { let mut s = Strategy::default(); s.exclude_techniques.insert("secretsdump".to_string()); assert!(!s.is_technique_allowed("secretsdump")); @@ -446,7 +446,7 @@ mod tests { } #[test] - fn test_technique_include_allowlist() { + fn technique_include_allowlist() { let mut s = Strategy::default(); s.include_techniques.insert("esc1".to_string()); s.include_techniques.insert("esc4".to_string()); @@ -456,34 +456,34 @@ mod tests { } #[test] - fn test_effective_priority_from_preset() { + fn effective_priority_from_preset() { let s = Strategy::from_preset(StrategyPreset::Fast); assert_eq!(s.effective_priority("dc_secretsdump"), 1); assert_eq!(s.effective_priority("esc1"), 5); } #[test] - fn test_effective_priority_with_override() { + fn effective_priority_with_override() { let mut s = Strategy::from_preset(StrategyPreset::Fast); s.weights.insert("esc1".to_string(), 1); assert_eq!(s.effective_priority("esc1"), 1); } #[test] - fn test_effective_priority_unknown_type() { + fn effective_priority_unknown_type() { let s = Strategy::default(); assert_eq!(s.effective_priority("unknown_technique"), 5); } #[test] - fn test_stealth_deprioritizes_noisy() { + fn stealth_deprioritizes_noisy() { let s = Strategy::from_preset(StrategyPreset::Stealth); assert!(s.effective_priority("password_spray") > s.effective_priority("esc1")); assert!(s.effective_priority("secretsdump") > s.effective_priority("acl_abuse")); } #[test] - fn test_comprehensive_flat_weights() { + fn comprehensive_flat_weights() { let s = Strategy::from_preset(StrategyPreset::Comprehensive); assert_eq!(s.effective_priority("secretsdump"), 3); assert_eq!(s.effective_priority("esc1"), 3); @@ -491,7 +491,7 @@ mod tests { } #[test] - fn test_preset_from_str_loose() { + fn preset_from_str_loose() { assert_eq!(StrategyPreset::from_str_loose("fast"), StrategyPreset::Fast); assert_eq!( StrategyPreset::from_str_loose("comprehensive"), @@ -516,7 +516,7 @@ mod tests { } #[test] - fn test_from_json_with_overrides() { + fn from_json_with_overrides() { let json = serde_json::json!({ "strategy": "fast", "technique_weights": { @@ -536,7 +536,7 @@ mod tests { } #[test] - fn test_parse_technique_list_json_array() { + fn parse_technique_list_json_array() { let json = serde_json::json!(["secretsdump", "golden_ticket"]); let result = parse_technique_list(Some(&json), "NONEXISTENT_ENV_KEY_12345"); assert!(result.contains("secretsdump")); @@ -545,7 +545,7 @@ mod tests { } /// Build a minimal AresConfig for testing YAML strategy resolution. - fn test_yaml_config( + fn yaml_config( strategy: &str, continue_after_da: bool, exclude: Vec<&str>, @@ -578,8 +578,8 @@ mod tests { } #[test] - fn test_resolve_with_yaml_config() { - let cfg = test_yaml_config( + fn resolve_with_yaml_config() { + let cfg = yaml_config( "comprehensive", true, vec!["password_spray"], @@ -600,8 +600,8 @@ mod tests { } #[test] - fn test_json_overrides_yaml() { - let cfg = test_yaml_config("stealth", false, vec![], vec![("esc1", 5)], vec![]); + fn json_overrides_yaml() { + let cfg = yaml_config("stealth", false, vec![], vec![("esc1", 5)], vec![]); // JSON payload overrides YAML let json = serde_json::json!({ @@ -616,14 +616,14 @@ mod tests { } #[test] - fn test_is_comprehensive() { + fn is_comprehensive() { assert!(Strategy::from_preset(StrategyPreset::Comprehensive).is_comprehensive()); assert!(!Strategy::from_preset(StrategyPreset::Fast).is_comprehensive()); assert!(!Strategy::from_preset(StrategyPreset::Stealth).is_comprehensive()); } #[test] - fn test_should_continue_after_da() { + fn should_continue_after_da() { let fast = Strategy::from_preset(StrategyPreset::Fast); assert!(!fast.should_continue_after_da()); @@ -635,7 +635,7 @@ mod tests { } #[test] - fn test_new_technique_weights_in_presets() { + fn new_technique_weights_in_presets() { // Verify that new techniques added in this branch are in all presets let new_techniques = ["rbcd", "shadow_credentials", "mssql_deep_exploitation"]; for preset in [ @@ -655,7 +655,7 @@ mod tests { } #[test] - fn test_comprehensive_has_equal_weights() { + fn comprehensive_has_equal_weights() { let s = Strategy::from_preset(StrategyPreset::Comprehensive); // All comprehensive weights should be 3 for (tech, weight) in &s.weights { @@ -664,7 +664,7 @@ mod tests { } #[test] - fn test_stealth_penalizes_noisy_techniques() { + fn stealth_penalizes_noisy_techniques() { let s = Strategy::from_preset(StrategyPreset::Stealth); // Password spray and SMB signing should be most penalized (8) assert_eq!(s.effective_priority("password_spray"), 8); @@ -675,7 +675,7 @@ mod tests { } #[test] - fn test_fast_prioritizes_secretsdump() { + fn fast_prioritizes_secretsdump() { let s = Strategy::from_preset(StrategyPreset::Fast); assert_eq!(s.effective_priority("dc_secretsdump"), 1); assert_eq!(s.effective_priority("golden_ticket"), 1); @@ -683,14 +683,14 @@ mod tests { } #[test] - fn test_preset_implies_continue_after_da() { + fn preset_implies_continue_after_da() { assert!(StrategyPreset::Comprehensive.implies_continue_after_da()); assert!(!StrategyPreset::Fast.implies_continue_after_da()); assert!(!StrategyPreset::Stealth.implies_continue_after_da()); } #[test] - fn test_include_and_exclude_interact() { + fn include_and_exclude_interact() { let mut s = Strategy::default(); // Include-only list s.include_techniques.insert("esc1".to_string()); diff --git a/ares-cli/src/orchestrator/tool_dispatcher/tests.rs b/ares-cli/src/orchestrator/tool_dispatcher/tests.rs index 00d9940a..eeabb95a 100644 --- a/ares-cli/src/orchestrator/tool_dispatcher/tests.rs +++ b/ares-cli/src/orchestrator/tool_dispatcher/tests.rs @@ -1,7 +1,7 @@ use super::*; #[test] -fn test_tool_exec_request_serialization() { +fn tool_exec_request_serialization() { let req = ToolExecRequest { call_id: "nmap_scan_abc123".into(), task_id: "recon_def456".into(), @@ -18,7 +18,7 @@ fn test_tool_exec_request_serialization() { } #[test] -fn test_tool_exec_response_deserialization() { +fn tool_exec_response_deserialization() { let json = r#"{"call_id":"nmap_scan_abc","output":"Found 5 hosts","error":null}"#; let resp: ToolExecResponse = serde_json::from_str(json).unwrap(); assert_eq!(resp.output, "Found 5 hosts"); @@ -26,14 +26,14 @@ fn test_tool_exec_response_deserialization() { } #[test] -fn test_tool_exec_response_with_error() { +fn tool_exec_response_with_error() { let json = r#"{"call_id":"x","output":"","error":"Connection refused"}"#; let resp: ToolExecResponse = serde_json::from_str(json).unwrap(); assert_eq!(resp.error.as_deref(), Some("Connection refused")); } #[test] -fn test_cross_role_routing_netexec_tools() { +fn cross_role_routing_netexec_tools() { // Netexec tools called from credential_access should route to recon assert_eq!( resolve_queue_role("credential_access", "password_spray"), @@ -70,7 +70,7 @@ fn test_cross_role_routing_netexec_tools() { } #[test] -fn test_cross_role_routing_native_tools_stay() { +fn cross_role_routing_native_tools_stay() { // Tools native to credential_access should stay on credential_access assert_eq!( resolve_queue_role("credential_access", "secretsdump"), @@ -87,7 +87,7 @@ fn test_cross_role_routing_native_tools_stay() { } #[test] -fn test_cross_role_routing_recon_stays_recon() { +fn cross_role_routing_recon_stays_recon() { // When recon itself calls these tools, they stay on recon assert_eq!(resolve_queue_role("recon", "password_spray"), "recon"); assert_eq!(resolve_queue_role("recon", "nmap_scan"), "recon"); diff --git a/ares-cli/src/util.rs b/ares-cli/src/util.rs index ba5f1b96..f0f580e0 100644 --- a/ares-cli/src/util.rs +++ b/ares-cli/src/util.rs @@ -102,67 +102,67 @@ mod tests { use super::*; #[test] - fn test_format_duration_seconds_only() { + fn format_duration_seconds_only() { assert_eq!(format_duration(42), "42s"); assert_eq!(format_duration(0), "0s"); } #[test] - fn test_format_duration_minutes() { + fn format_duration_minutes() { assert_eq!(format_duration(90), "1m 30s"); assert_eq!(format_duration(60), "1m 0s"); } #[test] - fn test_format_duration_hours() { + fn format_duration_hours() { assert_eq!(format_duration(3661), "1h 1m 1s"); assert_eq!(format_duration(7200), "2h 0m 0s"); } #[cfg(feature = "blue")] #[test] - fn test_parse_datetime_rfc3339() { + fn parse_datetime_rfc3339() { let dt = parse_datetime("2026-04-08T12:00:00+00:00").unwrap(); assert_eq!(dt.year(), 2026); } #[cfg(feature = "blue")] #[test] - fn test_parse_datetime_with_z() { + fn parse_datetime_with_z() { let dt = parse_datetime("2026-04-08T12:00:00Z").unwrap(); assert_eq!(dt.month(), 4); } #[cfg(feature = "blue")] #[test] - fn test_parse_datetime_naive() { + fn parse_datetime_naive() { let dt = parse_datetime("2026-04-08T12:00:00.000").unwrap(); assert_eq!(dt.day(), 8); } #[cfg(feature = "blue")] #[test] - fn test_parse_datetime_invalid() { + fn parse_datetime_invalid() { assert!(parse_datetime("not-a-date").is_err()); } #[test] - fn test_truncate_str_short() { + fn truncate_str_short() { assert_eq!(truncate_str("hello", 10), "hello"); } #[test] - fn test_truncate_str_exact() { + fn truncate_str_exact() { assert_eq!(truncate_str("hello", 5), "hello"); } #[test] - fn test_truncate_str_long() { + fn truncate_str_long() { assert_eq!(truncate_str("hello world", 5), "hello..."); } #[test] - fn test_compute_duration_str_completed() { + fn compute_duration_str_completed() { let start = Utc::now() - chrono::Duration::seconds(120); let end = Utc::now(); let s = compute_duration_str(start, Some(end)); @@ -170,7 +170,7 @@ mod tests { } #[test] - fn test_compute_duration_str_running() { + fn compute_duration_str_running() { let start = Utc::now() - chrono::Duration::seconds(30); let s = compute_duration_str(start, None); assert!(s.contains("(running)")); diff --git a/ares-cli/src/worker/blue_task_loop.rs b/ares-cli/src/worker/blue_task_loop.rs index 1941f400..1ec0dd23 100644 --- a/ares-cli/src/worker/blue_task_loop.rs +++ b/ares-cli/src/worker/blue_task_loop.rs @@ -368,7 +368,7 @@ mod tests { use super::*; #[test] - fn test_parse_blue_role() { + fn parses_blue_role() { assert_eq!(parse_blue_role("triage").as_str(), "triage"); assert_eq!(parse_blue_role("threat_hunter").as_str(), "threat_hunter"); assert_eq!( diff --git a/ares-cli/src/worker/hosts.rs b/ares-cli/src/worker/hosts.rs index c021f2ed..cedd6834 100644 --- a/ares-cli/src/worker/hosts.rs +++ b/ares-cli/src/worker/hosts.rs @@ -173,7 +173,7 @@ mod tests { } #[test] - fn test_build_host_entries_basic() { + fn build_host_entries_basic() { let hosts = vec![ make_host("192.168.58.10", "dc01.contoso.local", true), make_host("192.168.58.22", "ws01.contoso.local", false), @@ -190,7 +190,7 @@ mod tests { } #[test] - fn test_build_host_entries_dedup() { + fn build_host_entries_dedup() { let hosts = vec![make_host("192.168.58.10", "dc01.contoso.local", true)]; let mut already_written = HashSet::new(); already_written.insert("192.168.58.10".to_string()); @@ -199,7 +199,7 @@ mod tests { } #[test] - fn test_build_host_entries_skip_incomplete() { + fn build_host_entries_skip_incomplete() { let hosts = vec![ make_host("", "dc01.contoso.local", true), make_host("192.168.58.10", "", true), @@ -209,7 +209,7 @@ mod tests { } #[test] - fn test_build_host_entries_short_hostname() { + fn build_host_entries_short_hostname() { let hosts = vec![make_host("192.168.58.99", "fileserver", false)]; let entries = build_host_entries(&hosts, &HashSet::new()); assert_eq!(entries.len(), 1); @@ -218,7 +218,7 @@ mod tests { } #[test] - fn test_build_host_entries_dc_subdomain() { + fn build_host_entries_dc_subdomain() { let hosts = vec![make_host("192.168.58.15", "dc02.north.contoso.local", true)]; let entries = build_host_entries(&hosts, &HashSet::new()); assert_eq!(entries.len(), 1); @@ -229,7 +229,7 @@ mod tests { } #[test] - fn test_build_host_entries_lowercase() { + fn build_host_entries_lowercase() { let hosts = vec![make_host("192.168.58.10", "DC01.CONTOSO.LOCAL", true)]; let entries = build_host_entries(&hosts, &HashSet::new()); assert_eq!(entries.len(), 1); diff --git a/ares-cli/src/worker/task_loop/executor.rs b/ares-cli/src/worker/task_loop/executor.rs index c70ef0c5..5ff04c82 100644 --- a/ares-cli/src/worker/task_loop/executor.rs +++ b/ares-cli/src/worker/task_loop/executor.rs @@ -242,7 +242,7 @@ mod tests { // --- normalize_params --- #[test] - fn test_normalize_params_target_ip_to_target() { + fn normalize_params_target_ip_to_target() { let params = json!({"target_ip": "192.168.58.10"}); let norm = normalize_params(¶ms); assert_eq!(norm["target"], "192.168.58.10"); @@ -252,14 +252,14 @@ mod tests { } #[test] - fn test_normalize_params_existing_target_not_overwritten() { + fn normalize_params_existing_target_not_overwritten() { let params = json!({"target": "192.168.58.10", "target_ip": "192.168.58.20"}); let norm = normalize_params(¶ms); assert_eq!(norm["target"], "192.168.58.10"); // not overwritten } #[test] - fn test_normalize_params_credential_flattening() { + fn normalize_params_credential_flattening() { let params = json!({ "target_ip": "192.168.58.10", "credential": { @@ -275,7 +275,7 @@ mod tests { } #[test] - fn test_normalize_params_existing_fields_not_overwritten_by_cred() { + fn normalize_params_existing_fields_not_overwritten_by_cred() { let params = json!({ "domain": "fabrikam.local", "credential": { @@ -291,7 +291,7 @@ mod tests { // --- map_technique_to_tool --- #[test] - fn test_map_technique_to_tool_mapped() { + fn map_technique_to_tool_mapped() { assert_eq!(map_technique_to_tool("network_scan"), "nmap_scan"); assert_eq!(map_technique_to_tool("user_enumeration"), "enumerate_users"); assert_eq!( @@ -313,7 +313,7 @@ mod tests { } #[test] - fn test_map_technique_to_tool_passthrough() { + fn map_technique_to_tool_passthrough() { assert_eq!(map_technique_to_tool("nmap_scan"), "nmap_scan"); assert_eq!(map_technique_to_tool("secretsdump"), "secretsdump"); assert_eq!(map_technique_to_tool("kerberoast"), "kerberoast"); @@ -322,7 +322,7 @@ mod tests { // --- expand_task --- #[test] - fn test_expand_task_recon_with_techniques() { + fn expand_task_recon_with_techniques() { let params = json!({"techniques": ["network_scan", "user_enumeration"], "target_ip": "192.168.58.10"}); let tools = expand_task("recon", ¶ms); assert_eq!(tools.len(), 2); @@ -333,7 +333,7 @@ mod tests { } #[test] - fn test_expand_task_credential_access_single_technique() { + fn expand_task_credential_access_single_technique() { let params = json!({"technique": "secretsdump", "target_ip": "192.168.58.10"}); let tools = expand_task("credential_access", ¶ms); assert_eq!(tools.len(), 1); @@ -341,7 +341,7 @@ mod tests { } #[test] - fn test_expand_task_concrete_tool_returns_empty() { + fn expand_task_concrete_tool_returns_empty() { let params = json!({"target": "192.168.58.10"}); let tools = expand_task("nmap_scan", ¶ms); assert!(tools.is_empty()); @@ -350,7 +350,7 @@ mod tests { // --- expand_crack_task --- #[test] - fn test_expand_crack_task_default_hashcat() { + fn expand_crack_task_default_hashcat() { let params = json!({"hash_value": "abc123", "hash_type": "ntlm"}); let tools = expand_crack_task(¶ms); assert_eq!(tools.len(), 1); @@ -358,7 +358,7 @@ mod tests { } #[test] - fn test_expand_crack_task_john() { + fn expand_crack_task_john() { let params = json!({"hash_value": "abc123", "use_john": true}); let tools = expand_crack_task(¶ms); assert_eq!(tools[0].0, "crack_with_john"); @@ -367,7 +367,7 @@ mod tests { // --- expand_exploit_task --- #[test] - fn test_expand_exploit_delegation() { + fn expand_exploit_delegation() { let params = json!({"vuln_type": "constrained_delegation", "target_ip": "192.168.58.10"}); let tools = expand_exploit_task(¶ms); assert_eq!(tools.len(), 1); @@ -375,7 +375,7 @@ mod tests { } #[test] - fn test_expand_exploit_adcs_variants() { + fn expand_exploit_adcs_variants() { for (vuln_type, expected_tool) in &[ ("esc1", "certipy_request"), ("adcs_esc1", "certipy_request"), @@ -392,7 +392,7 @@ mod tests { } #[test] - fn test_expand_exploit_other_types() { + fn expand_exploit_other_types() { for (vuln_type, expected) in &[ ("krbtgt_hash", "generate_golden_ticket"), ("rbcd", "rbcd_write"), @@ -407,7 +407,7 @@ mod tests { } #[test] - fn test_expand_exploit_unknown_type_empty() { + fn expand_exploit_unknown_type_empty() { let params = json!({"vuln_type": "unknown_vuln"}); let tools = expand_exploit_task(¶ms); assert!(tools.is_empty()); diff --git a/ares-cli/src/worker/task_loop/types.rs b/ares-cli/src/worker/task_loop/types.rs index 4e5282b8..78b33725 100644 --- a/ares-cli/src/worker/task_loop/types.rs +++ b/ares-cli/src/worker/task_loop/types.rs @@ -108,7 +108,7 @@ mod tests { use serde_json::json; #[test] - fn test_task_result_success() { + fn task_result_success() { let result = TaskResult::success("task-1", json!({"output": "done"}), "pod-1", "recon"); assert!(result.success); assert!(result.error.is_none()); @@ -120,7 +120,7 @@ mod tests { } #[test] - fn test_task_result_failure() { + fn task_result_failure() { let result = TaskResult::failure( "task-2", "timeout".to_string(), @@ -134,14 +134,14 @@ mod tests { } #[test] - fn test_task_result_failure_no_result() { + fn task_result_failure_no_result() { let result = TaskResult::failure("task-3", "crash".to_string(), None, "pod-1", "recon"); assert!(!result.success); assert!(result.result.is_none()); } #[test] - fn test_task_message_deserialize() { + fn task_message_deserialize() { let json = json!({ "task_id": "t-1", "task_type": "recon", @@ -159,7 +159,7 @@ mod tests { } #[test] - fn test_task_message_default_priority() { + fn task_message_default_priority() { let json = json!({ "task_id": "t-1", "task_type": "recon", @@ -172,7 +172,7 @@ mod tests { } #[test] - fn test_task_result_serialization_skips_none() { + fn task_result_serialization_skips_none() { let result = TaskResult::success("t-1", json!({"ok": true}), "pod-1", "recon"); let serialized = serde_json::to_value(&result).unwrap(); assert!(serialized.get("error").is_none()); diff --git a/ares-core/src/config/defaults.rs b/ares-core/src/config/defaults.rs index ac3e638b..87555b7e 100644 --- a/ares-core/src/config/defaults.rs +++ b/ares-core/src/config/defaults.rs @@ -72,114 +72,114 @@ mod tests { use super::*; #[test] - fn test_default_checkpoint_interval() { + fn returns_default_checkpoint_interval() { assert_eq!(default_checkpoint_interval(), 60); } #[test] - fn test_default_max_concurrent() { + fn returns_default_max_concurrent() { assert_eq!(default_max_concurrent(), 8); } #[test] - fn test_default_max_steps() { + fn returns_default_max_steps() { assert_eq!(default_max_steps(), 100); } #[test] - fn test_default_true() { + fn returns_default_true() { assert!(default_true()); } #[test] - fn test_default_max_retries() { + fn returns_default_max_retries() { assert_eq!(default_max_retries(), 3); } #[test] - fn test_default_retry_delay() { + fn returns_default_retry_delay() { assert_eq!(default_retry_delay(), 10); } #[test] - fn test_default_lateral_admin_creds() { + fn returns_default_lateral_admin_creds() { assert_eq!(default_lateral_admin_creds(), 3); } #[test] - fn test_default_lateral_owned_hosts() { + fn returns_default_lateral_owned_hosts() { assert_eq!(default_lateral_owned_hosts(), 5); } #[test] - fn test_default_min_slots() { + fn returns_default_min_slots() { assert_eq!(default_min_slots(), 1); } #[test] - fn test_default_max_context_tokens() { + fn returns_default_max_context_tokens() { assert_eq!(default_max_context_tokens(), 50000); } #[test] - fn test_default_min_messages() { + fn returns_default_min_messages() { assert_eq!(default_min_messages(), 15); } #[test] - fn test_default_max_output_chars() { + fn returns_default_max_output_chars() { assert_eq!(default_max_output_chars(), 3000); } #[test] - fn test_default_log_level() { + fn returns_default_log_level() { assert_eq!(default_log_level(), "INFO"); } #[test] - fn test_default_log_format() { + fn returns_default_log_format() { let fmt = default_log_format(); assert!(fmt.contains("asctime")); assert!(fmt.contains("levelname")); } #[test] - fn test_default_log_file() { + fn returns_default_log_file() { assert_eq!(default_log_file(), "/var/log/ares/operation.log"); } #[test] - fn test_default_max_size_mb() { + fn returns_default_max_size_mb() { assert_eq!(default_max_size_mb(), 100); } #[test] - fn test_default_backup_count() { + fn returns_default_backup_count() { assert_eq!(default_backup_count(), 5); } #[test] - fn test_default_max_concurrent_resources() { + fn returns_default_max_concurrent_resources() { assert_eq!(default_max_concurrent_resources(), 10); } #[test] - fn test_default_max_creds_per_expansion() { + fn returns_default_max_creds_per_expansion() { assert_eq!(default_max_creds_per_expansion(), 100); } #[test] - fn test_default_max_hosts_per_scan() { + fn returns_default_max_hosts_per_scan() { assert_eq!(default_max_hosts_per_scan(), 50); } #[test] - fn test_default_cred_cache_ttl() { + fn returns_default_cred_cache_ttl() { assert_eq!(default_cred_cache_ttl(), 3600); } #[test] - fn test_default_max_rpm() { + fn returns_default_max_rpm() { assert_eq!(default_max_rpm(), 60); } } diff --git a/ares-core/src/config/mod.rs b/ares-core/src/config/mod.rs index 2b016b72..53da9893 100644 --- a/ares-core/src/config/mod.rs +++ b/ares-core/src/config/mod.rs @@ -212,7 +212,7 @@ security: } #[test] - fn test_load_minimal_config() { + fn load_minimal_config() { let f = write_temp_yaml(MINIMAL_YAML); let cfg = AresConfig::load(f.path()).unwrap(); assert_eq!(cfg.operation.name, "test-op"); @@ -221,7 +221,7 @@ security: } #[test] - fn test_model_for_role() { + fn resolves_model_for_role() { let f = write_temp_yaml(MINIMAL_YAML); let cfg = AresConfig::load(f.path()).unwrap(); assert_eq!(cfg.model_for_role("orchestrator"), Some("gpt-5.2")); @@ -230,7 +230,7 @@ security: } #[test] - fn test_agent_roles() { + fn returns_agent_roles() { let f = write_temp_yaml(MINIMAL_YAML); let cfg = AresConfig::load(f.path()).unwrap(); let mut roles = cfg.agent_roles(); @@ -239,7 +239,7 @@ security: } #[test] - fn test_vulnerability_priority() { + fn returns_vulnerability_priority() { let f = write_temp_yaml(MINIMAL_YAML); let cfg = AresConfig::load(f.path()).unwrap(); assert_eq!(cfg.vulnerability_priority("adcs_esc1"), 1); @@ -250,7 +250,7 @@ security: } #[test] - fn test_set_model_for_role() { + fn sets_model_for_role() { let f = write_temp_yaml(MINIMAL_YAML); let mut cfg = AresConfig::load(f.path()).unwrap(); let old = cfg.set_model_for_role("orchestrator", "gpt-4o"); @@ -259,7 +259,7 @@ security: } #[test] - fn test_set_model_for_new_role() { + fn set_model_for_new_role() { let f = write_temp_yaml(MINIMAL_YAML); let mut cfg = AresConfig::load(f.path()).unwrap(); let old = cfg.set_model_for_role("new_role", "gpt-4o-mini"); @@ -268,7 +268,7 @@ security: } #[test] - fn test_from_env_with_env_var() { + fn from_env_with_env_var() { let f = write_temp_yaml(MINIMAL_YAML); // Temporarily set env var let path_str = f.path().to_string_lossy().to_string(); @@ -279,7 +279,7 @@ security: } #[test] - fn test_from_env_missing_file() { + fn from_env_missing_file() { std::env::set_var("ARES_CONFIG", "/nonexistent/path/config.yaml"); let result = AresConfig::from_env(); assert!(result.is_err()); @@ -287,7 +287,7 @@ security: } #[test] - fn test_defaults_applied() { + fn defaults_applied() { let minimal = r#" operation: name: "test" @@ -312,7 +312,7 @@ security: {} } #[test] - fn test_load_production_config() { + fn load_production_config() { // Test against the actual production config if it exists at the expected relative path let prod_path = PathBuf::from(env!("CARGO_MANIFEST_DIR")) .parent() @@ -342,7 +342,7 @@ security: {} } #[test] - fn test_stop_criteria_mutually_exclusive() { + fn stop_criteria_mutually_exclusive() { let yaml = MINIMAL_YAML.replace( "namespace: \"test-ns\"", "namespace: \"test-ns\"\n stop_on_domain_admin: true\n stop_on_golden_ticket: true", @@ -358,7 +358,7 @@ security: {} } #[test] - fn test_stop_on_golden_ticket_alone_valid() { + fn stop_on_golden_ticket_alone_valid() { let yaml = MINIMAL_YAML.replace( "namespace: \"test-ns\"", "namespace: \"test-ns\"\n stop_on_golden_ticket: true", @@ -370,7 +370,7 @@ security: {} } #[test] - fn test_stop_on_domain_admin_alone_valid() { + fn stop_on_domain_admin_alone_valid() { let yaml = MINIMAL_YAML.replace( "namespace: \"test-ns\"", "namespace: \"test-ns\"\n stop_on_domain_admin: true", @@ -382,7 +382,7 @@ security: {} } #[test] - fn test_roundtrip_serialization() { + fn roundtrip_serialization() { let f = write_temp_yaml(MINIMAL_YAML); let cfg = AresConfig::load(f.path()).unwrap(); let yaml = serde_yaml::to_string(&cfg).unwrap(); @@ -392,7 +392,7 @@ security: {} } #[test] - fn test_grafana_optional() { + fn grafana_optional() { let f = write_temp_yaml(MINIMAL_YAML); let cfg = AresConfig::load(f.path()).unwrap(); assert!(cfg.grafana.is_none()); diff --git a/ares-core/src/config/sections.rs b/ares-core/src/config/sections.rs index eae623ad..2206a4fb 100644 --- a/ares-core/src/config/sections.rs +++ b/ares-core/src/config/sections.rs @@ -173,7 +173,7 @@ mod tests { use super::*; #[test] - fn test_recovery_config_defaults() { + fn recovery_config_defaults() { let cfg: RecoveryConfig = serde_json::from_str("{}").unwrap(); assert!(cfg.enabled); assert_eq!(cfg.max_retries, 3); @@ -183,7 +183,7 @@ mod tests { } #[test] - fn test_recovery_config_override() { + fn recovery_config_override() { let cfg: RecoveryConfig = serde_json::from_str(r#"{"enabled": false, "max_retries": 5}"#).unwrap(); assert!(!cfg.enabled); @@ -191,7 +191,7 @@ mod tests { } #[test] - fn test_phase_detection_config_defaults() { + fn phase_detection_config_defaults() { let cfg: PhaseDetectionConfig = serde_json::from_str("{}").unwrap(); assert_eq!(cfg.lateral_movement_admin_creds, 3); assert_eq!(cfg.lateral_movement_owned_hosts, 5); @@ -199,7 +199,7 @@ mod tests { } #[test] - fn test_context_management_config_defaults() { + fn context_management_config_defaults() { let cfg: ContextManagementConfig = serde_json::from_str("{}").unwrap(); assert_eq!(cfg.max_context_tokens, 50000); assert_eq!(cfg.min_messages_to_keep, 15); @@ -207,7 +207,7 @@ mod tests { } #[test] - fn test_logging_config_defaults() { + fn logging_config_defaults() { let cfg: LoggingConfig = serde_json::from_str("{}").unwrap(); assert_eq!(cfg.level, "INFO"); assert_eq!(cfg.max_size_mb, 100); @@ -215,7 +215,7 @@ mod tests { } #[test] - fn test_resource_config_defaults() { + fn resource_config_defaults() { let cfg: ResourceConfig = serde_json::from_str("{}").unwrap(); assert_eq!(cfg.max_concurrent_tasks, 10); assert_eq!(cfg.max_credentials_per_expansion, 100); @@ -224,7 +224,7 @@ mod tests { } #[test] - fn test_security_config_defaults() { + fn security_config_defaults() { let cfg: SecurityConfig = serde_json::from_str("{}").unwrap(); assert!(cfg.verify_ssl); assert!(!cfg.encrypted_state); @@ -232,14 +232,14 @@ mod tests { } #[test] - fn test_rate_limiting_config_defaults() { + fn rate_limiting_config_defaults() { let cfg: RateLimitingConfig = serde_json::from_str("{}").unwrap(); assert!(cfg.enabled); assert_eq!(cfg.max_requests_per_minute, 60); } #[test] - fn test_timeout_config_all_zero_defaults() { + fn timeout_config_all_zero_defaults() { let cfg: TimeoutConfig = serde_json::from_str("{}").unwrap(); assert_eq!(cfg.agent_heartbeat, 0); assert_eq!(cfg.task_timeout, 0); @@ -247,7 +247,7 @@ mod tests { } #[test] - fn test_agent_config_defaults() { + fn agent_config_defaults() { let cfg: AgentConfig = serde_json::from_str(r#"{"model": "openai/gpt-4.1"}"#).unwrap(); assert_eq!(cfg.model, "openai/gpt-4.1"); assert_eq!(cfg.max_steps, 100); diff --git a/ares-core/src/correlation/alert/cluster.rs b/ares-core/src/correlation/alert/cluster.rs index e7138d81..f729aa01 100644 --- a/ares-core/src/correlation/alert/cluster.rs +++ b/ares-core/src/correlation/alert/cluster.rs @@ -298,3 +298,233 @@ impl AlertCluster { summary } } + +#[cfg(test)] +mod tests { + use super::*; + use serde_json::json; + + fn make_alert(labels: Value, starts_at: Option<&str>) -> Value { + let mut alert = json!({ "labels": labels }); + if let Some(ts) = starts_at { + alert["startsAt"] = json!(ts); + } + alert + } + + #[test] + fn new_cluster_is_empty() { + let c = AlertCluster::new("c1".into()); + assert_eq!(c.cluster_id, "c1"); + assert!(c.alerts.is_empty()); + assert!(c.common_hosts.is_empty()); + assert!(c.common_users.is_empty()); + assert!(c.common_ips.is_empty()); + assert!(c.techniques.is_empty()); + assert!(c.time_range.is_none()); + assert!(c.operation_id.is_none()); + } + + #[test] + fn add_alert_extracts_hosts_from_labels() { + let mut c = AlertCluster::new("c1".into()); + let alert = make_alert(json!({"hostname": "DC01", "host": "SRV01"}), None); + c.add_alert(&alert); + assert!(c.common_hosts.contains("dc01")); + assert!(c.common_hosts.contains("srv01")); + assert_eq!(c.alerts.len(), 1); + } + + #[test] + fn add_alert_extracts_instance_host() { + let mut c = AlertCluster::new("c1".into()); + let alert = make_alert(json!({"instance": "webserver:8080"}), None); + c.add_alert(&alert); + assert!(c.common_hosts.contains("webserver")); + } + + #[test] + fn add_alert_skips_numeric_instance() { + let mut c = AlertCluster::new("c1".into()); + let alert = make_alert(json!({"instance": "192.168.1.1:8080"}), None); + c.add_alert(&alert); + assert!(c.common_hosts.is_empty()); + } + + #[test] + fn add_alert_extracts_users() { + let mut c = AlertCluster::new("c1".into()); + let alert = make_alert(json!({"user": "Admin", "TargetUserName": "SvcAcct"}), None); + c.add_alert(&alert); + assert!(c.common_users.contains("admin")); + assert!(c.common_users.contains("svcacct")); + } + + #[test] + fn add_alert_extracts_users_from_annotations() { + let mut c = AlertCluster::new("c1".into()); + let alert = json!({ + "labels": {}, + "annotations": {"username": "JDoe"} + }); + c.add_alert(&alert); + assert!(c.common_users.contains("jdoe")); + } + + #[test] + fn add_alert_extracts_ips() { + let mut c = AlertCluster::new("c1".into()); + let alert = make_alert(json!({"ip": "10.0.0.1", "source_ip": "10.0.0.2"}), None); + c.add_alert(&alert); + assert!(c.common_ips.contains("10.0.0.1")); + assert!(c.common_ips.contains("10.0.0.2")); + } + + #[test] + fn add_alert_extracts_techniques_string() { + let mut c = AlertCluster::new("c1".into()); + let alert = make_alert(json!({"mitre_technique": "T1021.002"}), None); + c.add_alert(&alert); + assert!(c.techniques.contains("T1021.002")); + } + + #[test] + fn add_alert_extracts_techniques_array() { + let mut c = AlertCluster::new("c1".into()); + let alert = make_alert(json!({"technique": ["T1021", "T1059"]}), None); + c.add_alert(&alert); + assert!(c.techniques.contains("T1021")); + assert!(c.techniques.contains("T1059")); + } + + #[test] + fn add_alert_updates_time_range() { + let mut c = AlertCluster::new("c1".into()); + let a1 = make_alert(json!({}), Some("2025-01-01T10:00:00Z")); + let a2 = make_alert(json!({}), Some("2025-01-01T12:00:00Z")); + c.add_alert(&a1); + c.add_alert(&a2); + let (start, end) = c.time_range.unwrap(); + assert!(start < end); + } + + #[test] + fn add_alert_extracts_operation_id() { + let mut c = AlertCluster::new("c1".into()); + let alert = json!({ + "labels": {}, + "operation_context": {"operation_id": "op-42"} + }); + c.add_alert(&alert); + assert_eq!(c.operation_id.as_deref(), Some("op-42")); + } + + #[test] + fn similarity_score_zero_for_unrelated() { + let mut c = AlertCluster::new("c1".into()); + c.add_alert(&make_alert(json!({"hostname": "DC01"}), None)); + let alert = make_alert(json!({"hostname": "UNRELATED"}), None); + let score = c.similarity_score(&alert); + assert!(score < 0.01, "expected ~0, got {score}"); + } + + #[test] + fn similarity_score_host_match() { + let mut c = AlertCluster::new("c1".into()); + c.add_alert(&make_alert(json!({"hostname": "DC01"}), None)); + let alert = make_alert(json!({"hostname": "DC01"}), None); + let score = c.similarity_score(&alert); + assert!(score >= 0.4, "expected >=0.4, got {score}"); + } + + #[test] + fn similarity_score_user_match() { + let mut c = AlertCluster::new("c1".into()); + c.add_alert(&make_alert(json!({"user": "admin"}), None)); + let alert = make_alert(json!({"user": "admin"}), None); + let score = c.similarity_score(&alert); + assert!(score >= 0.3, "expected >=0.3, got {score}"); + } + + #[test] + fn similarity_score_ip_match() { + let mut c = AlertCluster::new("c1".into()); + c.add_alert(&make_alert(json!({"ip": "10.0.0.1"}), None)); + let alert = make_alert(json!({"ip": "10.0.0.1"}), None); + let score = c.similarity_score(&alert); + assert!(score >= 0.2, "expected >=0.2, got {score}"); + } + + #[test] + fn similarity_score_technique_match() { + let mut c = AlertCluster::new("c1".into()); + c.add_alert(&make_alert(json!({"mitre_technique": "T1021"}), None)); + let alert = make_alert(json!({"mitre_technique": "T1021"}), None); + let score = c.similarity_score(&alert); + assert!(score >= 0.2, "expected >=0.2, got {score}"); + } + + #[test] + fn similarity_score_capped_at_one() { + let mut c = AlertCluster::new("c1".into()); + let rich = json!({ + "labels": { + "hostname": "DC01", + "user": "admin", + "ip": "10.0.0.1", + "mitre_technique": "T1021" + }, + "startsAt": "2025-01-01T10:00:00Z", + "operation_context": {"operation_id": "op-1"} + }); + c.add_alert(&rich); + let score = c.similarity_score(&rich); + assert!(score <= 1.0, "score must be <=1.0, got {score}"); + } + + #[test] + fn similarity_score_operation_id_bonus() { + let mut c = AlertCluster::new("c1".into()); + let a1 = json!({ + "labels": {}, + "operation_context": {"operation_id": "op-1"} + }); + c.add_alert(&a1); + let a2 = json!({ + "labels": {}, + "operation_context": {"operation_id": "op-1"} + }); + let score = c.similarity_score(&a2); + assert!(score >= 0.09, "expected op_id bonus ~0.1, got {score}"); + } + + #[test] + fn to_summary_contains_expected_keys() { + let mut c = AlertCluster::new("c1".into()); + c.add_alert(&make_alert(json!({"hostname": "DC01"}), None)); + let summary = c.to_summary(); + assert_eq!(summary["cluster_id"], Value::String("c1".into())); + assert_eq!(summary["alert_count"], Value::Number(1.into())); + assert!(summary.contains_key("common_hosts")); + assert!(summary.contains_key("techniques")); + assert!(summary.contains_key("time_range")); + } + + #[test] + fn similarity_time_proximity_bonus() { + let mut c = AlertCluster::new("c1".into()); + c.add_alert(&make_alert(json!({}), Some("2025-01-01T10:00:00Z"))); + let near = make_alert(json!({}), Some("2025-01-01T10:30:00Z")); + let score = c.similarity_score(&near); + assert!(score >= 0.1, "expected time bonus, got {score}"); + } + + #[test] + fn similarity_instance_host_match() { + let mut c = AlertCluster::new("c1".into()); + c.add_alert(&make_alert(json!({"instance": "webserver:9090"}), None)); + let alert = make_alert(json!({"instance": "webserver:8080"}), None); + let score = c.similarity_score(&alert); + assert!(score >= 0.3, "expected instance match, got {score}"); + } +} diff --git a/ares-core/src/correlation/alert/correlator.rs b/ares-core/src/correlation/alert/correlator.rs index 2c77443f..66a46b83 100644 --- a/ares-core/src/correlation/alert/correlator.rs +++ b/ares-core/src/correlation/alert/correlator.rs @@ -184,3 +184,141 @@ impl AlertCorrelator { self.alert_to_cluster.clear(); } } + +#[cfg(test)] +mod tests { + use super::*; + use serde_json::json; + + fn alert(fingerprint: &str, hostname: &str) -> Value { + json!({ + "fingerprint": fingerprint, + "labels": {"hostname": hostname} + }) + } + + #[test] + fn new_correlator_defaults() { + let c = AlertCorrelator::new(); + assert!((c.cluster_threshold - AlertCorrelator::DEFAULT_THRESHOLD).abs() < f64::EPSILON); + assert!(c.clusters().is_empty()); + } + + #[test] + fn with_threshold_sets_value() { + let c = AlertCorrelator::with_threshold(0.5); + assert!((c.cluster_threshold - 0.5).abs() < f64::EPSILON); + } + + #[test] + fn add_alert_creates_cluster() { + let mut c = AlertCorrelator::new(); + let a = alert("fp1", "DC01"); + let cluster = c.add_alert(&a); + assert_eq!(cluster.cluster_id, "cluster-0001"); + assert_eq!(cluster.alerts.len(), 1); + assert_eq!(c.clusters().len(), 1); + } + + #[test] + fn add_similar_alerts_same_cluster() { + let mut c = AlertCorrelator::new(); + let a1 = alert("fp1", "DC01"); + let a2 = alert("fp2", "DC01"); + c.add_alert(&a1); + c.add_alert(&a2); + // Same hostname => high similarity => same cluster + assert_eq!(c.clusters().len(), 1); + assert_eq!(c.clusters()[0].alerts.len(), 2); + } + + #[test] + fn add_dissimilar_alerts_different_clusters() { + let mut c = AlertCorrelator::new(); + let a1 = alert("fp1", "DC01"); + let a2 = alert("fp2", "UNRELATED"); + c.add_alert(&a1); + c.add_alert(&a2); + assert_eq!(c.clusters().len(), 2); + } + + #[test] + fn get_cluster_for_alert_found() { + let mut c = AlertCorrelator::new(); + let a = alert("fp1", "DC01"); + c.add_alert(&a); + let cluster = c.get_cluster_for_alert(&a); + assert_eq!( + cluster.expect("cluster should exist").cluster_id, + "cluster-0001" + ); + } + + #[test] + fn get_cluster_for_alert_not_found() { + let c = AlertCorrelator::new(); + let a = alert("fp-unknown", "DC01"); + assert!(c.get_cluster_for_alert(&a).is_none()); + } + + #[test] + fn get_cluster_context_no_cluster() { + let c = AlertCorrelator::new(); + let a = alert("fp1", "DC01"); + let ctx = c.get_cluster_context(&a); + assert!(ctx["cluster_id"].is_null()); + } + + #[test] + fn get_cluster_context_with_cluster() { + let mut c = AlertCorrelator::new(); + let a = alert("fp1", "DC01"); + c.add_alert(&a); + let ctx = c.get_cluster_context(&a); + assert_eq!(ctx["cluster_id"], "cluster-0001"); + } + + #[test] + fn get_related_alerts_excludes_self() { + let mut c = AlertCorrelator::new(); + let a1 = alert("fp1", "DC01"); + let a2 = alert("fp2", "DC01"); + c.add_alert(&a1); + c.add_alert(&a2); + let related = c.get_related_alerts(&a1); + assert_eq!(related.len(), 1); + assert_eq!(related[0]["fingerprint"].as_str().unwrap(), "fp2"); + } + + #[test] + fn get_related_alerts_empty_when_no_cluster() { + let c = AlertCorrelator::new(); + let a = alert("fp1", "DC01"); + assert!(c.get_related_alerts(&a).is_empty()); + } + + #[test] + fn get_all_clusters_summary() { + let mut c = AlertCorrelator::new(); + c.add_alert(&alert("fp1", "DC01")); + c.add_alert(&alert("fp2", "UNRELATED")); + let summaries = c.get_all_clusters_summary(); + assert_eq!(summaries.len(), 2); + } + + #[test] + fn reset_clears_state() { + let mut c = AlertCorrelator::new(); + c.add_alert(&alert("fp1", "DC01")); + assert_eq!(c.clusters().len(), 1); + c.reset(); + assert!(c.clusters().is_empty()); + } + + #[test] + fn default_impl_matches_new() { + let c = AlertCorrelator::default(); + assert!((c.cluster_threshold - AlertCorrelator::DEFAULT_THRESHOLD).abs() < f64::EPSILON); + assert!(c.clusters().is_empty()); + } +} diff --git a/ares-core/src/correlation/alert/tests.rs b/ares-core/src/correlation/alert/tests.rs index 3a09c8f1..a88d4bd5 100644 --- a/ares-core/src/correlation/alert/tests.rs +++ b/ares-core/src/correlation/alert/tests.rs @@ -14,7 +14,7 @@ fn make_alert(fingerprint: &str, host: &str, user: &str, technique: &str) -> Val } #[test] -fn test_cluster_add_alert_extracts_iocs() { +fn cluster_add_alert_extracts_iocs() { let mut cluster = AlertCluster::new("test-001".to_string()); let alert = json!({ "fingerprint": "abc123", @@ -42,21 +42,19 @@ fn test_cluster_add_alert_extracts_iocs() { } #[test] -fn test_cluster_similarity_host_match() { +fn cluster_similarity_host_match() { let mut cluster = AlertCluster::new("test-001".to_string()); cluster.add_alert(&make_alert("a1", "dc01", "admin", "T1003")); - // Same host → high similarity let similar = make_alert("a2", "dc01", "other_user", "T1110"); assert!(cluster.similarity_score(&similar) >= 0.4); - // Different host → lower similarity let different = make_alert("a3", "web01", "other_user", "T1110"); assert!(cluster.similarity_score(&different) < 0.3); } #[test] -fn test_cluster_similarity_user_match() { +fn cluster_similarity_user_match() { let mut cluster = AlertCluster::new("test-001".to_string()); cluster.add_alert(&make_alert("a1", "dc01", "admin", "T1003")); @@ -66,7 +64,7 @@ fn test_cluster_similarity_user_match() { } #[test] -fn test_cluster_similarity_technique_match() { +fn cluster_similarity_technique_match() { let mut cluster = AlertCluster::new("test-001".to_string()); cluster.add_alert(&make_alert("a1", "dc01", "admin", "T1003")); @@ -79,7 +77,7 @@ fn test_cluster_similarity_technique_match() { } #[test] -fn test_cluster_similarity_operation_id() { +fn cluster_similarity_operation_id() { let mut cluster = AlertCluster::new("test-001".to_string()); let alert1 = json!({ "fingerprint": "a1", @@ -96,7 +94,6 @@ fn test_cluster_similarity_operation_id() { "startsAt": "2026-04-08T12:30:00Z", }); - // Same operation_id gives small bonus, NOT enough to auto-cluster let score = cluster.similarity_score(&alert2); assert!( score < AlertCorrelator::DEFAULT_THRESHOLD, @@ -105,7 +102,7 @@ fn test_cluster_similarity_operation_id() { } #[test] -fn test_correlator_creates_new_cluster() { +fn correlator_creates_new_cluster() { let mut correlator = AlertCorrelator::new(); let alert = make_alert("a1", "dc01", "admin", "T1003"); correlator.add_alert(&alert); @@ -115,10 +112,9 @@ fn test_correlator_creates_new_cluster() { } #[test] -fn test_correlator_groups_similar_alerts() { +fn correlator_groups_similar_alerts() { let mut correlator = AlertCorrelator::new(); - // Two alerts sharing the same host should cluster let a1 = make_alert("a1", "dc01", "admin", "T1003"); let a2 = make_alert("a2", "dc01", "admin", "T1003.006"); correlator.add_alert(&a1); @@ -133,7 +129,7 @@ fn test_correlator_groups_similar_alerts() { } #[test] -fn test_correlator_separates_dissimilar_alerts() { +fn correlator_separates_dissimilar_alerts() { let mut correlator = AlertCorrelator::new(); let a1 = make_alert("a1", "dc01", "admin", "T1003"); @@ -149,7 +145,7 @@ fn test_correlator_separates_dissimilar_alerts() { } #[test] -fn test_correlator_get_related_alerts() { +fn correlator_get_related_alerts() { let mut correlator = AlertCorrelator::new(); let a1 = make_alert("a1", "dc01", "admin", "T1003"); @@ -163,7 +159,7 @@ fn test_correlator_get_related_alerts() { } #[test] -fn test_correlator_cluster_context() { +fn correlator_cluster_context() { let mut correlator = AlertCorrelator::new(); let alert = make_alert("a1", "dc01", "admin", "T1003"); @@ -175,17 +171,17 @@ fn test_correlator_cluster_context() { } #[test] -fn test_correlator_reset() { +fn correlator_reset() { let mut correlator = AlertCorrelator::new(); correlator.add_alert(&make_alert("a1", "dc01", "admin", "T1003")); assert_eq!(correlator.clusters().len(), 1); correlator.reset(); - assert_eq!(correlator.clusters().len(), 0); + assert!(correlator.clusters().is_empty()); } #[test] -fn test_cluster_summary() { +fn cluster_summary() { let mut cluster = AlertCluster::new("test-001".to_string()); cluster.add_alert(&make_alert("a1", "dc01", "admin", "T1003")); @@ -195,7 +191,7 @@ fn test_cluster_summary() { } #[test] -fn test_cluster_technique_array_labels() { +fn cluster_technique_array_labels() { let mut cluster = AlertCluster::new("test-001".to_string()); let alert = json!({ "fingerprint": "a1", diff --git a/ares-core/src/correlation/lateral/analyzer.rs b/ares-core/src/correlation/lateral/analyzer.rs index 87f50f9a..431d67f2 100644 --- a/ares-core/src/correlation/lateral/analyzer.rs +++ b/ares-core/src/correlation/lateral/analyzer.rs @@ -208,3 +208,103 @@ pub fn looks_like_hostname(value: &str) -> bool { } (4..=255).contains(&value.len()) } + +#[cfg(test)] +mod tests { + use super::*; + use serde_json::json; + + #[test] + fn looks_like_hostname_valid() { + assert!(looks_like_hostname("dc01.corp.local")); + assert!(looks_like_hostname("web.example.com")); + } + + #[test] + fn looks_like_hostname_no_dot() { + assert!(!looks_like_hostname("localhost")); + } + + #[test] + fn looks_like_hostname_ip_address() { + assert!(!looks_like_hostname("10.0.0.1")); + assert!(!looks_like_hostname("192.168.1.100")); + } + + #[test] + fn looks_like_hostname_starts_with_digit() { + assert!(!looks_like_hostname("1host.corp.local")); + } + + #[test] + fn looks_like_hostname_too_short() { + assert!(!looks_like_hostname("a.b")); + } + + #[test] + fn analyzer_default_creates_empty_graph() { + let analyzer = LateralMovementAnalyzer::default(); + assert!(analyzer.graph.connections.is_empty()); + } + + #[test] + fn analyze_query_result_extracts_hosts() { + let mut analyzer = LateralMovementAnalyzer::default(); + let data = json!({ + "computer": "dc01.corp.local", + "message": "logon from ws01.corp.local" + }); + let conns = analyzer.analyze_query_result(&data, Some("ws01.corp.local")); + // Should find dc01 as destination from ws01 + assert!(!conns.is_empty()); + } + + #[test] + fn analyze_query_result_no_source_no_connections() { + let mut analyzer = LateralMovementAnalyzer::default(); + let data = json!({"computer": "dc01.corp.local"}); + let conns = analyzer.analyze_query_result(&data, None); + assert!(conns.is_empty()); + } + + #[test] + fn analyze_query_result_same_host_no_self_connection() { + let mut analyzer = LateralMovementAnalyzer::default(); + let data = json!({"computer": "dc01.corp.local"}); + let conns = analyzer.analyze_query_result(&data, Some("dc01.corp.local")); + assert!(conns.is_empty()); + } + + #[test] + fn get_attack_path_empty_graph() { + let analyzer = LateralMovementAnalyzer::default(); + let path = analyzer.get_attack_path(); + assert!(path.is_empty()); + } + + #[test] + fn get_attack_path_linear_chain() { + let mut analyzer = LateralMovementAnalyzer::default(); + // ws01 -> dc01 + let data1 = json!({"computer": "dc01.corp.local"}); + analyzer.analyze_query_result(&data1, Some("ws01.corp.local")); + let path = analyzer.get_attack_path(); + assert!(!path.is_empty()); + // ws01 should be the entry point + assert_eq!(path[0], "ws01.corp.local"); + } + + #[test] + fn get_pivot_suggestions_returns_uninvestigated() { + let mut analyzer = LateralMovementAnalyzer::default(); + let data = json!({"computer": "dc01.corp.local"}); + analyzer.analyze_query_result(&data, Some("ws01.corp.local")); + let suggestions = analyzer.get_pivot_suggestions(); + // dc01 is uninvestigated target + let hosts: Vec<&str> = suggestions + .iter() + .filter_map(|s| s["host"].as_str()) + .collect(); + assert!(hosts.contains(&"dc01.corp.local")); + } +} diff --git a/ares-core/src/correlation/lateral/graph.rs b/ares-core/src/correlation/lateral/graph.rs index 7fb25c64..b0b30448 100644 --- a/ares-core/src/correlation/lateral/graph.rs +++ b/ares-core/src/correlation/lateral/graph.rs @@ -150,3 +150,150 @@ impl LateralGraph { pub fn mitre_for_connection(conn_type: &str) -> Option<&'static str> { crate::detection::mitre_for_connection_type(conn_type) } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn new_graph_is_empty() { + let g = LateralGraph::new(); + assert!(g.connections.is_empty()); + assert!(g.investigated_hosts.is_empty()); + assert!(g.pending_hosts.is_empty()); + } + + #[test] + fn add_connection_stores_and_returns() { + let mut g = LateralGraph::new(); + let conn = g.add_connection( + "host-a", + "host-b", + "smb", + None, + Some("admin"), + Some("ev1"), + Some("T1021"), + ); + let conn = conn.expect("add_connection should return connection"); + assert_eq!(conn.source_host, "host-a"); + assert_eq!(conn.destination_host, "host-b"); + assert_eq!(conn.connection_type, "smb"); + assert_eq!(conn.user.as_deref(), Some("admin")); + assert_eq!(conn.evidence_ids, vec!["ev1"]); + assert_eq!(conn.mitre_technique.as_deref(), Some("T1021")); + assert_eq!(g.connections.len(), 1); + } + + #[test] + fn add_connection_lowercases_hosts() { + let mut g = LateralGraph::new(); + g.add_connection("HOST-A", "HOST-B", "rdp", None, None, None, None); + assert_eq!(g.connections[0].source_host, "host-a"); + assert_eq!(g.connections[0].destination_host, "host-b"); + } + + #[test] + fn add_connection_self_loop_returns_none() { + let mut g = LateralGraph::new(); + let result = g.add_connection("host-a", "HOST-A", "smb", None, None, None, None); + assert!(result.is_none()); + assert!(g.connections.is_empty()); + } + + #[test] + fn add_connection_marks_destination_pending() { + let mut g = LateralGraph::new(); + g.add_connection("host-a", "host-b", "smb", None, None, None, None); + assert!(g.pending_hosts.contains("host-b")); + } + + #[test] + fn add_connection_skips_pending_if_investigated() { + let mut g = LateralGraph::new(); + g.mark_investigated("host-b"); + g.add_connection("host-a", "host-b", "smb", None, None, None, None); + assert!(!g.pending_hosts.contains("host-b")); + } + + #[test] + fn mark_investigated_removes_from_pending() { + let mut g = LateralGraph::new(); + g.add_connection("host-a", "host-b", "smb", None, None, None, None); + assert!(g.pending_hosts.contains("host-b")); + g.mark_investigated("host-b"); + assert!(!g.pending_hosts.contains("host-b")); + assert!(g.investigated_hosts.contains("host-b")); + } + + #[test] + fn get_uninvestigated_targets_respects_limit() { + let mut g = LateralGraph::new(); + g.add_connection("a", "b", "smb", None, None, None, None); + g.add_connection("a", "c", "smb", None, None, None, None); + g.add_connection("a", "d", "smb", None, None, None, None); + let targets = g.get_uninvestigated_targets(2); + assert_eq!(targets.len(), 2); + } + + #[test] + fn get_host_connections_both_directions() { + let mut g = LateralGraph::new(); + g.add_connection("a", "b", "smb", None, None, None, None); + g.add_connection("c", "b", "rdp", None, None, None, None); + let conns = g.get_host_connections("b"); + assert_eq!(conns.len(), 2); + } + + #[test] + fn get_outgoing_connections_filters() { + let mut g = LateralGraph::new(); + g.add_connection("a", "b", "smb", None, None, None, None); + g.add_connection("b", "c", "rdp", None, None, None, None); + let out = g.get_outgoing_connections("a"); + assert_eq!(out.len(), 1); + assert_eq!(out[0].destination_host, "b"); + } + + #[test] + fn get_incoming_connections_filters() { + let mut g = LateralGraph::new(); + g.add_connection("a", "b", "smb", None, None, None, None); + g.add_connection("c", "b", "rdp", None, None, None, None); + let inc = g.get_incoming_connections("b"); + assert_eq!(inc.len(), 2); + } + + #[test] + fn get_unique_users_collects_all() { + let mut g = LateralGraph::new(); + g.add_connection("a", "b", "smb", None, Some("admin"), None, None); + g.add_connection("b", "c", "rdp", None, Some("svc_sql"), None, None); + g.add_connection("c", "d", "wmi", None, None, None, None); + let users = g.get_unique_users(); + assert_eq!(users.len(), 2); + assert!(users.contains("admin")); + assert!(users.contains("svc_sql")); + } + + #[test] + fn to_summary_has_expected_fields() { + let mut g = LateralGraph::new(); + g.add_connection("a", "b", "smb", None, Some("admin"), None, None); + g.mark_investigated("a"); + let summary = g.to_summary(); + assert_eq!(summary["total_connections"], 1); + assert_eq!(summary["hosts_investigated"], 1); + assert_eq!(summary["hosts_pending"], 1); + } + + #[test] + fn add_connection_no_evidence_id() { + let mut g = LateralGraph::new(); + let conn = g.add_connection("a", "b", "smb", None, None, None, None); + assert!(conn + .expect("add_connection should return connection") + .evidence_ids + .is_empty()); + } +} diff --git a/ares-core/src/correlation/lateral/patterns.rs b/ares-core/src/correlation/lateral/patterns.rs index 6f331762..bad2da74 100644 --- a/ares-core/src/correlation/lateral/patterns.rs +++ b/ares-core/src/correlation/lateral/patterns.rs @@ -64,3 +64,47 @@ impl LateralPatterns { "unknown" } } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn hostname_re_matches_fqdn() { + assert!(HOSTNAME_RE.is_match("dc01.contoso.local")); + } + + #[test] + fn hostname_re_no_match_bare_word() { + assert!(!HOSTNAME_RE.is_match("dc01")); + } + + #[test] + fn ip_re_matches_ipv4() { + assert!(IP_RE.is_match("192.168.1.1")); + assert!(IP_RE.is_match("10.0.0.1")); + } + + #[test] + fn ip_re_no_match_hostname() { + assert!(!IP_RE.is_match("dc01.contoso.local")); + } + + #[test] + fn ip_re_no_match_partial() { + assert!(!IP_RE.is_match("192.168.1")); + } + + #[test] + fn lateral_patterns_default() { + let p = LateralPatterns; + let _ = p.detect("some text"); + } + + #[test] + fn lateral_patterns_unknown_text() { + let p = LateralPatterns::new(); + let result = p.detect("completely unrelated text"); + assert_eq!(result, "unknown"); + } +} diff --git a/ares-core/src/correlation/lateral/tests.rs b/ares-core/src/correlation/lateral/tests.rs index fab75158..a3c1659e 100644 --- a/ares-core/src/correlation/lateral/tests.rs +++ b/ares-core/src/correlation/lateral/tests.rs @@ -2,7 +2,7 @@ use super::*; use serde_json::json; #[test] -fn test_graph_add_connection() { +fn graph_add_connection() { let mut graph = LateralGraph::new(); let conn = graph.add_connection("DC01", "WEB01", "smb", None, Some("admin"), None, None); assert!(conn.is_some()); @@ -13,7 +13,7 @@ fn test_graph_add_connection() { } #[test] -fn test_graph_self_connection_rejected() { +fn graph_self_connection_rejected() { let mut graph = LateralGraph::new(); let conn = graph.add_connection("DC01", "dc01", "smb", None, None, None, None); assert!(conn.is_none()); @@ -21,7 +21,7 @@ fn test_graph_self_connection_rejected() { } #[test] -fn test_graph_mark_investigated() { +fn graph_mark_investigated() { let mut graph = LateralGraph::new(); graph.add_connection("DC01", "WEB01", "smb", None, None, None, None); assert!(graph.pending_hosts.contains("web01")); @@ -32,7 +32,7 @@ fn test_graph_mark_investigated() { } #[test] -fn test_graph_get_host_connections() { +fn graph_get_host_connections() { let mut graph = LateralGraph::new(); graph.add_connection("dc01", "web01", "smb", None, None, None, None); graph.add_connection("dc01", "sql01", "wmi", None, None, None, None); @@ -46,7 +46,7 @@ fn test_graph_get_host_connections() { } #[test] -fn test_graph_outgoing_incoming() { +fn graph_outgoing_incoming() { let mut graph = LateralGraph::new(); graph.add_connection("dc01", "web01", "smb", None, None, None, None); graph.add_connection("web01", "sql01", "rdp", None, None, None, None); @@ -57,7 +57,7 @@ fn test_graph_outgoing_incoming() { } #[test] -fn test_graph_unique_users() { +fn graph_unique_users() { let mut graph = LateralGraph::new(); graph.add_connection("dc01", "web01", "smb", None, Some("admin"), None, None); graph.add_connection("dc01", "sql01", "wmi", None, Some("admin"), None, None); @@ -70,7 +70,7 @@ fn test_graph_unique_users() { } #[test] -fn test_graph_summary() { +fn graph_summary() { let mut graph = LateralGraph::new(); graph.add_connection("dc01", "web01", "smb", None, None, None, None); graph.mark_investigated("dc01"); @@ -82,7 +82,7 @@ fn test_graph_summary() { } #[test] -fn test_looks_like_hostname() { +fn looks_like_hostname_variants() { assert!(looks_like_hostname("dc01.contoso.local")); assert!(looks_like_hostname("web.contoso.local")); assert!(!looks_like_hostname("192.168.58.10")); @@ -91,7 +91,7 @@ fn test_looks_like_hostname() { } #[test] -fn test_analyzer_detect_connection_type() { +fn analyzer_detect_connection_type() { let analyzer = LateralMovementAnalyzer::new(None); assert_eq!( @@ -110,7 +110,7 @@ fn test_analyzer_detect_connection_type() { } #[test] -fn test_analyzer_query_result() { +fn analyzer_query_result() { let mut analyzer = LateralMovementAnalyzer::new(None); let result = json!({ @@ -126,7 +126,7 @@ fn test_analyzer_query_result() { } #[test] -fn test_analyzer_attack_path_linear() { +fn analyzer_attack_path_linear() { let mut analyzer = LateralMovementAnalyzer::new(None); analyzer .graph @@ -140,13 +140,13 @@ fn test_analyzer_attack_path_linear() { } #[test] -fn test_analyzer_attack_path_empty() { +fn analyzer_attack_path_empty() { let analyzer = LateralMovementAnalyzer::new(None); assert!(analyzer.get_attack_path().is_empty()); } #[test] -fn test_analyzer_pivot_suggestions() { +fn analyzer_pivot_suggestions() { let mut analyzer = LateralMovementAnalyzer::new(None); analyzer .graph @@ -158,7 +158,6 @@ fn test_analyzer_pivot_suggestions() { let suggestions = analyzer.get_pivot_suggestions(); assert_eq!(suggestions.len(), 2); - // All suggestions should have required fields for s in &suggestions { assert!(s.get("host").is_some()); assert!(s.get("priority").is_some()); diff --git a/ares-core/src/correlation/redblue/report.rs b/ares-core/src/correlation/redblue/report.rs index 55e70da9..34810ed0 100644 --- a/ares-core/src/correlation/redblue/report.rs +++ b/ares-core/src/correlation/redblue/report.rs @@ -205,3 +205,249 @@ pub fn generate_report_markdown(report: &CorrelationReport) -> String { lines.join("\n") } + +#[cfg(test)] +mod tests { + use super::*; + use chrono::{TimeZone, Utc}; + use std::collections::HashMap; + + use super::super::types::{ + BlueTeamDetection, CorrelationMatch, CorrelationReport, DetectionGap, RedTeamActivity, + TechniqueCoverage, + }; + + fn make_red(tech: Option<&str>, ip: Option<&str>, action: &str) -> RedTeamActivity { + RedTeamActivity { + timestamp: Utc.with_ymd_and_hms(2024, 1, 15, 10, 0, 0).unwrap(), + technique_id: tech.map(String::from), + technique_name: None, + action: action.to_string(), + target_ip: ip.map(String::from), + target_host: None, + credential_used: None, + success: true, + metadata: HashMap::new(), + } + } + + fn make_blue(tech: Option<&str>, alert: &str, ip: Option<&str>) -> BlueTeamDetection { + BlueTeamDetection { + timestamp: Utc.with_ymd_and_hms(2024, 1, 15, 10, 2, 0).unwrap(), + alert_name: alert.to_string(), + technique_id: tech.map(String::from), + severity: "high".to_string(), + target_ip: ip.map(String::from), + target_host: None, + investigation_id: None, + status: "completed".to_string(), + evidence_count: 3, + highest_pyramid_level: 4, + metadata: HashMap::new(), + } + } + + fn empty_report(detection_rate: f64) -> CorrelationReport { + CorrelationReport { + analysis_timestamp: Utc.with_ymd_and_hms(2024, 1, 15, 12, 0, 0).unwrap(), + red_operation_id: "op-test".to_string(), + time_window_start: Utc.with_ymd_and_hms(2024, 1, 15, 8, 0, 0).unwrap(), + time_window_end: Utc.with_ymd_and_hms(2024, 1, 15, 16, 0, 0).unwrap(), + total_red_activities: 10, + total_blue_detections: 8, + matched_activities: 6, + undetected_activities: 4, + false_positive_detections: 2, + matches: vec![], + gaps: vec![], + false_positives: vec![], + detection_rate, + false_positive_rate: 0.25, + mean_time_to_detect: Some(45.0), + technique_coverage: HashMap::new(), + } + } + + #[test] + fn report_contains_header_and_operation_id() { + let report = empty_report(0.6); + let md = generate_report_markdown(&report); + assert!(md.contains("# Red-Blue Correlation Report")); + assert!(md.contains("op-test")); + } + + #[test] + fn report_executive_summary_metrics() { + let report = empty_report(0.6); + let md = generate_report_markdown(&report); + assert!(md.contains("| Red Team Activities | 10 |")); + assert!(md.contains("| Blue Team Detections | 8 |")); + assert!(md.contains("| Matched (Detected) | 6 |")); + assert!(md.contains("| Detection Gaps | 4 |")); + assert!(md.contains("60.0%")); + } + + #[test] + fn report_mttd_none_shows_na() { + let mut report = empty_report(0.5); + report.mean_time_to_detect = None; + let md = generate_report_markdown(&report); + assert!(md.contains("N/A")); + } + + #[test] + fn report_assessment_excellent() { + let report = empty_report(0.85); + let md = generate_report_markdown(&report); + assert!(md.contains("EXCELLENT")); + } + + #[test] + fn report_assessment_good() { + let report = empty_report(0.65); + let md = generate_report_markdown(&report); + assert!(md.contains("GOOD")); + } + + #[test] + fn report_assessment_moderate() { + let report = empty_report(0.45); + let md = generate_report_markdown(&report); + assert!(md.contains("MODERATE")); + } + + #[test] + fn report_assessment_poor() { + let report = empty_report(0.2); + let md = generate_report_markdown(&report); + assert!(md.contains("POOR")); + } + + #[test] + fn report_technique_coverage_section() { + let mut report = empty_report(0.6); + report.technique_coverage.insert( + "T1003".to_string(), + TechniqueCoverage { + total: 5, + detected: 4, + missed: 1, + detection_rate: 0.8, + }, + ); + let md = generate_report_markdown(&report); + assert!(md.contains("## Technique Coverage")); + assert!(md.contains("T1003")); + assert!(md.contains("[+] 80%")); + } + + #[test] + fn report_technique_coverage_indicators() { + let mut report = empty_report(0.6); + report.technique_coverage.insert( + "T1078".to_string(), + TechniqueCoverage { + total: 4, + detected: 2, + missed: 2, + detection_rate: 0.5, + }, + ); + report.technique_coverage.insert( + "T1110".to_string(), + TechniqueCoverage { + total: 3, + detected: 0, + missed: 3, + detection_rate: 0.0, + }, + ); + let md = generate_report_markdown(&report); + assert!(md.contains("[~]")); // 0.5 rate + assert!(md.contains("[-]")); // 0.0 rate + } + + #[test] + fn report_successful_detections_section() { + let mut report = empty_report(0.6); + report.matches.push(CorrelationMatch { + red_activity: make_red( + Some("T1003"), + Some("10.0.0.1"), + "credential dump via secretsdump", + ), + blue_detection: make_blue(Some("T1003"), "Credential Dumping Alert", Some("10.0.0.1")), + time_delta_seconds: 120.0, + technique_match: true, + target_match: true, + confidence: 0.95, + }); + let md = generate_report_markdown(&report); + assert!(md.contains("## Successful Detections")); + assert!(md.contains("T1003")); + assert!(md.contains("STRONG")); + } + + #[test] + fn report_detection_gaps_section() { + let mut report = empty_report(0.4); + report.gaps.push(DetectionGap { + red_activity: make_red(Some("T1558"), Some("10.0.0.5"), "kerberoasting attack"), + reason: "No detection rule for Kerberoasting".to_string(), + recommended_detection: Some("Add 4769 monitoring".to_string()), + mitre_data_sources: vec![], + }); + let md = generate_report_markdown(&report); + assert!(md.contains("## Detection Gaps")); + assert!(md.contains("T1558")); + } + + #[test] + fn report_false_positives_section() { + let mut report = empty_report(0.6); + report.false_positives.push(make_blue( + Some("T1110"), + "Brute Force Alert", + Some("10.0.0.9"), + )); + let md = generate_report_markdown(&report); + assert!(md.contains("## False Positives")); + assert!(md.contains("Brute Force Alert")); + } + + #[test] + fn report_recommendations_from_gaps() { + let mut report = empty_report(0.4); + report.gaps.push(DetectionGap { + red_activity: make_red(Some("T1003"), None, "secretsdump"), + reason: "No rule".to_string(), + recommended_detection: Some("Enable Sysmon Event ID 10".to_string()), + mitre_data_sources: vec![], + }); + let md = generate_report_markdown(&report); + assert!(md.contains("## Recommendations")); + assert!(md.contains("Enable Sysmon Event ID 10")); + } + + #[test] + fn report_general_improvements_when_low_rate() { + let report = empty_report(0.3); + let md = generate_report_markdown(&report); + assert!(md.contains("### General Improvements")); + assert!(md.contains("log ingestion latency")); + } + + #[test] + fn report_no_general_improvements_when_high_rate() { + let report = empty_report(0.9); + let md = generate_report_markdown(&report); + assert!(!md.contains("### General Improvements")); + } + + #[test] + fn report_footer_present() { + let report = empty_report(0.5); + let md = generate_report_markdown(&report); + assert!(md.contains("Ares Red-Blue Correlation Engine")); + } +} diff --git a/ares-core/src/correlation/redblue/tests.rs b/ares-core/src/correlation/redblue/tests.rs index 2ac590e2..fe9b8927 100644 --- a/ares-core/src/correlation/redblue/tests.rs +++ b/ares-core/src/correlation/redblue/tests.rs @@ -47,7 +47,7 @@ fn make_blue_detection( } #[test] -fn test_techniques_match_exact() { +fn techniques_match_exact() { assert!(RedBlueCorrelator::techniques_match( Some("T1003"), Some("T1003") @@ -55,7 +55,7 @@ fn test_techniques_match_exact() { } #[test] -fn test_techniques_match_parent_child() { +fn techniques_match_parent_child() { assert!(RedBlueCorrelator::techniques_match( Some("T1003"), Some("T1003.006") @@ -67,7 +67,7 @@ fn test_techniques_match_parent_child() { } #[test] -fn test_techniques_match_different() { +fn techniques_match_different() { assert!(!RedBlueCorrelator::techniques_match( Some("T1003"), Some("T1110") @@ -75,14 +75,14 @@ fn test_techniques_match_different() { } #[test] -fn test_techniques_match_none() { +fn techniques_match_none() { assert!(!RedBlueCorrelator::techniques_match(None, Some("T1003"))); assert!(!RedBlueCorrelator::techniques_match(Some("T1003"), None)); assert!(!RedBlueCorrelator::techniques_match(None, None)); } #[test] -fn test_techniques_match_case_insensitive() { +fn techniques_match_case_insensitive() { assert!(RedBlueCorrelator::techniques_match( Some("t1003"), Some("T1003") @@ -90,7 +90,7 @@ fn test_techniques_match_case_insensitive() { } #[test] -fn test_correlate_perfect_match() { +fn correlate_perfect_match() { let correlator = RedBlueCorrelator::new("/tmp", None); let red = vec![make_red_activity("T1003", "192.168.58.10", utc(12, 0))]; @@ -111,7 +111,7 @@ fn test_correlate_perfect_match() { } #[test] -fn test_correlate_technique_only_match() { +fn correlate_technique_only_match() { let correlator = RedBlueCorrelator::new("/tmp", None); let red = vec![make_red_activity("T1003", "192.168.58.10", utc(12, 0))]; @@ -128,7 +128,7 @@ fn test_correlate_technique_only_match() { } #[test] -fn test_correlate_gap_detected() { +fn correlate_gap_detected() { let correlator = RedBlueCorrelator::new("/tmp", None); // Use different IPs so target matching doesn't cause T1046 to match @@ -151,7 +151,7 @@ fn test_correlate_gap_detected() { } #[test] -fn test_correlate_false_positive() { +fn correlate_false_positive() { let correlator = RedBlueCorrelator::new("/tmp", None); let red = vec![make_red_activity("T1003", "192.168.58.10", utc(12, 0))]; @@ -171,7 +171,7 @@ fn test_correlate_false_positive() { } #[test] -fn test_correlate_outside_time_window() { +fn correlate_outside_time_window() { let correlator = RedBlueCorrelator::new("/tmp", Some(5)); // 5 minute window let red = vec![make_red_activity("T1003", "192.168.58.10", utc(12, 0))]; @@ -179,7 +179,7 @@ fn test_correlate_outside_time_window() { "Credential Dumping Alert", "T1003", "192.168.58.10", - utc(13, 0), // 1 hour later - outside 5 min window + utc(13, 0), )]; let report = correlator.correlate(&red, &blue, "op-test"); @@ -188,7 +188,7 @@ fn test_correlate_outside_time_window() { } #[test] -fn test_correlate_empty_inputs() { +fn correlate_empty_inputs() { let correlator = RedBlueCorrelator::new("/tmp", None); let report = correlator.correlate(&[], &[], "op-test"); assert_eq!(report.total_red_activities, 0); @@ -196,7 +196,7 @@ fn test_correlate_empty_inputs() { } #[test] -fn test_correlate_technique_coverage() { +fn correlate_technique_coverage() { let correlator = RedBlueCorrelator::new("/tmp", None); // Use different IPs so T1046 doesn't match via target matching @@ -226,7 +226,7 @@ fn test_correlate_technique_coverage() { } #[test] -fn test_correlate_mean_time_to_detect() { +fn correlate_mean_time_to_detect() { let correlator = RedBlueCorrelator::new("/tmp", None); let red = vec![make_red_activity("T1003", "192.168.58.10", utc(12, 0))]; @@ -234,12 +234,11 @@ fn test_correlate_mean_time_to_detect() { "Alert", "T1003", "192.168.58.10", - utc(12, 5), // 5 minutes later + utc(12, 5), )]; let report = correlator.correlate(&red, &blue, "op-test"); - assert!(report.mean_time_to_detect.is_some()); - let mttd = report.mean_time_to_detect.unwrap(); + let mttd = report.mean_time_to_detect.expect("MTTD should be present"); assert!( (mttd - 300.0).abs() < 1.0, "MTTD should be ~300s, got {mttd}" @@ -247,7 +246,7 @@ fn test_correlate_mean_time_to_detect() { } #[test] -fn test_generate_report_markdown() { +fn generate_report_markdown() { let correlator = RedBlueCorrelator::new("/tmp", None); let red = vec![make_red_activity("T1003", "192.168.58.10", utc(12, 0))]; @@ -268,7 +267,7 @@ fn test_generate_report_markdown() { } #[test] -fn test_report_to_value() { +fn report_to_value() { let correlator = RedBlueCorrelator::new("/tmp", None); let report = correlator.correlate(&[], &[], "op-test"); let val = report.to_value(); @@ -278,22 +277,17 @@ fn test_report_to_value() { } #[test] -fn test_recommend_detection() { +fn recommend_detection() { let activity = make_red_activity("T1003", "192.168.58.10", utc(12, 0)); let rec = RedBlueCorrelator::recommend_detection(&activity); - assert!(rec.is_some()); - assert!(rec.unwrap().contains("LSASS")); + assert!(rec.expect("should have recommendation").contains("LSASS")); let unknown = make_red_activity("T9999", "192.168.58.10", utc(12, 0)); assert!(RedBlueCorrelator::recommend_detection(&unknown).is_none()); } -// ──────────────────────────────────────────────────────────────────────────── -// Additional coverage tests -// ──────────────────────────────────────────────────────────────────────────── - #[test] -fn test_recommend_detection_all_known_techniques() { +fn recommend_detection_all_known_techniques() { let known_techniques = [ ("T1046", "scanning"), ("T1110", "authentication"), @@ -304,8 +298,9 @@ fn test_recommend_detection_all_known_techniques() { for (technique, expected_keyword) in known_techniques { let activity = make_red_activity(technique, "192.168.58.10", utc(12, 0)); let rec = RedBlueCorrelator::recommend_detection(&activity); - assert!(rec.is_some(), "Expected recommendation for {technique}"); - let rec_text = rec.unwrap().to_lowercase(); + let rec_text = rec + .unwrap_or_else(|| panic!("Expected recommendation for {technique}")) + .to_lowercase(); assert!( rec_text.contains(&expected_keyword.to_lowercase()), "Recommendation for {technique} should mention '{expected_keyword}', got: {rec_text}" @@ -314,7 +309,7 @@ fn test_recommend_detection_all_known_techniques() { } #[test] -fn test_recommend_detection_no_technique_id() { +fn recommend_detection_no_technique_id() { let mut activity = make_red_activity("T1003", "192.168.58.10", utc(12, 0)); activity.technique_id = None; let rec = RedBlueCorrelator::recommend_detection(&activity); @@ -322,7 +317,7 @@ fn test_recommend_detection_no_technique_id() { } #[test] -fn test_determine_gap_reason_no_technique() { +fn determine_gap_reason_no_technique() { let mut activity = make_red_activity("T1003", "192.168.58.10", utc(12, 0)); activity.technique_id = None; let reason = RedBlueCorrelator::determine_gap_reason(&activity, &[]); @@ -330,7 +325,7 @@ fn test_determine_gap_reason_no_technique() { } #[test] -fn test_determine_gap_reason_no_matching_alert() { +fn determine_gap_reason_no_matching_alert() { let activity = make_red_activity("T1003", "192.168.58.10", utc(12, 0)); let detections = vec![make_blue_detection( "Network Scan", @@ -343,7 +338,7 @@ fn test_determine_gap_reason_no_matching_alert() { } #[test] -fn test_determine_gap_reason_alert_exists_but_no_trigger() { +fn determine_gap_reason_alert_exists_but_no_trigger() { let activity = make_red_activity("T1003", "192.168.58.10", utc(12, 0)); let detections = vec![make_blue_detection( "Credential Dumping", @@ -356,7 +351,7 @@ fn test_determine_gap_reason_alert_exists_but_no_trigger() { } #[test] -fn test_determine_gap_reason_hierarchical_technique_match() { +fn determine_gap_reason_hierarchical_technique_match() { // T1003 alert should match T1003.006 activity let activity = make_red_activity("T1003.006", "192.168.58.10", utc(12, 0)); let detections = vec![make_blue_detection( @@ -371,7 +366,7 @@ fn test_determine_gap_reason_hierarchical_technique_match() { } #[test] -fn test_techniques_match_subtechnique_siblings() { +fn techniques_match_subtechnique_siblings() { // T1003.001 and T1003.006 share parent T1003 so they should match assert!(RedBlueCorrelator::techniques_match( Some("T1003.001"), @@ -380,7 +375,7 @@ fn test_techniques_match_subtechnique_siblings() { } #[test] -fn test_techniques_match_mixed_case() { +fn techniques_match_mixed_case() { assert!(RedBlueCorrelator::techniques_match( Some("t1558.001"), Some("T1558.003") @@ -388,7 +383,7 @@ fn test_techniques_match_mixed_case() { } #[test] -fn test_red_team_activity_key() { +fn red_team_activity_key() { let activity = make_red_activity("T1003", "192.168.58.10", utc(12, 0)); let key = activity.key(); assert!(key.contains("T1003")); @@ -396,7 +391,7 @@ fn test_red_team_activity_key() { } #[test] -fn test_red_team_activity_key_no_technique_no_ip() { +fn red_team_activity_key_no_technique_no_ip() { let mut activity = make_red_activity("T1003", "192.168.58.10", utc(12, 0)); activity.technique_id = None; activity.target_ip = None; @@ -405,7 +400,7 @@ fn test_red_team_activity_key_no_technique_no_ip() { } #[test] -fn test_blue_team_detection_key() { +fn blue_team_detection_key() { let detection = make_blue_detection( "Credential Dumping Alert", "T1003", @@ -418,7 +413,7 @@ fn test_blue_team_detection_key() { } #[test] -fn test_blue_team_detection_key_no_technique() { +fn blue_team_detection_key_no_technique() { let mut detection = make_blue_detection("Unknown Alert", "T1003", "192.168.58.10", utc(12, 2)); detection.technique_id = None; let key = detection.key(); @@ -427,8 +422,7 @@ fn test_blue_team_detection_key_no_technique() { } #[test] -fn test_match_quality_strong() { - // technique_match + target_match + small time delta +fn match_quality_strong() { let m = super::types::CorrelationMatch { red_activity: make_red_activity("T1003", "192.168.58.10", utc(12, 0)), blue_detection: make_blue_detection("Alert", "T1003", "192.168.58.10", utc(12, 2)), @@ -441,8 +435,7 @@ fn test_match_quality_strong() { } #[test] -fn test_match_quality_good() { - // technique_match, no target_match, moderate time delta +fn match_quality_good() { let m = super::types::CorrelationMatch { red_activity: make_red_activity("T1003", "192.168.58.10", utc(12, 0)), blue_detection: make_blue_detection("Alert", "T1003", "192.168.58.20", utc(12, 8)), @@ -455,7 +448,7 @@ fn test_match_quality_good() { } #[test] -fn test_match_quality_weak_technique_only() { +fn match_quality_weak_technique_only() { let m = super::types::CorrelationMatch { red_activity: make_red_activity("T1003", "192.168.58.10", utc(12, 0)), blue_detection: make_blue_detection("Alert", "T1003", "192.168.58.20", utc(12, 15)), @@ -468,7 +461,7 @@ fn test_match_quality_weak_technique_only() { } #[test] -fn test_match_quality_weak_target_close() { +fn match_quality_weak_target_close() { let m = super::types::CorrelationMatch { red_activity: make_red_activity("T1003", "192.168.58.10", utc(12, 0)), blue_detection: make_blue_detection("Alert", "T1046", "192.168.58.10", utc(12, 3)), @@ -481,7 +474,7 @@ fn test_match_quality_weak_target_close() { } #[test] -fn test_match_quality_tenuous() { +fn match_quality_tenuous() { let m = super::types::CorrelationMatch { red_activity: make_red_activity("T1003", "192.168.58.10", utc(12, 0)), blue_detection: make_blue_detection("Alert", "T1046", "192.168.58.20", utc(12, 10)), @@ -494,7 +487,7 @@ fn test_match_quality_tenuous() { } #[test] -fn test_correlate_multiple_red_activities_matched() { +fn correlate_multiple_red_activities_matched() { let correlator = RedBlueCorrelator::new("/tmp", None); let red = vec![ @@ -517,7 +510,7 @@ fn test_correlate_multiple_red_activities_matched() { } #[test] -fn test_correlate_hierarchical_technique_matching() { +fn correlate_hierarchical_technique_matching() { let correlator = RedBlueCorrelator::new("/tmp", None); // Red uses subtechnique T1003.006, blue detects parent T1003 @@ -535,7 +528,7 @@ fn test_correlate_hierarchical_technique_matching() { } #[test] -fn test_correlate_no_red_activities() { +fn correlate_no_red_activities() { let correlator = RedBlueCorrelator::new("/tmp", None); let blue = vec![make_blue_detection( @@ -553,7 +546,7 @@ fn test_correlate_no_red_activities() { } #[test] -fn test_correlate_no_blue_detections() { +fn correlate_no_blue_detections() { let correlator = RedBlueCorrelator::new("/tmp", None); let red = vec![ @@ -570,7 +563,7 @@ fn test_correlate_no_blue_detections() { } #[test] -fn test_correlate_confidence_threshold() { +fn correlate_confidence_threshold() { // Matches below 0.3 confidence should not be included let correlator = RedBlueCorrelator::new("/tmp", Some(30)); @@ -590,7 +583,7 @@ fn test_correlate_confidence_threshold() { } #[test] -fn test_correlate_detection_rate_partial() { +fn correlate_detection_rate_partial() { let correlator = RedBlueCorrelator::new("/tmp", None); let red = vec![ @@ -611,7 +604,7 @@ fn test_correlate_detection_rate_partial() { } #[test] -fn test_correlate_mean_time_to_detect_none_when_no_matches() { +fn correlate_mean_time_to_detect_none_when_no_matches() { let correlator = RedBlueCorrelator::new("/tmp", None); let red = vec![make_red_activity("T1003", "192.168.58.10", utc(12, 0))]; @@ -620,7 +613,7 @@ fn test_correlate_mean_time_to_detect_none_when_no_matches() { } #[test] -fn test_correlate_technique_coverage_multiple_techniques() { +fn correlate_technique_coverage_multiple_techniques() { let correlator = RedBlueCorrelator::new("/tmp", None); let red = vec![ @@ -647,7 +640,7 @@ fn test_correlate_technique_coverage_multiple_techniques() { } #[test] -fn test_correlate_false_positive_rate() { +fn correlate_false_positive_rate() { let correlator = RedBlueCorrelator::new("/tmp", None); let red = vec![make_red_activity("T1003", "192.168.58.10", utc(12, 0))]; @@ -664,7 +657,7 @@ fn test_correlate_false_positive_rate() { } #[test] -fn test_report_to_value_full_structure() { +fn report_to_value_full_structure() { let correlator = RedBlueCorrelator::new("/tmp", None); let red = vec![ @@ -705,7 +698,7 @@ fn test_report_to_value_full_structure() { } #[test] -fn test_correlate_best_match_selection() { +fn correlate_best_match_selection() { // When multiple detections could match, engine should pick best confidence let correlator = RedBlueCorrelator::new("/tmp", None); @@ -725,7 +718,7 @@ fn test_correlate_best_match_selection() { } #[test] -fn test_correlate_time_window_custom() { +fn correlate_time_window_custom() { // Custom time window of 2 minutes let correlator = RedBlueCorrelator::new("/tmp", Some(2)); @@ -743,7 +736,7 @@ fn test_correlate_time_window_custom() { } #[test] -fn test_correlate_detection_before_activity() { +fn correlate_detection_before_activity() { // Blue detection 2 minutes BEFORE red activity (should still match within window) let correlator = RedBlueCorrelator::new("/tmp", None); @@ -762,7 +755,7 @@ fn test_correlate_detection_before_activity() { } #[test] -fn test_new_default_time_window() { +fn new_default_time_window() { let correlator = RedBlueCorrelator::new("/tmp/reports", None); assert_eq!( correlator.time_window.num_minutes(), @@ -771,7 +764,7 @@ fn test_new_default_time_window() { } #[test] -fn test_new_custom_time_window() { +fn new_custom_time_window() { let correlator = RedBlueCorrelator::new("/tmp/reports", Some(60)); assert_eq!(correlator.time_window.num_minutes(), 60); } diff --git a/ares-core/src/correlation/redblue/types.rs b/ares-core/src/correlation/redblue/types.rs index 395c1ac8..1d7531e6 100644 --- a/ares-core/src/correlation/redblue/types.rs +++ b/ares-core/src/correlation/redblue/types.rs @@ -185,3 +185,237 @@ impl CorrelationReport { }) } } + +#[cfg(test)] +mod tests { + use super::*; + use chrono::TimeZone; + use std::collections::HashMap; + + fn make_red_activity( + technique_id: Option<&str>, + target_ip: Option<&str>, + action: &str, + ) -> RedTeamActivity { + RedTeamActivity { + timestamp: Utc.with_ymd_and_hms(2024, 1, 15, 10, 0, 0).unwrap(), + technique_id: technique_id.map(String::from), + technique_name: None, + action: action.to_string(), + target_ip: target_ip.map(String::from), + target_host: None, + credential_used: None, + success: true, + metadata: HashMap::new(), + } + } + + fn make_blue_detection( + technique_id: Option<&str>, + alert_name: &str, + target_ip: Option<&str>, + ) -> BlueTeamDetection { + BlueTeamDetection { + timestamp: Utc.with_ymd_and_hms(2024, 1, 15, 10, 2, 0).unwrap(), + alert_name: alert_name.to_string(), + technique_id: technique_id.map(String::from), + severity: "high".to_string(), + target_ip: target_ip.map(String::from), + target_host: None, + investigation_id: None, + status: "completed".to_string(), + evidence_count: 3, + highest_pyramid_level: 4, + metadata: HashMap::new(), + } + } + + #[test] + fn red_activity_key_with_all_fields() { + let activity = make_red_activity(Some("T1003"), Some("10.0.0.1"), "credential dump"); + let key = activity.key(); + assert!(key.contains("T1003")); + assert!(key.contains("10.0.0.1")); + } + + #[test] + fn red_activity_key_none_fields_use_none_string() { + let activity = make_red_activity(None, None, "scan"); + let key = activity.key(); + assert!(key.contains("none:none")); + } + + #[test] + fn blue_detection_key_includes_alert_name() { + let det = make_blue_detection(Some("T1003"), "Credential Dumping Alert", Some("10.0.0.1")); + let key = det.key(); + assert!(key.contains("T1003")); + assert!(key.contains("Credential Dumping Alert")); + } + + #[test] + fn blue_detection_key_none_technique() { + let det = make_blue_detection(None, "Generic Alert", None); + let key = det.key(); + assert!(key.contains("none")); + assert!(key.contains("Generic Alert")); + } + + #[test] + fn match_quality_strong() { + let m = CorrelationMatch { + red_activity: make_red_activity(Some("T1003"), Some("10.0.0.1"), "dump"), + blue_detection: make_blue_detection(Some("T1003"), "Alert", Some("10.0.0.1")), + time_delta_seconds: 120.0, + technique_match: true, + target_match: true, + confidence: 0.95, + }; + assert_eq!(m.match_quality(), "STRONG"); + } + + #[test] + fn match_quality_good() { + let m = CorrelationMatch { + red_activity: make_red_activity(Some("T1003"), Some("10.0.0.1"), "dump"), + blue_detection: make_blue_detection(Some("T1003"), "Alert", Some("10.0.0.2")), + time_delta_seconds: 400.0, + technique_match: true, + target_match: false, + confidence: 0.7, + }; + assert_eq!(m.match_quality(), "GOOD"); + } + + #[test] + fn match_quality_weak_technique_only() { + let m = CorrelationMatch { + red_activity: make_red_activity(Some("T1003"), Some("10.0.0.1"), "dump"), + blue_detection: make_blue_detection(Some("T1003"), "Alert", Some("10.0.0.2")), + time_delta_seconds: 700.0, + technique_match: true, + target_match: false, + confidence: 0.5, + }; + assert_eq!(m.match_quality(), "WEAK"); + } + + #[test] + fn match_quality_weak_target_within_window() { + let m = CorrelationMatch { + red_activity: make_red_activity(Some("T1003"), Some("10.0.0.1"), "dump"), + blue_detection: make_blue_detection(Some("T1003"), "Alert", Some("10.0.0.1")), + time_delta_seconds: 200.0, + technique_match: false, + target_match: true, + confidence: 0.4, + }; + assert_eq!(m.match_quality(), "WEAK"); + } + + #[test] + fn match_quality_tenuous() { + let m = CorrelationMatch { + red_activity: make_red_activity(Some("T1003"), Some("10.0.0.1"), "dump"), + blue_detection: make_blue_detection(Some("T1003"), "Alert", Some("10.0.0.2")), + time_delta_seconds: 700.0, + technique_match: false, + target_match: false, + confidence: 0.1, + }; + assert_eq!(m.match_quality(), "TENUOUS"); + } + + #[test] + fn match_quality_strong_boundary_just_under_300() { + let m = CorrelationMatch { + red_activity: make_red_activity(Some("T1003"), Some("10.0.0.1"), "dump"), + blue_detection: make_blue_detection(Some("T1003"), "Alert", Some("10.0.0.1")), + time_delta_seconds: 299.9, + technique_match: true, + target_match: true, + confidence: 0.9, + }; + assert_eq!(m.match_quality(), "STRONG"); + } + + #[test] + fn match_quality_not_strong_at_300() { + let m = CorrelationMatch { + red_activity: make_red_activity(Some("T1003"), Some("10.0.0.1"), "dump"), + blue_detection: make_blue_detection(Some("T1003"), "Alert", Some("10.0.0.1")), + time_delta_seconds: 300.0, + technique_match: true, + target_match: true, + confidence: 0.9, + }; + // At exactly 300, not < 300, so falls to GOOD + assert_eq!(m.match_quality(), "GOOD"); + } + + #[test] + fn match_quality_negative_time_delta() { + // Negative delta (detection before activity) + let m = CorrelationMatch { + red_activity: make_red_activity(Some("T1003"), Some("10.0.0.1"), "dump"), + blue_detection: make_blue_detection(Some("T1003"), "Alert", Some("10.0.0.1")), + time_delta_seconds: -100.0, + technique_match: true, + target_match: true, + confidence: 0.9, + }; + assert_eq!(m.match_quality(), "STRONG"); + } + + #[test] + fn correlation_report_to_value_has_expected_keys() { + let report = CorrelationReport { + analysis_timestamp: Utc.with_ymd_and_hms(2024, 1, 15, 12, 0, 0).unwrap(), + red_operation_id: "op-123".to_string(), + time_window_start: Utc.with_ymd_and_hms(2024, 1, 15, 8, 0, 0).unwrap(), + time_window_end: Utc.with_ymd_and_hms(2024, 1, 15, 16, 0, 0).unwrap(), + total_red_activities: 10, + total_blue_detections: 8, + matched_activities: 6, + undetected_activities: 4, + false_positive_detections: 2, + matches: vec![], + gaps: vec![], + false_positives: vec![], + detection_rate: 0.6, + false_positive_rate: 0.25, + mean_time_to_detect: Some(45.5), + technique_coverage: HashMap::new(), + }; + let val = report.to_value(); + assert_eq!(val["red_operation_id"], "op-123"); + assert_eq!(val["summary"]["total_red_activities"], 10); + assert_eq!(val["summary"]["matched_activities"], 6); + assert_eq!(val["summary"]["detection_rate"], "60.0%"); + assert_eq!(val["summary"]["mean_time_to_detect"], "45.5s"); + } + + #[test] + fn correlation_report_to_value_no_mttd() { + let report = CorrelationReport { + analysis_timestamp: Utc::now(), + red_operation_id: "op-456".to_string(), + time_window_start: Utc::now(), + time_window_end: Utc::now(), + total_red_activities: 0, + total_blue_detections: 0, + matched_activities: 0, + undetected_activities: 0, + false_positive_detections: 0, + matches: vec![], + gaps: vec![], + false_positives: vec![], + detection_rate: 0.0, + false_positive_rate: 0.0, + mean_time_to_detect: None, + technique_coverage: HashMap::new(), + }; + let val = report.to_value(); + assert_eq!(val["summary"]["mean_time_to_detect"], "N/A"); + } +} diff --git a/ares-core/src/detection/mod.rs b/ares-core/src/detection/mod.rs index ce54e2b3..49cfd949 100644 --- a/ares-core/src/detection/mod.rs +++ b/ares-core/src/detection/mod.rs @@ -153,3 +153,148 @@ pub fn templates_for_connection_type(conn_type: &str) -> Vec<&'static str> { .map(|(name, _)| name.as_str()) .collect() } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn detection_config_loads_successfully() { + let config = detection_config(); + assert!( + !config.templates.is_empty(), + "templates should not be empty" + ); + assert!( + !config.activity_scopes.is_empty(), + "activity_scopes should not be empty" + ); + } + + #[test] + fn detection_config_has_event_id_reference() { + let config = detection_config(); + assert!( + !config.event_id_reference.is_empty(), + "event_id_reference should not be empty" + ); + } + + #[test] + fn detection_config_singleton_returns_same_ref() { + let c1 = detection_config(); + let c2 = detection_config(); + assert!(std::ptr::eq(c1, c2)); + } + + #[test] + fn find_template_by_direct_name() { + let config = detection_config(); + let first_name = config.templates.keys().next().unwrap(); + let (key, _) = find_template(first_name).expect("should find template by name"); + assert_eq!(key, first_name.as_str()); + } + + #[test] + fn find_template_nonexistent_returns_none() { + assert!(find_template("nonexistent_xyz_42").is_none()); + } + + #[test] + fn find_template_by_alias() { + let config = detection_config(); + for (name, entry) in &config.templates { + if let Some(alias) = entry.aliases.first() { + let (key, _) = find_template(alias).expect("should find template by alias"); + assert_eq!(key, name.as_str()); + return; + } + } + } + + #[test] + fn template_entries_have_required_fields() { + let config = detection_config(); + for (name, entry) in &config.templates { + assert!( + !entry.description.is_empty(), + "'{name}' missing description" + ); + assert!(!entry.mitre_id.is_empty(), "'{name}' missing mitre_id"); + assert!(!entry.tactic.is_empty(), "'{name}' missing tactic"); + assert!(!entry.severity.is_empty(), "'{name}' missing severity"); + } + } + + #[test] + fn mitre_for_connection_type_known_types() { + let known = [ + "smb", + "rdp", + "wmi", + "psexec", + "winrm", + "ssh", + "dcom", + "scheduled_task", + "mssql", + "constrained_delegation", + "ntlm_relay", + ]; + for ct in &known { + assert!( + mitre_for_connection_type(ct).is_some(), + "'{ct}' should have a MITRE mapping" + ); + } + } + + #[test] + fn mitre_for_connection_type_unknown_returns_none() { + assert!(mitre_for_connection_type("unknown_proto_xyz").is_none()); + } + + #[test] + fn mitre_for_connection_type_smb_maps_to_t1021() { + let mitre = mitre_for_connection_type("smb").unwrap(); + assert!( + mitre.starts_with("T1021"), + "SMB should map to T1021.x, got {mitre}" + ); + } + + #[test] + fn templates_for_connection_type_smb_returns_entries() { + let t = templates_for_connection_type("smb"); + assert!(!t.is_empty(), "smb should have matching templates"); + } + + #[test] + fn templates_for_connection_type_unknown_empty() { + let t = templates_for_connection_type("unknown_xyz"); + assert!(t.is_empty()); + } + + #[test] + fn default_log_source_is_windows_security() { + assert_eq!(default_log_source(), "windows-security"); + } + + #[test] + fn lateral_patterns_present_in_config() { + let config = detection_config(); + // lateral_patterns may be empty but should be loadable + // If present, keys should be connection type names + for key in config.lateral_patterns.keys() { + assert!(!key.is_empty(), "lateral_patterns key should not be empty"); + } + } + + #[test] + fn activity_scopes_values_are_non_empty() { + let config = detection_config(); + for (scope, ids) in &config.activity_scopes { + assert!(!ids.is_empty(), "scope '{scope}' should have event IDs"); + } + } +} diff --git a/ares-core/src/eval/gap_analysis/analysis.rs b/ares-core/src/eval/gap_analysis/analysis.rs index 91cebe08..3e76e9fd 100644 --- a/ares-core/src/eval/gap_analysis/analysis.rs +++ b/ares-core/src/eval/gap_analysis/analysis.rs @@ -172,3 +172,171 @@ pub(crate) fn generate_summary(result: &EvaluationResult, gaps: &[String]) -> St parts.join(" ") } + +#[cfg(test)] +mod tests { + use super::*; + use crate::eval::ground_truth::{ExpectedIOC, ExpectedTechnique}; + use crate::eval::results::EvaluationResult; + use crate::models::PyramidLevel; + + fn make_ioc(ioc_type: &str, value: &str, required: bool) -> ExpectedIOC { + ExpectedIOC { + ioc_type: ioc_type.into(), + value: value.into(), + pyramid_level: PyramidLevel::IpAddresses, + mitre_techniques: vec![], + required, + source: String::new(), + } + } + + fn make_technique(id: &str, name: &str, required: bool) -> ExpectedTechnique { + ExpectedTechnique { + technique_id: id.into(), + technique_name: name.into(), + required, + parent_id: None, + } + } + + fn base_result() -> EvaluationResult { + EvaluationResult { + evaluation_id: "eval-1".into(), + operation_id: "op-1".into(), + overall_score: 0.8, + ioc_detection_rate: 0.7, + technique_coverage: 0.6, + alert_fired: true, + investigation_started: true, + investigation_completed: true, + highest_pyramid_level: 5, + ..Default::default() + } + } + + #[test] + fn describe_ioc_gap_required() { + let ioc = make_ioc("ip", "10.0.0.1", true); + let desc = describe_ioc_gap(&ioc); + assert!(desc.contains("ip")); + assert!(desc.contains("10.0.0.1")); + assert!(desc.contains("(required)")); + } + + #[test] + fn describe_ioc_gap_optional() { + let ioc = make_ioc("hash", "abc123", false); + let desc = describe_ioc_gap(&ioc); + assert!(desc.contains("hash")); + assert!(!desc.contains("(required)")); + } + + #[test] + fn describe_technique_gap_with_name() { + let t = make_technique("T1003", "OS Credential Dumping", true); + let desc = describe_technique_gap(&t); + assert!(desc.contains("T1003")); + assert!(desc.contains("OS Credential Dumping")); + assert!(desc.contains("(required)")); + } + + #[test] + fn describe_technique_gap_no_name() { + let t = make_technique("T1046", "", false); + let desc = describe_technique_gap(&t); + assert!(desc.contains("T1046")); + assert!(!desc.contains("(required)")); + } + + #[test] + fn summary_good_grade_with_alert() { + let r = base_result(); + let gaps: Vec = vec![]; + let summary = generate_summary(&r, &gaps); + assert!(summary.contains("grade of B")); + assert!(summary.contains("alert was successfully")); + assert!(summary.contains("No significant")); + } + + #[test] + fn summary_failing_grade_no_alert() { + let mut r = base_result(); + r.overall_score = 0.4; + r.alert_fired = false; + let gaps = vec!["gap1".into(), "gap2".into()]; + let summary = generate_summary(&r, &gaps); + assert!(summary.contains("grade of F")); + assert!(summary.contains("No alert was triggered")); + assert!(summary.contains("2 detection gaps")); + } + + #[test] + fn analyze_no_gaps_clean_result() { + let r = base_result(); + let report = analyze_detection_gaps(&r); + assert_eq!(report.evaluation_id, "eval-1"); + assert_eq!(report.overall_grade, "B"); + assert!(report.detection_gaps.is_empty()); + } + + #[test] + fn analyze_missed_iocs_and_techniques() { + let mut r = base_result(); + r.missed_iocs = vec![make_ioc("ip", "10.0.0.1", true)]; + r.missed_techniques = vec![make_technique("T1003", "OS Credential Dumping", true)]; + let report = analyze_detection_gaps(&r); + assert!(report.detection_gaps.len() >= 2); + } + + #[test] + fn analyze_no_alert_adds_critical_rec() { + let mut r = base_result(); + r.alert_fired = false; + let report = analyze_detection_gaps(&r); + assert!(report + .detection_gaps + .iter() + .any(|g| g.contains("No alert fired"))); + assert!(report + .recommendations + .iter() + .any(|rec| rec.priority == "critical")); + } + + #[test] + fn analyze_low_pyramid_adds_rec() { + let mut r = base_result(); + r.highest_pyramid_level = 2; + let report = analyze_detection_gaps(&r); + assert!(report + .detection_gaps + .iter() + .any(|g| g.contains("pyramid level 2/6"))); + } + + #[test] + fn analyze_incomplete_investigation() { + let mut r = base_result(); + r.investigation_completed = false; + let report = analyze_detection_gaps(&r); + assert!(report + .detection_gaps + .iter() + .any(|g| g.contains("did not complete"))); + } + + #[test] + fn recommendations_sorted_by_priority() { + let mut r = base_result(); + r.alert_fired = false; + r.highest_pyramid_level = 2; + r.investigation_completed = false; + let report = analyze_detection_gaps(&r); + let first = report + .recommendations + .first() + .expect("should have recommendations"); + assert_eq!(first.priority, "critical"); + } +} diff --git a/ares-core/src/eval/gap_analysis/tests.rs b/ares-core/src/eval/gap_analysis/tests.rs index 4c484352..bc15ab94 100644 --- a/ares-core/src/eval/gap_analysis/tests.rs +++ b/ares-core/src/eval/gap_analysis/tests.rs @@ -55,7 +55,7 @@ fn make_result_with_gaps() -> EvaluationResult { } #[test] -fn test_analyze_detection_gaps_basic() { +fn analyze_detection_gaps_basic() { let result = make_result_with_gaps(); let report = analyze_detection_gaps(&result); @@ -73,7 +73,7 @@ fn test_analyze_detection_gaps_basic() { } #[test] -fn test_analyze_no_gaps() { +fn analyze_no_gaps() { let result = EvaluationResult { evaluation_id: "eval-2".to_string(), operation_id: "op-2".to_string(), @@ -92,7 +92,7 @@ fn test_analyze_no_gaps() { } #[test] -fn test_ioc_gap_descriptions() { +fn ioc_gap_descriptions() { let ioc = ExpectedIOC { ioc_type: "ip".to_string(), value: "192.168.58.10".to_string(), @@ -108,7 +108,7 @@ fn test_ioc_gap_descriptions() { } #[test] -fn test_technique_gap_descriptions() { +fn technique_gap_descriptions() { let tech = ExpectedTechnique { technique_id: "T1003".to_string(), technique_name: "Credential Dumping".to_string(), @@ -122,7 +122,7 @@ fn test_technique_gap_descriptions() { } #[test] -fn test_recommend_for_known_technique() { +fn recommend_for_known_technique() { let tech = ExpectedTechnique { technique_id: "T1003".to_string(), technique_name: "Credential Dumping".to_string(), @@ -136,7 +136,7 @@ fn test_recommend_for_known_technique() { } #[test] -fn test_recommend_for_subtechnique_falls_back_to_parent() { +fn recommend_for_subtechnique_falls_back_to_parent() { // T1003.001 is not in the map, but T1003 is let tech = ExpectedTechnique { technique_id: "T1003.001".to_string(), @@ -150,7 +150,7 @@ fn test_recommend_for_subtechnique_falls_back_to_parent() { } #[test] -fn test_recommend_for_unknown_technique() { +fn recommend_for_unknown_technique() { let tech = ExpectedTechnique { technique_id: "T9999".to_string(), technique_name: "Novel Attack".to_string(), @@ -164,7 +164,7 @@ fn test_recommend_for_unknown_technique() { } #[test] -fn test_recommend_for_ioc_types() { +fn recommend_for_ioc_types() { let ip_ioc = ExpectedIOC { ioc_type: "ip".to_string(), value: "192.168.58.10".to_string(), @@ -207,7 +207,7 @@ fn test_recommend_for_ioc_types() { } #[test] -fn test_to_markdown() { +fn to_markdown() { let result = make_result_with_gaps(); let report = analyze_detection_gaps(&result); let md = report.to_markdown(); @@ -221,7 +221,7 @@ fn test_to_markdown() { } #[test] -fn test_recommendations_sorted_by_priority() { +fn recommendations_sorted_by_priority() { let result = make_result_with_gaps(); let report = analyze_detection_gaps(&result); @@ -247,7 +247,7 @@ fn test_recommendations_sorted_by_priority() { } #[test] -fn test_summary_generation() { +fn summary_generation() { let result = make_result_with_gaps(); let gaps = vec!["gap 1".to_string(), "gap 2".to_string()]; let summary = generate_summary(&result, &gaps); diff --git a/ares-core/src/eval/gap_analysis/types.rs b/ares-core/src/eval/gap_analysis/types.rs index 30770294..3f24b524 100644 --- a/ares-core/src/eval/gap_analysis/types.rs +++ b/ares-core/src/eval/gap_analysis/types.rs @@ -98,3 +98,99 @@ impl GapAnalysisReport { lines.join("\n") } } + +#[cfg(test)] +mod tests { + use super::*; + + fn make_report(gaps: Vec<&str>, recs: Vec) -> GapAnalysisReport { + GapAnalysisReport { + evaluation_id: "eval-1".to_string(), + operation_id: "op-1".to_string(), + overall_grade: "B".to_string(), + detection_gaps: gaps.into_iter().map(String::from).collect(), + recommendations: recs, + summary: "Test summary".to_string(), + } + } + + fn make_rec(priority: &str, title: &str, category: &str) -> DetectionRecommendation { + DetectionRecommendation { + category: category.to_string(), + priority: priority.to_string(), + title: title.to_string(), + description: "desc".to_string(), + techniques: vec![], + implementation_hint: String::new(), + } + } + + #[test] + fn to_markdown_contains_header_and_ids() { + let report = make_report(vec![], vec![]); + let md = report.to_markdown(); + assert!(md.contains("# Detection Gap Analysis Report")); + assert!(md.contains("**Evaluation ID:** eval-1")); + assert!(md.contains("**Operation ID:** op-1")); + assert!(md.contains("**Grade:** B")); + } + + #[test] + fn to_markdown_no_gaps_message() { + let report = make_report(vec![], vec![]); + let md = report.to_markdown(); + assert!(md.contains("No significant detection gaps identified.")); + } + + #[test] + fn to_markdown_lists_gaps() { + let report = make_report(vec!["Missing T1003", "No lateral detection"], vec![]); + let md = report.to_markdown(); + assert!(md.contains("- Missing T1003")); + assert!(md.contains("- No lateral detection")); + } + + #[test] + fn to_markdown_no_recs_message() { + let report = make_report(vec![], vec![]); + let md = report.to_markdown(); + assert!(md.contains("No specific recommendations at this time.")); + } + + #[test] + fn to_markdown_groups_recs_by_priority() { + let recs = vec![ + make_rec("high", "Add Sysmon", "log_source"), + make_rec("critical", "Fix SIEM", "rule"), + ]; + let report = make_report(vec![], recs); + let md = report.to_markdown(); + assert!(md.contains("### Critical Priority")); + assert!(md.contains("### High Priority")); + assert!(md.contains("#### Fix SIEM")); + assert!(md.contains("#### Add Sysmon")); + } + + #[test] + fn to_markdown_includes_techniques_when_present() { + let rec = DetectionRecommendation { + category: "rule".to_string(), + priority: "high".to_string(), + title: "Detect T1003".to_string(), + description: "Add rule".to_string(), + techniques: vec!["T1003".to_string(), "T1003.001".to_string()], + implementation_hint: "Use Sigma".to_string(), + }; + let report = make_report(vec![], vec![rec]); + let md = report.to_markdown(); + assert!(md.contains("**Techniques:** T1003, T1003.001")); + assert!(md.contains("**Implementation:** Use Sigma")); + } + + #[test] + fn to_markdown_includes_summary() { + let report = make_report(vec![], vec![]); + let md = report.to_markdown(); + assert!(md.contains("Test summary")); + } +} diff --git a/ares-core/src/eval/ground_truth/mappings.rs b/ares-core/src/eval/ground_truth/mappings.rs index 94c37c5b..fe748016 100644 --- a/ares-core/src/eval/ground_truth/mappings.rs +++ b/ares-core/src/eval/ground_truth/mappings.rs @@ -53,3 +53,85 @@ pub fn get_techniques_for_vuln_type(vuln_type: &str) -> Vec { .map(|v| v.iter().map(|s| s.to_string()).collect()) .unwrap_or_else(|| vec!["T1068".to_string()]) } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn is_technique_required_credential_dumping() { + assert!(is_technique_required("T1003")); + assert!(is_technique_required("T1003.001")); + assert!(is_technique_required("T1003.006")); + } + + #[test] + fn is_technique_required_valid_accounts() { + assert!(is_technique_required("T1078")); + assert!(is_technique_required("T1078.002")); + } + + #[test] + fn is_technique_required_kerberos() { + assert!(is_technique_required("T1558")); + assert!(is_technique_required("T1558.003")); + } + + #[test] + fn is_technique_required_brute_force() { + assert!(is_technique_required("T1110")); + } + + #[test] + fn is_technique_required_remote_services() { + assert!(is_technique_required("T1021")); + } + + #[test] + fn is_technique_required_alternate_auth() { + assert!(is_technique_required("T1550")); + } + + #[test] + fn is_technique_not_required_unknown() { + assert!(!is_technique_required("T1046")); + assert!(!is_technique_required("T9999")); + assert!(!is_technique_required("")); + } + + #[test] + fn get_techniques_adcs_esc1() { + let techs = get_techniques_for_vuln_type("ADCS_ESC1"); + assert_eq!(techs, vec!["T1649"]); + } + + #[test] + fn get_techniques_case_insensitive() { + let techs = get_techniques_for_vuln_type("adcs_esc1"); + assert_eq!(techs, vec!["T1649"]); + } + + #[test] + fn get_techniques_kerberoasting() { + let techs = get_techniques_for_vuln_type("KERBEROASTING"); + assert_eq!(techs, vec!["T1558.003"]); + } + + #[test] + fn get_techniques_acl_abuse_multiple() { + let techs = get_techniques_for_vuln_type("ACL_ABUSE"); + assert_eq!(techs, vec!["T1222", "T1484"]); + } + + #[test] + fn get_techniques_unknown_returns_default() { + let techs = get_techniques_for_vuln_type("UNKNOWN_VULN"); + assert_eq!(techs, vec!["T1068"]); + } + + #[test] + fn get_techniques_dcsync() { + let techs = get_techniques_for_vuln_type("DCSYNC"); + assert_eq!(techs, vec!["T1003.006"]); + } +} diff --git a/ares-core/src/eval/ground_truth/schema.rs b/ares-core/src/eval/ground_truth/schema.rs index a7a3e297..19343564 100644 --- a/ares-core/src/eval/ground_truth/schema.rs +++ b/ares-core/src/eval/ground_truth/schema.rs @@ -167,3 +167,129 @@ impl EvaluationGroundTruth { .collect() } } + +#[cfg(test)] +mod tests { + use super::*; + + fn make_technique(id: &str, required: bool) -> ExpectedTechnique { + ExpectedTechnique { + technique_id: id.to_string(), + technique_name: String::new(), + required, + parent_id: None, + } + } + + fn make_ioc(ioc_type: &str, value: &str, required: bool) -> ExpectedIOC { + ExpectedIOC { + ioc_type: ioc_type.to_string(), + value: value.to_string(), + pyramid_level: PyramidLevel::IpAddresses, + mitre_techniques: vec![], + required, + source: String::new(), + } + } + + fn make_gt() -> EvaluationGroundTruth { + EvaluationGroundTruth { + operation_id: "op-1".to_string(), + target_ip: "10.0.0.1".to_string(), + expected_iocs: vec![ + make_ioc("ip", "10.0.0.1", true), + make_ioc("user", "admin", true), + make_ioc("hash", "abc", false), + ], + expected_techniques: vec![ + make_technique("T1003", true), + make_technique("T1046", false), + ], + expected_timeline: vec![], + expected_shares: vec![], + expected_vulnerabilities: vec![], + min_pyramid_level: 4, + target_pyramid_level: 6, + min_technique_coverage: 0.6, + min_ioc_detection_rate: 0.5, + } + } + + #[test] + fn matches_exact_technique_id() { + let t = make_technique("T1003", true); + assert!(t.matches("T1003")); + } + + #[test] + fn matches_parent_to_subtechnique() { + let t = make_technique("T1003", true); + assert!(t.matches("T1003.001")); + } + + #[test] + fn matches_subtechnique_to_parent() { + let t = make_technique("T1003.001", true); + assert!(t.matches("T1003")); + } + + #[test] + fn no_match_unrelated_technique() { + let t = make_technique("T1003", true); + assert!(!t.matches("T1046")); + } + + #[test] + fn no_match_different_subtechnique() { + let t = make_technique("T1003.001", true); + assert!(!t.matches("T1003.002")); + } + + #[test] + fn required_iocs_filters_correctly() { + let gt = make_gt(); + let required = gt.required_iocs(); + assert_eq!(required.len(), 2); + assert!(required.iter().all(|i| i.required)); + } + + #[test] + fn optional_iocs_filters_correctly() { + let gt = make_gt(); + let optional = gt.optional_iocs(); + assert_eq!(optional.len(), 1); + assert!(optional.iter().all(|i| !i.required)); + } + + #[test] + fn required_techniques_filters_correctly() { + let gt = make_gt(); + let required = gt.required_techniques(); + assert_eq!(required.len(), 1); + assert_eq!(required[0].technique_id, "T1003"); + } + + #[test] + fn optional_techniques_filters_correctly() { + let gt = make_gt(); + let optional = gt.optional_techniques(); + assert_eq!(optional.len(), 1); + assert_eq!(optional[0].technique_id, "T1046"); + } + + #[test] + fn empty_iocs_returns_empty() { + let mut gt = make_gt(); + gt.expected_iocs.clear(); + assert!(gt.required_iocs().is_empty()); + assert!(gt.optional_iocs().is_empty()); + } + + #[test] + fn empty_techniques_returns_empty() { + let mut gt = make_gt(); + gt.expected_techniques.clear(); + assert!(gt.required_techniques().is_empty()); + assert!(gt.optional_techniques().is_empty()); + } +} diff --git a/ares-core/src/eval/ground_truth/tests.rs b/ares-core/src/eval/ground_truth/tests.rs index 4c9cb640..8204995d 100644 --- a/ares-core/src/eval/ground_truth/tests.rs +++ b/ares-core/src/eval/ground_truth/tests.rs @@ -4,7 +4,7 @@ use super::transform::create_ground_truth_from_red_state; use crate::models::PyramidLevel; #[test] -fn test_expected_technique_exact_match() { +fn expected_technique_exact_match() { let tech = ExpectedTechnique { technique_id: "T1003".to_string(), technique_name: "".to_string(), @@ -16,7 +16,7 @@ fn test_expected_technique_exact_match() { } #[test] -fn test_expected_technique_parent_child_match() { +fn expected_technique_parent_child_match() { let parent = ExpectedTechnique { technique_id: "T1003".to_string(), technique_name: "".to_string(), @@ -37,7 +37,7 @@ fn test_expected_technique_parent_child_match() { } #[test] -fn test_is_technique_required() { +fn technique_is_required() { assert!(is_technique_required("T1003")); assert!(is_technique_required("T1003.006")); assert!(is_technique_required("T1558.001")); @@ -46,7 +46,7 @@ fn test_is_technique_required() { } #[test] -fn test_get_techniques_for_vuln_type() { +fn techniques_for_vuln_type() { assert_eq!(get_techniques_for_vuln_type("ADCS_ESC1"), vec!["T1649"]); assert_eq!( get_techniques_for_vuln_type("KERBEROASTING"), @@ -56,7 +56,7 @@ fn test_get_techniques_for_vuln_type() { } #[test] -fn test_ground_truth_filters() { +fn ground_truth_filters() { let gt = EvaluationGroundTruth { operation_id: "op-1".to_string(), target_ip: "192.168.58.10".to_string(), @@ -108,7 +108,7 @@ fn test_ground_truth_filters() { } #[test] -fn test_create_ground_truth_from_red_state() { +fn creates_ground_truth_from_red_state() { use crate::models::{Credential, Hash, Host, SharedRedTeamState, Target, User}; let mut state = SharedRedTeamState::new("op-test".to_string()); @@ -202,7 +202,7 @@ fn test_create_ground_truth_from_red_state() { } #[test] -fn test_create_ground_truth_deduplicates() { +fn create_ground_truth_deduplicates() { use crate::models::{Credential, Host, SharedRedTeamState, User}; let mut state = SharedRedTeamState::new("op-dedup".to_string()); diff --git a/ares-core/src/eval/results.rs b/ares-core/src/eval/results.rs index 8cc6a8e5..a63c1602 100644 --- a/ares-core/src/eval/results.rs +++ b/ares-core/src/eval/results.rs @@ -459,7 +459,7 @@ mod tests { use super::*; #[test] - fn test_grade() { + fn grade() { let r = EvaluationResult { overall_score: 0.95, ..Default::default() @@ -488,7 +488,7 @@ mod tests { } #[test] - fn test_passed() { + fn passed() { let mut r = EvaluationResult::default(); assert!(!r.passed()); @@ -502,7 +502,7 @@ mod tests { } #[test] - fn test_dataset_aggregation() { + fn dataset_aggregation() { let ds = DatasetEvaluationResult { dataset_name: "test".to_string(), evaluated_at: Utc::now(), @@ -536,7 +536,7 @@ mod tests { } #[test] - fn test_result_to_value() { + fn result_to_value() { let r = EvaluationResult { evaluation_id: "eval-1".to_string(), operation_id: "op-1".to_string(), @@ -549,10 +549,8 @@ mod tests { assert_eq!(val["status"]["grade"], "B"); } - // ─── Default trait ────────────────────────────────────────────────────── - #[test] - fn test_default_creates_valid_result() { + fn default_creates_valid_result() { let r = EvaluationResult::default(); assert!(r.evaluation_id.is_empty()); assert!(r.operation_id.is_empty()); @@ -591,10 +589,8 @@ mod tests { assert!(r.error.is_none()); } - // ─── Serde round-trip ─────────────────────────────────────────────────── - #[test] - fn test_serde_roundtrip_default() { + fn serde_roundtrip_default() { let original = EvaluationResult::default(); let json = serde_json::to_string(&original).unwrap(); let deserialized: EvaluationResult = serde_json::from_str(&json).unwrap(); @@ -605,7 +601,7 @@ mod tests { } #[test] - fn test_serde_roundtrip_all_fields_populated() { + fn serde_roundtrip_all_fields_populated() { let original = EvaluationResult { evaluation_id: "eval-full".to_string(), operation_id: "op-full".to_string(), @@ -689,8 +685,7 @@ mod tests { } #[test] - fn test_serde_missing_optional_fields() { - // Minimal JSON with only required fields — optional/defaulted fields omitted + fn serde_missing_optional_fields() { let json = r#"{ "evaluation_id": "eval-min", "operation_id": "op-min", @@ -731,11 +726,8 @@ mod tests { assert!(r.error.is_none()); } - // ─── Grade boundary tests ─────────────────────────────────────────────── - #[test] - fn test_grade_boundaries() { - // Exact boundaries + fn grade_boundaries() { let at_90 = EvaluationResult { overall_score: 0.9, ..Default::default() @@ -797,10 +789,8 @@ mod tests { assert_eq!(perfect.grade(), "A"); } - // ─── passed() edge cases ──────────────────────────────────────────────── - #[test] - fn test_passed_boundary_exactly_half() { + fn passed_boundary_exactly_half() { let r = EvaluationResult { overall_score: 0.5, ioc_detection_rate: 0.5, @@ -811,7 +801,7 @@ mod tests { } #[test] - fn test_passed_fails_overall_below_threshold() { + fn passed_fails_overall_below_threshold() { let r = EvaluationResult { overall_score: 0.49, ioc_detection_rate: 0.8, @@ -822,7 +812,7 @@ mod tests { } #[test] - fn test_passed_fails_ioc_below_threshold() { + fn passed_fails_ioc_below_threshold() { let r = EvaluationResult { overall_score: 0.8, ioc_detection_rate: 0.49, @@ -832,10 +822,8 @@ mod tests { assert!(!r.passed()); } - // ─── investigation_status() via to_summary() ──────────────────────────── - #[test] - fn test_investigation_status_completed() { + fn investigation_status_completed() { let r = EvaluationResult { investigation_started: true, investigation_completed: true, @@ -846,7 +834,7 @@ mod tests { } #[test] - fn test_investigation_status_started() { + fn investigation_status_started() { let r = EvaluationResult { investigation_started: true, investigation_completed: false, @@ -857,7 +845,7 @@ mod tests { } #[test] - fn test_investigation_status_not_started() { + fn investigation_status_not_started() { let r = EvaluationResult { investigation_started: false, investigation_completed: false, @@ -867,10 +855,8 @@ mod tests { assert!(summary.contains("Investigation: Not Started")); } - // ─── to_value() structure ─────────────────────────────────────────────── - #[test] - fn test_to_value_contains_all_sections() { + fn to_value_contains_all_sections() { let r = EvaluationResult { evaluation_id: "eval-struct".to_string(), operation_id: "op-struct".to_string(), @@ -884,17 +870,6 @@ mod tests { }; let val = r.to_value(); - // Check all top-level sections exist - assert!(val.get("evaluation_id").is_some()); - assert!(val.get("operation_id").is_some()); - assert!(val.get("scores").is_some()); - assert!(val.get("gaps").is_some()); - assert!(val.get("stats").is_some()); - assert!(val.get("status").is_some()); - assert!(val.get("timing").is_some()); - assert!(val.get("cost").is_some()); - assert!(val.get("metadata").is_some()); - // Check nested values assert_eq!(val["status"]["alert_fired"], true); assert_eq!(val["status"]["passed"], false); // 0.7 overall but 0.0 ioc/tech @@ -904,7 +879,7 @@ mod tests { } #[test] - fn test_to_value_gaps_counts() { + fn to_value_gaps_counts() { let r = EvaluationResult { found_iocs: vec![ ExpectedIOC { @@ -939,10 +914,8 @@ mod tests { assert_eq!(val["gaps"]["missed_iocs"].as_array().unwrap().len(), 1); } - // ─── to_summary() formatting ──────────────────────────────────────────── - #[test] - fn test_to_summary_includes_timing_when_present() { + fn to_summary_includes_timing_when_present() { let r = EvaluationResult { duration_seconds: 120.0, time_to_first_evidence: Some(5.5), @@ -958,7 +931,7 @@ mod tests { } #[test] - fn test_to_summary_excludes_timing_when_absent() { + fn to_summary_excludes_timing_when_absent() { let r = EvaluationResult::default(); let summary = r.to_summary(); assert!(!summary.contains("Timing:")); @@ -966,7 +939,7 @@ mod tests { } #[test] - fn test_to_summary_includes_cost_when_tokens_present() { + fn to_summary_includes_cost_when_tokens_present() { let r = EvaluationResult { total_tokens: 5000, prompt_tokens: 3000, @@ -981,14 +954,14 @@ mod tests { } #[test] - fn test_to_summary_excludes_cost_when_no_tokens() { + fn to_summary_excludes_cost_when_no_tokens() { let r = EvaluationResult::default(); let summary = r.to_summary(); assert!(!summary.contains("Cost:")); } #[test] - fn test_to_summary_shows_missed_techniques() { + fn to_summary_shows_missed_techniques() { let r = EvaluationResult { missed_techniques: vec![ ExpectedTechnique { @@ -1013,7 +986,7 @@ mod tests { } #[test] - fn test_to_summary_truncates_missed_techniques_over_five() { + fn to_summary_truncates_missed_techniques_over_five() { let techniques: Vec = (0..8) .map(|i| ExpectedTechnique { technique_id: format!("T100{i}"), @@ -1031,7 +1004,7 @@ mod tests { } #[test] - fn test_to_summary_shows_error() { + fn to_summary_shows_error() { let r = EvaluationResult { error: Some("LLM rate limited".to_string()), ..Default::default() @@ -1040,10 +1013,8 @@ mod tests { assert!(summary.contains("Error: LLM rate limited")); } - // ─── DatasetEvaluationResult edge cases ───────────────────────────────── - #[test] - fn test_dataset_empty_results() { + fn dataset_empty_results() { let ds = DatasetEvaluationResult { dataset_name: "empty".to_string(), evaluated_at: Utc::now(), @@ -1062,7 +1033,7 @@ mod tests { } #[test] - fn test_dataset_all_passing() { + fn dataset_all_passing() { let ds = DatasetEvaluationResult { dataset_name: "all-pass".to_string(), evaluated_at: Utc::now(), @@ -1085,7 +1056,7 @@ mod tests { } #[test] - fn test_dataset_all_failing() { + fn dataset_all_failing() { let ds = DatasetEvaluationResult { dataset_name: "all-fail".to_string(), evaluated_at: Utc::now(), @@ -1108,7 +1079,7 @@ mod tests { } #[test] - fn test_dataset_to_value_structure() { + fn dataset_to_value_structure() { let ds = DatasetEvaluationResult { dataset_name: "test-ds".to_string(), evaluated_at: Utc::now(), @@ -1128,7 +1099,7 @@ mod tests { } #[test] - fn test_dataset_to_summary_grade_distribution() { + fn dataset_to_summary_grade_distribution() { let ds = DatasetEvaluationResult { dataset_name: "grade-dist".to_string(), evaluated_at: Utc::now(), @@ -1165,7 +1136,7 @@ mod tests { } #[test] - fn test_dataset_serde_roundtrip() { + fn dataset_serde_roundtrip() { let ds = DatasetEvaluationResult { dataset_name: "roundtrip".to_string(), evaluated_at: Utc::now(), @@ -1182,15 +1153,13 @@ mod tests { assert!((deserialized.results[0].overall_score - 0.75).abs() < f64::EPSILON); } - // ─── avg() helper ─────────────────────────────────────────────────────── - #[test] - fn test_avg_empty() { + fn avg_empty() { assert_eq!(avg(&[], |r| r.overall_score), 0.0); } #[test] - fn test_avg_single() { + fn avg_single() { let results = vec![EvaluationResult { overall_score: 0.8, ..Default::default() @@ -1199,7 +1168,7 @@ mod tests { } #[test] - fn test_avg_multiple() { + fn avg_multiple() { let results = vec![ EvaluationResult { overall_score: 0.6, @@ -1216,4 +1185,275 @@ mod tests { ]; assert!((avg(&results, |r| r.overall_score) - 0.8).abs() < f64::EPSILON); } + + #[test] + fn to_summary_with_missed_techniques() { + let r = EvaluationResult { + missed_techniques: vec![ + ExpectedTechnique { + technique_id: "T1003".into(), + technique_name: "Credential Dumping".into(), + required: true, + parent_id: None, + }, + ExpectedTechnique { + technique_id: "T1021".into(), + technique_name: "Remote Services".into(), + required: false, + parent_id: None, + }, + ], + ..Default::default() + }; + let summary = r.to_summary(); + assert!(summary.contains("Missed Techniques:")); + assert!(summary.contains("T1003")); + assert!(summary.contains("Credential Dumping")); + assert!(summary.contains("T1021")); + } + + #[test] + fn to_summary_truncates_over_five_missed() { + let techniques: Vec = (0..8) + .map(|i| ExpectedTechnique { + technique_id: format!("T100{i}"), + technique_name: format!("Tech {i}"), + required: true, + parent_id: None, + }) + .collect(); + let r = EvaluationResult { + missed_techniques: techniques, + ..Default::default() + }; + let summary = r.to_summary(); + assert!(summary.contains("... and 3 more")); + } + + #[test] + fn to_value_scores_all_fields() { + let r = EvaluationResult { + overall_score: 0.75, + detection_score: 0.8, + quality_score: 0.7, + completeness_score: 0.65, + stage_score: 0.5, + ioc_detection_rate: 0.6, + technique_coverage: 0.55, + pyramid_elevation_score: 0.4, + timeline_accuracy: 0.9, + evidence_quality_score: 0.85, + ..Default::default() + }; + let val = r.to_value(); + let scores = &val["scores"]; + assert_eq!(scores["overall"], 0.75); + assert_eq!(scores["detection"], 0.8); + assert_eq!(scores["quality"], 0.7); + assert_eq!(scores["completeness"], 0.65); + assert_eq!(scores["stage"], 0.5); + assert_eq!(scores["ioc_detection_rate"], 0.6); + assert_eq!(scores["technique_coverage"], 0.55); + assert_eq!(scores["pyramid_elevation"], 0.4); + assert_eq!(scores["timeline_accuracy"], 0.9); + assert_eq!(scores["evidence_quality"], 0.85); + } + + #[test] + fn to_value_timing_section() { + let r = EvaluationResult { + duration_seconds: 99.9, + time_to_first_evidence: Some(1.5), + time_to_technique_identification: None, + time_to_ttp_elevation: Some(50.0), + ..Default::default() + }; + let val = r.to_value(); + let timing = &val["timing"]; + assert_eq!(timing["duration_seconds"], 99.9); + assert_eq!(timing["time_to_first_evidence"], 1.5); + assert!(timing["time_to_technique_identification"].is_null()); + assert_eq!(timing["time_to_ttp_elevation"], 50.0); + } + + #[test] + fn to_value_cost_section() { + let r = EvaluationResult { + total_tokens: 10000, + prompt_tokens: 7000, + completion_tokens: 3000, + estimated_cost_usd: 0.123, + ..Default::default() + }; + let val = r.to_value(); + let cost = &val["cost"]; + assert_eq!(cost["total_tokens"], 10000); + assert_eq!(cost["prompt_tokens"], 7000); + assert_eq!(cost["completion_tokens"], 3000); + assert_eq!(cost["estimated_cost_usd"], 0.123); + } + + #[test] + fn dataset_to_summary_contains_sections() { + let ds = DatasetEvaluationResult { + dataset_name: "pentest-eval".to_string(), + evaluated_at: Utc::now(), + results: vec![ + EvaluationResult { + overall_score: 0.95, + alert_fired: true, + investigation_completed: true, + estimated_cost_usd: 0.10, + total_tokens: 8000, + duration_seconds: 60.0, + ..Default::default() + }, + EvaluationResult { + overall_score: 0.55, + alert_fired: false, + investigation_completed: false, + estimated_cost_usd: 0.05, + total_tokens: 4000, + duration_seconds: 30.0, + ..Default::default() + }, + ], + }; + let summary = ds.to_summary(); + assert!(summary.contains("pentest-eval")); + assert!(summary.contains("Scenarios: 2")); + assert!(summary.contains("Pass Rate:")); + assert!(summary.contains("Grade Distribution:")); + assert!(summary.contains("Total Cost:")); + } + + #[test] + fn dataset_total_tokens_sums() { + let ds = DatasetEvaluationResult { + dataset_name: "t".into(), + evaluated_at: Utc::now(), + results: vec![ + EvaluationResult { + total_tokens: 1000, + ..Default::default() + }, + EvaluationResult { + total_tokens: 2500, + ..Default::default() + }, + ], + }; + assert_eq!(ds.total_tokens(), 3500); + } + + #[test] + fn dataset_avg_duration() { + let ds = DatasetEvaluationResult { + dataset_name: "t".into(), + evaluated_at: Utc::now(), + results: vec![ + EvaluationResult { + duration_seconds: 10.0, + ..Default::default() + }, + EvaluationResult { + duration_seconds: 20.0, + ..Default::default() + }, + ], + }; + assert!((ds.avg_duration_seconds() - 15.0).abs() < f64::EPSILON); + } + + #[test] + fn to_summary_timing_with_all_fields() { + let r = EvaluationResult { + duration_seconds: 45.0, + time_to_first_evidence: Some(5.2), + time_to_technique_identification: Some(12.3), + time_to_ttp_elevation: Some(30.0), + ..Default::default() + }; + let summary = r.to_summary(); + assert!(summary.contains("45.0s")); + assert!(summary.contains("5.2s")); + assert!(summary.contains("12.3s")); + assert!(summary.contains("30.0s")); + } + + #[test] + fn to_summary_cost_section() { + let r = EvaluationResult { + total_tokens: 5000, + prompt_tokens: 3000, + completion_tokens: 2000, + estimated_cost_usd: 0.05, + ..Default::default() + }; + let summary = r.to_summary(); + assert!(summary.contains("5000")); + assert!(summary.contains("3000")); + assert!(summary.contains("2000")); + assert!(summary.contains("$0.0500")); + } + + #[test] + fn to_value_gaps_section() { + use crate::models::PyramidLevel; + let r = EvaluationResult { + missed_iocs: vec![ExpectedIOC { + ioc_type: "ip".into(), + value: "10.0.0.1".into(), + required: true, + pyramid_level: PyramidLevel::IpAddresses, + mitre_techniques: vec![], + source: String::new(), + }], + found_iocs: vec![ + ExpectedIOC { + ioc_type: "hash".into(), + value: "abc123".into(), + required: true, + pyramid_level: PyramidLevel::HashValues, + mitre_techniques: vec![], + source: String::new(), + }, + ExpectedIOC { + ioc_type: "domain".into(), + value: "evil.com".into(), + required: false, + pyramid_level: PyramidLevel::DomainNames, + mitre_techniques: vec![], + source: String::new(), + }, + ], + ..Default::default() + }; + let val = r.to_value(); + let gaps = &val["gaps"]; + assert_eq!(gaps["found_iocs_count"], 2); + assert_eq!(gaps["missed_iocs"].as_array().unwrap().len(), 1); + assert_eq!(gaps["missed_iocs"][0]["type"], "ip"); + assert_eq!(gaps["missed_iocs"][0]["value"], "10.0.0.1"); + } + + #[test] + fn to_value_status_section() { + let r = EvaluationResult { + overall_score: 0.85, + ioc_detection_rate: 0.7, + technique_coverage: 0.6, + alert_fired: true, + investigation_started: true, + investigation_completed: false, + ..Default::default() + }; + let val = r.to_value(); + let status = &val["status"]; + assert_eq!(status["passed"], true); + assert_eq!(status["grade"], "B"); + assert_eq!(status["alert_fired"], true); + assert_eq!(status["investigation_started"], true); + assert_eq!(status["investigation_completed"], false); + } } diff --git a/ares-core/src/eval/scorers/scoring.rs b/ares-core/src/eval/scorers/scoring.rs index b38d0b65..3960c6bb 100644 --- a/ares-core/src/eval/scorers/scoring.rs +++ b/ares-core/src/eval/scorers/scoring.rs @@ -361,3 +361,380 @@ pub fn score_investigation_overall( weighted_sum / total_weight } + +#[cfg(test)] +mod tests { + use super::*; + use approx::assert_abs_diff_eq; + use rstest::rstest; + use std::collections::HashSet; + + use crate::eval::ground_truth::{ + EvaluationGroundTruth, ExpectedIOC, ExpectedTechnique, ExpectedTimelineEvent, + }; + use crate::eval::scorers::types::{EvidenceItem, InvestigationSnapshot, TimelineEvent}; + use crate::models::PyramidLevel; + + // -- helpers -- + + fn empty_snap() -> InvestigationSnapshot { + InvestigationSnapshot::default() + } + + fn empty_gt() -> EvaluationGroundTruth { + EvaluationGroundTruth { + operation_id: "op-1".into(), + target_ip: "10.0.0.1".into(), + expected_iocs: vec![], + expected_techniques: vec![], + expected_timeline: vec![], + expected_shares: vec![], + expected_vulnerabilities: vec![], + min_pyramid_level: 4, + target_pyramid_level: 6, + min_technique_coverage: 0.6, + min_ioc_detection_rate: 0.5, + } + } + + fn make_ioc(ioc_type: &str, value: &str, required: bool) -> ExpectedIOC { + ExpectedIOC { + ioc_type: ioc_type.into(), + value: value.into(), + pyramid_level: PyramidLevel::IpAddresses, + mitre_techniques: vec![], + required, + source: String::new(), + } + } + + fn make_technique(id: &str, required: bool) -> ExpectedTechnique { + ExpectedTechnique { + technique_id: id.into(), + technique_name: String::new(), + required, + parent_id: None, + } + } + + fn make_evidence( + etype: &str, + value: &str, + pyramid: u32, + confidence: f64, + validated: bool, + ) -> EvidenceItem { + EvidenceItem { + evidence_type: etype.into(), + value: value.into(), + pyramid_level: pyramid, + confidence, + validated, + } + } + + #[rstest] + #[case(None, 0.0)] + #[case(Some("triage"), 0.25)] + #[case(Some("causation"), 0.50)] + #[case(Some("lateral"), 0.75)] + #[case(Some("synthesis"), 1.0)] + #[case(Some("unknown"), 0.0)] + fn stage_progress_scores(#[case] stage: Option<&str>, #[case] expected: f64) { + let mut snap = empty_snap(); + snap.stage = stage.map(String::from); + assert_abs_diff_eq!(score_stage_progress(&snap), expected, epsilon = 0.001); + } + + #[test] + fn ioc_detection_empty_gt_returns_one() { + let snap = empty_snap(); + let gt = empty_gt(); + assert_abs_diff_eq!(score_ioc_detection(&snap, >), 1.0, epsilon = 0.001); + } + + #[test] + fn ioc_detection_all_found() { + let mut snap = empty_snap(); + snap.evidence_values + .push(make_evidence("ip", "10.0.0.1", 1, 0.9, true)); + snap.evidence_values + .push(make_evidence("user", "admin", 2, 0.8, true)); + + let mut gt = empty_gt(); + gt.expected_iocs = vec![ + make_ioc("ip", "10.0.0.1", true), + make_ioc("user", "admin", false), + ]; + + assert_abs_diff_eq!(score_ioc_detection(&snap, >), 1.0, epsilon = 0.001); + } + + #[test] + fn ioc_detection_none_found() { + let snap = empty_snap(); + let mut gt = empty_gt(); + gt.expected_iocs = vec![ + make_ioc("ip", "10.0.0.1", true), + make_ioc("user", "admin", false), + ]; + + assert_abs_diff_eq!(score_ioc_detection(&snap, >), 0.0, epsilon = 0.001); + } + + #[test] + fn ioc_detection_partial_required_only() { + let mut snap = empty_snap(); + snap.evidence_values + .push(make_evidence("ip", "10.0.0.1", 1, 0.9, true)); + + let mut gt = empty_gt(); + gt.expected_iocs = vec![ + make_ioc("ip", "10.0.0.1", true), + make_ioc("ip", "192.168.1.1", true), + ]; + + // 1/2 required = 0.5, no optional => 1.0 + // 0.5*0.6 + 1.0*0.4 = 0.7 + assert_abs_diff_eq!(score_ioc_detection(&snap, >), 0.7, epsilon = 0.001); + } + + #[test] + fn ioc_matches_exact() { + let ioc = make_ioc("ip", "10.0.0.1", true); + let found: HashSet = ["10.0.0.1".into()].into_iter().collect(); + assert!(ioc_matches(&ioc, &found)); + } + + #[test] + fn ioc_matches_case_insensitive() { + let ioc = make_ioc("ip", "DC01.CORP.LOCAL", true); + let found: HashSet = ["dc01.corp.local".into()].into_iter().collect(); + assert!(ioc_matches(&ioc, &found)); + } + + #[test] + fn ioc_matches_hostname_partial() { + let ioc = make_ioc("hostname", "dc01.corp.local", true); + let found: HashSet = ["dc01".into()].into_iter().collect(); + assert!(ioc_matches(&ioc, &found)); + } + + #[test] + fn ioc_matches_user_backslash() { + let ioc = make_ioc("user", "CORP\\admin", true); + let found: HashSet = ["admin".into()].into_iter().collect(); + assert!(ioc_matches(&ioc, &found)); + } + + #[test] + fn ioc_matches_user_at_sign() { + let ioc = make_ioc("user", "admin@corp.local", true); + let found: HashSet = ["admin".into()].into_iter().collect(); + assert!(ioc_matches(&ioc, &found)); + } + + #[test] + fn ioc_no_match_unrelated() { + let ioc = make_ioc("ip", "10.0.0.1", true); + let found: HashSet = ["192.168.1.1".into()].into_iter().collect(); + assert!(!ioc_matches(&ioc, &found)); + } + + #[test] + fn build_found_values_includes_evidence_and_queries() { + let mut snap = empty_snap(); + snap.evidence_values + .push(make_evidence("ip", "10.0.0.1", 1, 0.9, true)); + snap.queried_hosts.insert("DC01".into()); + snap.queried_users.insert("Admin".into()); + + let found = build_found_values(&snap); + assert!(found.contains("10.0.0.1")); + assert!(found.contains("dc01")); + assert!(found.contains("admin")); + } + + #[test] + fn build_found_values_hostname_splits() { + let mut snap = empty_snap(); + snap.evidence_values + .push(make_evidence("hostname", "dc01.corp.local", 2, 0.8, true)); + let found = build_found_values(&snap); + assert!(found.contains("dc01.corp.local")); + assert!(found.contains("dc01")); + } + + #[test] + fn technique_coverage_empty_gt_returns_one() { + let snap = empty_snap(); + let gt = empty_gt(); + assert_abs_diff_eq!(score_technique_coverage(&snap, >), 1.0, epsilon = 0.001); + } + + #[test] + fn technique_coverage_all_found() { + let mut snap = empty_snap(); + snap.identified_techniques.insert("T1003".into()); + snap.identified_techniques.insert("T1046".into()); + + let mut gt = empty_gt(); + gt.expected_techniques = vec![ + make_technique("T1003", true), + make_technique("T1046", false), + ]; + + assert_abs_diff_eq!(score_technique_coverage(&snap, >), 1.0, epsilon = 0.001); + } + + #[test] + fn technique_coverage_none_found() { + let snap = empty_snap(); + let mut gt = empty_gt(); + gt.expected_techniques = vec![make_technique("T1003", true)]; + // 0 required found => required_rate=0, no optional => 1.0 + // 0.0*0.6 + 1.0*0.4 = 0.4 + assert_abs_diff_eq!(score_technique_coverage(&snap, >), 0.4, epsilon = 0.01); + } + + #[test] + fn pyramid_elevation_empty_evidence() { + let snap = empty_snap(); + assert_abs_diff_eq!(score_pyramid_elevation(&snap), 0.0, epsilon = 0.001); + } + + #[test] + fn pyramid_elevation_max_level() { + let mut snap = empty_snap(); + snap.highest_pyramid_level = 6; + snap.evidence_values + .push(make_evidence("ttp", "T1003", 6, 0.9, true)); + assert_abs_diff_eq!(score_pyramid_elevation(&snap), 1.0, epsilon = 0.001); + } + + #[test] + fn pyramid_elevation_mixed_levels() { + let mut snap = empty_snap(); + snap.highest_pyramid_level = 5; + snap.evidence_values + .push(make_evidence("ip", "10.0.0.1", 1, 0.9, true)); + snap.evidence_values + .push(make_evidence("tool", "mimikatz", 5, 0.9, true)); + // highest_score = 5/6 ≈ 0.833 + // high_ratio = 1/2 = 0.5 + // 0.833*0.7 + 0.5*0.3 ≈ 0.733 + assert_abs_diff_eq!(score_pyramid_elevation(&snap), 0.733, epsilon = 0.01); + } + + #[test] + fn evidence_quality_empty() { + let snap = empty_snap(); + assert_abs_diff_eq!(score_evidence_quality(&snap), 0.0, epsilon = 0.001); + } + + #[test] + fn evidence_quality_perfect() { + let mut snap = empty_snap(); + snap.evidence_values + .push(make_evidence("ttp", "T1003", 6, 1.0, true)); + assert_abs_diff_eq!(score_evidence_quality(&snap), 1.0, epsilon = 0.001); + } + + #[test] + fn evidence_quality_mixed() { + let mut snap = empty_snap(); + snap.evidence_values + .push(make_evidence("ip", "10.0.0.1", 1, 0.8, true)); + snap.evidence_values + .push(make_evidence("ip", "10.0.0.2", 2, 0.6, false)); + // avg_conf=0.7, validation=0.5, ttp_ratio=0.0 + // 0.7*0.4 + 0.5*0.3 + 0.0*0.3 = 0.43 + assert_abs_diff_eq!(score_evidence_quality(&snap), 0.43, epsilon = 0.01); + } + + #[test] + fn timeline_accuracy_empty_gt_returns_one() { + let snap = empty_snap(); + let gt = empty_gt(); + assert_abs_diff_eq!(score_timeline_accuracy(&snap, >), 1.0, epsilon = 0.001); + } + + #[test] + fn timeline_accuracy_empty_snap_returns_zero() { + let snap = empty_snap(); + let mut gt = empty_gt(); + gt.expected_timeline = vec![ExpectedTimelineEvent { + description_pattern: "credential dump".into(), + mitre_techniques: vec![], + timestamp_range: None, + required: true, + }]; + assert_abs_diff_eq!(score_timeline_accuracy(&snap, >), 0.0, epsilon = 0.001); + } + + #[test] + fn timeline_accuracy_matching_event() { + let mut snap = empty_snap(); + snap.timeline.push(TimelineEvent { + description: "credential dump via secretsdump".into(), + mitre_techniques: HashSet::new(), + }); + + let mut gt = empty_gt(); + gt.expected_timeline = vec![ExpectedTimelineEvent { + description_pattern: "credential dump".into(), + mitre_techniques: vec![], + timestamp_range: None, + required: true, + }]; + + assert_abs_diff_eq!(score_timeline_accuracy(&snap, >), 1.0, epsilon = 0.001); + } + + #[test] + fn timeline_event_matches_substring() { + let descs = vec!["credential dump via secretsdump".into()]; + assert!(timeline_event_matches("credential dump", &descs)); + } + + #[test] + fn timeline_event_matches_no_match() { + let descs = vec!["port scan completed".into()]; + assert!(!timeline_event_matches("credential dump", &descs)); + } + + #[test] + fn timeline_event_matches_regex() { + let descs = vec!["lateral movement to dc01".into()]; + assert!(timeline_event_matches("lateral.*dc\\d+", &descs)); + } + + #[test] + fn technique_matches_exact() { + let t = make_technique("T1003", true); + let found: HashSet = ["T1003".into()].into_iter().collect(); + assert!(technique_matches(&t, &found)); + } + + #[test] + fn technique_matches_parent_to_sub() { + let t = make_technique("T1003", true); + let found: HashSet = ["T1003.001".into()].into_iter().collect(); + assert!(technique_matches(&t, &found)); + } + + #[test] + fn technique_no_match() { + let t = make_technique("T1003", true); + let found: HashSet = ["T1046".into()].into_iter().collect(); + assert!(!technique_matches(&t, &found)); + } + + #[test] + fn overall_score_empty_is_bounded() { + let snap = empty_snap(); + let gt = empty_gt(); + let score = score_investigation_overall(&snap, >); + assert!((0.0..=1.0).contains(&score)); + } +} diff --git a/ares-core/src/eval/scorers/tests.rs b/ares-core/src/eval/scorers/tests.rs index 0d7e8dc0..6181126a 100644 --- a/ares-core/src/eval/scorers/tests.rs +++ b/ares-core/src/eval/scorers/tests.rs @@ -102,7 +102,7 @@ fn make_snapshot() -> InvestigationSnapshot { } #[test] -fn test_stage_progress() { +fn stage_progress() { let mut snap = InvestigationSnapshot::default(); assert_eq!(score_stage_progress(&snap), 0.0); @@ -114,7 +114,7 @@ fn test_stage_progress() { } #[test] -fn test_ioc_detection_all_found() { +fn ioc_detection_all_found() { let snap = make_snapshot(); let mut gt = make_gt(); // Remove hash IOC since snapshot doesn't have it @@ -128,7 +128,7 @@ fn test_ioc_detection_all_found() { } #[test] -fn test_ioc_detection_none_found() { +fn ioc_detection_none_found() { let snap = InvestigationSnapshot::default(); let gt = make_gt(); let score = score_ioc_detection(&snap, >); @@ -136,7 +136,7 @@ fn test_ioc_detection_none_found() { } #[test] -fn test_ioc_user_domain_prefix() { +fn ioc_user_domain_prefix() { let snap = InvestigationSnapshot { evidence_values: vec![EvidenceItem { evidence_type: "user".to_string(), @@ -162,7 +162,7 @@ fn test_ioc_user_domain_prefix() { } #[test] -fn test_technique_coverage_all() { +fn technique_coverage_all() { let snap = make_snapshot(); let gt = make_gt(); let score = score_technique_coverage(&snap, >); @@ -173,7 +173,7 @@ fn test_technique_coverage_all() { } #[test] -fn test_technique_coverage_partial() { +fn technique_coverage_partial() { let mut snap = make_snapshot(); snap.identified_techniques = HashSet::from(["T1003".to_string()]); let gt = make_gt(); @@ -187,7 +187,7 @@ fn test_technique_coverage_partial() { } #[test] -fn test_pyramid_elevation() { +fn pyramid_elevation() { let snap = make_snapshot(); let score = score_pyramid_elevation(&snap); // highest_level=6/6 * 0.7 = 0.7 @@ -197,7 +197,7 @@ fn test_pyramid_elevation() { } #[test] -fn test_evidence_quality() { +fn evidence_quality() { let snap = make_snapshot(); let score = score_evidence_quality(&snap); // avg_confidence = (0.9+0.8+0.7)/3 = 0.8 * 0.4 = 0.32 @@ -208,7 +208,7 @@ fn test_evidence_quality() { } #[test] -fn test_overall_score() { +fn overall_score() { let snap = make_snapshot(); let gt = make_gt(); let score = score_investigation_overall(&snap, >); @@ -216,14 +216,14 @@ fn test_overall_score() { } #[test] -fn test_timeline_event_matches_substring() { +fn timeline_event_matches_substring() { let descriptions = vec!["credential dumping via lsass access".to_string()]; assert!(timeline_event_matches("lsass access", &descriptions)); assert!(!timeline_event_matches("rdp brute force", &descriptions)); } #[test] -fn test_timeline_event_matches_keyword() { +fn timeline_event_matches_keyword() { let descriptions = vec!["detected credential dumping using mimikatz tool".to_string()]; assert!(timeline_event_matches( "credential dumping mimikatz", @@ -232,7 +232,7 @@ fn test_timeline_event_matches_keyword() { } #[test] -fn test_evaluate_builds_result() { +fn evaluate_builds_result() { let snap = make_snapshot(); let gt = make_gt(); let result = evaluate("eval-1", &snap, >, true, "claude-opus-4-6", 120.0); @@ -251,7 +251,7 @@ fn test_evaluate_builds_result() { } #[test] -fn test_missed_and_found_iocs() { +fn missed_and_found_iocs() { let snap = make_snapshot(); let gt = make_gt(); let missed = get_missed_iocs(&snap, >); @@ -264,7 +264,7 @@ fn test_missed_and_found_iocs() { } #[test] -fn test_missed_and_found_techniques() { +fn missed_and_found_techniques() { let snap = make_snapshot(); let gt = make_gt(); let missed = get_missed_techniques(&snap, >); diff --git a/ares-core/src/eval/workflow/costs.rs b/ares-core/src/eval/workflow/costs.rs index 3c643753..e63e12b0 100644 --- a/ares-core/src/eval/workflow/costs.rs +++ b/ares-core/src/eval/workflow/costs.rs @@ -55,3 +55,52 @@ pub fn estimate_cost(model: &str, prompt_tokens: u64, completion_tokens: u64) -> + completion_tokens as f64 * costs.output_per_million) / 1_000_000.0 } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn estimate_cost_known_model_claude_sonnet() { + let cost = estimate_cost("claude-sonnet-4-20250514", 1_000_000, 0); + assert!((cost - 3.0).abs() < 1e-9); + } + + #[test] + fn estimate_cost_known_model_output_tokens() { + let cost = estimate_cost("claude-sonnet-4-20250514", 0, 1_000_000); + assert!((cost - 15.0).abs() < 1e-9); + } + + #[test] + fn estimate_cost_known_model_mixed() { + let cost = estimate_cost("gpt-4o", 500_000, 200_000); + let expected = (500_000.0 * 2.5 + 200_000.0 * 10.0) / 1_000_000.0; + assert!((cost - expected).abs() < 1e-9); + } + + #[test] + fn estimate_cost_unknown_model_uses_default() { + let cost = estimate_cost("unknown-model-xyz", 1_000_000, 0); + assert!((cost - 5.0).abs() < 1e-9); + } + + #[test] + fn estimate_cost_zero_tokens() { + let cost = estimate_cost("gpt-4o", 0, 0); + assert_eq!(cost, 0.0); + } + + #[test] + fn estimate_cost_claude_opus() { + let cost = estimate_cost("claude-opus-4-20250514", 1_000_000, 1_000_000); + assert!((cost - 90.0).abs() < 1e-9); + } + + #[test] + fn estimate_cost_gpt4_turbo() { + let cost = estimate_cost("gpt-4-turbo", 100_000, 50_000); + let expected = (100_000.0 * 10.0 + 50_000.0 * 30.0) / 1_000_000.0; + assert!((cost - expected).abs() < 1e-9); + } +} diff --git a/ares-core/src/eval/workflow/tests.rs b/ares-core/src/eval/workflow/tests.rs index 9d071ebf..42ed62b5 100644 --- a/ares-core/src/eval/workflow/tests.rs +++ b/ares-core/src/eval/workflow/tests.rs @@ -37,7 +37,7 @@ fn sample_state_json() -> &'static str { } #[test] -fn test_load_red_state_from_file() { +fn loads_red_state_from_file() { let dir = TempDir::new().unwrap(); let path = write_state_file(dir.path(), "state.json", sample_state_json()); @@ -52,7 +52,7 @@ fn test_load_red_state_from_file() { } #[test] -fn test_evaluate_scenario_from_file() { +fn evaluate_scenario_from_file() { let dir = TempDir::new().unwrap(); let path = write_state_file(dir.path(), "state.json", sample_state_json()); @@ -73,7 +73,7 @@ fn test_evaluate_scenario_from_file() { } #[test] -fn test_dataset_from_directory() { +fn dataset_from_directory() { let dir = TempDir::new().unwrap(); write_state_file(dir.path(), "op1.json", sample_state_json()); write_state_file( @@ -90,7 +90,7 @@ fn test_dataset_from_directory() { } #[test] -fn test_evaluate_dataset() { +fn evaluates_dataset() { let dir = TempDir::new().unwrap(); write_state_file(dir.path(), "op1.json", sample_state_json()); @@ -103,7 +103,7 @@ fn test_evaluate_dataset() { } #[test] -fn test_estimate_cost() { +fn estimates_cost() { let cost = estimate_cost("claude-sonnet-4-20250514", 1_000_000, 500_000); // 1M * 3.0/1M + 500K * 15.0/1M = 3.0 + 7.5 = 10.5 assert!((cost - 10.5).abs() < 0.01); @@ -114,7 +114,7 @@ fn test_estimate_cost() { } #[test] -fn test_save_evaluation_result() { +fn saves_evaluation_result() { let dir = TempDir::new().unwrap(); let result = EvaluationResult { evaluation_id: "eval-1".to_string(), diff --git a/ares-core/src/models/blue.rs b/ares-core/src/models/blue.rs index 06708b86..4058dc50 100644 --- a/ares-core/src/models/blue.rs +++ b/ares-core/src/models/blue.rs @@ -249,7 +249,7 @@ mod tests { // ─── PyramidLevel ──────────────────────────────────────────────────── #[test] - fn test_pyramid_level_display() { + fn pyramid_level_display() { assert_eq!(PyramidLevel::HashValues.to_string(), "hash_values"); assert_eq!(PyramidLevel::IpAddresses.to_string(), "ip_addresses"); assert_eq!(PyramidLevel::DomainNames.to_string(), "domain_names"); @@ -262,7 +262,7 @@ mod tests { } #[test] - fn test_pyramid_level_values() { + fn pyramid_level_values() { assert_eq!(PyramidLevel::HashValues as i32, 1); assert_eq!(PyramidLevel::Ttps as i32, 6); } @@ -270,7 +270,7 @@ mod tests { // ─── InvestigationStage ────────────────────────────────────────────── #[test] - fn test_investigation_stage_display() { + fn investigation_stage_display() { assert_eq!(InvestigationStage::Triage.to_string(), "triage"); assert_eq!(InvestigationStage::Causation.to_string(), "causation"); assert_eq!(InvestigationStage::Lateral.to_string(), "lateral"); @@ -278,7 +278,7 @@ mod tests { } #[test] - fn test_investigation_stage_serde() { + fn investigation_stage_serde() { let stage = InvestigationStage::Causation; let json_str = serde_json::to_string(&stage).unwrap(); assert_eq!(json_str, r#""causation""#); @@ -289,7 +289,7 @@ mod tests { // ─── TriageDecision ────────────────────────────────────────────────── #[test] - fn test_triage_decision_display() { + fn triage_decision_display() { assert_eq!(TriageDecision::Pending.to_string(), "pending"); assert_eq!(TriageDecision::Confirmed.to_string(), "confirmed"); assert_eq!(TriageDecision::Downgraded.to_string(), "downgraded"); @@ -298,7 +298,7 @@ mod tests { } #[test] - fn test_triage_decision_serde() { + fn triage_decision_serde() { let d = TriageDecision::Confirmed; let json_str = serde_json::to_string(&d).unwrap(); assert_eq!(json_str, r#""confirmed""#); @@ -309,7 +309,7 @@ mod tests { // ─── Evidence serde ────────────────────────────────────────────────── #[test] - fn test_evidence_deserialize_minimal() { + fn evidence_deserialize_minimal() { let j = json!({ "id": "ev-001", "type": "ip", @@ -326,7 +326,7 @@ mod tests { } #[test] - fn test_evidence_type_rename() { + fn evidence_type_rename() { let j = json!({ "id": "ev-002", "type": "technique", @@ -346,7 +346,7 @@ mod tests { // ─── BlueTaskInfo serde ────────────────────────────────────────────── #[test] - fn test_blue_task_info_defaults() { + fn blue_task_info_defaults() { let j = json!({ "task_id": "bt-001", "task_type": "query_logs" @@ -362,7 +362,7 @@ mod tests { // ─── SharedBlueTeamState::new ──────────────────────────────────────── #[test] - fn test_shared_blue_team_state_new() { + fn shared_blue_team_state_new() { let state = SharedBlueTeamState::new("inv-001".to_string()); assert_eq!(state.investigation_id, "inv-001"); assert_eq!(state.stage, "triage"); @@ -377,7 +377,7 @@ mod tests { // ─── TriageRecord serde ────────────────────────────────────────────── #[test] - fn test_triage_record_deserialize() { + fn triage_record_deserialize() { let j = json!({ "decision": "confirmed", "reasoning": "Multiple IOCs match known attack pattern", diff --git a/ares-core/src/models/core.rs b/ares-core/src/models/core.rs index 06c930f7..1001d7c2 100644 --- a/ares-core/src/models/core.rs +++ b/ares-core/src/models/core.rs @@ -153,31 +153,31 @@ mod tests { } #[test] - fn test_detect_dc_by_kerberos_service() { + fn detect_dc_by_kerberos_service() { let host = make_host("srv01", vec!["88/tcp (kerberos-sec)"], vec![]); assert!(host.detect_dc()); } #[test] - fn test_detect_dc_by_ldap_service() { + fn detect_dc_by_ldap_service() { let host = make_host("srv01", vec!["389/tcp (ldap)"], vec![]); assert!(host.detect_dc()); } #[test] - fn test_detect_dc_by_hostname_prefix() { + fn detect_dc_by_hostname_prefix() { let host = make_host("dc01.contoso.local", vec![], vec![]); assert!(host.detect_dc()); } #[test] - fn test_detect_dc_by_role() { + fn detect_dc_by_role() { let host = make_host("srv01", vec![], vec!["domain controller"]); assert!(host.detect_dc()); } #[test] - fn test_detect_dc_not_dc() { + fn detect_dc_not_dc() { let host = make_host( "srv01.contoso.local", vec!["445/tcp (microsoft-ds)"], @@ -187,31 +187,31 @@ mod tests { } #[test] - fn test_detect_dc_empty() { + fn detect_dc_empty() { let host = make_host("", vec![], vec![]); assert!(!host.detect_dc()); } #[test] - fn test_detect_dc_case_insensitive() { + fn detect_dc_case_insensitive() { let host = make_host("DC01.CONTOSO.LOCAL", vec![], vec![]); assert!(host.detect_dc()); } #[test] - fn test_detect_dc_by_kerberos_service_name() { + fn detect_dc_by_kerberos_service_name() { let host = make_host("server", vec!["kerberos"], vec![]); assert!(host.detect_dc()); } #[test] - fn test_detect_dc_by_ldap_service_name() { + fn detect_dc_by_ldap_service_name() { let host = make_host("server", vec!["ldap"], vec![]); assert!(host.detect_dc()); } #[test] - fn test_trust_info_is_parent_child() { + fn trust_info_is_parent_child() { let t = TrustInfo { domain: "child.corp.local".to_string(), flat_name: "CHILD".to_string(), @@ -224,7 +224,7 @@ mod tests { } #[test] - fn test_trust_info_is_cross_forest() { + fn trust_info_is_cross_forest() { let t = TrustInfo { domain: "fabrikam.local".to_string(), flat_name: "FABRIKAM".to_string(), @@ -237,7 +237,7 @@ mod tests { } #[test] - fn test_trust_info_external_is_cross_forest() { + fn trust_info_external_is_cross_forest() { let t = TrustInfo { domain: "other.local".to_string(), flat_name: "OTHER".to_string(), @@ -249,7 +249,7 @@ mod tests { } #[test] - fn test_trust_info_unknown_type_not_cross_forest() { + fn trust_info_unknown_type_not_cross_forest() { let t = TrustInfo { domain: "x.local".to_string(), flat_name: String::new(), @@ -260,6 +260,214 @@ mod tests { assert!(!t.is_cross_forest()); assert!(!t.is_parent_child()); } + + #[test] + fn host_serde_roundtrip() { + let host = Host { + ip: "10.0.0.1".to_string(), + hostname: "web01".to_string(), + os: "Windows Server 2019".to_string(), + roles: vec!["web".to_string()], + services: vec!["80/tcp".to_string(), "443/tcp".to_string()], + is_dc: false, + owned: true, + }; + let json = serde_json::to_string(&host).unwrap(); + let deser: Host = serde_json::from_str(&json).unwrap(); + assert_eq!(host, deser); + } + + #[test] + fn host_serde_defaults() { + let json = r#"{"ip":"10.0.0.1"}"#; + let host: Host = serde_json::from_str(json).unwrap(); + assert_eq!(host.ip, "10.0.0.1"); + assert!(host.hostname.is_empty()); + assert!(host.os.is_empty()); + assert!(host.roles.is_empty()); + assert!(host.services.is_empty()); + assert!(!host.is_dc); + assert!(!host.owned); + } + + #[test] + fn credential_serde_roundtrip() { + let cred = Credential { + id: "test-id".to_string(), + username: "admin".to_string(), + password: "P@ssw0rd".to_string(), + domain: "CORP".to_string(), + source: "secretsdump".to_string(), + discovered_at: None, + is_admin: true, + parent_id: Some("parent-1".to_string()), + attack_step: 2, + }; + let json = serde_json::to_string(&cred).unwrap(); + let deser: Credential = serde_json::from_str(&json).unwrap(); + assert_eq!(cred, deser); + } + + #[test] + fn hash_serde_defaults() { + let json = r#"{"username":"admin","hash_value":"aad3b435"}"#; + let hash: Hash = serde_json::from_str(json).unwrap(); + assert_eq!(hash.username, "admin"); + assert_eq!(hash.hash_value, "aad3b435"); + assert_eq!(hash.hash_type, "NTLM"); + assert!(hash.domain.is_empty()); + assert!(hash.cracked_password.is_none()); + assert!(hash.aes_key.is_none()); + assert_eq!(hash.attack_step, 0); + } + + #[test] + fn hash_serde_with_aes_key() { + let hash = Hash { + id: "h1".to_string(), + username: "krbtgt".to_string(), + hash_value: "abc123".to_string(), + hash_type: "NTLM".to_string(), + domain: "CORP".to_string(), + cracked_password: None, + source: "dcsync".to_string(), + discovered_at: None, + parent_id: None, + attack_step: 1, + aes_key: Some("aes256key".to_string()), + }; + let json = serde_json::to_string(&hash).unwrap(); + assert!(json.contains("aes256key")); + let deser: Hash = serde_json::from_str(&json).unwrap(); + assert_eq!(hash, deser); + } + + #[test] + fn share_serde_roundtrip() { + let share = Share { + host: "10.0.0.5".to_string(), + name: "ADMIN$".to_string(), + permissions: "READ".to_string(), + comment: "Remote Admin".to_string(), + }; + let json = serde_json::to_string(&share).unwrap(); + let deser: Share = serde_json::from_str(&json).unwrap(); + assert_eq!(share, deser); + } + + #[test] + fn share_serde_defaults() { + let json = r#"{"host":"10.0.0.5","name":"C$"}"#; + let share: Share = serde_json::from_str(json).unwrap(); + assert_eq!(share.host, "10.0.0.5"); + assert_eq!(share.name, "C$"); + assert!(share.permissions.is_empty()); + assert!(share.comment.is_empty()); + } + + #[test] + fn user_serde_roundtrip() { + let user = User { + username: "jdoe".to_string(), + domain: "CORP".to_string(), + description: "John Doe".to_string(), + is_admin: true, + source: "ldap".to_string(), + }; + let json = serde_json::to_string(&user).unwrap(); + let deser: User = serde_json::from_str(&json).unwrap(); + assert_eq!(user, deser); + } + + #[test] + fn user_serde_defaults() { + let json = r#"{"username":"guest"}"#; + let user: User = serde_json::from_str(json).unwrap(); + assert_eq!(user.username, "guest"); + assert!(user.domain.is_empty()); + assert!(user.description.is_empty()); + assert!(!user.is_admin); + assert!(user.source.is_empty()); + } + + #[test] + fn target_serde_roundtrip() { + let target = Target { + ip: "192.168.1.1".to_string(), + hostname: "dc01".to_string(), + domain: "corp.local".to_string(), + environment: "prod".to_string(), + }; + let json = serde_json::to_string(&target).unwrap(); + let deser: Target = serde_json::from_str(&json).unwrap(); + assert_eq!(target, deser); + } + + #[test] + fn target_serde_skip_empty() { + let target = Target { + ip: "10.0.0.1".to_string(), + hostname: String::new(), + domain: String::new(), + environment: String::new(), + }; + let json = serde_json::to_string(&target).unwrap(); + assert!(!json.contains("hostname")); + assert!(!json.contains("domain")); + assert!(!json.contains("environment")); + } + + #[test] + fn trust_info_serde_roundtrip() { + let trust = TrustInfo { + domain: "child.corp.local".to_string(), + flat_name: "CHILD".to_string(), + direction: "bidirectional".to_string(), + trust_type: "parent_child".to_string(), + sid_filtering: true, + }; + let json = serde_json::to_string(&trust).unwrap(); + let deser: TrustInfo = serde_json::from_str(&json).unwrap(); + assert_eq!(trust, deser); + } + + #[test] + fn detect_dc_by_multiple_services() { + let host = make_host( + "srv01", + vec!["88/tcp (kerberos)", "389/tcp (ldap)", "445/tcp"], + vec![], + ); + assert!(host.detect_dc()); + } + + #[test] + fn detect_dc_non_dc_services_only() { + let host = make_host( + "fileserver", + vec!["445/tcp (microsoft-ds)", "139/tcp (netbios-ssn)"], + vec!["file server"], + ); + assert!(!host.detect_dc()); + } + + #[test] + fn host_skip_empty_fields_in_json() { + let host = Host { + ip: "10.0.0.1".to_string(), + hostname: String::new(), + os: String::new(), + roles: vec![], + services: vec![], + is_dc: false, + owned: false, + }; + let json = serde_json::to_string(&host).unwrap(); + assert!(!json.contains("hostname")); + assert!(!json.contains("os")); + assert!(!json.contains("roles")); + assert!(!json.contains("services")); + } } /// Trust relationship metadata for an AD domain trust. diff --git a/ares-core/src/models/mod.rs b/ares-core/src/models/mod.rs index a42605cd..ce1d432e 100644 --- a/ares-core/src/models/mod.rs +++ b/ares-core/src/models/mod.rs @@ -28,7 +28,7 @@ mod tests { use std::collections::HashMap; #[test] - fn test_credential_roundtrip() { + fn credential_roundtrip() { // Match the exact compact JSON format used by Python state_backend let json = r#"{"id":"abc","username":"testuser","password":"P@ssw0rd!","domain":"contoso.local","source":"manual-inject","parent_id":null,"attack_step":0}"#; // pragma: allowlist secret let cred: Credential = serde_json::from_str(json).unwrap(); @@ -40,7 +40,7 @@ mod tests { } #[test] - fn test_hash_roundtrip() { + fn hash_roundtrip() { let json = r#"{"id":"def","username":"krbtgt","hash_type":"NTLM","hash_value":"aad3b435b51404ee","domain":"contoso.local","source":"secretsdump","cracked_password":null,"discovered_at":"2025-01-28T12:00:00Z","parent_id":null,"attack_step":0}"#; // pragma: allowlist secret let h: Hash = serde_json::from_str(json).unwrap(); assert_eq!(h.username, "krbtgt"); @@ -49,7 +49,7 @@ mod tests { } #[test] - fn test_host_roundtrip() { + fn host_roundtrip() { let json = r#"{"ip":"192.168.58.10","hostname":"dc01.contoso.local","os":"Windows Server 2019","roles":["Domain Controller"],"services":["88/tcp kerberos","389/tcp ldap"],"is_dc":true}"#; let host: Host = serde_json::from_str(json).unwrap(); assert_eq!(host.ip, "192.168.58.10"); @@ -58,7 +58,7 @@ mod tests { } #[test] - fn test_user_roundtrip() { + fn user_roundtrip() { let json = r#"{"username":"testuser","domain":"contoso.local","source":"netexec_smb"}"#; let user: User = serde_json::from_str(json).unwrap(); assert_eq!(user.username, "testuser"); @@ -66,14 +66,14 @@ mod tests { } #[test] - fn test_share_roundtrip() { + fn share_roundtrip() { let json = r#"{"host":"192.168.58.10","name":"SYSVOL","permissions":"READ","comment":""}"#; let share: Share = serde_json::from_str(json).unwrap(); assert_eq!(share.name, "SYSVOL"); } #[test] - fn test_vulnerability_roundtrip() { + fn vulnerability_roundtrip() { let json = r#"{"vuln_id":"esc1_192.168.58.10_svc","vuln_type":"ADCS_ESC1","target":"192.168.58.10","discovered_by":"recon","discovered_at":"2025-01-28T12:00:00Z","details":{"target_ip":"192.168.58.10"},"recommended_agent":"privesc","priority":1}"#; let vuln: VulnerabilityInfo = serde_json::from_str(json).unwrap(); assert_eq!(vuln.vuln_type, "ADCS_ESC1"); @@ -81,7 +81,7 @@ mod tests { } #[test] - fn test_operation_meta_from_hash() { + fn operation_meta_from_hash() { let mut data = HashMap::new(); data.insert("has_domain_admin".to_string(), "True".to_string()); data.insert("has_golden_ticket".to_string(), "false".to_string()); @@ -102,7 +102,7 @@ mod tests { } #[test] - fn test_operation_meta_json_encoded() { + fn operation_meta_json_encoded() { // Python stores meta values via json.dumps(), so booleans become "true"/"false", // strings become "\"value\"", and arrays become "[\"a\",\"b\"]". let mut data = HashMap::new(); @@ -139,7 +139,7 @@ mod tests { } #[test] - fn test_meta_null_and_empty() { + fn meta_null_and_empty() { let mut data = HashMap::new(); data.insert("target_domain".to_string(), "null".to_string()); data.insert("target_ip".to_string(), "\"\"".to_string()); @@ -152,7 +152,7 @@ mod tests { } #[test] - fn test_task_status_display() { + fn task_status_display() { assert_eq!(TaskStatus::InProgress.to_string(), "in_progress"); assert_eq!(TaskStatus::Pending.to_string(), "pending"); } diff --git a/ares-core/src/models/operation.rs b/ares-core/src/models/operation.rs index 7fccc3ce..2f337b0e 100644 --- a/ares-core/src/models/operation.rs +++ b/ares-core/src/models/operation.rs @@ -149,17 +149,15 @@ fn parse_meta_string_list(raw: &str) -> Vec { mod tests { use super::*; - // ─── parse_meta_bool ───────────────────────────────────────────────────── - #[test] - fn test_parse_meta_bool_true_variants() { + fn parse_meta_bool_true_variants() { assert!(parse_meta_bool("true")); assert!(parse_meta_bool("True")); assert!(parse_meta_bool("1")); } #[test] - fn test_parse_meta_bool_false_variants() { + fn parse_meta_bool_false_variants() { assert!(!parse_meta_bool("false")); assert!(!parse_meta_bool("False")); assert!(!parse_meta_bool("0")); @@ -168,10 +166,8 @@ mod tests { assert!(!parse_meta_bool("random")); } - // ─── parse_meta_string ─────────────────────────────────────────────────── - #[test] - fn test_parse_meta_string_json_quoted() { + fn parse_meta_string_json_quoted() { assert_eq!( parse_meta_string(r#""contoso.local""#), Some("contoso.local".to_string()) @@ -179,7 +175,7 @@ mod tests { } #[test] - fn test_parse_meta_string_raw() { + fn parse_meta_string_raw() { assert_eq!( parse_meta_string("contoso.local"), Some("contoso.local".to_string()) @@ -187,93 +183,89 @@ mod tests { } #[test] - fn test_parse_meta_string_null() { + fn parse_meta_string_null() { assert_eq!(parse_meta_string("null"), None); } #[test] - fn test_parse_meta_string_empty() { + fn parse_meta_string_empty() { assert_eq!(parse_meta_string(""), None); } #[test] - fn test_parse_meta_string_json_empty() { + fn parse_meta_string_json_empty() { assert_eq!(parse_meta_string(r#""""#), None); } #[test] - fn test_parse_meta_string_with_spaces() { + fn parse_meta_string_with_spaces() { assert_eq!( parse_meta_string(r#""admin -> DA via secretsdump""#), Some("admin -> DA via secretsdump".to_string()) ); } - // ─── parse_meta_datetime ───────────────────────────────────────────────── - #[test] - fn test_parse_meta_datetime_rfc3339() { + fn parse_meta_datetime_rfc3339() { assert!(parse_meta_datetime("2025-01-28T12:00:00+00:00").is_some()); } #[test] - fn test_parse_meta_datetime_json_quoted() { + fn parse_meta_datetime_json_quoted() { assert!(parse_meta_datetime(r#""2025-01-28T12:00:00+00:00""#).is_some()); } #[test] - fn test_parse_meta_datetime_null() { + fn parse_meta_datetime_null() { assert!(parse_meta_datetime("null").is_none()); } #[test] - fn test_parse_meta_datetime_empty() { + fn parse_meta_datetime_empty() { assert!(parse_meta_datetime("").is_none()); } #[test] - fn test_parse_meta_datetime_invalid() { + fn parse_meta_datetime_invalid() { assert!(parse_meta_datetime("not-a-date").is_none()); } #[test] - fn test_parse_meta_datetime_utc_z() { + fn parse_meta_datetime_utc_z() { assert!(parse_meta_datetime("2025-01-28T12:00:00Z").is_some()); } - // ─── parse_meta_string_list ────────────────────────────────────────────── - #[test] - fn test_parse_meta_string_list_json_array() { + fn parse_meta_string_list_json_array() { let list = parse_meta_string_list(r#"["192.168.58.10","192.168.58.20"]"#); assert_eq!(list, vec!["192.168.58.10", "192.168.58.20"]); } #[test] - fn test_parse_meta_string_list_comma_separated() { + fn parse_meta_string_list_comma_separated() { let list = parse_meta_string_list("192.168.58.10,192.168.58.20"); assert_eq!(list, vec!["192.168.58.10", "192.168.58.20"]); } #[test] - fn test_parse_meta_string_list_json_encoded_comma() { + fn parse_meta_string_list_json_encoded_comma() { let list = parse_meta_string_list(r#""192.168.58.10,192.168.58.20""#); assert_eq!(list, vec!["192.168.58.10", "192.168.58.20"]); } #[test] - fn test_parse_meta_string_list_single() { + fn parse_meta_string_list_single() { let list = parse_meta_string_list("192.168.58.10"); assert_eq!(list, vec!["192.168.58.10"]); } #[test] - fn test_parse_meta_string_list_empty() { + fn parse_meta_string_list_empty() { assert!(parse_meta_string_list("").is_empty()); } #[test] - fn test_parse_meta_string_list_with_spaces() { + fn parse_meta_string_list_with_spaces() { let list = parse_meta_string_list("192.168.58.10, 192.168.58.20 , 192.168.58.30"); assert_eq!( list, @@ -282,15 +274,13 @@ mod tests { } #[test] - fn test_parse_meta_string_list_filters_empty() { + fn parse_meta_string_list_filters_empty() { let list = parse_meta_string_list(r#"["192.168.58.10","","192.168.58.20"]"#); assert_eq!(list, vec!["192.168.58.10", "192.168.58.20"]); } - // ─── OperationMeta::from_redis_hash ────────────────────────────────────── - #[test] - fn test_operation_meta_empty_hash() { + fn operation_meta_empty_hash() { let data = HashMap::new(); let meta = OperationMeta::from_redis_hash(&data); assert!(!meta.has_domain_admin); @@ -304,7 +294,7 @@ mod tests { } #[test] - fn test_operation_meta_full() { + fn operation_meta_full() { let mut data = HashMap::new(); data.insert("has_domain_admin".to_string(), "true".to_string()); data.insert("has_golden_ticket".to_string(), "true".to_string()); @@ -345,7 +335,7 @@ mod tests { } #[test] - fn test_operation_meta_completed_at_bare() { + fn operation_meta_completed_at_bare() { let mut data = HashMap::new(); data.insert( "completed_at".to_string(), @@ -356,32 +346,28 @@ mod tests { } #[test] - fn test_operation_meta_default_derives() { + fn operation_meta_default_derives() { let meta = OperationMeta::default(); assert!(!meta.has_domain_admin); assert!(!meta.has_golden_ticket); assert!(meta.target_ips.is_empty()); } - // ─── parse_meta_bool edge cases ───────────────────────────────────────── - #[test] - fn test_parse_meta_bool_whitespace() { + fn parse_meta_bool_whitespace() { assert!(!parse_meta_bool(" true")); assert!(!parse_meta_bool("true ")); } #[test] - fn test_parse_meta_bool_json_encoded_true() { + fn parse_meta_bool_json_encoded_true() { // Python json.dumps(True) = "true", json.dumps(False) = "false" assert!(parse_meta_bool("true")); assert!(!parse_meta_bool("false")); } - // ─── parse_meta_string edge cases ─────────────────────────────────────── - #[test] - fn test_parse_meta_string_ip_address() { + fn parse_meta_string_ip_address() { assert_eq!( parse_meta_string(r#""192.168.58.10""#), Some("192.168.58.10".to_string()) @@ -389,7 +375,7 @@ mod tests { } #[test] - fn test_parse_meta_string_raw_ip() { + fn parse_meta_string_raw_ip() { assert_eq!( parse_meta_string("192.168.58.10"), Some("192.168.58.10".to_string()) @@ -397,104 +383,98 @@ mod tests { } #[test] - fn test_parse_meta_string_json_number_falls_through() { + fn parse_meta_string_json_number_falls_through() { // A JSON number shouldn't parse as a JSON string assert_eq!(parse_meta_string("42"), Some("42".to_string())); } #[test] - fn test_parse_meta_string_json_boolean_falls_through() { + fn parse_meta_string_json_boolean_falls_through() { assert_eq!(parse_meta_string("true"), Some("true".to_string())); assert_eq!(parse_meta_string("false"), Some("false".to_string())); } #[test] - fn test_parse_meta_string_nested_quotes() { + fn parse_meta_string_nested_quotes() { // Double-encoded string (rare but possible) let result = parse_meta_string(r#""contoso.local\\admin""#); assert_eq!(result, Some(r"contoso.local\admin".to_string())); } #[test] - fn test_parse_meta_string_unicode() { + fn parse_meta_string_unicode() { assert_eq!( parse_meta_string(r#""dc01\u002econtoso.local""#), Some("dc01.contoso.local".to_string()) ); } - // ─── parse_meta_datetime edge cases ───────────────────────────────────── - #[test] - fn test_parse_meta_datetime_with_offset() { + fn parse_meta_datetime_with_offset() { let result = parse_meta_datetime("2025-06-15T08:30:00+05:30"); assert!(result.is_some()); } #[test] - fn test_parse_meta_datetime_negative_offset() { + fn parse_meta_datetime_negative_offset() { let result = parse_meta_datetime("2025-06-15T08:30:00-07:00"); assert!(result.is_some()); } #[test] - fn test_parse_meta_datetime_json_null_string() { + fn parse_meta_datetime_json_null_string() { assert!(parse_meta_datetime(r#""null""#).is_none()); } #[test] - fn test_parse_meta_datetime_json_empty_string() { + fn parse_meta_datetime_json_empty_string() { assert!(parse_meta_datetime(r#""""#).is_none()); } #[test] - fn test_parse_meta_datetime_partial_date() { + fn parse_meta_datetime_partial_date() { assert!(parse_meta_datetime("2025-06-15").is_none()); } - // ─── parse_meta_string_list edge cases ────────────────────────────────── - #[test] - fn test_parse_meta_string_list_json_array_single() { + fn parse_meta_string_list_json_array_single() { let list = parse_meta_string_list(r#"["192.168.58.10"]"#); assert_eq!(list, vec!["192.168.58.10"]); } #[test] - fn test_parse_meta_string_list_json_array_empty() { + fn parse_meta_string_list_json_array_empty() { let list = parse_meta_string_list("[]"); assert!(list.is_empty()); } #[test] - fn test_parse_meta_string_list_trailing_comma() { + fn parse_meta_string_list_trailing_comma() { let list = parse_meta_string_list("192.168.58.10,192.168.58.20,"); assert_eq!(list, vec!["192.168.58.10", "192.168.58.20"]); } #[test] - fn test_parse_meta_string_list_leading_comma() { + fn parse_meta_string_list_leading_comma() { let list = parse_meta_string_list(",192.168.58.10"); assert_eq!(list, vec!["192.168.58.10"]); } #[test] - fn test_parse_meta_string_list_all_empty_entries() { + fn parse_meta_string_list_all_empty_entries() { let list = parse_meta_string_list(",,,"); assert!(list.is_empty()); } #[test] - fn test_parse_meta_string_list_json_array_with_numbers() { + fn parse_meta_string_list_json_array_with_numbers() { // Non-string JSON array elements are filtered out let list = parse_meta_string_list(r#"[1, 2, 3]"#); assert!(list.is_empty()); } - // ─── OperationMeta::from_redis_hash edge cases ────────────────────────── - #[test] - fn test_operation_meta_legacy_bool_values() { + fn operation_meta_legacy_bool_values() { let mut data = HashMap::new(); data.insert("has_domain_admin".to_string(), "True".to_string()); data.insert("has_golden_ticket".to_string(), "1".to_string()); @@ -504,7 +484,7 @@ mod tests { } #[test] - fn test_operation_meta_false_bool_values() { + fn operation_meta_false_bool_values() { let mut data = HashMap::new(); data.insert("has_domain_admin".to_string(), "false".to_string()); data.insert("has_golden_ticket".to_string(), "0".to_string()); @@ -514,7 +494,7 @@ mod tests { } #[test] - fn test_operation_meta_target_ips_comma_separated() { + fn operation_meta_target_ips_comma_separated() { let mut data = HashMap::new(); data.insert( "target_ips".to_string(), @@ -527,7 +507,7 @@ mod tests { } #[test] - fn test_operation_meta_target_ips_json_encoded_comma() { + fn operation_meta_target_ips_json_encoded_comma() { let mut data = HashMap::new(); data.insert( "target_ips".to_string(), @@ -538,7 +518,7 @@ mod tests { } #[test] - fn test_operation_meta_null_domain_admin_path() { + fn operation_meta_null_domain_admin_path() { let mut data = HashMap::new(); data.insert("domain_admin_path".to_string(), "null".to_string()); let meta = OperationMeta::from_redis_hash(&data); @@ -546,7 +526,7 @@ mod tests { } #[test] - fn test_operation_meta_invalid_datetime() { + fn operation_meta_invalid_datetime() { let mut data = HashMap::new(); data.insert("started_at".to_string(), "not-a-date".to_string()); let meta = OperationMeta::from_redis_hash(&data); @@ -554,7 +534,7 @@ mod tests { } #[test] - fn test_operation_meta_extra_unknown_fields_ignored() { + fn operation_meta_extra_unknown_fields_ignored() { let mut data = HashMap::new(); data.insert("unknown_field".to_string(), "some_value".to_string()); data.insert("has_domain_admin".to_string(), "true".to_string()); @@ -563,7 +543,7 @@ mod tests { } #[test] - fn test_operation_meta_empty_target_ips() { + fn operation_meta_empty_target_ips() { let mut data = HashMap::new(); data.insert("target_ips".to_string(), "".to_string()); let meta = OperationMeta::from_redis_hash(&data); @@ -571,17 +551,15 @@ mod tests { } #[test] - fn test_operation_meta_empty_json_array_target_ips() { + fn operation_meta_empty_json_array_target_ips() { let mut data = HashMap::new(); data.insert("target_ips".to_string(), "[]".to_string()); let meta = OperationMeta::from_redis_hash(&data); assert!(meta.target_ips.is_empty()); } - // ─── SharedRedTeamState ───────────────────────────────────────────────── - #[test] - fn test_shared_state_new() { + fn shared_state_new() { let state = SharedRedTeamState::new("op-test-001".to_string()); assert_eq!(state.operation_id, "op-test-001"); assert!(state.target.is_none()); @@ -604,17 +582,15 @@ mod tests { assert!(state.all_techniques.is_empty()); } - // ─── build_attack_chain ───────────────────────────────────────────────── - #[test] - fn test_build_attack_chain_empty_state() { + fn build_attack_chain_empty_state() { let state = SharedRedTeamState::new("op-chain-empty".to_string()); let chain = state.build_attack_chain("nonexistent-id"); assert!(chain.is_empty()); } #[test] - fn test_build_attack_chain_single_credential() { + fn build_attack_chain_single_credential() { let mut state = SharedRedTeamState::new("op-chain-single".to_string()); state.all_credentials.push(Credential { id: "cred-1".to_string(), @@ -636,7 +612,7 @@ mod tests { } #[test] - fn test_build_attack_chain_multi_step() { + fn build_attack_chain_multi_step() { let mut state = SharedRedTeamState::new("op-chain-multi".to_string()); state.all_credentials.push(Credential { id: "cred-1".to_string(), @@ -672,7 +648,7 @@ mod tests { } #[test] - fn test_build_attack_chain_cycle_guard() { + fn build_attack_chain_cycle_guard() { let mut state = SharedRedTeamState::new("op-cycle".to_string()); // Create a cycle: cred-1 -> cred-2 -> cred-1 state.all_credentials.push(Credential { @@ -702,17 +678,15 @@ mod tests { assert!(chain.len() <= 2); } - // ─── build_domain_admin_chain ─────────────────────────────────────────── - #[test] - fn test_build_domain_admin_chain_no_krbtgt() { + fn build_domain_admin_chain_no_krbtgt() { let state = SharedRedTeamState::new("op-no-krbtgt".to_string()); let chain = state.build_domain_admin_chain(); assert!(chain.is_empty()); } #[test] - fn test_build_domain_admin_chain_with_krbtgt() { + fn build_domain_admin_chain_with_krbtgt() { let mut state = SharedRedTeamState::new("op-da".to_string()); state.all_credentials.push(Credential { id: "cred-init".to_string(), @@ -745,7 +719,7 @@ mod tests { } #[test] - fn test_build_domain_admin_chain_case_insensitive_krbtgt() { + fn build_domain_admin_chain_case_insensitive_krbtgt() { let mut state = SharedRedTeamState::new("op-da-case".to_string()); state.all_hashes.push(Hash { id: "hash-krbtgt".to_string(), @@ -766,7 +740,7 @@ mod tests { } #[test] - fn test_build_domain_admin_chain_ignores_non_ntlm_krbtgt() { + fn build_domain_admin_chain_ignores_non_ntlm_krbtgt() { let mut state = SharedRedTeamState::new("op-da-aes".to_string()); state.all_hashes.push(Hash { id: "hash-aes".to_string(), @@ -785,16 +759,14 @@ mod tests { assert!(chain.is_empty()); } - // ─── format_attack_chain ──────────────────────────────────────────────── - #[test] - fn test_format_attack_chain_empty() { + fn format_attack_chain_empty() { let result = SharedRedTeamState::format_attack_chain(&[]); assert!(result.is_empty()); } #[test] - fn test_format_attack_chain_single_credential() { + fn format_attack_chain_single_credential() { let chain = vec![AttackChainStep { step_number: 1, item_type: "credential".to_string(), @@ -810,7 +782,7 @@ mod tests { } #[test] - fn test_format_attack_chain_credential_then_hash() { + fn format_attack_chain_credential_then_hash() { let chain = vec![ AttackChainStep { step_number: 1, @@ -844,7 +816,7 @@ mod tests { } #[test] - fn test_format_attack_chain_no_source() { + fn format_attack_chain_no_source() { let chain = vec![AttackChainStep { step_number: 1, item_type: "credential".to_string(), diff --git a/ares-core/src/models/task.rs b/ares-core/src/models/task.rs index bfb4123e..e9bdf533 100644 --- a/ares-core/src/models/task.rs +++ b/ares-core/src/models/task.rs @@ -157,10 +157,8 @@ mod tests { use super::*; use serde_json::json; - // ─── AgentRole Display ─────────────────────────────────────────────────── - #[test] - fn test_agent_role_display() { + fn agent_role_display() { assert_eq!(AgentRole::Orchestrator.to_string(), "orchestrator"); assert_eq!(AgentRole::Recon.to_string(), "recon"); assert_eq!(AgentRole::CredentialAccess.to_string(), "credential_access"); @@ -171,10 +169,8 @@ mod tests { assert_eq!(AgentRole::Coercion.to_string(), "coercion"); } - // ─── AgentRole serde ───────────────────────────────────────────────────── - #[test] - fn test_agent_role_serde_roundtrip() { + fn agent_role_serde_roundtrip() { let role = AgentRole::CredentialAccess; let json = serde_json::to_string(&role).unwrap(); assert_eq!(json, r#""credential_access""#); @@ -183,7 +179,7 @@ mod tests { } #[test] - fn test_agent_role_deserialize_all() { + fn agent_role_deserialize_all() { for (s, expected) in [ (r#""orchestrator""#, AgentRole::Orchestrator), (r#""recon""#, AgentRole::Recon), @@ -199,10 +195,8 @@ mod tests { } } - // ─── TaskStatus Display ────────────────────────────────────────────────── - #[test] - fn test_task_status_display_all() { + fn task_status_display_all() { assert_eq!(TaskStatus::Pending.to_string(), "pending"); assert_eq!(TaskStatus::InProgress.to_string(), "in_progress"); assert_eq!(TaskStatus::Completed.to_string(), "completed"); @@ -211,10 +205,8 @@ mod tests { assert_eq!(TaskStatus::Retrying.to_string(), "retrying"); } - // ─── TaskStatus serde ──────────────────────────────────────────────────── - #[test] - fn test_task_status_serde_roundtrip() { + fn task_status_serde_roundtrip() { let status = TaskStatus::InProgress; let json = serde_json::to_string(&status).unwrap(); assert_eq!(json, r#""in_progress""#); @@ -222,10 +214,8 @@ mod tests { assert_eq!(back, TaskStatus::InProgress); } - // ─── TaskInfo serde ────────────────────────────────────────────────────── - #[test] - fn test_task_info_deserialize_minimal() { + fn task_info_deserialize_minimal() { let json = json!({ "task_id": "t-001", "task_type": "recon", @@ -243,7 +233,7 @@ mod tests { } #[test] - fn test_task_info_with_status() { + fn task_info_with_status() { let json = json!({ "task_id": "t-002", "task_type": "crack", @@ -261,7 +251,7 @@ mod tests { } #[test] - fn test_task_info_serialization_skips_none() { + fn task_info_serialization_skips_none() { let json = json!({ "task_id": "t-003", "task_type": "lateral", @@ -276,10 +266,8 @@ mod tests { assert!(serialized.get("error").is_none()); } - // ─── TaskResult serde ──────────────────────────────────────────────────── - #[test] - fn test_task_result_deserialize() { + fn task_result_deserialize() { let json = json!({ "task_id": "t-010", "success": true, @@ -293,7 +281,7 @@ mod tests { } #[test] - fn test_task_result_failure() { + fn task_result_failure() { let json = json!({ "task_id": "t-011", "success": false, @@ -305,10 +293,8 @@ mod tests { assert!(result.result.is_none()); } - // ─── VulnerabilityInfo serde ───────────────────────────────────────────── - #[test] - fn test_vulnerability_info_defaults() { + fn vulnerability_info_defaults() { let json = json!({ "vuln_id": "esc1_192.168.58.10", "vuln_type": "ADCS_ESC1", @@ -323,7 +309,7 @@ mod tests { } #[test] - fn test_vulnerability_info_with_details() { + fn vulnerability_info_with_details() { let json = json!({ "vuln_id": "deleg_svc_sql", "vuln_type": "constrained_delegation", @@ -339,10 +325,8 @@ mod tests { assert_eq!(vuln.details.len(), 2); } - // ─── AgentInfo serde ───────────────────────────────────────────────────── - #[test] - fn test_agent_info_deserialize() { + fn agent_info_deserialize() { let json = json!({ "name": "recon-1", "pod_name": "ares-recon-abc123", @@ -357,7 +341,7 @@ mod tests { } #[test] - fn test_agent_info_with_capabilities() { + fn agent_info_with_capabilities() { let json = json!({ "name": "privesc-1", "pod_name": "ares-privesc-def456", @@ -397,3 +381,88 @@ pub struct TaskStatusRecord { #[serde(default, skip_serializing_if = "Option::is_none")] pub payload: Option, } + +#[cfg(test)] +mod task_status_record_tests { + use super::*; + use serde_json::json; + + #[test] + fn task_status_record_minimal() { + let json = json!({ + "operation_id": "op-001", + "status": "running" + }); + let rec: TaskStatusRecord = serde_json::from_value(json).unwrap(); + assert_eq!(rec.operation_id, "op-001"); + assert_eq!(rec.status, "running"); + assert!(rec.started_at.is_none()); + assert!(rec.ended_at.is_none()); + assert!(rec.pod_name.is_none()); + assert!(rec.role.is_none()); + assert!(rec.task_type.is_none()); + assert!(rec.error.is_none()); + assert!(rec.payload.is_none()); + } + + #[test] + fn task_status_record_full() { + let json = json!({ + "operation_id": "op-002", + "status": "completed", + "started_at": "2025-01-01T00:00:00Z", + "ended_at": "2025-01-01T01:00:00Z", + "pod_name": "ares-recon-xyz", + "role": "recon", + "task_type": "network_scan", + "error": null, + "payload": {"targets": ["192.168.1.0/24"]} + }); + let rec: TaskStatusRecord = serde_json::from_value(json).unwrap(); + assert_eq!(rec.operation_id, "op-002"); + assert_eq!(rec.status, "completed"); + assert_eq!(rec.started_at.as_deref(), Some("2025-01-01T00:00:00Z")); + assert_eq!(rec.ended_at.as_deref(), Some("2025-01-01T01:00:00Z")); + assert_eq!(rec.pod_name.as_deref(), Some("ares-recon-xyz")); + assert_eq!(rec.role.as_deref(), Some("recon")); + assert_eq!(rec.task_type.as_deref(), Some("network_scan")); + assert!(rec.error.is_none()); + assert!(rec.payload.is_some()); + } + + #[test] + fn task_status_record_with_error() { + let json = json!({ + "operation_id": "op-003", + "status": "failed", + "error": "connection timeout" + }); + let rec: TaskStatusRecord = serde_json::from_value(json).unwrap(); + assert_eq!(rec.status, "failed"); + assert_eq!(rec.error.as_deref(), Some("connection timeout")); + } + + #[test] + fn task_status_record_roundtrip() { + let rec = TaskStatusRecord { + operation_id: "op-rt".to_string(), + status: "pending".to_string(), + started_at: Some("2025-06-01T12:00:00Z".to_string()), + ended_at: None, + pod_name: Some("pod-1".to_string()), + role: Some("lateral".to_string()), + task_type: Some("smb_relay".to_string()), + error: None, + payload: Some(json!({"key": "value"})), + }; + let serialized = serde_json::to_value(&rec).unwrap(); + let deserialized: TaskStatusRecord = serde_json::from_value(serialized).unwrap(); + assert_eq!(deserialized.operation_id, "op-rt"); + assert_eq!(deserialized.status, "pending"); + assert!(deserialized.ended_at.is_none()); + assert!(deserialized.error.is_none()); + let json_str = serde_json::to_string(&rec).unwrap(); + assert!(!json_str.contains("ended_at")); + assert!(!json_str.contains("\"error\"")); + } +} diff --git a/ares-core/src/models/util.rs b/ares-core/src/models/util.rs index dbc32b92..667d727c 100644 --- a/ares-core/src/models/util.rs +++ b/ares-core/src/models/util.rs @@ -44,60 +44,40 @@ mod tests { use super::*; #[test] - fn test_new_uuid_format() { + fn new_uuid_format() { let uuid = new_uuid(); - assert_eq!(uuid.len(), 36); // standard UUID format: 8-4-4-4-12 + assert_eq!(uuid.len(), 36); assert_eq!(uuid.chars().filter(|c| *c == '-').count(), 4); } #[test] - fn test_new_uuid_unique() { + fn new_uuid_unique() { let u1 = new_uuid(); let u2 = new_uuid(); assert_ne!(u1, u2); } #[test] - fn test_default_hash_type() { - assert_eq!(default_hash_type(), "NTLM"); + fn new_uuid_is_valid_v4() { + let id = new_uuid(); + let parsed = uuid::Uuid::parse_str(&id).unwrap(); + assert_eq!(parsed.get_version_num(), 4); } #[test] - fn test_default_task_status() { - let status = default_task_status(); - assert_eq!(status.to_string(), "pending"); - } - - #[test] - fn test_default_max_retries() { + fn defaults() { + assert_eq!(default_hash_type(), "NTLM"); + assert_eq!(default_task_status().to_string(), "pending"); assert_eq!(default_max_retries(), 3); - } - - #[test] - fn test_default_priority() { assert_eq!(default_priority(), 5); - } - - #[test] - fn test_default_agent_status() { assert_eq!(default_agent_status(), "idle"); } #[cfg(feature = "blue")] #[test] - fn test_default_confidence() { + fn blue_defaults() { assert!((default_confidence() - 0.5).abs() < f64::EPSILON); - } - - #[cfg(feature = "blue")] - #[test] - fn test_default_timeline_source() { assert_eq!(default_timeline_source(), "investigation"); - } - - #[cfg(feature = "blue")] - #[test] - fn test_default_blue_task_status() { assert_eq!(default_blue_task_status(), "pending"); } } diff --git a/ares-core/src/parsing/delegation.rs b/ares-core/src/parsing/delegation.rs index beb52a70..436ed28b 100644 --- a/ares-core/src/parsing/delegation.rs +++ b/ares-core/src/parsing/delegation.rs @@ -131,7 +131,7 @@ mod tests { use super::*; #[test] - fn test_extract_delegations() { + fn extract_delegations_basic() { let output = r#"Impacket v0.12.0 - Copyright Fortra, LLC and its affiliated companies AccountName AccountType DelegationType DelegationRightsTo @@ -160,7 +160,7 @@ DC01$ Computer Unconstrained N/A } #[test] - fn test_extract_delegations_rbcd() { + fn extract_delegations_rbcd() { let output = r#"AccountName AccountType DelegationType DelegationRightsTo ----------- ----------- --------------- ------------------ WEB01$ Computer Resource-Based Constrained Delegation SRV01$ @@ -172,13 +172,13 @@ WEB01$ Computer Resource-Based Constrained Delegation SRV01$ } #[test] - fn test_extract_delegations_empty() { + fn extract_delegations_empty() { assert!(extract_delegations("").is_empty()); assert!(extract_delegations("No entries found.\n").is_empty()); } #[test] - fn test_delegations_with_preamble() { + fn delegations_with_preamble() { let output = r#"Impacket v0.12.0 - Copyright 2023 Fortra AccountName AccountType DelegationType DelegationRightsTo @@ -194,4 +194,41 @@ web_svc Person Unconstrained N/A ); assert_eq!(delegations[0].target_spn, None); } + + #[test] + fn extract_delegations_unknown_type_skipped() { + let output = r#"AccountName AccountType DelegationType DelegationRightsTo +----------- ----------- --------------- ------------------ +svc_x Person SomethingElse cifs/dc01.contoso.local +"#; + let delegations = extract_delegations(output); + assert!(delegations.is_empty()); + } + + #[test] + fn extract_delegations_short_line_fallback() { + let output = r#"AccountName AccountType DelegationType DelegationRightsTo +----------- ----------- -------------- ------------------ +svc short Constrained target +"#; + let delegations = extract_delegations(output); + assert_eq!(delegations.len(), 1); + assert_eq!(delegations[0].account, "svc"); + } + + #[test] + fn extract_delegations_no_header() { + let output = "svc_sql Person Constrained cifs/dc01.contoso.local\n"; + let delegations = extract_delegations(output); + assert!(delegations.is_empty()); + } + + #[test] + fn extract_delegations_only_separator() { + let output = r#"AccountName AccountType DelegationType DelegationRightsTo +------ ------ ------ ------ +"#; + let delegations = extract_delegations(output); + assert!(delegations.is_empty()); + } } diff --git a/ares-core/src/parsing/domain_sid.rs b/ares-core/src/parsing/domain_sid.rs index 7a9fdcc5..025a5955 100644 --- a/ares-core/src/parsing/domain_sid.rs +++ b/ares-core/src/parsing/domain_sid.rs @@ -32,7 +32,7 @@ mod tests { use super::*; #[test] - fn test_extract_domain_sid() { + fn extracts_domain_sid() { let output = "[*] Domain SID is: S-1-5-21-1328384573-4090356449-2552632942\n[*] Done.\n"; let sid = extract_domain_sid(output); assert_eq!( @@ -42,20 +42,20 @@ mod tests { } #[test] - fn test_extract_domain_sid_embedded() { + fn extract_domain_sid_embedded() { let output = "some prefix S-1-5-21-111-222-333 suffix\n"; let sid = extract_domain_sid(output); assert_eq!(sid, Some("S-1-5-21-111-222-333".to_string())); } #[test] - fn test_extract_domain_sid_none() { + fn extract_domain_sid_none() { assert_eq!(extract_domain_sid("no SID here"), None); assert_eq!(extract_domain_sid(""), None); } #[test] - fn test_extract_domain_sid_first_match() { + fn extract_domain_sid_first_match() { let output = "SID1: S-1-5-21-100-200-300\nSID2: S-1-5-21-400-500-600\n"; let sid = extract_domain_sid(output); assert_eq!(sid, Some("S-1-5-21-100-200-300".to_string())); @@ -64,7 +64,7 @@ mod tests { // --- extract_rid500_name --- #[test] - fn test_extract_rid500_name_standard() { + fn extract_rid500_name_standard() { let output = "[*] Domain SID is: S-1-5-21-1328384573-4090356449-2552632942\n\ 500: CONTOSO\\Administrator (SidTypeUser)\n\ 501: CONTOSO\\Guest (SidTypeUser)\n\ @@ -76,7 +76,7 @@ mod tests { } #[test] - fn test_extract_rid500_name_renamed() { + fn extract_rid500_name_renamed() { let output = "[*] Domain SID is: S-1-5-21-111-222-333\n\ 500: CONTOSO\\DomainAdmin01 (SidTypeUser)\n\ 501: CONTOSO\\Guest (SidTypeUser)\n"; @@ -87,7 +87,7 @@ mod tests { } #[test] - fn test_extract_rid500_name_no_match() { + fn extract_rid500_name_no_match() { assert_eq!(extract_rid500_name("no RID here"), None); assert_eq!(extract_rid500_name(""), None); // RID 501, not 500 @@ -98,7 +98,7 @@ mod tests { } #[test] - fn test_extract_rid500_name_wrong_sid_type() { + fn extract_rid500_name_wrong_sid_type() { // SidTypeGroup should not match — only SidTypeUser assert_eq!( extract_rid500_name("500: DOMAIN\\DomainAdmins (SidTypeGroup)"), diff --git a/ares-core/src/parsing/hosts.rs b/ares-core/src/parsing/hosts.rs index 4dd25b27..75415ec4 100644 --- a/ares-core/src/parsing/hosts.rs +++ b/ares-core/src/parsing/hosts.rs @@ -83,7 +83,7 @@ mod tests { use super::*; #[test] - fn test_extract_hosts_banner() { + fn extract_hosts_banner() { let output = "SMB 192.168.58.10 445 DC01 [*] Windows Server 2019 Standard (name:DC01) (domain:contoso.local) (signing:True)\nSMB 192.168.58.11 445 SRV01 [*] Windows Server 2019 Standard (name:SRV01) (domain:contoso.local)\n"; let hosts = extract_hosts(output); assert_eq!(hosts.len(), 2); @@ -99,7 +99,7 @@ mod tests { } #[test] - fn test_extract_hosts_simple() { + fn extract_hosts_simple() { let output = "SMB 192.168.58.1 445 HOST01 some other data\n"; let hosts = extract_hosts(output); assert_eq!(hosts.len(), 1); @@ -108,13 +108,13 @@ mod tests { } #[test] - fn test_extract_hosts_empty() { + fn extract_hosts_empty() { assert!(extract_hosts("").is_empty()); assert!(extract_hosts("no smb output here\n").is_empty()); } #[test] - fn test_hosts_with_signing_info() { + fn hosts_with_signing_info() { let output = "SMB 192.168.58.10 445 DC01 [*] Windows Server 2022 (name:DC01) (domain:contoso.local) (signing:True) (SMBv1:False)\n"; let hosts = extract_hosts(output); assert_eq!(hosts.len(), 1); @@ -122,4 +122,42 @@ mod tests { assert_eq!(hosts[0].os, "Windows Server 2022"); assert_eq!(hosts[0].domain, "contoso.local"); } + + #[test] + fn extract_hosts_skips_blank_lines() { + let output = "\n\n\n"; + assert!(extract_hosts(output).is_empty()); + } + + #[test] + fn extract_hosts_multiple_same_ip() { + let output = "SMB 192.168.58.10 445 SRV01 [*] Windows 10 (name:SRV01) (domain:contoso.local) (signing:True)\nSMB 192.168.58.10 445 SRV01 [*] Windows 10 (name:SRV01) (domain:contoso.local) (signing:True)\n"; + let hosts = extract_hosts(output); + assert_eq!(hosts.len(), 2); + assert_eq!(hosts[0].ip, "192.168.58.10"); + } + + #[test] + fn extract_hosts_no_domain_field() { + let output = "SMB 192.168.58.20 445 STANDALONE [*] Windows 10 (name:STANDALONE)\n"; + let hosts = extract_hosts(output); + assert_eq!(hosts.len(), 1); + assert_eq!(hosts[0].hostname, "STANDALONE"); + assert_eq!(hosts[0].domain, ""); + } + + #[test] + fn extract_hosts_mixed_banner_and_simple() { + let output = "\ +SMB 192.168.58.10 445 DC01 [*] Windows Server 2019 (name:DC01) (domain:contoso.local)\n\ +SMB 192.168.58.11 445 SRV01 [*] Windows Server 2016 (name:SRV01) (domain:contoso.local)\n\ +SMB 192.168.58.12 445 WS01 some other data\n"; + let hosts = extract_hosts(output); + assert_eq!(hosts.len(), 3); + assert_eq!(hosts[0].ip, "192.168.58.10"); + assert_eq!(hosts[1].ip, "192.168.58.11"); + assert_eq!(hosts[2].ip, "192.168.58.12"); + assert_eq!(hosts[2].hostname, "WS01"); + assert_eq!(hosts[2].domain, ""); + } } diff --git a/ares-core/src/parsing/kerberos.rs b/ares-core/src/parsing/kerberos.rs index 45fbb72f..fada89a1 100644 --- a/ares-core/src/parsing/kerberos.rs +++ b/ares-core/src/parsing/kerberos.rs @@ -54,28 +54,50 @@ mod tests { use super::*; #[test] - fn test_extract_kerberos_tgs() { + fn extract_tgs_valid_line() { let output = "$krb5tgs$23$*svc_sql$CONTOSO.LOCAL$cifs/dc01.contoso.local@CONTOSO.LOCAL$abc123def456\n"; - let hashes = extract_kerberos_hashes(output); - assert_eq!(hashes.len(), 1); - assert_eq!(hashes[0].username, "svc_sql"); - assert_eq!(hashes[0].domain, "CONTOSO.LOCAL"); - assert_eq!(hashes[0].hash_type, KerberosHashType::TGS); - assert!(hashes[0].hash_value.starts_with("$krb5tgs$")); + let results = extract_kerberos_hashes(output); + assert_eq!(results.len(), 1); + assert_eq!(results[0].username, "svc_sql"); + assert_eq!(results[0].domain, "CONTOSO.LOCAL"); + assert_eq!(results[0].hash_type, KerberosHashType::TGS); + assert!(results[0].hash_value.starts_with("$krb5tgs$")); } #[test] - fn test_extract_kerberos_asrep() { - let output = "$krb5asrep$23$jsmith@CONTOSO.LOCAL:abc123def456\n"; - let hashes = extract_kerberos_hashes(output); - assert_eq!(hashes.len(), 1); - assert_eq!(hashes[0].username, "jsmith"); - assert_eq!(hashes[0].domain, "CONTOSO.LOCAL"); - assert_eq!(hashes[0].hash_type, KerberosHashType::AsRep); + fn extract_tgs_multiple() { + let output = "$krb5tgs$23$*svc_a$DOM.LOCAL$http/web@DOM.LOCAL$aabb1122\n\ + $krb5tgs$23$*svc_b$DOM.LOCAL$cifs/fs@DOM.LOCAL$ccdd3344\n"; + let results = extract_kerberos_hashes(output); + assert_eq!(results.len(), 2); + assert_eq!(results[0].username, "svc_a"); + assert_eq!(results[1].username, "svc_b"); } #[test] - fn test_extract_kerberos_mixed() { + fn extract_tgs_empty_input() { + assert!(extract_kerberos_hashes("").is_empty()); + } + + #[test] + fn extract_tgs_no_match() { + let output = "some random output\nno hashes here\n"; + assert!(extract_kerberos_hashes(output).is_empty()); + } + + #[test] + fn extract_asrep_valid() { + let output = "$krb5asrep$23$user1@CONTOSO.LOCAL:abcdef0123456789\n"; + let results = extract_kerberos_hashes(output); + assert_eq!(results.len(), 1); + assert_eq!(results[0].username, "user1"); + assert_eq!(results[0].domain, "CONTOSO.LOCAL"); + assert_eq!(results[0].hash_type, KerberosHashType::AsRep); + assert_eq!(results[0].hash_value, output.trim()); + } + + #[test] + fn extract_mixed_tgs_and_asrep() { let output = "Some preamble text\n$krb5tgs$23$*svc_http$CONTOSO.LOCAL$http/web01.contoso.local@CONTOSO.LOCAL$aabbccdd\n[*] Some status line\n$krb5asrep$23$nopreauth@FABRIKAM.LOCAL:11223344\n"; let hashes = extract_kerberos_hashes(output); assert_eq!(hashes.len(), 2); @@ -87,17 +109,35 @@ mod tests { } #[test] - fn test_extract_kerberos_empty() { - assert!(extract_kerberos_hashes("").is_empty()); - assert!(extract_kerberos_hashes("no hashes here\n").is_empty()); + fn extract_tgs_hash_value_preserved() { + let line = + "$krb5tgs$23$*svc_sql$CONTOSO.LOCAL$cifs/dc01.contoso.local@CONTOSO.LOCAL$abc123def456"; + let output = format!("{}\n", line); + let results = extract_kerberos_hashes(&output); + assert_eq!(results.len(), 1); + assert_eq!(results[0].hash_value, line); } #[test] - fn test_kerberos_tgs_full_hash() { - let output = "$krb5tgs$23$*svc_sql$CONTOSO.LOCAL$cifs/dc01.contoso.local@CONTOSO.LOCAL$abcdef1234567890abcdef1234567890abcdef1234567890\n"; - let hashes = extract_kerberos_hashes(output); - assert_eq!(hashes.len(), 1); - assert_eq!(hashes[0].username, "svc_sql"); - assert_eq!(hashes[0].domain, "CONTOSO.LOCAL"); + fn extract_asrep_domain_parsed() { + let output = "$krb5asrep$23$jdoe@CONTOSO.LOCAL:aabbccdd11223344\n"; + let results = extract_kerberos_hashes(output); + assert_eq!(results.len(), 1); + assert_eq!(results[0].username, "jdoe"); + assert_eq!(results[0].domain, "CONTOSO.LOCAL"); + } + + #[test] + fn extract_kerberos_whitespace_lines_skipped() { + let output = " \n\n \n$krb5tgs$23$*svc_a$DOM.LOCAL$http/web@DOM.LOCAL$aabb\n \n"; + let results = extract_kerberos_hashes(output); + assert_eq!(results.len(), 1); + assert_eq!(results[0].username, "svc_a"); + } + + #[test] + fn extract_kerberos_status_lines_ignored() { + let output = "[*] Getting TGT for user\n[*] Requesting service ticket\nno hashes\n"; + assert!(extract_kerberos_hashes(output).is_empty()); } } diff --git a/ares-core/src/parsing/ntlm.rs b/ares-core/src/parsing/ntlm.rs index ff6eefd9..35ab5113 100644 --- a/ares-core/src/parsing/ntlm.rs +++ b/ares-core/src/parsing/ntlm.rs @@ -183,7 +183,7 @@ mod tests { use super::*; #[test] - fn test_extract_ntlm_domain_prefixed() { + fn extract_ntlm_domain_prefixed() { let output = "CONTOSO\\Administrator:500:aad3b435b51404eeaad3b435b51404ee:209c6174da490caeb422f3fa5a7ae634:::\n"; let hashes = extract_ntlm_hashes(output); @@ -194,7 +194,7 @@ mod tests { } #[test] - fn test_extract_ntlm_plain() { + fn extract_ntlm_plain() { let output = "Administrator:500:aad3b435b51404eeaad3b435b51404ee:209c6174da490caeb422f3fa5a7ae634:::\n"; let hashes = extract_ntlm_hashes(output); @@ -205,7 +205,7 @@ mod tests { } #[test] - fn test_extract_ntlm_skips_empty() { + fn extract_ntlm_skips_empty() { let output = "Guest:501:aad3b435b51404eeaad3b435b51404ee:31d6cfe0d16ae931b73c59d7e0c089c0:::\n"; let hashes = extract_ntlm_hashes(output); @@ -213,8 +213,7 @@ mod tests { } #[test] - fn test_extract_ntlm_line_wrapped() { - // NT hash split across two lines + fn extract_ntlm_line_wrapped() { let output = "CONTOSO\\svc_sql:1105:aad3b435b51404eeaad3b435b51404ee:a87f3a337d73085c\n45f9416be5787d86\n"; let hashes = extract_ntlm_hashes(output); @@ -224,7 +223,7 @@ mod tests { } #[test] - fn test_extract_ntlm_machine_account() { + fn extract_ntlm_machine_account() { let output = "DC01$:1000:aad3b435b51404eeaad3b435b51404ee:7c4f7e73b23d56a3c48c0c8c1e4b8a6f:::\n"; let hashes = extract_ntlm_hashes(output); @@ -233,7 +232,7 @@ mod tests { } #[test] - fn test_ntlm_multiple_hashes() { + fn ntlm_multiple_hashes() { let output = "CONTOSO\\Administrator:500:aad3b435b51404eeaad3b435b51404ee:209c6174da490caeb422f3fa5a7ae634:::\nCONTOSO\\krbtgt:502:aad3b435b51404eeaad3b435b51404ee:e3c61a68f7b313e24acee19ba61cf4dd:::\nCONTOSO\\DC01$:1000:aad3b435b51404eeaad3b435b51404ee:7c4f7e73b23d56a3c48c0c8c1e4b8a6f:::\n"; let hashes = extract_ntlm_hashes(output); assert_eq!(hashes.len(), 3); @@ -241,4 +240,66 @@ mod tests { assert!(hashes[1].is_krbtgt); assert!(hashes[2].is_machine_account); } + + #[test] + fn extract_ntlm_empty_input() { + assert!(extract_ntlm_hashes("").is_empty()); + } + + #[test] + fn extract_ntlm_no_match_lines() { + let output = "[*] Starting dump\n[*] Done\nrandom text\n"; + assert!(extract_ntlm_hashes(output).is_empty()); + } + + #[test] + fn extract_ntlm_hash_value_format() { + let output = + "CONTOSO\\svc_sql:1105:aad3b435b51404eeaad3b435b51404ee:a87f3a337d73085c45f9416be5787d86:::\n"; + let hashes = extract_ntlm_hashes(output); + assert_eq!(hashes.len(), 1); + assert_eq!( + hashes[0].hash_value, + "aad3b435b51404eeaad3b435b51404ee:a87f3a337d73085c45f9416be5787d86" + ); + } + + #[test] + fn extract_ntlm_krbtgt_by_rid() { + let output = + "CONTOSO\\someuser:502:aad3b435b51404eeaad3b435b51404ee:abcdef0123456789abcdef0123456789:::\n"; + let hashes = extract_ntlm_hashes(output); + assert_eq!(hashes.len(), 1); + assert!(hashes[0].is_krbtgt); + } + + #[test] + fn extract_ntlm_krbtgt_by_name() { + let output = + "CONTOSO\\krbtgt:9999:aad3b435b51404eeaad3b435b51404ee:abcdef0123456789abcdef0123456789:::\n"; + let hashes = extract_ntlm_hashes(output); + assert_eq!(hashes.len(), 1); + assert!(hashes[0].is_krbtgt); + } + + #[test] + fn extract_ntlm_uppercase_hashes_lowered() { + let output = + "CONTOSO\\admin:500:AAD3B435B51404EEAAD3B435B51404EE:ABCDEF0123456789ABCDEF0123456789:::\n"; + let hashes = extract_ntlm_hashes(output); + assert_eq!(hashes.len(), 1); + assert_eq!(hashes[0].lm_hash, "aad3b435b51404eeaad3b435b51404ee"); + assert_eq!(hashes[0].nt_hash, "abcdef0123456789abcdef0123456789"); + } + + #[test] + fn extract_ntlm_plain_line_wrapped() { + let output = + "localuser:1001:aad3b435b51404eeaad3b435b51404ee:a87f3a337d73085c\n45f9416be5787d86\n"; + let hashes = extract_ntlm_hashes(output); + assert_eq!(hashes.len(), 1); + assert_eq!(hashes[0].username, "localuser"); + assert_eq!(hashes[0].domain, ""); + assert_eq!(hashes[0].nt_hash, "a87f3a337d73085c45f9416be5787d86"); + } } diff --git a/ares-core/src/parsing/secretsdump.rs b/ares-core/src/parsing/secretsdump.rs index d15ea70c..6273c8a3 100644 --- a/ares-core/src/parsing/secretsdump.rs +++ b/ares-core/src/parsing/secretsdump.rs @@ -69,7 +69,7 @@ mod tests { use super::*; #[test] - fn test_parse_secretsdump_basic() { + fn parse_secretsdump_basic() { let output = r#"[*] Dumping local SAM hashes (uid:rid:lmhash:nthash) Administrator:500:aad3b435b51404eeaad3b435b51404ee:209c6174da490caeb422f3fa5a7ae634::: Guest:501:aad3b435b51404eeaad3b435b51404ee:31d6cfe0d16ae931b73c59d7e0c089c0::: @@ -79,10 +79,8 @@ DC01$:1000:aad3b435b51404eeaad3b435b51404ee:7c4f7e73b23d56a3c48c0c8c1e4b8a6f::: "#; let hashes = parse_secretsdump(output); - // Guest should be skipped (empty NT hash) assert_eq!(hashes.len(), 4); - // Administrator let admin = &hashes[0]; assert_eq!(admin.username, "Administrator"); assert_eq!(admin.domain, ""); @@ -92,7 +90,6 @@ DC01$:1000:aad3b435b51404eeaad3b435b51404ee:7c4f7e73b23d56a3c48c0c8c1e4b8a6f::: assert!(!admin.is_machine_account); assert_eq!(admin.nt_hash, "209c6174da490caeb422f3fa5a7ae634"); - // krbtgt let krbtgt = &hashes[1]; assert_eq!(krbtgt.username, "krbtgt"); assert_eq!(krbtgt.domain, "CONTOSO"); @@ -100,7 +97,6 @@ DC01$:1000:aad3b435b51404eeaad3b435b51404ee:7c4f7e73b23d56a3c48c0c8c1e4b8a6f::: assert!(krbtgt.is_krbtgt); assert!(!krbtgt.is_administrator); - // svc_sql let svc = &hashes[2]; assert_eq!(svc.username, "svc_sql"); assert_eq!(svc.domain, "CONTOSO"); @@ -109,20 +105,19 @@ DC01$:1000:aad3b435b51404eeaad3b435b51404ee:7c4f7e73b23d56a3c48c0c8c1e4b8a6f::: assert!(!svc.is_administrator); assert!(!svc.is_machine_account); - // Machine account let machine = &hashes[3]; assert_eq!(machine.username, "DC01$"); assert!(machine.is_machine_account); } #[test] - fn test_parse_secretsdump_empty() { + fn parse_secretsdump_empty() { let hashes = parse_secretsdump(""); assert!(hashes.is_empty()); } #[test] - fn test_parse_secretsdump_hash_value_format() { + fn parse_secretsdump_hash_value_format() { let output = "Administrator:500:aad3b435b51404eeaad3b435b51404ee:209c6174da490caeb422f3fa5a7ae634:::\n"; let hashes = parse_secretsdump(output); @@ -134,14 +129,14 @@ DC01$:1000:aad3b435b51404eeaad3b435b51404ee:7c4f7e73b23d56a3c48c0c8c1e4b8a6f::: } #[test] - fn test_parse_secretsdump_skips_non_matching() { + fn parse_secretsdump_skips_non_matching() { let output = "[*] Service RemoteRegistry is in stopped state\n[*] Starting service\nAdministrator:500:aad3b435b51404eeaad3b435b51404ee:209c6174da490caeb422f3fa5a7ae634:::\n[*] Cleaning up...\n"; let hashes = parse_secretsdump(output); assert_eq!(hashes.len(), 1); } #[test] - fn test_parse_secretsdump_administrator_by_name() { + fn parse_secretsdump_administrator_by_name() { let output = "CONTOSO\\administrator:9999:aad3b435b51404eeaad3b435b51404ee:abcdef1234567890abcdef1234567890:::\n"; let hashes = parse_secretsdump(output); @@ -150,7 +145,7 @@ DC01$:1000:aad3b435b51404eeaad3b435b51404ee:7c4f7e73b23d56a3c48c0c8c1e4b8a6f::: } #[test] - fn test_secretsdump_case_insensitive_krbtgt() { + fn secretsdump_case_insensitive_krbtgt() { let output = "CONTOSO\\KRBTGT:502:aad3b435b51404eeaad3b435b51404ee:e3c61a68f7b313e24acee19ba61cf4dd:::\n"; let hashes = parse_secretsdump(output); @@ -159,7 +154,7 @@ DC01$:1000:aad3b435b51404eeaad3b435b51404ee:7c4f7e73b23d56a3c48c0c8c1e4b8a6f::: } #[test] - fn test_secretsdump_no_domain() { + fn secretsdump_no_domain() { let output = "localuser:1001:aad3b435b51404eeaad3b435b51404ee:abcdef0123456789abcdef0123456789:::\n"; let hashes = parse_secretsdump(output); @@ -167,4 +162,47 @@ DC01$:1000:aad3b435b51404eeaad3b435b51404ee:7c4f7e73b23d56a3c48c0c8c1e4b8a6f::: assert_eq!(hashes[0].domain, ""); assert_eq!(hashes[0].username, "localuser"); } + + #[test] + fn parse_secretsdump_all_empty_hashes_skipped() { + let output = "CONTOSO\\svc_backup:1100:aad3b435b51404eeaad3b435b51404ee:31d6cfe0d16ae931b73c59d7e0c089c0:::\nCONTOSO\\svc_web:1101:aad3b435b51404eeaad3b435b51404ee:31d6cfe0d16ae931b73c59d7e0c089c0:::\n"; + assert!(parse_secretsdump(output).is_empty()); + } + + #[test] + fn parse_secretsdump_malformed_rid() { + // RID is not a number — should be skipped + let output = "CONTOSO\\svc_sql:abc:aad3b435b51404eeaad3b435b51404ee:abcdef0123456789abcdef0123456789:::\n"; + assert!(parse_secretsdump(output).is_empty()); + } + + #[test] + fn parse_secretsdump_uppercase_hashes_lowered() { + let output = "CONTOSO\\Administrator:500:AAD3B435B51404EEAAD3B435B51404EE:ABCDEF0123456789ABCDEF0123456789:::\n"; + let hashes = parse_secretsdump(output); + assert_eq!(hashes.len(), 1); + assert_eq!(hashes[0].nt_hash, "abcdef0123456789abcdef0123456789"); + assert_eq!(hashes[0].lm_hash, "aad3b435b51404eeaad3b435b51404ee"); + } + + #[test] + fn parse_secretsdump_whitespace_only() { + assert!(parse_secretsdump(" \n \n").is_empty()); + } + + #[test] + fn parse_secretsdump_whitespace_lines_with_valid_entry() { + let output = " \n\n \nCONTOSO\\Administrator:500:aad3b435b51404eeaad3b435b51404ee:209c6174da490caeb422f3fa5a7ae634:::\n"; + let hashes = parse_secretsdump(output); + assert_eq!(hashes.len(), 1); + assert_eq!(hashes[0].username, "Administrator"); + } + + #[test] + fn parse_secretsdump_krbtgt_by_rid_not_name() { + let output = "CONTOSO\\svc_random:502:aad3b435b51404eeaad3b435b51404ee:abcdef0123456789abcdef0123456789:::\n"; + let hashes = parse_secretsdump(output); + assert_eq!(hashes.len(), 1); + assert!(hashes[0].is_krbtgt); + } } diff --git a/ares-core/src/parsing/shares.rs b/ares-core/src/parsing/shares.rs index f1c89e4d..c11943e1 100644 --- a/ares-core/src/parsing/shares.rs +++ b/ares-core/src/parsing/shares.rs @@ -82,7 +82,7 @@ mod tests { use super::*; #[test] - fn test_extract_shares() { + fn extract_shares_basic() { let output = "SMB 192.168.58.10 445 DC01 ADMIN$ READ Remote Admin\nSMB 192.168.58.10 445 DC01 C$ READ,WRITE Default share\nSMB 192.168.58.10 445 DC01 IPC$ READ Remote IPC\nSMB 192.168.58.10 445 DC01 NETLOGON READ Logon server share\n"; let shares = extract_shares(output); assert_eq!(shares.len(), 4); @@ -97,7 +97,7 @@ mod tests { } #[test] - fn test_extract_shares_skips_banners() { + fn extract_shares_skips_banners() { let output = "SMB 192.168.58.10 445 DC01 [*] Windows Server 2019\nSMB 192.168.58.10 445 DC01 SYSVOL READ Logon server share\n"; let shares = extract_shares(output); assert_eq!(shares.len(), 1); @@ -105,7 +105,42 @@ mod tests { } #[test] - fn test_extract_shares_empty() { + fn extract_shares_empty() { assert!(extract_shares("").is_empty()); } + + #[test] + fn extract_shares_no_access_permission() { + let output = "SMB 192.168.58.10 445 DC01 SHARE1 NO ACCESS Some comment\n"; + let shares = extract_shares(output); + assert_eq!(shares.len(), 1); + assert_eq!(shares[0].permissions, "NO ACCESS"); + } + + #[test] + fn extract_shares_write_only() { + let output = "SMB 192.168.58.10 445 DC01 UPLOADS WRITE Upload folder\n"; + let shares = extract_shares(output); + assert_eq!(shares.len(), 1); + assert_eq!(shares[0].name, "UPLOADS"); + assert_eq!(shares[0].permissions, "WRITE"); + } + + #[test] + fn extract_shares_skips_status_markers() { + let output = "SMB 192.168.58.10 445 DC01 [+] Authenticated successfully\n"; + assert!(extract_shares(output).is_empty()); + } + + #[test] + fn extract_shares_skips_minus_markers() { + let output = "SMB 192.168.58.10 445 DC01 [-] Auth failed\n"; + assert!(extract_shares(output).is_empty()); + } + + #[test] + fn extract_shares_non_smb_lines_ignored() { + let output = "random text\nnot SMB\n"; + assert!(extract_shares(output).is_empty()); + } } diff --git a/ares-core/src/parsing/types.rs b/ares-core/src/parsing/types.rs index 5de59fc9..1d38105a 100644 --- a/ares-core/src/parsing/types.rs +++ b/ares-core/src/parsing/types.rs @@ -117,74 +117,69 @@ pub struct ParsedShare { mod tests { use super::*; - // --- DelegationType Display --- - #[test] - fn test_delegation_type_display() { + fn delegation_type_display() { assert_eq!(DelegationType::Unconstrained.to_string(), "Unconstrained"); assert_eq!(DelegationType::Constrained.to_string(), "Constrained"); assert_eq!(DelegationType::RBCD.to_string(), "RBCD"); } - // --- DelegationType FromStr --- - #[test] - fn test_delegation_type_from_str_unconstrained() { + fn delegation_type_from_str_unconstrained() { let dt: DelegationType = "Unconstrained".parse().unwrap(); assert_eq!(dt, DelegationType::Unconstrained); } #[test] - fn test_delegation_type_from_str_constrained() { + fn delegation_type_from_str_constrained() { let dt: DelegationType = "Constrained".parse().unwrap(); assert_eq!(dt, DelegationType::Constrained); } #[test] - fn test_delegation_type_from_str_rbcd() { + fn delegation_type_from_str_rbcd() { let dt: DelegationType = "Resource-Based Constrained Delegation".parse().unwrap(); assert_eq!(dt, DelegationType::RBCD); } #[test] - fn test_delegation_type_from_str_rbcd_short() { + fn delegation_type_from_str_rbcd_short() { let dt: DelegationType = "RBCD".parse().unwrap(); assert_eq!(dt, DelegationType::RBCD); } #[test] - fn test_delegation_type_from_str_case_insensitive() { + fn delegation_type_from_str_case_insensitive() { let dt: DelegationType = "UNCONSTRAINED".parse().unwrap(); assert_eq!(dt, DelegationType::Unconstrained); } #[test] - fn test_delegation_type_from_str_unknown() { - let result = "something_else".parse::(); - assert!(result.is_err()); - let err = result.unwrap_err(); + fn delegation_type_from_str_unknown() { + let err = "something_else".parse::().unwrap_err(); assert_eq!(err.0, "something_else"); } #[test] - fn test_delegation_type_resource_constrained_is_rbcd() { - // "Resource-based constrained" contains both "resource" and "constrained" - // but should be RBCD because "resource" is checked first + fn delegation_type_resource_constrained_is_rbcd() { let dt: DelegationType = "resource-based constrained".parse().unwrap(); assert_eq!(dt, DelegationType::RBCD); } - // --- KerberosHashType --- - #[test] - fn test_kerberos_hash_type_equality() { - assert_eq!(KerberosHashType::TGS, KerberosHashType::TGS); + fn kerberos_hash_type_equality() { assert_ne!(KerberosHashType::TGS, KerberosHashType::AsRep); } #[test] - fn test_parse_delegation_type_error_display() { + fn parse_delegation_type_error_display() { let err = ParseDelegationTypeError("bogus".to_string()); assert_eq!(err.to_string(), "unknown delegation type: bogus"); } + + #[test] + fn delegation_type_constrained_lowercase() { + let dt: DelegationType = "constrained delegation".parse().unwrap(); + assert_eq!(dt, DelegationType::Constrained); + } } diff --git a/ares-core/src/persistent_store/config.rs b/ares-core/src/persistent_store/config.rs index e27e7d44..6ed72570 100644 --- a/ares-core/src/persistent_store/config.rs +++ b/ares-core/src/persistent_store/config.rs @@ -132,7 +132,7 @@ mod tests { use super::*; #[test] - fn test_default_config() { + fn default_config() { let config = PersistentStoreConfig::default(); assert!(config.database_url.is_none()); assert!(!config.is_enabled()); @@ -142,7 +142,7 @@ mod tests { } #[test] - fn test_default_retention() { + fn default_retention() { let retention = RetentionConfig::default(); assert_eq!(retention.operations_default_days, 90); assert_eq!(retention.operations_with_da_days, 365); @@ -150,7 +150,7 @@ mod tests { } #[test] - fn test_is_enabled() { + fn checks_enabled() { let mut config = PersistentStoreConfig::default(); assert!(!config.is_enabled()); @@ -159,7 +159,7 @@ mod tests { } #[test] - fn test_pool_timeout_duration() { + fn pool_timeout_duration() { let config = PersistentStoreConfig::default(); assert_eq!(config.pool_timeout(), Duration::from_secs(30)); } diff --git a/ares-core/src/persistent_store/store.rs b/ares-core/src/persistent_store/store.rs index 65bc447e..2504cdd1 100644 --- a/ares-core/src/persistent_store/store.rs +++ b/ares-core/src/persistent_store/store.rs @@ -602,8 +602,6 @@ fn is_ip(value: &str) -> bool { mod tests { use super::*; - // ─── sha256_prefix ────────────────────────────────────────────────────── - #[test] fn sha256_prefix_returns_correct_length() { let result = sha256_prefix("P@ssw0rd!", 16); // pragma: allowlist secret @@ -659,8 +657,6 @@ mod tests { } } - // ─── is_ip ────────────────────────────────────────────────────────────── - #[test] fn is_ip_valid_ipv4() { assert!(is_ip("192.168.58.10")); diff --git a/ares-core/src/reports/context.rs b/ares-core/src/reports/context.rs index 6d3c8b1e..9fd0fac2 100644 --- a/ares-core/src/reports/context.rs +++ b/ares-core/src/reports/context.rs @@ -284,7 +284,7 @@ mod tests { use std::collections::HashMap; #[test] - fn test_host_ctx_from_host_with_hostname() { + fn host_ctx_from_host_with_hostname() { let host = Host { ip: "192.168.58.10".to_string(), hostname: "dc01.contoso.local".to_string(), @@ -303,7 +303,7 @@ mod tests { } #[test] - fn test_host_ctx_from_host_no_hostname() { + fn host_ctx_from_host_no_hostname() { let host = Host { ip: "192.168.58.20".to_string(), hostname: String::new(), @@ -320,7 +320,7 @@ mod tests { } #[test] - fn test_user_ctx_from_user() { + fn user_ctx_from_user() { let user = User { username: "admin".to_string(), domain: "contoso.local".to_string(), @@ -335,7 +335,7 @@ mod tests { } #[test] - fn test_user_ctx_non_admin() { + fn user_ctx_non_admin() { let user = User { username: "jdoe".to_string(), domain: "contoso.local".to_string(), @@ -349,7 +349,7 @@ mod tests { } #[test] - fn test_cred_ctx_empty_domain() { + fn cred_ctx_empty_domain() { let cred = Credential { id: String::new(), username: "admin".to_string(), @@ -366,7 +366,7 @@ mod tests { } #[test] - fn test_hash_ctx_from_hash() { + fn hash_ctx_from_hash() { let hash = Hash { id: String::new(), username: "krbtgt".to_string(), @@ -389,7 +389,7 @@ mod tests { } #[test] - fn test_hash_ctx_truncates_long_hash() { + fn hash_ctx_truncates_long_hash() { let long_hash = "a".repeat(200); let hash = Hash { id: String::new(), @@ -413,7 +413,7 @@ mod tests { } #[test] - fn test_share_ctx_from_share() { + fn share_ctx_from_share() { let share = Share { host: "192.168.58.10".to_string(), name: "SYSVOL".to_string(), @@ -427,7 +427,7 @@ mod tests { } #[test] - fn test_share_ctx_empty_fields() { + fn share_ctx_empty_fields() { let share = Share { host: "192.168.58.10".to_string(), name: "C$".to_string(), @@ -440,7 +440,7 @@ mod tests { } #[test] - fn test_build_vuln_ctx_not_exploited() { + fn build_vuln_ctx_not_exploited() { let vuln = VulnerabilityInfo { vuln_id: "smb_signing_192.168.58.10".to_string(), vuln_type: "smb_signing_disabled".to_string(), @@ -460,7 +460,7 @@ mod tests { } #[test] - fn test_build_vuln_ctx_details_list() { + fn build_vuln_ctx_details_list() { let mut details = HashMap::new(); details.insert("account".to_string(), serde_json::json!("john.smith")); details.insert("domain".to_string(), serde_json::json!("contoso.local")); @@ -482,7 +482,7 @@ mod tests { } #[test] - fn test_build_vuln_ctx_exploited() { + fn build_vuln_ctx_exploited() { let vuln = VulnerabilityInfo { vuln_id: "esc1_192.168.58.10".to_string(), vuln_type: "adcs_esc1".to_string(), @@ -503,7 +503,7 @@ mod tests { } #[test] - fn test_host_ctx_deduplicates_services() { + fn host_ctx_deduplicates_services() { let host = Host { ip: "192.168.58.10".to_string(), hostname: "dc01.contoso.local".to_string(), @@ -522,7 +522,7 @@ mod tests { } #[test] - fn test_host_ctx_filters_pseudo_services() { + fn host_ctx_filters_pseudo_services() { let host = Host { ip: "192.168.58.23".to_string(), hostname: String::new(), @@ -541,7 +541,7 @@ mod tests { } #[test] - fn test_cred_ctx_lowercases_domain() { + fn cred_ctx_lowercases_domain() { let cred = Credential { id: String::new(), username: "admin".to_string(), @@ -558,7 +558,7 @@ mod tests { } #[test] - fn test_hash_ctx_lowercases_domain() { + fn hash_ctx_lowercases_domain() { let hash = Hash { id: String::new(), username: "admin".to_string(), diff --git a/ares-core/src/reports/mitre.rs b/ares-core/src/reports/mitre.rs index fbcc0ab4..8562f586 100644 --- a/ares-core/src/reports/mitre.rs +++ b/ares-core/src/reports/mitre.rs @@ -17,3 +17,32 @@ pub fn get_technique_display(technique_id: &str) -> String { None => technique_id.to_string(), } } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn technique_display_known_id() { + let display = get_technique_display("T1003"); + assert!(display.starts_with("T1003")); + assert!(display.contains('(')); + } + + #[test] + fn technique_display_unknown_id_returns_raw() { + let display = get_technique_display("T9999.999"); + assert_eq!(display, "T9999.999"); + } + + #[test] + fn technique_display_empty_string() { + let display = get_technique_display(""); + assert_eq!(display, ""); + } + + #[test] + fn mitre_techniques_map_loads() { + let _ = MITRE_TECHNIQUES.len(); + } +} diff --git a/ares-core/src/reports/mod.rs b/ares-core/src/reports/mod.rs index 1e6006e3..3e1440d0 100644 --- a/ares-core/src/reports/mod.rs +++ b/ares-core/src/reports/mod.rs @@ -30,19 +30,19 @@ mod tests { use chrono::Utc; #[test] - fn test_mitre_lookup() { + fn mitre_lookup() { assert_eq!(get_technique_display("T1003.006"), "T1003.006 (DCSync)"); assert_eq!(get_technique_display("T9999"), "T9999"); } #[test] - fn test_format_vuln_details_empty() { + fn format_vuln_details_empty() { let details = HashMap::new(); assert_eq!(format_vuln_details(&details), "-"); } #[test] - fn test_format_vuln_details_with_values() { + fn format_vuln_details_with_values() { let mut details = HashMap::new(); details.insert( "account".to_string(), @@ -58,7 +58,7 @@ mod tests { } #[test] - fn test_dedup_credentials() { + fn deduplicates_credentials() { let creds = vec![ Credential { id: "1".to_string(), @@ -88,7 +88,7 @@ mod tests { } #[test] - fn test_dedup_hashes() { + fn deduplicates_hashes() { let hashes = vec![ Hash { id: "1".to_string(), @@ -124,7 +124,7 @@ mod tests { } #[test] - fn test_redteam_summary_renders() { + fn redteam_summary_renders() { let gen = RedTeamReportGenerator::new().unwrap(); let state = SharedRedTeamState { operation_id: "test-op-001".to_string(), @@ -164,7 +164,7 @@ mod tests { } #[test] - fn test_redteam_comprehensive_renders() { + fn redteam_comprehensive_renders() { let gen = RedTeamReportGenerator::new().unwrap(); let state = SharedRedTeamState { operation_id: "test-op-002".to_string(), @@ -237,7 +237,7 @@ mod tests { #[cfg(feature = "blue")] #[test] - fn test_blueteam_report_renders() { + fn blueteam_report_renders() { let gen = BlueTeamReportGenerator::new().unwrap(); let input = BlueTeamReportInput { operation_id: "blue-test-001".to_string(), @@ -277,7 +277,7 @@ mod tests { #[cfg(feature = "blue")] #[test] - fn test_blueteam_investigation_report_renders() { + fn blueteam_investigation_report_renders() { use crate::models::{Evidence, SharedBlueTeamState, TimelineEvent}; let gen = BlueTeamReportGenerator::new().unwrap(); @@ -375,7 +375,7 @@ mod tests { #[cfg(feature = "blue")] #[test] - fn test_blueteam_generate_from_states() { + fn blueteam_generate_from_states() { use crate::models::{Evidence, SharedBlueTeamState}; let gen = BlueTeamReportGenerator::new().unwrap(); diff --git a/ares-core/src/reports/redteam.rs b/ares-core/src/reports/redteam.rs index 6c555103..b8fa09b2 100644 --- a/ares-core/src/reports/redteam.rs +++ b/ares-core/src/reports/redteam.rs @@ -495,3 +495,148 @@ pub(crate) fn generate_executive_summary( summary_parts.join("") } + +#[cfg(test)] +mod tests { + use super::*; + use crate::models::{Credential, Host, Share, SharedRedTeamState, User}; + + fn empty_state() -> SharedRedTeamState { + SharedRedTeamState::new("op-test-1".to_string()) + } + + fn make_user(name: &str, domain: &str) -> User { + User { + username: name.to_string(), + domain: domain.to_string(), + description: String::new(), + is_admin: false, + source: String::new(), + } + } + + fn make_cred(name: &str, domain: &str, pass: &str, admin: bool) -> Credential { + Credential { + id: "id".to_string(), + username: name.to_string(), + password: pass.to_string(), + domain: domain.to_string(), + source: String::new(), + discovered_at: None, + is_admin: admin, + parent_id: None, + attack_step: 0, + } + } + + #[test] + fn executive_summary_critical_when_domain_admin() { + let mut state = empty_state(); + state.has_domain_admin = true; + let users = vec![make_user("admin", "contoso.local")]; + let creds = vec![make_cred("admin", "contoso.local", "pass", true)]; + let summary = generate_executive_summary(&state, &users, &creds); + assert!(summary.contains("**CRITICAL**")); + assert!(summary.contains("op-test-1")); + } + + #[test] + fn executive_summary_critical_when_golden_ticket() { + let mut state = empty_state(); + state.has_golden_ticket = true; + let summary = generate_executive_summary(&state, &[], &[]); + assert!(summary.contains("**CRITICAL**")); + assert!(summary.contains("Golden ticket")); + } + + #[test] + fn executive_summary_high_when_admin_creds() { + let state = empty_state(); + let creds = vec![make_cred("admin", "contoso.local", "pass", true)]; + let summary = generate_executive_summary(&state, &[], &creds); + assert!(summary.contains("**HIGH**")); + } + + #[test] + fn executive_summary_medium_when_creds_no_admin() { + let state = empty_state(); + let creds = vec![make_cred("user1", "contoso.local", "pass", false)]; + let summary = generate_executive_summary(&state, &[], &creds); + assert!(summary.contains("**MEDIUM**")); + } + + #[test] + fn executive_summary_low_when_no_findings() { + let state = empty_state(); + let summary = generate_executive_summary(&state, &[], &[]); + assert!(summary.contains("**LOW**")); + assert!(summary.contains("resilience")); + } + + #[test] + fn executive_summary_single_target() { + let mut state = empty_state(); + state.target_ips = vec!["10.0.0.1".to_string()]; + let summary = generate_executive_summary(&state, &[], &[]); + assert!(summary.contains("**10.0.0.1**")); + } + + #[test] + fn executive_summary_multiple_targets_truncated() { + let mut state = empty_state(); + state.target_ips = vec![ + "10.0.0.1".to_string(), + "10.0.0.2".to_string(), + "10.0.0.3".to_string(), + "10.0.0.4".to_string(), + ]; + let summary = generate_executive_summary(&state, &[], &[]); + assert!(summary.contains("**4 targets**")); + assert!(summary.contains("...")); + } + + #[test] + fn executive_summary_domain_admin_path() { + let mut state = empty_state(); + state.has_domain_admin = true; + state.domain_admin_path = Some("user1 -> admin -> DA".to_string()); + let summary = generate_executive_summary(&state, &[], &[]); + assert!(summary.contains("user1 -> admin -> DA")); + } + + #[test] + fn executive_summary_discovery_stats() { + let mut state = empty_state(); + state.all_hosts = vec![Host { + ip: "10.0.0.1".to_string(), + hostname: "dc01".to_string(), + os: String::new(), + roles: vec![], + services: vec![], + is_dc: false, + owned: false, + }]; + state.all_shares = vec![Share { + host: "10.0.0.1".to_string(), + name: "SYSVOL".to_string(), + permissions: "READ".to_string(), + comment: String::new(), + }]; + let users = vec![make_user("u1", "d")]; + let summary = generate_executive_summary(&state, &users, &[]); + assert!(summary.contains("Hosts Discovered: 1")); + assert!(summary.contains("User Accounts: 1")); + assert!(summary.contains("Network Shares: 1")); + } + + #[test] + fn report_generator_new_succeeds() { + let gen = RedTeamReportGenerator::new(); + assert!(gen.is_ok()); + } + + #[test] + fn report_generator_default_succeeds() { + let _gen = RedTeamReportGenerator::default(); + } +} diff --git a/ares-core/src/reports/util.rs b/ares-core/src/reports/util.rs index 6d51a049..ebbb082c 100644 --- a/ares-core/src/reports/util.rs +++ b/ares-core/src/reports/util.rs @@ -60,7 +60,7 @@ mod tests { use serde_json::json; #[test] - fn test_timeline_event_from_json_full() { + fn timeline_event_from_json_full() { let event = json!({ "timestamp": "2026-04-08T12:00:00Z", "description": "Credential dumped via secretsdump", @@ -77,7 +77,7 @@ mod tests { } #[test] - fn test_timeline_event_from_json_defaults() { + fn timeline_event_from_json_defaults() { let event = json!({}); let ctx = timeline_event_from_json(&event); assert_eq!(ctx.timestamp, "-"); @@ -87,7 +87,7 @@ mod tests { } #[test] - fn test_timeline_event_long_description_truncated() { + fn timeline_event_long_description_truncated() { let long_desc = "A".repeat(100); let event = json!({"description": long_desc}); let ctx = timeline_event_from_json(&event); @@ -97,25 +97,25 @@ mod tests { } #[test] - fn test_format_duration_chrono_zero() { + fn format_duration_chrono_zero() { let d = chrono::Duration::seconds(0); assert_eq!(format_duration_chrono(d), "0:00:00"); } #[test] - fn test_format_duration_chrono_minutes() { + fn format_duration_chrono_minutes() { let d = chrono::Duration::seconds(150); assert_eq!(format_duration_chrono(d), "0:02:30"); } #[test] - fn test_format_duration_chrono_hours() { + fn format_duration_chrono_hours() { let d = chrono::Duration::seconds(3661); assert_eq!(format_duration_chrono(d), "1:01:01"); } #[test] - fn test_format_duration_chrono_negative_clamped() { + fn format_duration_chrono_negative_clamped() { let d = chrono::Duration::seconds(-10); assert_eq!(format_duration_chrono(d), "0:00:00"); } diff --git a/ares-core/src/reports/vuln_details.rs b/ares-core/src/reports/vuln_details.rs index 14bf47ba..16ebbd59 100644 --- a/ares-core/src/reports/vuln_details.rs +++ b/ares-core/src/reports/vuln_details.rs @@ -107,13 +107,13 @@ mod tests { use std::collections::HashMap; #[test] - fn test_format_vuln_details_empty() { + fn format_vuln_details_empty() { let details = HashMap::new(); assert_eq!(format_vuln_details(&details), "-"); } #[test] - fn test_format_vuln_details_ordered_keys() { + fn format_vuln_details_ordered_keys() { let mut details = HashMap::new(); details.insert("account_name".to_string(), serde_json::json!("svc_sql$")); details.insert("domain".to_string(), serde_json::json!("contoso.local")); @@ -123,7 +123,7 @@ mod tests { } #[test] - fn test_format_vuln_details_skip_keys() { + fn format_vuln_details_skip_keys() { let mut details = HashMap::new(); details.insert("has_credentials".to_string(), serde_json::json!(true)); details.insert("services".to_string(), serde_json::json!(["smb"])); @@ -135,7 +135,7 @@ mod tests { } #[test] - fn test_format_vuln_details_custom_keys_title_cased() { + fn format_vuln_details_custom_keys_title_cased() { let mut details = HashMap::new(); details.insert("custom_field".to_string(), serde_json::json!("value")); let result = format_vuln_details(&details); @@ -143,7 +143,7 @@ mod tests { } #[test] - fn test_format_vuln_details_skips_null_and_empty() { + fn format_vuln_details_skips_null_and_empty() { let mut details = HashMap::new(); details.insert("domain".to_string(), serde_json::Value::Null); details.insert("account".to_string(), serde_json::json!("")); @@ -152,7 +152,7 @@ mod tests { } #[test] - fn test_format_vuln_details_bool_and_number() { + fn format_vuln_details_bool_and_number() { let mut details = HashMap::new(); details.insert("some_flag".to_string(), serde_json::json!(true)); details.insert("some_count".to_string(), serde_json::json!(42)); @@ -162,7 +162,7 @@ mod tests { } #[test] - fn test_format_vuln_details_skips_complex_types() { + fn format_vuln_details_skips_complex_types() { let mut details = HashMap::new(); details.insert("nested".to_string(), serde_json::json!({"a": 1})); details.insert("list".to_string(), serde_json::json!([1, 2, 3])); @@ -171,7 +171,7 @@ mod tests { } #[test] - fn test_value_to_display() { + fn converts_value_to_display() { assert_eq!(value_to_display(&serde_json::Value::Null), None); assert_eq!(value_to_display(&serde_json::json!("")), None); assert_eq!( diff --git a/ares-core/src/state/dedup_keys.rs b/ares-core/src/state/dedup_keys.rs index d4e9aba9..ae7b0c07 100644 --- a/ares-core/src/state/dedup_keys.rs +++ b/ares-core/src/state/dedup_keys.rs @@ -119,7 +119,7 @@ mod tests { // ─── build_credential_dedup_key ────────────────────────────────────── #[test] - fn test_cred_dedup_key_format() { + fn cred_dedup_key_format() { let cred = make_cred("admin", "contoso.local", "P@ss1"); let key = build_credential_dedup_key(&cred); assert!(key.starts_with("cred:contoso.local:admin:")); @@ -130,14 +130,14 @@ mod tests { } #[test] - fn test_cred_dedup_key_lowercased() { + fn cred_dedup_key_lowercased() { let cred = make_cred("Admin", "CONTOSO.LOCAL", "P@ss1"); let key = build_credential_dedup_key(&cred); assert!(key.starts_with("cred:contoso.local:admin:")); } #[test] - fn test_cred_dedup_key_different_passwords() { + fn cred_dedup_key_different_passwords() { let c1 = make_cred("admin", "contoso.local", "P@ss1"); let c2 = make_cred("admin", "contoso.local", "P@ss2"); let k1 = build_credential_dedup_key(&c1); @@ -146,7 +146,7 @@ mod tests { } #[test] - fn test_cred_dedup_key_same_password_deterministic() { + fn cred_dedup_key_same_password_deterministic() { let c1 = make_cred("admin", "contoso.local", "P@ss1"); let c2 = make_cred("admin", "contoso.local", "P@ss1"); assert_eq!( @@ -156,7 +156,7 @@ mod tests { } #[test] - fn test_cred_dedup_key_trims_whitespace() { + fn cred_dedup_key_trims_whitespace() { let cred = make_cred(" admin ", " contoso.local ", "P@ss1"); let key = build_credential_dedup_key(&cred); assert!(key.starts_with("cred:contoso.local:admin:")); @@ -165,7 +165,7 @@ mod tests { // ─── build_hash_dedup_key ──────────────────────────────────────────── #[test] - fn test_hash_dedup_key_ntlm() { + fn hash_dedup_key_ntlm() { let h = make_hash( "admin", "contoso.local", @@ -177,7 +177,7 @@ mod tests { } #[test] - fn test_hash_dedup_key_asrep_by_type() { + fn hash_dedup_key_asrep_by_type() { let h = make_hash( "jsmith", "contoso.local", @@ -189,7 +189,7 @@ mod tests { } #[test] - fn test_hash_dedup_key_asrep_by_value() { + fn hash_dedup_key_asrep_by_value() { let h = make_hash( "jsmith", "contoso.local", @@ -201,7 +201,7 @@ mod tests { } #[test] - fn test_hash_dedup_key_kerberoast_with_spn() { + fn hash_dedup_key_kerberoast_with_spn() { let h = make_hash( "svc_sql", "contoso.local", @@ -214,7 +214,7 @@ mod tests { } #[test] - fn test_hash_dedup_key_kerberoast_no_spn() { + fn hash_dedup_key_kerberoast_no_spn() { let h = make_hash( "svc_sql", "contoso.local", @@ -226,7 +226,7 @@ mod tests { } #[test] - fn test_hash_dedup_key_case_insensitive() { + fn hash_dedup_key_case_insensitive() { let h1 = make_hash( "Admin", "CONTOSO.LOCAL", @@ -245,7 +245,7 @@ mod tests { // ─── extract_kerberoast_spn_key ────────────────────────────────────── #[test] - fn test_extract_kerberoast_spn_key_valid() { + fn extract_kerberoast_spn_key_valid() { let hash = "$krb5tgs$23$*svc_sql$CONTOSO.LOCAL$cifs/dc01.contoso.local*$checksum$encrypted"; let key = extract_kerberoast_spn_key(hash); assert!(key.is_some()); @@ -254,12 +254,12 @@ mod tests { } #[test] - fn test_extract_kerberoast_spn_key_not_krb() { + fn extract_kerberoast_spn_key_not_krb() { assert!(extract_kerberoast_spn_key("not_a_kerberos_hash").is_none()); } #[test] - fn test_extract_kerberoast_spn_key_too_few_parts() { + fn extract_kerberoast_spn_key_too_few_parts() { assert!(extract_kerberoast_spn_key("$krb5tgs$").is_none()); } } diff --git a/ares-core/src/state/keys.rs b/ares-core/src/state/keys.rs index 9f096391..7160b7d4 100644 --- a/ares-core/src/state/keys.rs +++ b/ares-core/src/state/keys.rs @@ -160,3 +160,138 @@ pub const BLUE_OP_PREFIX: &str = "ares:blue:op"; /// Redis key prefix for investigation status. #[cfg(feature = "blue")] pub const BLUE_STATUS_PREFIX: &str = "ares:blue:inv"; + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn key_prefix_format() { + assert_eq!(KEY_PREFIX, "ares:op"); + assert_eq!(LOCK_PREFIX, "ares:lock"); + assert_eq!(TASK_STATUS_PREFIX, "ares:task_status"); + } + + #[test] + fn collection_key_suffixes_non_empty() { + let suffixes = [ + KEY_CREDENTIALS, + KEY_HASHES, + KEY_HOSTS, + KEY_USERS, + KEY_SHARES, + KEY_DOMAINS, + KEY_VULNS, + KEY_EXPLOITED, + KEY_META, + KEY_DC_MAP, + KEY_NETBIOS_MAP, + KEY_ARTIFACTS, + KEY_TIMELINE, + KEY_GOLDEN_TICKETS, + KEY_ADMINSD_BACKDOORS, + KEY_ACL_CHAINS, + KEY_GMSA_ACCOUNTS, + KEY_DEDUP_PREFIX, + KEY_TECHNIQUES, + KEY_MSSQL_ENUM_DISPATCHED, + KEY_PENDING_TASKS, + KEY_COMPLETED_TASKS, + KEY_VULN_TYPE_FAILURES, + KEY_DOMAIN_SIDS, + KEY_ADMIN_NAMES, + KEY_TRUSTED_DOMAINS, + KEY_STATUS, + KEY_MODEL, + KEY_STOP_REQUESTED, + ]; + for suffix in &suffixes { + assert!(!suffix.is_empty(), "Key suffix must not be empty"); + assert!( + !suffix.contains(':'), + "Suffix '{suffix}' should not contain ':'", + ); + } + } + + #[test] + fn state_update_channel_prefix() { + assert_eq!(STATE_UPDATE_CHANNEL_PREFIX, "ares:state:updates"); + } + + #[test] + fn key_suffixes_unique() { + let suffixes = vec![ + KEY_CREDENTIALS, + KEY_HASHES, + KEY_HOSTS, + KEY_USERS, + KEY_SHARES, + KEY_DOMAINS, + KEY_VULNS, + KEY_EXPLOITED, + KEY_META, + KEY_DC_MAP, + KEY_NETBIOS_MAP, + KEY_ARTIFACTS, + KEY_TIMELINE, + KEY_GOLDEN_TICKETS, + KEY_ADMINSD_BACKDOORS, + KEY_ACL_CHAINS, + KEY_GMSA_ACCOUNTS, + KEY_TECHNIQUES, + KEY_MSSQL_ENUM_DISPATCHED, + KEY_PENDING_TASKS, + KEY_COMPLETED_TASKS, + KEY_VULN_TYPE_FAILURES, + KEY_DOMAIN_SIDS, + KEY_ADMIN_NAMES, + KEY_TRUSTED_DOMAINS, + KEY_STATUS, + KEY_MODEL, + KEY_STOP_REQUESTED, + ]; + let mut seen = std::collections::HashSet::new(); + for s in &suffixes { + assert!(seen.insert(*s), "Duplicate key suffix: {s}"); + } + } + + #[cfg(feature = "blue")] + #[test] + fn blue_key_prefixes() { + assert_eq!(BLUE_KEY_PREFIX, "ares:blue:inv"); + assert_eq!(BLUE_LOCK_PREFIX, "ares:blue:lock"); + assert_eq!(BLUE_TASK_QUEUE_PREFIX, "ares:blue:tasks"); + assert_eq!(BLUE_RESULT_QUEUE_PREFIX, "ares:blue:results"); + assert_eq!(BLUE_HEARTBEAT_PREFIX, "ares:blue:heartbeat"); + } + + #[cfg(feature = "blue")] + #[test] + fn blue_collection_suffixes_non_empty() { + let suffixes = [ + BLUE_KEY_EVIDENCE, + BLUE_KEY_TIMELINE, + BLUE_KEY_TECHNIQUES, + BLUE_KEY_TACTICS, + BLUE_KEY_HOSTS, + BLUE_KEY_USERS, + BLUE_KEY_QUERY_TYPES, + BLUE_KEY_META, + BLUE_KEY_PENDING_TASKS, + BLUE_KEY_COMPLETED_TASKS, + BLUE_KEY_TECHNIQUE_NAMES, + BLUE_KEY_RECOMMENDATIONS, + BLUE_KEY_TRIAGE_DECISION, + BLUE_KEY_TRIAGE_RECORDS, + BLUE_KEY_QUERIES, + BLUE_KEY_LATERAL, + BLUE_KEY_PIVOT_QUEUE, + BLUE_KEY_CHAIN_QUEUE, + ]; + for suffix in &suffixes { + assert!(!suffix.is_empty(), "Blue key suffix must not be empty"); + } + } +} diff --git a/ares-core/src/state/mod.rs b/ares-core/src/state/mod.rs index 5524ca18..6b19e90d 100644 --- a/ares-core/src/state/mod.rs +++ b/ares-core/src/state/mod.rs @@ -115,7 +115,7 @@ mod tests { use crate::models::Credential; #[test] - fn test_build_key() { + fn builds_key() { assert_eq!(build_key("op-123", "meta"), "ares:op:op-123:meta"); assert_eq!( build_key("op-123", "credentials"), @@ -124,12 +124,12 @@ mod tests { } #[test] - fn test_build_lock_key() { + fn builds_lock_key() { assert_eq!(build_lock_key("op-123"), "ares:lock:op-123"); } #[test] - fn test_credential_dedup_key() { + fn credential_dedup_key() { let cred = Credential { id: "test".to_string(), username: "TestUser".to_string(), diff --git a/ares-core/src/telemetry/mitre.rs b/ares-core/src/telemetry/mitre.rs index 7fc08305..125d392e 100644 --- a/ares-core/src/telemetry/mitre.rs +++ b/ares-core/src/telemetry/mitre.rs @@ -410,7 +410,7 @@ mod tests { use super::*; #[test] - fn test_role_tactic_mappings() { + fn role_tactic_mappings() { assert_eq!(ROLE_TO_TACTIC.get("recon"), Some(&"discovery")); assert_eq!( ROLE_TO_TACTIC.get("credential_access"), @@ -420,7 +420,7 @@ mod tests { } #[test] - fn test_tool_to_technique() { + fn tool_to_technique() { assert_eq!(TOOL_TO_TECHNIQUE.get("nmap_scan"), Some(&"T1046")); assert_eq!(TOOL_TO_TECHNIQUE.get("secretsdump"), Some(&"T1003.006")); assert_eq!(TOOL_TO_TECHNIQUE.get("psexec"), Some(&"T1021.002")); @@ -441,7 +441,7 @@ mod tests { } #[test] - fn test_tool_to_category() { + fn tool_to_category() { assert_eq!( TOOL_TO_CATEGORY.get("nmap_scan"), Some(&"NetworkEnumerationTools") @@ -462,7 +462,7 @@ mod tests { } #[test] - fn test_tactic_from_technique() { + fn resolves_tactic_from_technique() { assert_eq!(tactic_from_technique("T1046"), Some("discovery")); assert_eq!( tactic_from_technique("T1003.006"), @@ -475,7 +475,7 @@ mod tests { } #[test] - fn test_get_tool_mitre_info() { + fn gets_tool_mitre_info() { let (tech, tactic) = get_tool_mitre_info("kerberoast"); assert_eq!(tech, Some("T1558.003")); assert_eq!(tactic, Some("credential-access")); @@ -486,7 +486,7 @@ mod tests { } #[test] - fn test_get_tool_category() { + fn gets_tool_category() { assert_eq!( get_tool_category("secretsdump"), Some("CredentialHarvestingTools") diff --git a/ares-core/src/telemetry/spans/mod.rs b/ares-core/src/telemetry/spans/mod.rs index 5b69ffdb..880a8e70 100644 --- a/ares-core/src/telemetry/spans/mod.rs +++ b/ares-core/src/telemetry/spans/mod.rs @@ -89,7 +89,7 @@ mod tests { } #[test] - fn test_agent_span_builder_basic() { + fn agent_span_builder_basic() { init_test_subscriber(); let span = AgentSpanBuilder::new("test_op", "recon", Team::Red) .tool("nmap_scan") @@ -102,7 +102,7 @@ mod tests { } #[test] - fn test_trace_tool_call() { + fn traces_tool_call() { init_test_subscriber(); let span = trace_tool_call( "credential_access", @@ -120,7 +120,7 @@ mod tests { } #[test] - fn test_trace_discovery() { + fn traces_discovery() { init_test_subscriber(); let span = trace_discovery( "credential", @@ -136,7 +136,7 @@ mod tests { } #[test] - fn test_trace_decision() { + fn traces_decision() { init_test_subscriber(); let tools = vec!["nmap_scan".to_string(), "smb_sweep".to_string()]; let span = trace_decision("recon", Team::Red, "nmap_scan", &tools, Some(0.9), None); @@ -144,7 +144,7 @@ mod tests { } #[test] - fn test_service_graph_spans() { + fn service_graph_spans() { init_test_subscriber(); let c = client_span("dispatch", "orchestrator", Team::Red, "ares-recon-agent"); assert!(!c.is_disabled()); @@ -165,7 +165,7 @@ mod tests { } #[test] - fn test_error_span() { + fn error_span() { init_test_subscriber(); let span = AgentSpanBuilder::new("tool_call", "lateral", Team::Red) .tool("psexec") diff --git a/ares-core/src/telemetry/target.rs b/ares-core/src/telemetry/target.rs index af0b5270..d7fd9f26 100644 --- a/ares-core/src/telemetry/target.rs +++ b/ares-core/src/telemetry/target.rs @@ -127,39 +127,39 @@ mod tests { use super::*; #[test] - fn test_infer_target_type_dc() { + fn infer_target_type_dc() { assert_eq!(infer_target_type("dc01.contoso.local"), "domain_controller"); assert_eq!(infer_target_type("DC02"), "domain_controller"); } #[test] - fn test_infer_target_type_sql() { + fn infer_target_type_sql() { assert_eq!(infer_target_type("sql01.contoso.local"), "sql_server"); assert_eq!(infer_target_type("mssql.contoso.local"), "sql_server"); assert_eq!(infer_target_type("db01"), "sql_server"); } #[test] - fn test_infer_target_type_web() { + fn infer_target_type_web() { assert_eq!(infer_target_type("web01.contoso.local"), "web_server"); assert_eq!(infer_target_type("iis01"), "web_server"); } #[test] - fn test_infer_target_type_workstation() { + fn infer_target_type_workstation() { assert_eq!(infer_target_type("ws01.contoso.local"), "workstation"); assert_eq!(infer_target_type("pc01"), "workstation"); assert_eq!(infer_target_type("desktop-user1"), "workstation"); } #[test] - fn test_infer_target_type_server_fallback() { + fn infer_target_type_server_fallback() { assert_eq!(infer_target_type("fileserver01.contoso.local"), "server"); assert_eq!(infer_target_type("app01"), "server"); } #[test] - fn test_extract_target_info_ip() { + fn extract_target_info_ip() { let args = serde_json::json!({"target_ip": "192.168.58.10", "username": "admin"}); let info = extract_target_info(&args); assert_eq!(info.target_ip.as_deref(), Some("192.168.58.10")); @@ -167,7 +167,7 @@ mod tests { } #[test] - fn test_extract_target_info_fqdn() { + fn extract_target_info_fqdn() { let args = serde_json::json!({"target": "dc01.contoso.local"}); let info = extract_target_info(&args); assert_eq!(info.target_fqdn.as_deref(), Some("dc01.contoso.local")); @@ -175,7 +175,7 @@ mod tests { } #[test] - fn test_extract_target_info_ip_in_target() { + fn extract_target_info_ip_in_target() { let args = serde_json::json!({"target": "192.168.58.10"}); let info = extract_target_info(&args); assert_eq!(info.target_ip.as_deref(), Some("192.168.58.10")); @@ -183,7 +183,7 @@ mod tests { } #[test] - fn test_infer_from_info_fqdn() { + fn infer_from_info_fqdn() { let info = ToolTargetInfo { target_fqdn: Some("dc01.contoso.local".to_string()), target_user: Some("admin".to_string()), @@ -196,7 +196,7 @@ mod tests { } #[test] - fn test_infer_from_info_user_only() { + fn infer_from_info_user_only() { let info = ToolTargetInfo { target_user: Some("svc_backup".to_string()), ..Default::default() @@ -205,7 +205,7 @@ mod tests { } #[test] - fn test_infer_from_info_nothing() { + fn infer_from_info_nothing() { let info = ToolTargetInfo::default(); assert_eq!(infer_target_type_from_info(&info), None); } diff --git a/ares-core/src/token_usage.rs b/ares-core/src/token_usage.rs index 33968afd..f4777d6e 100644 --- a/ares-core/src/token_usage.rs +++ b/ares-core/src/token_usage.rs @@ -409,7 +409,7 @@ mod tests { use super::*; #[test] - fn test_model_field_roundtrip() { + fn model_field_roundtrip() { let field = model_field("openai/gpt-4.1-mini", "input_tokens"); assert!(field.starts_with("model:")); assert!(field.ends_with(":input_tokens")); @@ -420,7 +420,7 @@ mod tests { } #[test] - fn test_model_field_with_slashes_and_dots() { + fn model_field_with_slashes_and_dots() { // Ensure models with special chars survive encoding let names = [ "anthropic/claude-sonnet-4-20250514", @@ -436,14 +436,14 @@ mod tests { } #[test] - fn test_parse_non_model_fields() { + fn parse_non_model_fields() { assert!(parse_model_field("input_tokens").is_none()); assert!(parse_model_field("output_tokens").is_none()); assert!(parse_model_field("model").is_none()); } #[test] - fn test_estimate_usage_cost_single_model() { + fn estimate_usage_cost_single_model() { let usage = OperationTokenUsage { input_tokens: 1_000_000, output_tokens: 500_000, @@ -468,7 +468,7 @@ mod tests { } #[test] - fn test_estimate_usage_cost_multi_model() { + fn estimate_usage_cost_multi_model() { let usage = OperationTokenUsage { input_tokens: 2_000_000, output_tokens: 1_000_000, @@ -502,7 +502,7 @@ mod tests { } #[test] - fn test_estimate_usage_cost_unknown_model() { + fn estimate_usage_cost_unknown_model() { let usage = OperationTokenUsage { input_tokens: 100, output_tokens: 50, @@ -523,7 +523,7 @@ mod tests { } #[test] - fn test_estimate_usage_cost_empty() { + fn estimate_usage_cost_empty() { let usage = OperationTokenUsage::default(); let (total, breakdown, unpriced) = estimate_usage_cost(&usage); assert!(total.is_none()); @@ -532,9 +532,9 @@ mod tests { } #[test] - fn test_token_usage_key() { + fn token_usage_key_basic() { assert_eq!( - token_usage_key("op-abc-123"), + super::token_usage_key("op-abc-123"), "ares:op:op-abc-123:token_usage" ); } @@ -550,8 +550,7 @@ mod tests { #[test] fn lookup_model_cost_exact_match() { let result = lookup_model_cost("gpt-4o"); - assert!(result.is_some()); - let (input, output) = result.unwrap(); + let (input, output) = result.expect("gpt-4o should have known cost"); assert!((input - 2.50).abs() < 0.001); assert!((output - 10.0).abs() < 0.001); } @@ -603,8 +602,6 @@ mod tests { assert_eq!(breakdown[0].output_tokens, 500_000); } - // ─── TokenUsage struct ────────────────────────────────────────────────── - #[test] fn token_usage_default() { let t = TokenUsage::default(); @@ -649,8 +646,6 @@ mod tests { assert!(t.model.is_none()); } - // ─── OperationTokenUsage / ModelTokenUsage defaults ───────────────────── - #[test] fn operation_token_usage_default() { let o = OperationTokenUsage::default(); @@ -667,8 +662,6 @@ mod tests { assert_eq!(m.output_tokens, 0); } - // ─── lookup_model_cost variations ─────────────────────────────────────── - #[test] fn lookup_model_cost_all_known_models() { // Verify every model in the pricing table can be looked up @@ -719,8 +712,6 @@ mod tests { assert!((output - 0.40).abs() < 0.001); } - // ─── model_field edge cases ───────────────────────────────────────────── - #[test] fn model_field_empty_model_name() { let field = model_field("", "input_tokens"); @@ -746,8 +737,6 @@ mod tests { assert!(result.is_none()); } - // ─── estimate_usage_cost with mixed priced/unpriced ───────────────────── - #[test] fn estimate_usage_cost_mixed_models() { let usage = OperationTokenUsage { @@ -808,8 +797,6 @@ mod tests { assert_eq!(breakdown[1].model, "gpt-4o"); } - // ─── Key format tests ─────────────────────────────────────────────────── - #[test] fn token_usage_key_format_various() { assert_eq!(token_usage_key("op-123"), "ares:op:op-123:token_usage"); @@ -825,8 +812,6 @@ mod tests { assert_eq!(blue_token_usage_key(""), "ares:blue:inv::token_usage"); } - // ─── ModelCostBreakdown serialization ─────────────────────────────────── - #[test] fn model_cost_breakdown_serialize() { let b = ModelCostBreakdown { @@ -842,8 +827,6 @@ mod tests { assert!((json["cost"].as_f64().unwrap() - 0.006).abs() < 0.0001); } - // ─── OperationTokenUsage serialization ────────────────────────────────── - #[test] fn operation_token_usage_serialize() { let usage = OperationTokenUsage { @@ -865,8 +848,6 @@ mod tests { assert!(json["models"]["gpt-4o"].is_object()); } - // ─── Zero-cost edge case ──────────────────────────────────────────────── - #[test] fn estimate_usage_cost_zero_tokens_known_model() { let usage = OperationTokenUsage { @@ -882,9 +863,97 @@ mod tests { )]), }; let (total, breakdown, unpriced) = estimate_usage_cost(&usage); - assert!(total.is_some()); - assert_eq!(total.unwrap(), 0.0); + assert_eq!(total.expect("total should be set"), 0.0); + assert_eq!(breakdown.len(), 1); + assert!(unpriced.is_empty()); + } + + #[test] + fn blue_token_usage_key_with_dashes() { + assert_eq!( + blue_token_usage_key("inv-123"), + "ares:blue:inv:inv-123:token_usage" + ); + } + + #[test] + fn estimate_usage_cost_empty_models() { + let usage = OperationTokenUsage { + input_tokens: 100, + output_tokens: 50, + model: "gpt-4o".to_string(), + models: HashMap::new(), + }; + let (total, breakdown, unpriced) = estimate_usage_cost(&usage); + assert!(total.is_none()); + assert!(breakdown.is_empty()); + assert!(unpriced.is_empty()); + } + + #[test] + fn estimate_usage_cost_all_unpriced() { + let usage = OperationTokenUsage { + input_tokens: 1000, + output_tokens: 500, + model: "unknown".to_string(), + models: HashMap::from([( + "unknown-model".to_string(), + ModelTokenUsage { + input_tokens: 1000, + output_tokens: 500, + }, + )]), + }; + let (total, breakdown, unpriced) = estimate_usage_cost(&usage); + assert!(total.is_none()); + assert!(breakdown.is_empty()); + assert_eq!(unpriced.len(), 1); + } + + #[test] + fn estimate_usage_cost_single_priced_model() { + let usage = OperationTokenUsage { + input_tokens: 1_000_000, + output_tokens: 500_000, + model: "gpt-4o".to_string(), + models: HashMap::from([( + "gpt-4o".to_string(), + ModelTokenUsage { + input_tokens: 1_000_000, + output_tokens: 500_000, + }, + )]), + }; + let (total, breakdown, unpriced) = estimate_usage_cost(&usage); + let cost = total.expect("total should be set"); + // gpt-4o: 2.50/M input + 10.0/M output + // 1M * 2.50/1M + 0.5M * 10.0/1M = 2.50 + 5.0 = 7.50 + assert!((cost - 7.50).abs() < 0.01); assert_eq!(breakdown.len(), 1); assert!(unpriced.is_empty()); } + + #[test] + fn lookup_model_cost_prefixed_openai() { + let result = lookup_model_cost("openai/gpt-4o-mini"); + let (input, output) = result.expect("gpt-4o-mini should have known cost"); + assert!((input - 0.15).abs() < 0.001); + assert!((output - 0.60).abs() < 0.001); + } + + #[test] + fn lookup_model_cost_claude_opus() { + let result = lookup_model_cost("claude-opus-4-20250514"); + let (input, output) = result.expect("claude-opus should have known cost"); + assert!((input - 15.0).abs() < 0.001); + assert!((output - 75.0).abs() < 0.001); + } + + #[test] + fn lookup_model_cost_haiku() { + let result = lookup_model_cost("claude-haiku-3-5-20241022"); + let (input, output) = result.expect("claude-haiku should have known cost"); + assert!((input - 0.80).abs() < 0.001); + assert!((output - 4.0).abs() < 0.001); + } } diff --git a/ares-llm/src/agent_loop/callbacks.rs b/ares-llm/src/agent_loop/callbacks.rs index 53b841f3..a64c1317 100644 --- a/ares-llm/src/agent_loop/callbacks.rs +++ b/ares-llm/src/agent_loop/callbacks.rs @@ -203,7 +203,7 @@ mod tests { } #[test] - fn test_list_credentials_fallback() { + fn list_credentials_fallback() { let call = make_call("list_credentials", serde_json::json!({})); let result = handle_builtin_callback(&call).unwrap(); match result { @@ -216,7 +216,7 @@ mod tests { } #[test] - fn test_task_complete_string_result() { + fn task_complete_string_result() { let call = make_call( "task_complete", serde_json::json!({"task_id": "t-123", "result": "done"}), @@ -232,7 +232,7 @@ mod tests { } #[test] - fn test_task_complete_json_result() { + fn task_complete_json_result() { let call = make_call( "task_complete", serde_json::json!({"task_id": "t-456", "result": {"status": "success"}}), @@ -248,7 +248,7 @@ mod tests { } #[test] - fn test_request_assistance() { + fn request_assistance() { let call = make_call( "request_assistance", serde_json::json!({"issue": "stuck", "context": "ldap failed"}), @@ -264,7 +264,7 @@ mod tests { } #[test] - fn test_record_credential_disabled() { + fn record_credential_disabled() { let call = make_call( "record_credential", serde_json::json!({ @@ -283,7 +283,7 @@ mod tests { } #[test] - fn test_orchestrator_only_tools() { + fn orchestrator_only_tools() { for tool_name in [ "get_credential_summary", "get_hash_summary", @@ -302,7 +302,7 @@ mod tests { } #[test] - fn test_unknown_callback() { + fn unknown_callback() { let call = make_call("nonexistent_tool", serde_json::json!({})); let result = handle_builtin_callback(&call); assert!(result.is_err()); diff --git a/ares-llm/src/agent_loop/config.rs b/ares-llm/src/agent_loop/config.rs index c9998392..38b66d93 100644 --- a/ares-llm/src/agent_loop/config.rs +++ b/ares-llm/src/agent_loop/config.rs @@ -77,3 +77,34 @@ impl Default for RetryConfig { } } } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn agent_loop_config_defaults() { + let cfg = AgentLoopConfig::default(); + assert_eq!(cfg.model, "claude-sonnet-4-20250514"); + assert_eq!(cfg.max_steps, 75); + assert_eq!(cfg.max_tokens, 4096); + assert!(cfg.temperature.is_none()); + assert_eq!(cfg.max_tool_calls_per_name, 10); + } + + #[test] + fn context_config_defaults() { + let cfg = ContextConfig::default(); + assert_eq!(cfg.max_context_tokens, 180_000); + assert_eq!(cfg.max_tool_output_chars, 30_000); + assert_eq!(cfg.min_recent_messages, 10); + } + + #[test] + fn retry_config_defaults() { + let cfg = RetryConfig::default(); + assert_eq!(cfg.max_retries, 5); + assert_eq!(cfg.base_delay_ms, 1_000); + assert_eq!(cfg.max_delay_ms, 60_000); + } +} diff --git a/ares-llm/src/agent_loop/context.rs b/ares-llm/src/agent_loop/context.rs index 53f8878e..a3686eca 100644 --- a/ares-llm/src/agent_loop/context.rs +++ b/ares-llm/src/agent_loop/context.rs @@ -188,3 +188,174 @@ pub(super) fn has_tool_calls(msg: &ChatMessage) -> bool { } false } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn estimate_tokens_empty() { + assert_eq!(estimate_tokens(""), 0); + } + + #[test] + fn estimate_tokens_short_string() { + // 12 bytes -> ceil(12/4) = 3 + assert_eq!(estimate_tokens("hello world!"), 3); + } + + #[test] + fn estimate_tokens_exact_multiple() { + // 8 bytes -> 8/4 = 2 + assert_eq!(estimate_tokens("abcdefgh"), 2); + } + + #[test] + fn estimate_tokens_rounds_up() { + // 5 bytes -> ceil(5/4) = 2 + assert_eq!(estimate_tokens("abcde"), 2); + } + + #[test] + fn estimate_message_tokens_text_content() { + let msg = ChatMessage::text(Role::User, "hello"); + // 4 (overhead) + ceil(5/4) = 4 + 2 = 6 + assert_eq!(estimate_message_tokens(&msg), 6); + } + + #[test] + fn estimate_message_tokens_no_content() { + let msg = ChatMessage { + role: Role::Assistant, + content: None, + parts: None, + }; + assert_eq!(estimate_message_tokens(&msg), 4); + } + + #[test] + fn truncate_tool_output_short_unchanged() { + let output = "short output"; + let result = truncate_tool_output(output, 100); + assert_eq!(result, output); + } + + #[test] + fn truncate_tool_output_zero_max_unchanged() { + let output = "any output"; + let result = truncate_tool_output(output, 0); + assert_eq!(result, output); + } + + #[test] + fn truncate_tool_output_long_truncated() { + let output = "a".repeat(1000); + let result = truncate_tool_output(&output, 200); + assert!(result.len() < 1000); + assert!(result.contains("truncated")); + } + + #[test] + fn truncate_tool_output_preserves_head_and_tail() { + let head = "HEAD".repeat(50); + let middle = "M".repeat(600); + let tail = "TAIL".repeat(50); + let output = format!("{head}{middle}{tail}"); + let result = truncate_tool_output(&output, 300); + assert!(result.starts_with("HEAD")); + assert!(result.ends_with("TAIL")); + } + + #[test] + fn is_tool_result_tool_role() { + let msg = ChatMessage { + role: Role::Tool, + content: Some("result".to_string()), + parts: None, + }; + assert!(is_tool_result(&msg)); + } + + #[test] + fn is_tool_result_user_no_parts() { + let msg = ChatMessage::text(Role::User, "hello"); + assert!(!is_tool_result(&msg)); + } + + #[test] + fn is_tool_result_user_with_tool_result_part() { + let msg = ChatMessage { + role: Role::User, + content: None, + parts: Some(vec![ContentPart::ToolResult { + tool_use_id: "id1".to_string(), + content: "output".to_string(), + }]), + }; + assert!(is_tool_result(&msg)); + } + + #[test] + fn has_tool_calls_assistant_with_tool_use() { + let msg = ChatMessage { + role: Role::Assistant, + content: None, + parts: Some(vec![ContentPart::ToolUse { + id: "id1".to_string(), + name: "tool".to_string(), + input: serde_json::json!({}), + }]), + }; + assert!(has_tool_calls(&msg)); + } + + #[test] + fn has_tool_calls_user_role_false() { + let msg = ChatMessage { + role: Role::User, + content: None, + parts: Some(vec![ContentPart::ToolUse { + id: "id1".to_string(), + name: "tool".to_string(), + input: serde_json::json!({}), + }]), + }; + assert!(!has_tool_calls(&msg)); + } + + #[test] + fn has_tool_calls_assistant_no_parts() { + let msg = ChatMessage::text(Role::Assistant, "hello"); + assert!(!has_tool_calls(&msg)); + } + + #[test] + fn trim_conversation_no_limit() { + let config = ContextConfig { + max_context_tokens: 0, + max_tool_output_chars: 30_000, + min_recent_messages: 10, + }; + let mut msgs = vec![ + ChatMessage::text(Role::User, "first"), + ChatMessage::text(Role::Assistant, "second"), + ]; + trim_conversation(&mut msgs, "system", &[], &config); + assert_eq!(msgs.len(), 2); + } + + #[test] + fn trim_conversation_under_budget_unchanged() { + let config = ContextConfig { + max_context_tokens: 100_000, + max_tool_output_chars: 30_000, + min_recent_messages: 2, + }; + let mut msgs = vec![ + ChatMessage::text(Role::User, "first"), + ChatMessage::text(Role::Assistant, "second"), + ]; + trim_conversation(&mut msgs, "sys", &[], &config); + assert_eq!(msgs.len(), 2); + } +} diff --git a/ares-llm/src/agent_loop/retry.rs b/ares-llm/src/agent_loop/retry.rs index 6e14d2ca..4ef8493b 100644 --- a/ares-llm/src/agent_loop/retry.rs +++ b/ares-llm/src/agent_loop/retry.rs @@ -71,3 +71,43 @@ pub(super) fn simple_hash(attempt: u32, task_id: &str) -> u64 { h = h.wrapping_mul(0x100000001b3); h } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn simple_hash_deterministic() { + let h1 = simple_hash(0, "task-123"); + let h2 = simple_hash(0, "task-123"); + assert_eq!(h1, h2); + } + + #[test] + fn simple_hash_different_attempts() { + let h0 = simple_hash(0, "task-abc"); + let h1 = simple_hash(1, "task-abc"); + assert_ne!(h0, h1); + } + + #[test] + fn simple_hash_different_task_ids() { + let ha = simple_hash(0, "task-a"); + let hb = simple_hash(0, "task-b"); + assert_ne!(ha, hb); + } + + #[test] + fn simple_hash_empty_task_id() { + // Should not panic + let h = simple_hash(0, ""); + assert_ne!(h, 0); + } + + #[test] + fn simple_hash_large_attempt() { + // Should not panic or overflow + let h = simple_hash(u32::MAX, "task-xyz"); + assert_ne!(h, 0); + } +} diff --git a/ares-llm/src/agent_loop/tests.rs b/ares-llm/src/agent_loop/tests.rs index 462474a6..3584ac73 100644 --- a/ares-llm/src/agent_loop/tests.rs +++ b/ares-llm/src/agent_loop/tests.rs @@ -6,7 +6,7 @@ use super::types::CallbackResult; use crate::provider::{ChatMessage, LlmError, Role, ToolCall}; #[test] -fn test_handle_task_complete_callback() { +fn handle_task_complete_callback() { let call = ToolCall { id: "call_1".into(), name: "task_complete".into(), @@ -26,7 +26,7 @@ fn test_handle_task_complete_callback() { } #[test] -fn test_handle_request_assistance_callback() { +fn handle_request_assistance_callback() { let call = ToolCall { id: "call_2".into(), name: "request_assistance".into(), @@ -46,7 +46,7 @@ fn test_handle_request_assistance_callback() { } #[test] -fn test_handle_report_finding_callback() { +fn handle_report_finding_callback() { let call = ToolCall { id: "call_3".into(), name: "report_finding".into(), @@ -65,7 +65,7 @@ fn test_handle_report_finding_callback() { } #[test] -fn test_unknown_callback() { +fn unknown_callback() { let call = ToolCall { id: "call_x".into(), name: "unknown_callback".into(), @@ -75,7 +75,7 @@ fn test_unknown_callback() { } #[test] -fn test_agent_loop_config_defaults() { +fn agent_loop_config_defaults() { let config = AgentLoopConfig::default(); assert_eq!(config.max_steps, 75); assert_eq!(config.max_tokens, 4096); @@ -85,7 +85,7 @@ fn test_agent_loop_config_defaults() { } #[test] -fn test_retry_config_defaults() { +fn retry_config_defaults() { let config = RetryConfig::default(); assert_eq!(config.max_retries, 5); assert_eq!(config.base_delay_ms, 1_000); @@ -93,7 +93,7 @@ fn test_retry_config_defaults() { } #[test] -fn test_llm_error_retryable() { +fn llm_error_retryable() { assert!(LlmError::RateLimited { retry_after_ms: Some(1000) } @@ -119,7 +119,7 @@ fn test_llm_error_retryable() { } #[test] -fn test_llm_error_retry_after() { +fn llm_error_retry_after() { assert_eq!( LlmError::RateLimited { retry_after_ms: Some(5000) @@ -138,7 +138,7 @@ fn test_llm_error_retry_after() { } #[test] -fn test_simple_hash_deterministic() { +fn simple_hash_deterministic() { let h1 = simple_hash(0, "task-001"); let h2 = simple_hash(0, "task-001"); assert_eq!(h1, h2); @@ -153,26 +153,26 @@ fn test_simple_hash_deterministic() { // Context management tests #[test] -fn test_estimate_tokens() { +fn estimates_tokens() { assert_eq!(estimate_tokens(""), 0); // (0 + 3) / 4 = 0 assert_eq!(estimate_tokens("hello"), 2); // (5 + 3) / 4 = 2 assert_eq!(estimate_tokens(&"a".repeat(400)), 100); // (400 + 3) / 4 = 100 } #[test] -fn test_truncate_tool_output_short() { +fn truncate_tool_output_short() { let output = "short output"; assert_eq!(truncate_tool_output(output, 100), output); } #[test] -fn test_truncate_tool_output_no_limit() { +fn truncate_tool_output_no_limit() { let output = "a".repeat(100_000); assert_eq!(truncate_tool_output(&output, 0), output); } #[test] -fn test_truncate_tool_output_long() { +fn truncate_tool_output_long() { let output = "a".repeat(50_000); let truncated = truncate_tool_output(&output, 1000); assert!(truncated.len() < 1200); // Slightly over due to notice @@ -182,7 +182,7 @@ fn test_truncate_tool_output_long() { } #[test] -fn test_context_config_defaults() { +fn context_config_defaults() { let config = ContextConfig::default(); assert_eq!(config.max_context_tokens, 180_000); assert_eq!(config.max_tool_output_chars, 30_000); @@ -190,7 +190,7 @@ fn test_context_config_defaults() { } #[test] -fn test_trim_conversation_under_limit() { +fn trim_conversation_under_limit() { let mut messages = vec![ ChatMessage::text(Role::User, "task prompt"), ChatMessage::text(Role::Assistant, "I'll scan."), @@ -207,7 +207,7 @@ fn test_trim_conversation_under_limit() { } #[test] -fn test_trim_conversation_disabled() { +fn trim_conversation_disabled() { let mut messages = vec![ChatMessage::text( Role::User, "a".repeat(1_000_000).as_str(), @@ -222,7 +222,7 @@ fn test_trim_conversation_disabled() { } #[test] -fn test_trim_conversation_drops_middle() { +fn trim_conversation_drops_middle() { // Create a conversation that exceeds the limit let mut messages = Vec::new(); messages.push(ChatMessage::text(Role::User, "task prompt")); diff --git a/ares-llm/src/prompt/helpers.rs b/ares-llm/src/prompt/helpers.rs index 78fc9142..3d218649 100644 --- a/ares-llm/src/prompt/helpers.rs +++ b/ares-llm/src/prompt/helpers.rs @@ -129,43 +129,43 @@ mod tests { // --- is_pass_the_hash_compatible --- #[test] - fn test_pth_compat_lm_nt() { + fn pth_compat_lm_nt() { assert!(is_pass_the_hash_compatible(Some( "aad3b435b51404eeaad3b435b51404ee:313b6f423a71d74c0a1b8a2f43b22d4c" ))); } #[test] - fn test_pth_compat_nt_only() { + fn pth_compat_nt_only() { assert!(is_pass_the_hash_compatible(Some( "313b6f423a71d74c0a1b8a2f43b22d4c" ))); } #[test] - fn test_pth_compat_none() { + fn pth_compat_none() { assert!(!is_pass_the_hash_compatible(None)); } #[test] - fn test_pth_compat_empty() { + fn pth_compat_empty() { assert!(!is_pass_the_hash_compatible(Some(""))); } #[test] - fn test_pth_compat_kerberos_hash() { + fn pth_compat_kerberos_hash() { assert!(!is_pass_the_hash_compatible(Some( "$krb5tgs$23$*svc_sql$contoso.local" ))); } #[test] - fn test_pth_compat_multiple_colons() { + fn pth_compat_multiple_colons() { assert!(!is_pass_the_hash_compatible(Some("aad3:b435:b514"))); } #[test] - fn test_pth_compat_lm_empty_nt_valid() { + fn pth_compat_lm_empty_nt_valid() { // Empty LM part with valid NT assert!(is_pass_the_hash_compatible(Some( ":313b6f423a71d74c0a1b8a2f43b22d4c" @@ -175,21 +175,21 @@ mod tests { // --- payload_techniques --- #[test] - fn test_payload_techniques_present() { + fn payload_techniques_present() { let payload = json!({"techniques": ["network_scan", "user_enumeration"]}); let techs = payload_techniques(&payload); assert_eq!(techs, vec!["network_scan", "user_enumeration"]); } #[test] - fn test_payload_techniques_missing() { + fn payload_techniques_missing() { let payload = json!({"target": "192.168.58.10"}); let techs = payload_techniques(&payload); assert!(techs.is_empty()); } #[test] - fn test_payload_techniques_empty_array() { + fn payload_techniques_empty_array() { let payload = json!({"techniques": []}); let techs = payload_techniques(&payload); assert!(techs.is_empty()); @@ -198,25 +198,25 @@ mod tests { // --- cred_param_str --- #[test] - fn test_cred_param_str_password() { + fn cred_param_str_password() { let payload = json!({"password": "P@ss1"}); assert_eq!(cred_param_str(&payload, None), "password='P@ss1'"); } #[test] - fn test_cred_param_str_nested_password() { + fn cred_param_str_nested_password() { let payload = json!({"credential": {"username": "admin", "domain": "contoso.local", "password": "Summer2025"}}); assert_eq!(cred_param_str(&payload, None), "password='Summer2025'"); } #[test] - fn test_cred_param_str_nested_takes_precedence() { + fn cred_param_str_nested_takes_precedence() { let payload = json!({"password": "flat", "credential": {"password": "nested"}}); assert_eq!(cred_param_str(&payload, None), "password='nested'"); } #[test] - fn test_cred_param_str_hash() { + fn cred_param_str_hash() { let payload = json!({}); assert_eq!( cred_param_str(&payload, Some("aabbccdd")), @@ -225,19 +225,19 @@ mod tests { } #[test] - fn test_cred_param_str_fallback() { + fn cred_param_str_fallback() { let payload = json!({}); assert_eq!(cred_param_str(&payload, None), "password='N/A'"); } #[test] - fn test_cred_param_str_empty_password_uses_hash() { + fn cred_param_str_empty_password_uses_hash() { let payload = json!({"password": ""}); assert_eq!(cred_param_str(&payload, Some("aabb")), "hashes='aabb'"); } #[test] - fn test_cred_param_str_nested_empty_uses_hash() { + fn cred_param_str_nested_empty_uses_hash() { let payload = json!({"credential": {"password": ""}}); assert_eq!(cred_param_str(&payload, Some("aabb")), "hashes='aabb'"); } @@ -245,19 +245,19 @@ mod tests { // --- cred_display_str --- #[test] - fn test_cred_display_str_password() { + fn cred_display_str_password() { let payload = json!({"password": "Secret123"}); assert_eq!(cred_display_str(&payload, None), "Secret123"); } #[test] - fn test_cred_display_str_nested_password() { + fn cred_display_str_nested_password() { let payload = json!({"credential": {"password": "Summer2025"}}); assert_eq!(cred_display_str(&payload, None), "Summer2025"); } #[test] - fn test_cred_display_str_hash() { + fn cred_display_str_hash() { let payload = json!({}); assert_eq!( cred_display_str(&payload, Some("aabbccdd")), @@ -266,7 +266,7 @@ mod tests { } #[test] - fn test_cred_display_str_fallback() { + fn cred_display_str_fallback() { let payload = json!({}); assert_eq!(cred_display_str(&payload, None), "N/A"); } @@ -274,7 +274,7 @@ mod tests { // --- insert_credential_context --- #[test] - fn test_insert_credential_context_with_password() { + fn insert_credential_context_with_password() { let payload = json!({ "credential": { "username": "admin", @@ -292,7 +292,7 @@ mod tests { } #[test] - fn test_insert_credential_context_with_hash() { + fn insert_credential_context_with_hash() { let payload = json!({ "credential": { "username": "admin", @@ -306,7 +306,7 @@ mod tests { } #[test] - fn test_insert_credential_context_no_cred() { + fn insert_credential_context_no_cred() { let payload = json!({"target": "192.168.58.10"}); let mut ctx = Context::new(); insert_credential_context(&mut ctx, &payload); diff --git a/ares-llm/src/prompt/state_context.rs b/ares-llm/src/prompt/state_context.rs index 62545022..142e31bd 100644 --- a/ares-llm/src/prompt/state_context.rs +++ b/ares-llm/src/prompt/state_context.rs @@ -181,14 +181,14 @@ mod tests { } #[test] - fn test_format_state_context_empty() { + fn format_state_context_empty() { let snap = make_snapshot(); let ctx = format_state_context(&snap, "recon", None); assert!(ctx.is_empty()); } #[test] - fn test_format_state_context_domains() { + fn format_state_context_domains() { let mut snap = make_snapshot(); snap.domains = vec!["contoso.local".to_string()]; let ctx = format_state_context(&snap, "recon", None); @@ -197,7 +197,7 @@ mod tests { } #[test] - fn test_format_state_context_credentials_shown_for_lateral() { + fn format_state_context_credentials_shown_for_lateral() { let mut snap = make_snapshot(); snap.credentials = vec![Credential { id: String::new(), @@ -216,7 +216,7 @@ mod tests { } #[test] - fn test_format_state_context_credentials_hidden_for_recon() { + fn format_state_context_credentials_hidden_for_recon() { let mut snap = make_snapshot(); snap.credentials = vec![Credential { id: String::new(), @@ -234,7 +234,7 @@ mod tests { } #[test] - fn test_format_state_context_truncates_credentials() { + fn format_state_context_truncates_credentials() { let mut snap = make_snapshot(); snap.credentials = (0..12) .map(|i| Credential { @@ -254,7 +254,7 @@ mod tests { } #[test] - fn test_format_state_context_hosts_split_dc_and_other() { + fn format_state_context_hosts_split_dc_and_other() { let mut snap = make_snapshot(); snap.hosts = vec![ Host { @@ -285,7 +285,7 @@ mod tests { } #[test] - fn test_format_state_context_cracked_hashes() { + fn format_state_context_cracked_hashes() { let mut snap = make_snapshot(); snap.hashes = vec![Hash { id: String::new(), @@ -306,7 +306,7 @@ mod tests { } #[test] - fn test_format_state_context_uncracked_hashes_not_shown() { + fn format_state_context_uncracked_hashes_not_shown() { let mut snap = make_snapshot(); snap.hashes = vec![Hash { id: String::new(), diff --git a/ares-llm/src/prompt/templates.rs b/ares-llm/src/prompt/templates.rs index 0947bd93..c5b353f4 100644 --- a/ares-llm/src/prompt/templates.rs +++ b/ares-llm/src/prompt/templates.rs @@ -463,7 +463,7 @@ mod tests { use super::*; #[test] - fn test_render_recon_template() { + fn render_recon_template() { let capabilities = vec![ "nmap_scan".to_string(), "enumerate_users".to_string(), @@ -478,14 +478,14 @@ mod tests { } #[test] - fn test_render_recon_empty_capabilities() { + fn render_recon_empty_capabilities() { let result = render_agent_instructions(TEMPLATE_RECON, &[], false, &[]).unwrap(); assert!(result.contains("RECON Worker Agent")); assert!(result.contains("## Available Tools")); } #[test] - fn test_render_credential_access_template() { + fn render_credential_access_template() { let capabilities = vec!["secretsdump".to_string(), "kerberoast".to_string()]; let result = render_agent_instructions(TEMPLATE_CREDENTIAL_ACCESS, &capabilities, false, &[]) @@ -496,7 +496,7 @@ mod tests { } #[test] - fn test_render_cracker_template() { + fn render_cracker_template() { let capabilities = vec!["crack_with_hashcat".to_string()]; let result = render_agent_instructions(TEMPLATE_CRACKER, &capabilities, false, &[]).unwrap(); @@ -505,7 +505,7 @@ mod tests { } #[test] - fn test_render_acl_template() { + fn render_acl_template() { let capabilities = vec!["pywhisker".to_string(), "dacl_edit".to_string()]; let result = render_agent_instructions(TEMPLATE_ACL, &capabilities, false, &[]).unwrap(); assert!(result.contains("ACL Exploitation Agent")); @@ -513,7 +513,7 @@ mod tests { } #[test] - fn test_render_privesc_template() { + fn render_privesc_template() { let capabilities = vec!["certipy_find".to_string(), "s4u_attack".to_string()]; let result = render_agent_instructions(TEMPLATE_PRIVESC, &capabilities, false, &[]).unwrap(); @@ -522,7 +522,7 @@ mod tests { } #[test] - fn test_render_lateral_template() { + fn render_lateral_template() { let capabilities = vec!["psexec".to_string(), "evil_winrm".to_string()]; let result = render_agent_instructions(TEMPLATE_LATERAL, &capabilities, false, &[]).unwrap(); @@ -531,7 +531,7 @@ mod tests { } #[test] - fn test_render_coercion_template() { + fn render_coercion_template() { let capabilities = vec!["petitpotam".to_string(), "start_responder".to_string()]; let result = render_agent_instructions(TEMPLATE_COERCION, &capabilities, false, &[]).unwrap(); @@ -540,7 +540,7 @@ mod tests { } #[test] - fn test_render_orchestrator_template() { + fn render_orchestrator_template() { let capabilities = vec!["dispatch_recon".to_string()]; let result = render_agent_instructions(TEMPLATE_ORCHESTRATOR, &capabilities, false, &[]).unwrap(); @@ -548,7 +548,7 @@ mod tests { } #[test] - fn test_render_system_instructions_with_capabilities() { + fn render_system_instructions_with_capabilities() { let mut caps: HashMap> = HashMap::new(); caps.insert("recon".to_string(), vec!["nmap_scan".to_string()]); caps.insert( @@ -567,7 +567,7 @@ mod tests { } #[test] - fn test_render_system_instructions_without_capabilities() { + fn render_system_instructions_without_capabilities() { let result = render_system_instructions(None, None).unwrap(); // Falls back to hardcoded defaults assert!(result.contains("nmap, netexec, rpcclient")); @@ -577,7 +577,7 @@ mod tests { } #[test] - fn test_render_system_instructions_with_priorities() { + fn render_system_instructions_with_priorities() { let priorities = vec![ ("dc_secretsdump".to_string(), 1), ("golden_ticket".to_string(), 1), @@ -602,7 +602,7 @@ mod tests { } #[test] - fn test_render_initial_task() { + fn render_initial_task() { let mut vars = HashMap::new(); vars.insert( "target_ip".to_string(), @@ -614,7 +614,7 @@ mod tests { } #[test] - fn test_render_cracker_task() { + fn render_cracker_task() { let mut vars = HashMap::new(); vars.insert( "hash_value".to_string(), @@ -627,7 +627,7 @@ mod tests { } #[test] - fn test_render_golden_ticket_task() { + fn render_golden_ticket_task() { let mut vars = HashMap::new(); vars.insert("krbtgt_hash".to_string(), "aad3b435:5703ad15".to_string()); vars.insert("user_name".to_string(), "admin".to_string()); @@ -647,7 +647,7 @@ mod tests { } #[test] - fn test_render_share_pilfer_task() { + fn render_share_pilfer_task() { let mut vars = HashMap::new(); vars.insert("target".to_string(), "192.168.58.10".to_string()); vars.insert("share_name".to_string(), "SYSVOL".to_string()); @@ -659,7 +659,7 @@ mod tests { } #[test] - fn test_render_static_templates() { + fn render_static_templates() { // Templates with no variables should render cleanly let empty: HashMap = HashMap::new(); let result = render_task_template(TEMPLATE_CRACKER_INSTRUCTIONS, &empty).unwrap(); @@ -673,7 +673,7 @@ mod tests { } #[test] - fn test_invalid_template_name() { + fn invalid_template_name() { let result = render_agent_instructions("nonexistent", &[], false, &[]); assert!(result.is_err()); } diff --git a/ares-llm/src/prompt/tests.rs b/ares-llm/src/prompt/tests.rs index 5a0b35ef..7f751a8c 100644 --- a/ares-llm/src/prompt/tests.rs +++ b/ares-llm/src/prompt/tests.rs @@ -30,7 +30,7 @@ fn sample_state() -> StateSnapshot { } #[test] -fn test_generate_recon_prompt() { +fn generate_recon_prompt() { let payload = serde_json::json!({ "target_ip": "192.168.58.0/24", "domain": "contoso.local", @@ -45,7 +45,7 @@ fn test_generate_recon_prompt() { } #[test] -fn test_generate_crack_prompt() { +fn generate_crack_prompt() { let payload = serde_json::json!({ "hash_type": "ntlm", "hash_value": "aad3b435b51404eeaad3b435b51404ee:31d6cfe0d16ae931b73c59d7e0c089c0", @@ -59,7 +59,7 @@ fn test_generate_crack_prompt() { } #[test] -fn test_generate_credential_access_prompt() { +fn generate_credential_access_prompt() { let payload = serde_json::json!({ "technique": "secretsdump", "target_ip": "192.168.58.10", @@ -77,7 +77,7 @@ fn test_generate_credential_access_prompt() { } #[test] -fn test_generate_lateral_prompt() { +fn generate_lateral_prompt() { let payload = serde_json::json!({ "technique": "psexec", "target_ip": "192.168.58.20", @@ -93,7 +93,7 @@ fn test_generate_lateral_prompt() { } #[test] -fn test_generate_exploit_prompt() { +fn generate_exploit_prompt() { let payload = serde_json::json!({ "vuln_type": "constrained_delegation", "target": "192.168.58.30", @@ -109,7 +109,7 @@ fn test_generate_exploit_prompt() { } #[test] -fn test_generate_coercion_prompt() { +fn generate_coercion_prompt() { let payload = serde_json::json!({ "target_ip": "192.168.58.10", "listener_ip": "192.168.58.100", @@ -122,7 +122,7 @@ fn test_generate_coercion_prompt() { } #[test] -fn test_generate_privesc_prompt() { +fn generate_privesc_prompt() { let payload = serde_json::json!({ "technique": "find_delegation", "target_ip": "192.168.58.10", @@ -134,7 +134,7 @@ fn test_generate_privesc_prompt() { } #[test] -fn test_generate_acl_prompt() { +fn generate_acl_prompt() { let payload = serde_json::json!({ "chain": [{"source": "user1", "target": "admin", "right": "GenericAll"}] }); @@ -144,7 +144,7 @@ fn test_generate_acl_prompt() { } #[test] -fn test_generate_command_prompt() { +fn generate_command_prompt() { let payload = serde_json::json!({"command": "whoami"}); let prompt = generate_task_prompt("command", "task-009", &payload, None).unwrap(); assert!(prompt.contains("whoami")); @@ -152,7 +152,7 @@ fn test_generate_command_prompt() { } #[test] -fn test_format_state_context_truncation() { +fn format_state_context_truncation() { let mut state = StateSnapshot::default(); for i in 0..20 { state.credentials.push(Credential { @@ -172,13 +172,13 @@ fn test_format_state_context_truncation() { } #[test] -fn test_unknown_task_type_returns_none() { +fn unknown_task_type_returns_none() { let payload = serde_json::json!({}); assert!(generate_task_prompt("unknown_type", "task-x", &payload, None).is_none()); } #[test] -fn test_state_context_injected_into_template() { +fn state_context_injected_into_template() { let payload = serde_json::json!({ "technique": "secretsdump", "target_ip": "192.168.58.10", @@ -197,34 +197,34 @@ fn test_state_context_injected_into_template() { // ----------------------------------------------------------------------- #[test] -fn test_pth_compatible_lm_nt() { +fn pth_compatible_lm_nt() { assert!(helpers::is_pass_the_hash_compatible(Some( "aad3b435b51404eeaad3b435b51404ee:31d6cfe0d16ae931b73c59d7e0c089c0" ))); } #[test] -fn test_pth_compatible_nt_only() { +fn pth_compatible_nt_only() { assert!(helpers::is_pass_the_hash_compatible(Some( "31d6cfe0d16ae931b73c59d7e0c089c0" ))); } #[test] -fn test_pth_rejects_kerberos_hash() { +fn pth_rejects_kerberos_hash() { assert!(!helpers::is_pass_the_hash_compatible(Some( "$krb5tgs$23$*svc_sql$" ))); } #[test] -fn test_pth_rejects_empty() { +fn pth_rejects_empty() { assert!(!helpers::is_pass_the_hash_compatible(None)); assert!(!helpers::is_pass_the_hash_compatible(Some(""))); } #[test] -fn test_pth_rejects_triple_colon() { +fn pth_rejects_triple_colon() { assert!(!helpers::is_pass_the_hash_compatible(Some("aaa:bbb:ccc"))); } @@ -233,7 +233,7 @@ fn test_pth_rejects_triple_colon() { // ----------------------------------------------------------------------- #[test] -fn test_credaccess_kerberos_ticket_secretsdump() { +fn credaccess_kerberos_ticket_secretsdump() { let payload = serde_json::json!({ "techniques": ["secretsdump"], "target_ips": ["192.168.58.10"], @@ -252,7 +252,7 @@ fn test_credaccess_kerberos_ticket_secretsdump() { } #[test] -fn test_credaccess_low_hanging_fruit_with_creds() { +fn credaccess_low_hanging_fruit_with_creds() { let payload = serde_json::json!({ "domain": "contoso.local", "dc_ip": "192.168.58.10", @@ -269,7 +269,7 @@ fn test_credaccess_low_hanging_fruit_with_creds() { } #[test] -fn test_credaccess_username_as_password_spray() { +fn credaccess_username_as_password_spray() { let payload = serde_json::json!({ "domain": "contoso.local", "dc_ip": "192.168.58.10", @@ -283,7 +283,7 @@ fn test_credaccess_username_as_password_spray() { } #[test] -fn test_credaccess_share_spider() { +fn credaccess_share_spider() { let payload = serde_json::json!({ "domain": "contoso.local", "username": "admin", @@ -300,7 +300,7 @@ fn test_credaccess_share_spider() { } #[test] -fn test_credaccess_no_cred_techniques() { +fn credaccess_no_cred_techniques() { let payload = serde_json::json!({ "domain": "contoso.local", "dc_ip": "192.168.58.10", @@ -314,7 +314,7 @@ fn test_credaccess_no_cred_techniques() { } #[test] -fn test_credaccess_low_hanging_no_creds() { +fn credaccess_low_hanging_no_creds() { let payload = serde_json::json!({ "domain": "contoso.local", "dc_ip": "192.168.58.10", @@ -328,7 +328,7 @@ fn test_credaccess_low_hanging_no_creds() { } #[test] -fn test_credaccess_technique_enforcement_with_creds() { +fn credaccess_technique_enforcement_with_creds() { let payload = serde_json::json!({ "domain": "contoso.local", "dc_ip": "192.168.58.10", @@ -346,7 +346,7 @@ fn test_credaccess_technique_enforcement_with_creds() { } #[test] -fn test_credaccess_technique_enforcement_with_hash() { +fn credaccess_technique_enforcement_with_hash() { let payload = serde_json::json!({ "domain": "contoso.local", "dc_ip": "192.168.58.10", @@ -361,7 +361,7 @@ fn test_credaccess_technique_enforcement_with_hash() { } #[test] -fn test_credaccess_non_pth_hash_strips_techniques() { +fn credaccess_non_pth_hash_strips_techniques() { let payload = serde_json::json!({ "domain": "contoso.local", "dc_ip": "192.168.58.10", @@ -375,7 +375,7 @@ fn test_credaccess_non_pth_hash_strips_techniques() { } #[test] -fn test_credaccess_generic_fallback() { +fn credaccess_generic_fallback() { let payload = serde_json::json!({ "domain": "contoso.local", "username": "admin", @@ -390,7 +390,7 @@ fn test_credaccess_generic_fallback() { } #[test] -fn test_credaccess_generic_fallback_non_pth_hash() { +fn credaccess_generic_fallback_non_pth_hash() { let payload = serde_json::json!({ "domain": "contoso.local", "username": "admin", @@ -408,7 +408,7 @@ fn test_credaccess_generic_fallback_non_pth_hash() { // ----------------------------------------------------------------------- #[test] -fn test_exploit_adcs_enumerate() { +fn exploit_adcs_enumerate() { let payload = serde_json::json!({ "vuln_type": "adcs_enumerate", "target": "192.168.58.15", @@ -425,7 +425,7 @@ fn test_exploit_adcs_enumerate() { } #[test] -fn test_exploit_mssql() { +fn exploit_mssql() { let payload = serde_json::json!({ "vuln_type": "mssql_impersonation", "target": "192.168.58.30", @@ -444,7 +444,7 @@ fn test_exploit_mssql() { } #[test] -fn test_exploit_constrained_delegation_with_state() { +fn exploit_constrained_delegation_with_state() { let state = StateSnapshot { credentials: vec![Credential { id: "c1".into(), @@ -478,7 +478,7 @@ fn test_exploit_constrained_delegation_with_state() { } #[test] -fn test_exploit_unconstrained_delegation() { +fn exploit_unconstrained_delegation() { let payload = serde_json::json!({ "vuln_type": "unconstrained_delegation", "target": "192.168.58.30", @@ -494,7 +494,7 @@ fn test_exploit_unconstrained_delegation() { } #[test] -fn test_exploit_adcs_esc1() { +fn exploit_adcs_esc1() { let payload = serde_json::json!({ "vuln_type": "adcs_esc1", "target": "192.168.58.15", @@ -511,7 +511,7 @@ fn test_exploit_adcs_esc1() { } #[test] -fn test_exploit_adcs_esc8() { +fn exploit_adcs_esc8() { let payload = serde_json::json!({ "vuln_type": "adcs_esc8", "target": "192.168.58.15", @@ -526,7 +526,7 @@ fn test_exploit_adcs_esc8() { } #[test] -fn test_exploit_trust_key_extraction() { +fn exploit_trust_key_extraction() { let payload = serde_json::json!({ "vuln_type": "trust_key", "target": "192.168.58.10", @@ -545,7 +545,7 @@ fn test_exploit_trust_key_extraction() { } #[test] -fn test_exploit_child_to_parent_has_raise_child() { +fn exploit_child_to_parent_has_raise_child() { let payload = serde_json::json!({ "vuln_type": "child_to_parent", "target": "192.168.58.10", @@ -562,7 +562,7 @@ fn test_exploit_child_to_parent_has_raise_child() { } #[test] -fn test_exploit_mssql_lateral_enumeration() { +fn exploit_mssql_lateral_enumeration() { let state = StateSnapshot { credentials: vec![Credential { id: "c1".into(), @@ -591,7 +591,7 @@ fn test_exploit_mssql_lateral_enumeration() { } #[test] -fn test_exploit_generic_fallback() { +fn exploit_generic_fallback() { let payload = serde_json::json!({ "vuln_type": "unknown_vuln", "target": "192.168.58.30", diff --git a/ares-llm/src/provider/anthropic.rs b/ares-llm/src/provider/anthropic.rs index e246a976..d30fae4d 100644 --- a/ares-llm/src/provider/anthropic.rs +++ b/ares-llm/src/provider/anthropic.rs @@ -338,7 +338,7 @@ mod tests { use super::*; #[test] - fn test_convert_simple_message() { + fn convert_simple_message() { let msg = ChatMessage::text(Role::User, "hello"); let api_msg = convert_message(&msg); assert_eq!(api_msg.role, "user"); @@ -349,7 +349,7 @@ mod tests { } #[test] - fn test_convert_tool_result_message() { + fn convert_tool_result_message() { let msg = ChatMessage::tool_result("call_1", "scan complete"); let api_msg = convert_message(&msg); assert_eq!(api_msg.role, "user"); @@ -362,7 +362,7 @@ mod tests { } #[test] - fn test_parse_stop_reasons() { + fn parse_stop_reasons() { assert_eq!(parse_stop_reason(Some("end_turn")), StopReason::EndTurn); assert_eq!(parse_stop_reason(Some("tool_use")), StopReason::ToolUse); assert_eq!(parse_stop_reason(Some("max_tokens")), StopReason::MaxTokens); @@ -374,7 +374,7 @@ mod tests { } #[test] - fn test_convert_tools() { + fn converts_tools() { let tools = vec![super::super::ToolDefinition { name: "nmap_scan".into(), description: "Run nmap".into(), @@ -390,7 +390,7 @@ mod tests { } #[test] - fn test_deserialize_response() { + fn deserialize_response() { let json = r#"{ "content": [ {"type": "text", "text": "I'll scan the network."}, @@ -405,7 +405,7 @@ mod tests { } #[test] - fn test_serialize_api_request() { + fn serialize_api_request() { let req = ApiRequest { model: "claude-sonnet-4-20250514".to_string(), max_tokens: 4096, diff --git a/ares-llm/src/provider/mod.rs b/ares-llm/src/provider/mod.rs index f95d873a..d27c72d0 100644 --- a/ares-llm/src/provider/mod.rs +++ b/ares-llm/src/provider/mod.rs @@ -311,20 +311,20 @@ mod tests { use super::*; #[test] - fn test_chat_message_text() { + fn chat_message_text() { let msg = ChatMessage::text(Role::User, "hello"); assert_eq!(msg.text_content(), Some("hello")); } #[test] - fn test_chat_message_tool_result() { + fn chat_message_tool_result() { let msg = ChatMessage::tool_result("call_1", "output"); assert_eq!(msg.role, Role::User); assert!(msg.parts.is_some()); } #[test] - fn test_chat_message_assistant_tool_use() { + fn chat_message_assistant_tool_use() { let calls = vec![ToolCall { id: "call_1".into(), name: "nmap_scan".into(), @@ -337,7 +337,7 @@ mod tests { } #[test] - fn test_llm_request_builder() { + fn llm_request_builder() { let req = LlmRequest::new("claude-sonnet-4-20250514"); assert_eq!(req.model, "claude-sonnet-4-20250514"); assert_eq!(req.max_tokens, 4096); @@ -345,14 +345,14 @@ mod tests { } #[test] - fn test_stop_reason_equality() { + fn stop_reason_equality() { assert_eq!(StopReason::EndTurn, StopReason::EndTurn); assert_eq!(StopReason::ToolUse, StopReason::ToolUse); assert_ne!(StopReason::EndTurn, StopReason::ToolUse); } #[test] - fn test_tool_definition_serialize() { + fn tool_definition_serialize() { let tool = ToolDefinition { name: "nmap_scan".into(), description: "Run an nmap scan".into(), diff --git a/ares-llm/src/provider/ollama.rs b/ares-llm/src/provider/ollama.rs index c96213c1..31139304 100644 --- a/ares-llm/src/provider/ollama.rs +++ b/ares-llm/src/provider/ollama.rs @@ -38,13 +38,13 @@ mod tests { use super::*; #[test] - fn test_ollama_provider_creation() { + fn ollama_provider_creation() { let provider = OllamaProvider::new("http://localhost:11434".to_string()); assert_eq!(provider.name(), "ollama"); } #[test] - fn test_ollama_url_trailing_slash() { + fn ollama_url_trailing_slash() { // Should handle trailing slash gracefully let _provider = OllamaProvider::new("http://localhost:11434/".to_string()); } diff --git a/ares-llm/src/provider/openai.rs b/ares-llm/src/provider/openai.rs index 3929cc92..3209b03b 100644 --- a/ares-llm/src/provider/openai.rs +++ b/ares-llm/src/provider/openai.rs @@ -423,7 +423,7 @@ mod tests { use super::*; #[test] - fn test_convert_user_message() { + fn convert_user_message() { let msg = ChatMessage::text(Role::User, "scan the network"); let api_msg = convert_message(&msg); assert_eq!(api_msg.role, "user"); @@ -431,7 +431,7 @@ mod tests { } #[test] - fn test_convert_tool_result() { + fn convert_tool_result() { let msg = ChatMessage::tool_result("call_1", "scan done"); let api_msg = convert_message(&msg); assert_eq!(api_msg.role, "tool"); @@ -439,14 +439,14 @@ mod tests { } #[test] - fn test_parse_openai_stop_reasons() { + fn parse_openai_stop_reasons() { assert_eq!(parse_stop_reason(Some("stop")), StopReason::EndTurn); assert_eq!(parse_stop_reason(Some("tool_calls")), StopReason::ToolUse); assert_eq!(parse_stop_reason(Some("length")), StopReason::MaxTokens); } #[test] - fn test_deserialize_openai_response() { + fn deserialize_openai_response() { let json = r#"{ "choices": [{ "message": { @@ -473,7 +473,7 @@ mod tests { } #[test] - fn test_convert_openai_tools() { + fn convert_openai_tools() { let tools = vec![super::super::ToolDefinition { name: "nmap_scan".into(), description: "Run nmap".into(), @@ -485,7 +485,7 @@ mod tests { } #[test] - fn test_gpt5_uses_max_completion_tokens() { + fn gpt5_uses_max_completion_tokens() { assert!(uses_max_completion_tokens("gpt-5.2")); assert!(uses_max_completion_tokens("openai/gpt-5.2")); assert!(!uses_max_completion_tokens("gpt-4o-mini")); diff --git a/ares-llm/src/routing/credentials.rs b/ares-llm/src/routing/credentials.rs index f5155059..e5a4594f 100644 --- a/ares-llm/src/routing/credentials.rs +++ b/ares-llm/src/routing/credentials.rs @@ -107,7 +107,7 @@ mod tests { } #[test] - fn test_find_domain_credential_with_password() { + fn find_domain_credential_with_password() { let map = HashMap::new(); let trusts = HashMap::new(); let creds = vec![ @@ -120,7 +120,7 @@ mod tests { } #[test] - fn test_find_domain_credential_prefers_password() { + fn find_domain_credential_prefers_password() { let map = HashMap::new(); let trusts = HashMap::new(); let creds = vec![ @@ -132,7 +132,7 @@ mod tests { } #[test] - fn test_find_domain_credential_falls_back_to_no_password() { + fn find_domain_credential_falls_back_to_no_password() { let map = HashMap::new(); let trusts = HashMap::new(); let creds = vec![make_cred("hash_user", "contoso.local", "")]; @@ -141,7 +141,7 @@ mod tests { } #[test] - fn test_find_domain_credential_none_for_wrong_domain() { + fn find_domain_credential_none_for_wrong_domain() { let map = HashMap::new(); let trusts = HashMap::new(); let creds = vec![make_cred("admin", "fabrikam.local", "P@ss1")]; @@ -150,7 +150,7 @@ mod tests { } #[test] - fn test_find_domain_credential_netbios_resolution() { + fn find_domain_credential_netbios_resolution() { let mut map = HashMap::new(); map.insert("contoso".to_string(), "contoso.local".to_string()); let trusts = HashMap::new(); @@ -160,7 +160,7 @@ mod tests { } #[test] - fn test_find_domain_credential_empty() { + fn find_domain_credential_empty() { let map = HashMap::new(); let trusts = HashMap::new(); let creds: Vec = vec![]; @@ -170,7 +170,7 @@ mod tests { // --- Trust-scope validation tests --- #[test] - fn test_same_domain_valid() { + fn same_domain_valid() { let trusts = HashMap::new(); assert!(is_valid_credential_for_domain( "contoso.local", @@ -180,7 +180,7 @@ mod tests { } #[test] - fn test_parent_to_child_valid() { + fn parent_to_child_valid() { let trusts = HashMap::new(); assert!(is_valid_credential_for_domain( "contoso.local", @@ -190,7 +190,7 @@ mod tests { } #[test] - fn test_child_to_parent_blocked() { + fn child_to_parent_blocked() { let trusts = HashMap::new(); assert!(!is_valid_credential_for_domain( "north.contoso.local", @@ -200,7 +200,7 @@ mod tests { } #[test] - fn test_cross_forest_blocked() { + fn cross_forest_blocked() { let mut trusts = HashMap::new(); trusts.insert( "fabrikam.local".to_string(), @@ -220,7 +220,7 @@ mod tests { } #[test] - fn test_parent_cred_for_child_domain() { + fn parent_cred_for_child_domain() { let trusts = HashMap::new(); let creds = vec![make_cred("admin", "contoso.local", "P@ss1")]; let map = HashMap::new(); @@ -230,7 +230,7 @@ mod tests { } #[test] - fn test_child_cred_blocked_for_parent_domain() { + fn child_cred_blocked_for_parent_domain() { let trusts = HashMap::new(); let creds = vec![make_cred("admin", "north.contoso.local", "P@ss1")]; let map = HashMap::new(); diff --git a/ares-llm/src/routing/dc_discovery.rs b/ares-llm/src/routing/dc_discovery.rs index 7d4fb59a..3d8029d9 100644 --- a/ares-llm/src/routing/dc_discovery.rs +++ b/ares-llm/src/routing/dc_discovery.rs @@ -286,20 +286,20 @@ mod tests { // --- has_dc_role --- #[test] - fn test_has_dc_role_explicit_flag() { + fn has_dc_role_explicit_flag() { let host = make_host("192.168.58.10", "dc01", true, vec![]); assert!(has_dc_role(&host)); } #[test] - fn test_has_dc_role_from_role_string() { + fn has_dc_role_from_role_string() { let mut host = make_host("192.168.58.10", "srv01", false, vec![]); host.roles = vec!["AD DC".to_string()]; assert!(has_dc_role(&host)); } #[test] - fn test_has_dc_role_none() { + fn has_dc_role_none() { let host = make_host("192.168.58.20", "srv01", false, vec![]); assert!(!has_dc_role(&host)); } @@ -307,19 +307,19 @@ mod tests { // --- has_dc_services --- #[test] - fn test_has_dc_services_kerberos_port() { + fn has_dc_services_kerberos_port() { let host = make_host("192.168.58.10", "srv01", false, vec!["88/tcp (kerberos)"]); assert!(has_dc_services(&host)); } #[test] - fn test_has_dc_services_ldap_port() { + fn has_dc_services_ldap_port() { let host = make_host("192.168.58.10", "srv01", false, vec!["389/tcp (ldap)"]); assert!(has_dc_services(&host)); } #[test] - fn test_has_dc_services_no_dc_services() { + fn has_dc_services_no_dc_services() { let host = make_host( "192.168.58.20", "srv01", @@ -330,7 +330,7 @@ mod tests { } #[test] - fn test_has_dc_services_3389_not_389() { + fn has_dc_services_3389_not_389() { // 3389 (RDP) should NOT match 389 prefix let host = make_host( "192.168.58.20", @@ -344,7 +344,7 @@ mod tests { // --- find_dc_ip --- #[test] - fn test_find_dc_ip_tier0_cached() { + fn find_dc_ip_tier0_cached() { let mut dc_map = HashMap::new(); dc_map.insert("contoso.local".to_string(), "192.168.58.10".to_string()); let result = find_dc_ip("contoso.local", &[], &dc_map, &HashMap::new(), None); @@ -356,7 +356,7 @@ mod tests { } #[test] - fn test_find_dc_ip_tier1_role() { + fn find_dc_ip_tier1_role() { let hosts = vec![make_host( "192.168.58.10", "dc01.contoso.local", @@ -377,7 +377,7 @@ mod tests { } #[test] - fn test_find_dc_ip_tier2_hostname_pattern() { + fn find_dc_ip_tier2_hostname_pattern() { let mut host = make_host("192.168.58.10", "dc01.contoso.local", false, vec![]); host.roles.clear(); let result = find_dc_ip( @@ -392,7 +392,7 @@ mod tests { } #[test] - fn test_find_dc_ip_tier3_services() { + fn find_dc_ip_tier3_services() { let host = make_host( "192.168.58.10", "srv01.contoso.local", @@ -411,7 +411,7 @@ mod tests { } #[test] - fn test_find_dc_ip_tier5_fallback_role() { + fn find_dc_ip_tier5_fallback_role() { // DC exists but for a different domain let hosts = vec![make_host( "192.168.58.10", @@ -432,19 +432,19 @@ mod tests { } #[test] - fn test_find_dc_ip_none() { + fn find_dc_ip_none() { let result = find_dc_ip("contoso.local", &[], &HashMap::new(), &HashMap::new(), None); assert!(result.is_none()); } #[test] - fn test_find_dc_ip_empty_domain() { + fn find_dc_ip_empty_domain() { let result = find_dc_ip("", &[], &HashMap::new(), &HashMap::new(), None); assert!(result.is_none()); } #[test] - fn test_find_dc_ip_forest_parent_fallback() { + fn find_dc_ip_forest_parent_fallback() { let mut dc_map = HashMap::new(); dc_map.insert("contoso.local".to_string(), "192.168.58.10".to_string()); // Child domain with no specific DC, but parent DC exists @@ -458,7 +458,7 @@ mod tests { // --- find_dc_ip_cached --- #[test] - fn test_find_dc_ip_cached_hit() { + fn find_dc_ip_cached_hit() { let mut dc_map = HashMap::new(); dc_map.insert("contoso.local".to_string(), "192.168.58.10".to_string()); assert_eq!( @@ -468,7 +468,7 @@ mod tests { } #[test] - fn test_find_dc_ip_cached_miss() { + fn find_dc_ip_cached_miss() { assert_eq!( find_dc_ip_cached("contoso.local", &HashMap::new(), &HashMap::new()), None @@ -478,7 +478,7 @@ mod tests { // --- DcTier Display --- #[test] - fn test_dc_tier_display() { + fn dc_tier_display() { assert_eq!(DcTier::Cached.to_string(), "cached"); assert_eq!(DcTier::Role.to_string(), "role"); assert_eq!(DcTier::Forest.to_string(), "forest"); diff --git a/ares-llm/src/routing/domain.rs b/ares-llm/src/routing/domain.rs index 223b5c76..8942c33b 100644 --- a/ares-llm/src/routing/domain.rs +++ b/ares-llm/src/routing/domain.rs @@ -61,44 +61,44 @@ mod tests { // --- normalize_domain --- #[test] - fn test_normalize_domain_fqdn_passthrough() { + fn normalize_domain_fqdn_passthrough() { let map = make_map(); assert_eq!(normalize_domain("contoso.local", &map), "contoso.local"); } #[test] - fn test_normalize_domain_fqdn_lowercased() { + fn normalize_domain_fqdn_lowercased() { let map = make_map(); assert_eq!(normalize_domain("CONTOSO.LOCAL", &map), "contoso.local"); } #[test] - fn test_normalize_domain_netbios_lowercase_key() { + fn normalize_domain_netbios_lowercase_key() { let map = make_map(); assert_eq!(normalize_domain("contoso", &map), "contoso.local"); } #[test] - fn test_normalize_domain_netbios_uppercase_key() { + fn normalize_domain_netbios_uppercase_key() { let map = make_map(); assert_eq!(normalize_domain("FABRIKAM", &map), "fabrikam.local"); } #[test] - fn test_normalize_domain_netbios_mixed_case() { + fn normalize_domain_netbios_mixed_case() { let map = make_map(); // "Fabrikam" → to_lowercase "fabrikam" not in map, to_uppercase "FABRIKAM" IS in map assert_eq!(normalize_domain("Fabrikam", &map), "fabrikam.local"); } #[test] - fn test_normalize_domain_unknown_netbios() { + fn normalize_domain_unknown_netbios() { let map = make_map(); assert_eq!(normalize_domain("UNKNOWN", &map), "unknown"); } #[test] - fn test_normalize_domain_empty() { + fn normalize_domain_empty() { let map = make_map(); assert_eq!(normalize_domain("", &map), ""); } @@ -106,7 +106,7 @@ mod tests { // --- hostname_matches_domain --- #[test] - fn test_hostname_matches_domain_basic() { + fn hostname_matches_domain_basic() { assert!(hostname_matches_domain( "dc01.contoso.local", "contoso.local" @@ -114,7 +114,7 @@ mod tests { } #[test] - fn test_hostname_matches_domain_case_insensitive() { + fn hostname_matches_domain_case_insensitive() { assert!(hostname_matches_domain( "DC01.CONTOSO.LOCAL", "contoso.local" @@ -122,7 +122,7 @@ mod tests { } #[test] - fn test_hostname_matches_domain_child_not_parent() { + fn hostname_matches_domain_child_not_parent() { // dc01.child.contoso.local should match child.contoso.local, NOT contoso.local assert!(hostname_matches_domain( "dc01.child.contoso.local", @@ -135,19 +135,19 @@ mod tests { } #[test] - fn test_hostname_matches_domain_empty_inputs() { + fn hostname_matches_domain_empty_inputs() { assert!(!hostname_matches_domain("", "contoso.local")); assert!(!hostname_matches_domain("dc01.contoso.local", "")); assert!(!hostname_matches_domain("", "")); } #[test] - fn test_hostname_matches_domain_no_dots() { + fn hostname_matches_domain_no_dots() { assert!(!hostname_matches_domain("dc01", "contoso.local")); } #[test] - fn test_hostname_is_domain() { + fn hostname_is_domain() { assert!(hostname_matches_domain("contoso.local", "contoso.local")); } } diff --git a/ares-llm/src/routing/enrichment.rs b/ares-llm/src/routing/enrichment.rs index 2c33a491..c1136015 100644 --- a/ares-llm/src/routing/enrichment.rs +++ b/ares-llm/src/routing/enrichment.rs @@ -170,7 +170,7 @@ mod tests { // --- enrich_delegation_payload --- #[test] - fn test_enrich_delegation_payload_adds_password() { + fn enrich_delegation_payload_adds_password() { let creds = vec![make_cred("svc_sql", "contoso.local", "SvcP@ss1")]; let mut payload = json!({"account_name": "svc_sql$", "domain": "contoso.local"}); enrich_delegation_payload(&mut payload, "constrained_delegation", &creds, &[]); @@ -178,7 +178,7 @@ mod tests { } #[test] - fn test_enrich_delegation_payload_skips_non_delegation() { + fn enrich_delegation_payload_skips_non_delegation() { let creds = vec![make_cred("svc_sql", "contoso.local", "SvcP@ss1")]; let mut payload = json!({"account_name": "svc_sql$"}); enrich_delegation_payload(&mut payload, "smb_signing", &creds, &[]); @@ -186,7 +186,7 @@ mod tests { } #[test] - fn test_enrich_delegation_payload_doesnt_overwrite_password() { + fn enrich_delegation_payload_doesnt_overwrite_password() { let creds = vec![make_cred("svc_sql", "contoso.local", "SvcP@ss1")]; let mut payload = json!({"account_name": "svc_sql$", "password": "existing"}); enrich_delegation_payload(&mut payload, "constrained_delegation", &creds, &[]); @@ -194,7 +194,7 @@ mod tests { } #[test] - fn test_enrich_delegation_payload_resolves_target_ip_from_spn() { + fn enrich_delegation_payload_resolves_target_ip_from_spn() { let hosts = vec![make_host("192.168.58.10", "dc01.contoso.local", true)]; let mut payload = json!({ "account_name": "svc_sql$", @@ -205,7 +205,7 @@ mod tests { } #[test] - fn test_enrich_delegation_payload_sets_domain_from_cred() { + fn enrich_delegation_payload_sets_domain_from_cred() { let creds = vec![make_cred("svc_sql", "contoso.local", "SvcP@ss1")]; let mut payload = json!({"account_name": "svc_sql$"}); enrich_delegation_payload(&mut payload, "unconstrained_delegation", &creds, &[]); @@ -215,28 +215,28 @@ mod tests { // --- resolve_dc_for_payload --- #[test] - fn test_resolve_dc_skips_if_already_set() { + fn resolve_dc_skips_if_already_set() { let mut payload = json!({"dc_ip": "192.168.58.10", "domain": "contoso.local"}); resolve_dc_for_payload(&mut payload, &[], &HashMap::new(), &HashMap::new(), None); assert_eq!(payload["dc_ip"], "192.168.58.10"); } #[test] - fn test_resolve_dc_no_domain_skips() { + fn resolve_dc_no_domain_skips() { let mut payload = json!({"target": "192.168.58.20"}); resolve_dc_for_payload(&mut payload, &[], &HashMap::new(), &HashMap::new(), None); assert!(payload.get("dc_ip").is_none()); } #[test] - fn test_resolve_dc_falls_back_to_target_ip() { + fn resolve_dc_falls_back_to_target_ip() { let mut payload = json!({"domain": "contoso.local", "target_ip": "192.168.58.20"}); resolve_dc_for_payload(&mut payload, &[], &HashMap::new(), &HashMap::new(), None); assert_eq!(payload["dc_ip"], "192.168.58.20"); } #[test] - fn test_resolve_dc_falls_back_to_operation_target() { + fn resolve_dc_falls_back_to_operation_target() { let mut payload = json!({"domain": "contoso.local"}); resolve_dc_for_payload( &mut payload, @@ -249,7 +249,7 @@ mod tests { } #[test] - fn test_resolve_dc_from_dc_map() { + fn resolve_dc_from_dc_map() { let mut dc_map = HashMap::new(); dc_map.insert("contoso.local".to_string(), "192.168.58.10".to_string()); let mut payload = json!({"domain": "contoso.local"}); diff --git a/ares-llm/src/routing/mod.rs b/ares-llm/src/routing/mod.rs index f6a7e54a..ab056c9a 100644 --- a/ares-llm/src/routing/mod.rs +++ b/ares-llm/src/routing/mod.rs @@ -65,21 +65,21 @@ mod tests { // --- Domain normalization --- #[test] - fn test_normalize_domain_fqdn() { + fn normalize_domain_fqdn() { let map = sample_netbios_map(); assert_eq!(normalize_domain("contoso.local", &map), "contoso.local"); assert_eq!(normalize_domain("CONTOSO.LOCAL", &map), "contoso.local"); } #[test] - fn test_normalize_domain_netbios() { + fn normalize_domain_netbios() { let map = sample_netbios_map(); assert_eq!(normalize_domain("CONTOSO", &map), "contoso.local"); assert_eq!(normalize_domain("contoso", &map), "contoso.local"); } #[test] - fn test_normalize_domain_unknown() { + fn normalize_domain_unknown() { let map = sample_netbios_map(); assert_eq!(normalize_domain("UNKNOWN", &map), "unknown"); } @@ -87,7 +87,7 @@ mod tests { // --- Hostname matching --- #[test] - fn test_hostname_matches_domain() { + fn hostname_matches_domain() { assert!(domain::hostname_matches_domain( "dc01.contoso.local", "contoso.local" @@ -113,7 +113,7 @@ mod tests { // --- DC indicator checks --- #[test] - fn test_has_dc_role() { + fn has_dc_role() { let dc = make_host( "192.168.58.1", "dc01.contoso.local", @@ -133,7 +133,7 @@ mod tests { } #[test] - fn test_has_dc_services() { + fn has_dc_services() { let with_kerberos = make_host("192.168.58.1", "dc01", vec![], vec!["88/tcp kerberos"]); assert!(dc_discovery::has_dc_services(&with_kerberos)); @@ -153,7 +153,7 @@ mod tests { // --- Credential lookup --- #[test] - fn test_find_domain_credential() { + fn finds_domain_credential() { let map = sample_netbios_map(); let creds = vec![ make_cred("user1", "contoso.local", ""), @@ -167,7 +167,7 @@ mod tests { // --- Multi-tier DC discovery --- #[test] - fn test_find_dc_ip_tier0_cached() { + fn find_dc_ip_tier0_cached() { let mut dcs = HashMap::new(); dcs.insert("contoso.local".to_string(), "192.168.58.10".to_string()); let result = find_dc_ip("contoso.local", &[], &dcs, &HashMap::new(), None); @@ -175,7 +175,7 @@ mod tests { } #[test] - fn test_find_dc_ip_tier1_role() { + fn find_dc_ip_tier1_role() { let hosts = vec![make_host( "192.168.58.10", "dc01.contoso.local", @@ -196,7 +196,7 @@ mod tests { } #[test] - fn test_find_dc_ip_tier2_hostname_pattern() { + fn find_dc_ip_tier2_hostname_pattern() { let hosts = vec![make_host( "192.168.58.10", "dc01.contoso.local", @@ -215,7 +215,7 @@ mod tests { } #[test] - fn test_find_dc_ip_tier3_services() { + fn find_dc_ip_tier3_services() { let hosts = vec![make_host( "192.168.58.10", "srv01.contoso.local", @@ -234,7 +234,7 @@ mod tests { } #[test] - fn test_find_dc_ip_tier3_5_forest_child() { + fn find_dc_ip_tier3_5_forest_child() { let mut dcs = HashMap::new(); dcs.insert("contoso.local".to_string(), "192.168.58.10".to_string()); let hosts = vec![ @@ -255,7 +255,7 @@ mod tests { } #[test] - fn test_find_dc_ip_tier3_5_parent_fallback_not_cached() { + fn find_dc_ip_tier3_5_parent_fallback_not_cached() { let mut dcs = HashMap::new(); dcs.insert("contoso.local".to_string(), "192.168.58.10".to_string()); // No child DC exists @@ -267,7 +267,7 @@ mod tests { } #[test] - fn test_find_dc_ip_tier5_fallback_role() { + fn find_dc_ip_tier5_fallback_role() { let hosts = vec![make_host( "192.168.58.1", "dc01.other.local", @@ -288,7 +288,7 @@ mod tests { } #[test] - fn test_find_dc_ip_tier6_last_resort() { + fn find_dc_ip_tier6_last_resort() { let hosts = vec![make_host( "192.168.58.1", "unknown-host", @@ -307,7 +307,7 @@ mod tests { } #[test] - fn test_find_dc_ip_none() { + fn find_dc_ip_none() { let result = find_dc_ip("contoso.local", &[], &HashMap::new(), &HashMap::new(), None); assert!(result.is_none()); } @@ -315,7 +315,7 @@ mod tests { // --- Payload enrichment --- #[test] - fn test_enrich_delegation_payload_credential() { + fn enrich_delegation_payload_credential() { let creds = vec![make_cred("svc_sql", "contoso.local", "SqlPass1")]; let mut payload = serde_json::json!({ "account_name": "svc_sql$", @@ -327,14 +327,14 @@ mod tests { } #[test] - fn test_enrich_delegation_skips_non_delegation() { + fn enrich_delegation_skips_non_delegation() { let mut payload = serde_json::json!({"account_name": "svc_sql"}); enrich_delegation_payload(&mut payload, "zerologon", &[], &[]); assert!(payload.get("password").is_none()); } #[test] - fn test_enrich_delegation_resolves_target_ip() { + fn enrich_delegation_resolves_target_ip() { let hosts = vec![make_host( "192.168.58.20", "db01.contoso.local", @@ -349,7 +349,7 @@ mod tests { } #[test] - fn test_resolve_dc_for_payload() { + fn resolves_dc_for_payload() { let mut dcs = HashMap::new(); dcs.insert("contoso.local".to_string(), "192.168.58.10".to_string()); let mut payload = serde_json::json!({"domain": "contoso.local"}); @@ -358,7 +358,7 @@ mod tests { } #[test] - fn test_resolve_dc_skips_if_already_set() { + fn resolve_dc_skips_if_already_set() { let mut payload = serde_json::json!({"domain": "contoso.local", "dc_ip": "192.168.58.1"}); resolve_dc_for_payload(&mut payload, &[], &HashMap::new(), &HashMap::new(), None); assert_eq!(payload["dc_ip"].as_str(), Some("192.168.58.1")); // unchanged @@ -367,7 +367,7 @@ mod tests { // --- Utility --- #[test] - fn test_is_pass_the_hash_compatible() { + fn pass_the_hash_compatibility() { assert!(is_pass_the_hash_compatible( "aad3b435b51404eeaad3b435b51404ee:31d6cfe0d16ae931b73c59d7e0c089c0" )); @@ -380,7 +380,7 @@ mod tests { } #[test] - fn test_extract_ticket_path() { + fn extracts_ticket_path() { let output = "Saving ticket in Administrator.ccache\nDone."; assert_eq!( extract_ticket_path(output), @@ -389,7 +389,7 @@ mod tests { } #[test] - fn test_extract_host_from_spn() { + fn extracts_host_from_spn() { assert_eq!( extract_host_from_spn("MSSQLSvc/db01.contoso.local"), Some("db01.contoso.local".to_string()) diff --git a/ares-llm/src/routing/util.rs b/ares-llm/src/routing/util.rs index efe84bb8..5727fd31 100644 --- a/ares-llm/src/routing/util.rs +++ b/ares-llm/src/routing/util.rs @@ -62,43 +62,43 @@ mod tests { // --- is_pass_the_hash_compatible --- #[test] - fn test_pth_compatible_lm_nt_format() { + fn pth_compatible_lm_nt_format() { assert!(is_pass_the_hash_compatible( "aad3b435b51404eeaad3b435b51404ee:313b6f423a71d74c0a1b8a2f43b22d4c" )); } #[test] - fn test_pth_compatible_nt_only() { + fn pth_compatible_nt_only() { assert!(is_pass_the_hash_compatible( "313b6f423a71d74c0a1b8a2f43b22d4c" )); } #[test] - fn test_pth_not_compatible_empty() { + fn pth_not_compatible_empty() { assert!(!is_pass_the_hash_compatible("")); } #[test] - fn test_pth_not_compatible_dollar_sign() { + fn pth_not_compatible_dollar_sign() { assert!(!is_pass_the_hash_compatible("$krb5tgs$23$svc_sql")); } #[test] - fn test_pth_not_compatible_short() { + fn pth_not_compatible_short() { assert!(!is_pass_the_hash_compatible("aabbccdd")); } #[test] - fn test_pth_not_compatible_non_hex() { + fn pth_not_compatible_non_hex() { assert!(!is_pass_the_hash_compatible( "zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz" )); } #[test] - fn test_pth_compatible_with_whitespace() { + fn pth_compatible_with_whitespace() { assert!(is_pass_the_hash_compatible( " 313b6f423a71d74c0a1b8a2f43b22d4c " )); @@ -107,7 +107,7 @@ mod tests { // --- extract_ticket_path --- #[test] - fn test_extract_ticket_path_saving_format() { + fn extract_ticket_path_saving_format() { let output = "[*] Saving ticket in admin.ccache"; assert_eq!( extract_ticket_path(output), @@ -116,7 +116,7 @@ mod tests { } #[test] - fn test_extract_ticket_path_fallback() { + fn extract_ticket_path_fallback() { let output = "Ticket written to krbtgt_contoso.ccache"; assert_eq!( extract_ticket_path(output), @@ -125,19 +125,19 @@ mod tests { } #[test] - fn test_extract_ticket_path_none() { + fn extract_ticket_path_none() { assert_eq!(extract_ticket_path("No ticket found"), None); } #[test] - fn test_extract_ticket_path_empty() { + fn extract_ticket_path_empty() { assert_eq!(extract_ticket_path(""), None); } // --- extract_host_from_spn --- #[test] - fn test_extract_host_from_spn_mssql() { + fn extract_host_from_spn_mssql() { assert_eq!( extract_host_from_spn("MSSQLSvc/db01.contoso.local"), Some("db01.contoso.local".to_string()) @@ -145,7 +145,7 @@ mod tests { } #[test] - fn test_extract_host_from_spn_with_port() { + fn extract_host_from_spn_with_port() { assert_eq!( extract_host_from_spn("MSSQLSvc/db01.contoso.local:1433"), Some("db01.contoso.local".to_string()) @@ -153,7 +153,7 @@ mod tests { } #[test] - fn test_extract_host_from_spn_cifs() { + fn extract_host_from_spn_cifs() { assert_eq!( extract_host_from_spn("CIFS/dc01.contoso.local"), Some("dc01.contoso.local".to_string()) @@ -161,12 +161,12 @@ mod tests { } #[test] - fn test_extract_host_from_spn_no_slash() { + fn extract_host_from_spn_no_slash() { assert_eq!(extract_host_from_spn("krbtgt"), None); } #[test] - fn test_extract_host_from_spn_no_dots() { + fn extract_host_from_spn_no_dots() { assert_eq!(extract_host_from_spn("HTTP/localhost"), None); } } diff --git a/ares-llm/src/tool_registry/mod.rs b/ares-llm/src/tool_registry/mod.rs index f5768b84..d74109b4 100644 --- a/ares-llm/src/tool_registry/mod.rs +++ b/ares-llm/src/tool_registry/mod.rs @@ -272,7 +272,7 @@ mod tests { use super::*; #[test] - fn test_recon_tools_include_callbacks() { + fn recon_tools_include_callbacks() { let tools = tools_for_role(AgentRole::Recon); let names: Vec<&str> = tools.iter().map(|t| t.name.as_str()).collect(); assert!(names.contains(&"nmap_scan")); @@ -281,7 +281,7 @@ mod tests { } #[test] - fn test_callback_tool_detection() { + fn callback_tool_detection() { assert!(is_callback_tool("task_complete")); assert!(is_callback_tool("request_assistance")); assert!(is_callback_tool("report_lateral_success")); @@ -299,7 +299,7 @@ mod tests { } #[test] - fn test_tool_schemas_valid_json() { + fn tool_schemas_valid_json() { for role in [ AgentRole::Recon, AgentRole::CredentialAccess, @@ -329,7 +329,7 @@ mod tests { } #[test] - fn test_tools_for_capabilities() { + fn returns_tools_for_capabilities() { let caps = vec!["nmap_scan".to_string(), "secretsdump".to_string()]; let tools = tools_for_capabilities(&caps); let names: Vec<&str> = tools.iter().map(|t| t.name.as_str()).collect(); @@ -341,14 +341,14 @@ mod tests { } #[test] - fn test_agent_role_str() { + fn agent_role_str() { assert_eq!(AgentRole::Recon.as_str(), "recon"); assert_eq!(AgentRole::Orchestrator.as_str(), "orchestrator"); assert_eq!(AgentRole::CredentialAccess.as_str(), "credential_access"); } #[test] - fn test_cracker_has_crack_callbacks() { + fn cracker_has_crack_callbacks() { let tools = tools_for_role(AgentRole::Cracker); let names: Vec<&str> = tools.iter().map(|t| t.name.as_str()).collect(); assert!(names.contains(&"crack_with_hashcat")); @@ -357,7 +357,7 @@ mod tests { } #[test] - fn test_lateral_has_lateral_callbacks() { + fn lateral_has_lateral_callbacks() { let tools = tools_for_role(AgentRole::Lateral); let names: Vec<&str> = tools.iter().map(|t| t.name.as_str()).collect(); assert!(names.contains(&"psexec")); @@ -368,7 +368,7 @@ mod tests { } #[test] - fn test_orchestrator_has_management_tools() { + fn orchestrator_has_management_tools() { let tools = tools_for_role(AgentRole::Orchestrator); let names: Vec<&str> = tools.iter().map(|t| t.name.as_str()).collect(); assert!(names.contains(&"get_pending_tasks")); @@ -377,7 +377,7 @@ mod tests { } #[test] - fn test_all_roles_have_reporting() { + fn all_roles_have_reporting() { for role in [ AgentRole::Recon, AgentRole::CredentialAccess, @@ -416,7 +416,7 @@ mod tests { } #[test] - fn test_no_duplicate_tool_names_per_role() { + fn no_duplicate_tool_names_per_role() { for role in [ AgentRole::Recon, AgentRole::CredentialAccess, @@ -441,7 +441,7 @@ mod tests { } #[test] - fn test_credential_access_has_key_tools() { + fn credential_access_has_key_tools() { let tools = tools_for_role(AgentRole::CredentialAccess); let names: Vec<&str> = tools.iter().map(|t| t.name.as_str()).collect(); assert!(names.contains(&"secretsdump")); @@ -458,7 +458,7 @@ mod tests { } #[test] - fn test_recon_has_credential_discovery_tools() { + fn recon_has_credential_discovery_tools() { let tools = tools_for_role(AgentRole::Recon); let names: Vec<&str> = tools.iter().map(|t| t.name.as_str()).collect(); // Shared credential discovery tools (from netexec_tools) @@ -477,7 +477,7 @@ mod tests { } #[test] - fn test_privesc_has_key_tools() { + fn privesc_has_key_tools() { let tools = tools_for_role(AgentRole::Privesc); let names: Vec<&str> = tools.iter().map(|t| t.name.as_str()).collect(); assert!(names.contains(&"certipy_find")); @@ -493,7 +493,7 @@ mod tests { } #[test] - fn test_coercion_has_relay_tools() { + fn coercion_has_relay_tools() { let tools = tools_for_role(AgentRole::Coercion); let names: Vec<&str> = tools.iter().map(|t| t.name.as_str()).collect(); assert!(names.contains(&"start_responder")); diff --git a/ares-llm/tests/integration_agent_loop.rs b/ares-llm/tests/integration_agent_loop.rs index fe991c20..f0f260e5 100644 --- a/ares-llm/tests/integration_agent_loop.rs +++ b/ares-llm/tests/integration_agent_loop.rs @@ -173,7 +173,7 @@ fn tool_use_response(tool_calls: Vec) -> LlmResponse { // --------------------------------------------------------------------------- #[tokio::test] -async fn test_multi_turn_tool_use_then_task_complete() { +async fn multi_turn_tool_use_then_task_complete() { // Turn 1: LLM requests nmap_scan let turn1 = tool_use_response(vec![ToolCall { id: "call_1".into(), @@ -238,7 +238,7 @@ async fn test_multi_turn_tool_use_then_task_complete() { // --------------------------------------------------------------------------- #[tokio::test] -async fn test_max_steps_limit() { +async fn max_steps_limit() { // LLM always returns a tool call, never calls task_complete let responses: Vec = (0..5) .map(|i| { @@ -293,7 +293,7 @@ async fn test_max_steps_limit() { // --------------------------------------------------------------------------- #[tokio::test] -async fn test_end_turn_no_tool_calls() { +async fn end_turn_no_tool_calls() { let response = LlmResponse { content: "I have analyzed the network and there is nothing more to do.".into(), tool_calls: vec![], @@ -338,7 +338,7 @@ async fn test_end_turn_no_tool_calls() { // --------------------------------------------------------------------------- #[tokio::test] -async fn test_tool_dispatch_error_fed_back() { +async fn tool_dispatch_error_fed_back() { // Turn 1: LLM requests nmap_scan let turn1 = tool_use_response(vec![ToolCall { id: "call_1".into(), @@ -398,7 +398,7 @@ async fn test_tool_dispatch_error_fed_back() { // --------------------------------------------------------------------------- #[tokio::test] -async fn test_tool_dispatch_hard_error_fed_back() { +async fn tool_dispatch_hard_error_fed_back() { // Turn 1: LLM requests nmap_scan let turn1 = tool_use_response(vec![ToolCall { id: "call_1".into(), @@ -454,7 +454,7 @@ async fn test_tool_dispatch_hard_error_fed_back() { // --------------------------------------------------------------------------- #[tokio::test] -async fn test_request_assistance_callback() { +async fn request_assistance_callback() { let response = tool_use_response(vec![ToolCall { id: "call_1".into(), name: "request_assistance".into(), @@ -503,7 +503,7 @@ async fn test_request_assistance_callback() { // --------------------------------------------------------------------------- #[tokio::test] -async fn test_token_usage_accumulates() { +async fn token_usage_accumulates() { let turn1 = LlmResponse { content: String::new(), tool_calls: vec![ToolCall { @@ -572,7 +572,7 @@ async fn test_token_usage_accumulates() { // --------------------------------------------------------------------------- #[tokio::test] -async fn test_llm_error_returns_error_outcome() { +async fn llm_error_returns_error_outcome() { // Provider with no responses queued -- will return an error let provider = MockProvider::new(vec![]); let dispatcher = Arc::new(MockDispatcher::new(vec![])); @@ -646,7 +646,7 @@ impl LlmProvider for RetryMockProvider { } #[tokio::test] -async fn test_rate_limit_retry_succeeds() { +async fn rate_limit_retry_succeeds() { // First call returns 429, second call returns EndTurn let success = LlmResponse { content: "Recovered after rate limit.".into(), @@ -709,7 +709,7 @@ impl LlmProvider for AuthErrorMockProvider { } #[tokio::test] -async fn test_auth_error_fails_immediately() { +async fn auth_error_fails_immediately() { let provider = AuthErrorMockProvider; let dispatcher = Arc::new(MockDispatcher::new(vec![])); diff --git a/ares-tools/src/acl.rs b/ares-tools/src/acl.rs index f7e58e6e..312cb229 100644 --- a/ares-tools/src/acl.rs +++ b/ares-tools/src/acl.rs @@ -338,12 +338,12 @@ mod tests { // ── domain_to_base_dn ────────────────────────────────────────────── #[test] - fn test_domain_to_base_dn_simple() { + fn domain_to_base_dn_simple() { assert_eq!(domain_to_base_dn("contoso.local"), "DC=contoso,DC=local"); } #[test] - fn test_domain_to_base_dn_nested() { + fn domain_to_base_dn_nested() { assert_eq!( domain_to_base_dn("north.contoso.local"), "DC=north,DC=contoso,DC=local" @@ -351,17 +351,17 @@ mod tests { } #[test] - fn test_domain_to_base_dn_single() { + fn domain_to_base_dn_single() { assert_eq!(domain_to_base_dn("local"), "DC=local"); } #[test] - fn test_domain_to_base_dn_fabrikam() { + fn domain_to_base_dn_fabrikam() { assert_eq!(domain_to_base_dn("fabrikam.local"), "DC=fabrikam,DC=local"); } #[test] - fn test_domain_to_base_dn_deep_nesting() { + fn domain_to_base_dn_deep_nesting() { assert_eq!( domain_to_base_dn("sub.child.contoso.local"), "DC=sub,DC=child,DC=contoso,DC=local" @@ -369,7 +369,7 @@ mod tests { } #[test] - fn test_adminsd_holder_dn_format() { + fn adminsd_holder_dn_format() { let domain = "contoso.local"; let base_dn = domain_to_base_dn(domain); let adminsd_dn = format!("CN=AdminSDHolder,CN=System,{base_dn}"); @@ -377,7 +377,7 @@ mod tests { } #[test] - fn test_adminsd_holder_dn_fabrikam() { + fn adminsd_holder_dn_fabrikam() { let base_dn = domain_to_base_dn("fabrikam.local"); let adminsd_dn = format!("CN=AdminSDHolder,CN=System,{base_dn}"); assert_eq!( @@ -389,7 +389,7 @@ mod tests { // ── bloodyad_add_group_member arg validation ─────────────────────── #[test] - fn test_bloodyad_add_group_member_missing_domain() { + fn bloodyad_add_group_member_missing_domain() { let args = json!({ "username": "admin", "password": "P@ssw0rd!", @@ -401,7 +401,7 @@ mod tests { } #[test] - fn test_bloodyad_add_group_member_all_args_parse() { + fn bloodyad_add_group_member_all_args_parse() { let args = json!({ "domain": "contoso.local", "username": "admin", @@ -421,7 +421,7 @@ mod tests { // ── bloodyad_set_password arg validation ─────────────────────────── #[test] - fn test_bloodyad_set_password_missing_new_password() { + fn bloodyad_set_password_missing_new_password() { let args = json!({ "domain": "contoso.local", "username": "admin", @@ -433,7 +433,7 @@ mod tests { } #[test] - fn test_bloodyad_set_password_all_args_parse() { + fn bloodyad_set_password_all_args_parse() { let args = json!({ "domain": "contoso.local", "username": "admin", @@ -449,7 +449,7 @@ mod tests { // ── bloodyad_add_genericall arg validation ───────────────────────── #[test] - fn test_bloodyad_genericall_missing_target_dn() { + fn bloodyad_genericall_missing_target_dn() { let args = json!({ "domain": "contoso.local", "username": "admin", @@ -461,7 +461,7 @@ mod tests { } #[test] - fn test_bloodyad_genericall_all_args() { + fn bloodyad_genericall_all_args() { let args = json!({ "domain": "contoso.local", "username": "admin", @@ -480,7 +480,7 @@ mod tests { // ── adminsd_holder_add_ace arg validation ────────────────────────── #[test] - fn test_adminsd_holder_right_default() { + fn adminsd_holder_right_default() { let args = json!({ "domain": "contoso.local", "username": "admin", @@ -493,7 +493,7 @@ mod tests { } #[test] - fn test_adminsd_holder_custom_right() { + fn adminsd_holder_custom_right() { let args = json!({ "domain": "contoso.local", "username": "admin", @@ -507,7 +507,7 @@ mod tests { } #[test] - fn test_adminsd_holder_dn_construction() { + fn adminsd_holder_dn_construction() { let domain = "contoso.local"; let base_dn = domain_to_base_dn(domain); let adminsd_dn = format!("CN=AdminSDHolder,CN=System,{base_dn}"); @@ -518,7 +518,7 @@ mod tests { // ── gmsa_read_password arg validation ────────────────────────────── #[test] - fn test_gmsa_read_password_missing_account() { + fn gmsa_read_password_missing_account() { let args = json!({ "domain": "contoso.local", "username": "admin", @@ -529,7 +529,7 @@ mod tests { } #[test] - fn test_gmsa_read_password_args() { + fn gmsa_read_password_args() { let args = json!({ "domain": "contoso.local", "username": "admin", @@ -543,7 +543,7 @@ mod tests { // ── pywhisker arg validation ─────────────────────────────────────── #[test] - fn test_pywhisker_default_action() { + fn pywhisker_default_action() { let args = json!({ "domain": "contoso.local", "username": "admin", @@ -556,7 +556,7 @@ mod tests { } #[test] - fn test_pywhisker_custom_action() { + fn pywhisker_custom_action() { let args = json!({ "domain": "contoso.local", "username": "admin", @@ -570,7 +570,7 @@ mod tests { } #[test] - fn test_pywhisker_missing_target_sam() { + fn pywhisker_missing_target_sam() { let args = json!({ "domain": "contoso.local", "username": "admin", @@ -583,7 +583,7 @@ mod tests { // ── targeted_kerberoast arg validation ───────────────────────────── #[test] - fn test_targeted_kerberoast_missing_target_user() { + fn targeted_kerberoast_missing_target_user() { let args = json!({ "domain": "contoso.local", "username": "admin", @@ -594,7 +594,7 @@ mod tests { } #[test] - fn test_targeted_kerberoast_args() { + fn targeted_kerberoast_args() { let args = json!({ "domain": "contoso.local", "username": "admin", @@ -608,7 +608,7 @@ mod tests { // ── sharpgpoabuse arg validation ─────────────────────────────────── #[test] - fn test_sharpgpoabuse_default_action() { + fn sharpgpoabuse_default_action() { let args = json!({ "gpo_name": "Default Domain Policy", "domain": "contoso.local", @@ -623,7 +623,7 @@ mod tests { } #[test] - fn test_sharpgpoabuse_user_to_add_default_fallback() { + fn sharpgpoabuse_user_to_add_default_fallback() { let args = json!({ "gpo_name": "Default Domain Policy", "domain": "contoso.local", @@ -637,7 +637,7 @@ mod tests { } #[test] - fn test_sharpgpoabuse_explicit_user_to_add() { + fn sharpgpoabuse_explicit_user_to_add() { let args = json!({ "gpo_name": "Default Domain Policy", "domain": "contoso.local", @@ -652,7 +652,7 @@ mod tests { } #[test] - fn test_sharpgpoabuse_computer_target_optional() { + fn sharpgpoabuse_computer_target_optional() { let args = json!({ "gpo_name": "Default Domain Policy", "domain": "contoso.local", @@ -668,7 +668,7 @@ mod tests { } #[test] - fn test_sharpgpoabuse_computer_target_absent() { + fn sharpgpoabuse_computer_target_absent() { let args = json!({ "gpo_name": "Default Domain Policy", "domain": "contoso.local", @@ -682,7 +682,7 @@ mod tests { // ── pygpoabuse_immediate_task arg validation ─────────────────────── #[test] - fn test_pygpoabuse_default_taskname() { + fn pygpoabuse_default_taskname() { let args = json!({ "domain": "contoso.local", "username": "admin", @@ -696,7 +696,7 @@ mod tests { } #[test] - fn test_pygpoabuse_default_force() { + fn pygpoabuse_default_force() { let args = json!({ "domain": "contoso.local", "username": "admin", @@ -710,7 +710,7 @@ mod tests { } #[test] - fn test_pygpoabuse_force_false() { + fn pygpoabuse_force_false() { let args = json!({ "domain": "contoso.local", "username": "admin", @@ -725,7 +725,7 @@ mod tests { } #[test] - fn test_pygpoabuse_missing_gpo_id() { + fn pygpoabuse_missing_gpo_id() { let args = json!({ "domain": "contoso.local", "username": "admin", @@ -739,7 +739,7 @@ mod tests { // ── dacl_edit arg validation ─────────────────────────────────────── #[test] - fn test_dacl_edit_default_action() { + fn dacl_edit_default_action() { let args = json!({ "domain": "contoso.local", "username": "admin", @@ -754,7 +754,7 @@ mod tests { } #[test] - fn test_dacl_edit_custom_action() { + fn dacl_edit_custom_action() { let args = json!({ "domain": "contoso.local", "username": "admin", @@ -770,7 +770,7 @@ mod tests { } #[test] - fn test_dacl_edit_missing_rights() { + fn dacl_edit_missing_rights() { let args = json!({ "domain": "contoso.local", "username": "admin", @@ -783,7 +783,7 @@ mod tests { } #[test] - fn test_dacl_edit_missing_principal() { + fn dacl_edit_missing_principal() { let args = json!({ "domain": "contoso.local", "username": "admin", @@ -798,7 +798,7 @@ mod tests { // ── credential helper integration ────────────────────────────────── #[test] - fn test_bloodyad_creds_format() { + fn bloodyad_creds_format() { let creds = credentials::bloodyad_creds("contoso.local", "admin", "P@ssw0rd!", "192.168.58.10"); assert_eq!( @@ -817,7 +817,7 @@ mod tests { } #[test] - fn test_impacket_target_with_domain_and_password() { + fn impacket_target_with_domain_and_password() { let target = credentials::impacket_target( Some("contoso.local"), "admin", @@ -828,14 +828,14 @@ mod tests { } #[test] - fn test_impacket_target_without_password() { + fn impacket_target_without_password() { let target = credentials::impacket_target(Some("contoso.local"), "admin", None, "contoso.local"); assert_eq!(target, "contoso.local/admin@contoso.local"); } #[test] - fn test_impacket_target_without_domain() { + fn impacket_target_without_domain() { let target = credentials::impacket_target(None, "admin", Some("P@ssw0rd!"), "192.168.58.10"); assert_eq!(target, "admin:P@ssw0rd!@192.168.58.10"); diff --git a/ares-tools/src/args.rs b/ares-tools/src/args.rs index deceb839..f120422a 100644 --- a/ares-tools/src/args.rs +++ b/ares-tools/src/args.rs @@ -21,3 +21,93 @@ pub fn optional_i64(args: &Value, field: &str) -> Option { pub fn optional_bool(args: &Value, field: &str) -> Option { args.get(field).and_then(Value::as_bool) } + +#[cfg(test)] +mod tests { + use super::*; + use serde_json::json; + + #[test] + fn required_str_present() { + let args = json!({"name": "alice"}); + let val = required_str(&args, "name").unwrap(); + assert_eq!(val, "alice"); + } + + #[test] + fn required_str_missing_returns_error() { + let args = json!({"other": "value"}); + let err = required_str(&args, "name").unwrap_err(); + assert!( + err.to_string().contains("missing required argument: name"), + "got: {err}" + ); + } + + #[test] + fn required_str_wrong_type_returns_error() { + let args = json!({"count": 42}); + let err = required_str(&args, "count").unwrap_err(); + assert!(err.to_string().contains("missing required argument")); + } + + #[test] + fn optional_str_present() { + let args = json!({"host": "10.0.0.1"}); + assert_eq!(optional_str(&args, "host"), Some("10.0.0.1")); + } + + #[test] + fn optional_str_missing() { + let args = json!({}); + assert_eq!(optional_str(&args, "host"), None); + } + + #[test] + fn optional_str_wrong_type() { + let args = json!({"port": 8080}); + assert_eq!(optional_str(&args, "port"), None); + } + + #[test] + fn optional_i64_present() { + let args = json!({"port": 445}); + assert_eq!(optional_i64(&args, "port"), Some(445)); + } + + #[test] + fn optional_i64_missing() { + let args = json!({}); + assert_eq!(optional_i64(&args, "port"), None); + } + + #[test] + fn optional_i64_wrong_type() { + let args = json!({"port": "not_a_number"}); + assert_eq!(optional_i64(&args, "port"), None); + } + + #[test] + fn optional_bool_present_true() { + let args = json!({"verbose": true}); + assert_eq!(optional_bool(&args, "verbose"), Some(true)); + } + + #[test] + fn optional_bool_present_false() { + let args = json!({"verbose": false}); + assert_eq!(optional_bool(&args, "verbose"), Some(false)); + } + + #[test] + fn optional_bool_missing() { + let args = json!({}); + assert_eq!(optional_bool(&args, "verbose"), None); + } + + #[test] + fn optional_bool_wrong_type() { + let args = json!({"verbose": "yes"}); + assert_eq!(optional_bool(&args, "verbose"), None); + } +} diff --git a/ares-tools/src/blue/engines/tools.rs b/ares-tools/src/blue/engines/tools.rs index 263d0462..2220fa87 100644 --- a/ares-tools/src/blue/engines/tools.rs +++ b/ares-tools/src/blue/engines/tools.rs @@ -332,7 +332,7 @@ mod tests { use crate::blue::engines::pyramid::{assess_pyramid, generate_pyramid_questions, EvidenceItem}; #[test] - fn test_attack_chains_load() { + fn attack_chains_load() { let chains = attack_chains(); assert!(chains.contains_key("T1003.006"), "DCSync should be present"); assert!( @@ -343,7 +343,7 @@ mod tests { } #[test] - fn test_detection_recipes_load() { + fn detection_recipes_load() { let recipes = detection_recipes(); assert!( recipes.contains_key("dcsync"), @@ -361,7 +361,7 @@ mod tests { } #[test] - fn test_climb_strategies_load() { + fn climb_strategies_load() { let strategies = climb_strategies(); assert!( strategies.contains_key("hash_values"), @@ -375,7 +375,7 @@ mod tests { } #[test] - fn test_generate_mitre_questions() { + fn generates_mitre_questions() { let mut techniques = HashSet::new(); techniques.insert("T1003.006".to_string()); @@ -395,7 +395,7 @@ mod tests { } #[test] - fn test_generate_pyramid_questions() { + fn generates_pyramid_questions() { let evidence = vec![ EvidenceItem { value: "192.168.58.10".to_string(), @@ -419,7 +419,7 @@ mod tests { } #[test] - fn test_pyramid_questions_skip_ttps() { + fn pyramid_questions_skip_ttps() { let evidence = vec![EvidenceItem { value: "T1003".to_string(), pyramid_level: "ttps".to_string(), @@ -433,7 +433,7 @@ mod tests { } #[test] - fn test_assess_pyramid() { + fn assesses_pyramid() { let evidence = vec![ EvidenceItem { value: "192.168.58.10".to_string(), @@ -464,7 +464,7 @@ mod tests { } #[test] - fn test_assess_pyramid_empty() { + fn assess_pyramid_empty() { let assessment = assess_pyramid(&[]); assert_eq!( assessment @@ -476,7 +476,7 @@ mod tests { } #[test] - fn test_get_attack_chain_precursors() { + fn gets_attack_chain_precursors() { let args = serde_json::json!({ "technique_id": "T1003.006" }); let result = get_attack_chain_precursors(&args).unwrap(); assert!(result.success); @@ -485,7 +485,7 @@ mod tests { } #[test] - fn test_get_attack_chain_unknown() { + fn get_attack_chain_unknown() { let args = serde_json::json!({ "technique_id": "T9999" }); let result = get_attack_chain_precursors(&args).unwrap(); assert!(result.success); @@ -493,7 +493,7 @@ mod tests { } #[test] - fn test_get_detection_recipe() { + fn gets_detection_recipe() { let args = serde_json::json!({ "recipe_name": "dcsync" }); let result = get_detection_recipe(&args).unwrap(); assert!(result.success); @@ -501,7 +501,7 @@ mod tests { } #[test] - fn test_get_detection_recipe_unknown() { + fn get_detection_recipe_unknown() { let args = serde_json::json!({ "recipe_name": "nonexistent" }); let result = get_detection_recipe(&args).unwrap(); assert!(result.success); @@ -510,7 +510,7 @@ mod tests { } #[test] - fn test_list_detection_recipes() { + fn lists_detection_recipes() { let args = serde_json::json!({}); let result = list_detection_recipes(&args).unwrap(); assert!(result.success); @@ -518,7 +518,7 @@ mod tests { } #[test] - fn test_technique_to_recipe_mapping() { + fn technique_to_recipe_mapping() { let map = technique_to_recipe(); assert_eq!(map.get("T1003.006"), Some(&"dcsync")); assert_eq!(map.get("T1110.003"), Some(&"password_spray")); diff --git a/ares-tools/src/blue/evidence_validator.rs b/ares-tools/src/blue/evidence_validator.rs index 7e839d1c..08986e9f 100644 --- a/ares-tools/src/blue/evidence_validator.rs +++ b/ares-tools/src/blue/evidence_validator.rs @@ -401,7 +401,7 @@ mod tests { use super::*; #[test] - fn test_extract_ips() { + fn extract_ips() { let text = "Source IP: 192.168.58.10, Destination: 192.168.58.10"; let iocs = extract_iocs_from_text(text); assert!(iocs.contains("192.168.58.10")); @@ -409,7 +409,7 @@ mod tests { } #[test] - fn test_extract_hostnames() { + fn extract_hostnames() { let text = r#"Computer: dc01.contoso.local, accessing share on web01.contoso.local"#; let iocs = extract_iocs_from_text(text); assert!(iocs.contains("dc01.contoso.local")); @@ -417,34 +417,34 @@ mod tests { } #[test] - fn test_extract_users() { + fn extract_users() { let text = r#""TargetUserName": "jsmith", "Computer": "DC01""#; let iocs = extract_iocs_from_text(text); assert!(iocs.contains("jsmith")); } #[test] - fn test_extract_hashes() { + fn extract_hashes() { let text = "Hash: aad3b435b51404eeaad3b435b51404ee"; let iocs = extract_iocs_from_text(text); assert!(iocs.contains("aad3b435b51404eeaad3b435b51404ee")); } #[test] - fn test_exclude_file_extensions() { + fn exclude_file_extensions() { assert!(!is_hostname_like("cmd.exe")); assert!(!is_hostname_like("config.json")); assert!(is_hostname_like("dc01.contoso.local")); } #[test] - fn test_validate_mitre_technique() { + fn validate_mitre_technique() { let (valid, _) = validate_evidence_value("T1003.006"); assert!(valid); } #[test] - fn test_store_and_validate() { + fn store_and_validate() { store_query_result("Connected from 192.168.58.50 to dc01.contoso.local"); let (valid, qid) = validate_evidence_value("192.168.58.50"); assert!(valid); @@ -452,14 +452,14 @@ mod tests { } #[test] - fn test_adjust_confidence() { + fn adjusts_confidence() { assert_eq!(adjust_confidence(0.8, true), 0.8); assert!((adjust_confidence(0.8, false) - 0.65).abs() < 0.001); assert!((adjust_confidence(0.1, false) - 0.1).abs() < 0.001); // floor at 0.1 } #[test] - fn test_classify_ioc() { + fn classifies_ioc() { assert_eq!(classify_ioc("192.168.58.10"), Some("ip")); assert_eq!(classify_ioc("dc01.contoso.local"), Some("hostname")); assert_eq!( diff --git a/ares-tools/src/blue/learning/playbook.rs b/ares-tools/src/blue/learning/playbook.rs index 0fa551fd..da617fc5 100644 --- a/ares-tools/src/blue/learning/playbook.rs +++ b/ares-tools/src/blue/learning/playbook.rs @@ -431,7 +431,7 @@ mod tests { use serde_json::json; #[test] - fn test_lookup_known_technique() { + fn lookup_known_technique() { let args = json!({"technique_id": "T1003"}); let result = lookup_technique(&args).unwrap(); assert!(result.success); @@ -440,7 +440,7 @@ mod tests { } #[test] - fn test_lookup_subtechnique() { + fn lookup_subtechnique() { let args = json!({"technique_id": "T1003.001"}); let result = lookup_technique(&args).unwrap(); assert!(result.success); @@ -448,7 +448,7 @@ mod tests { } #[test] - fn test_lookup_unknown_falls_back_to_parent() { + fn lookup_unknown_falls_back_to_parent() { let args = json!({"technique_id": "T1003.999"}); let result = lookup_technique(&args).unwrap(); assert!(result.success); @@ -457,7 +457,7 @@ mod tests { } #[test] - fn test_lookup_completely_unknown() { + fn lookup_completely_unknown() { let args = json!({"technique_id": "T9999"}); let result = lookup_technique(&args).unwrap(); assert!(!result.success); @@ -465,7 +465,7 @@ mod tests { } #[test] - fn test_lookup_case_insensitive() { + fn lookup_case_insensitive() { let args = json!({"technique_id": "t1003"}); let result = lookup_technique(&args).unwrap(); assert!(result.success); @@ -473,7 +473,7 @@ mod tests { } #[test] - fn test_suggest_credential_access() { + fn suggest_credential_access() { let args = json!({"evidence_type": "credential_access"}); let result = suggest_techniques(&args).unwrap(); assert!(result.success); @@ -482,7 +482,7 @@ mod tests { } #[test] - fn test_suggest_lateral_movement() { + fn suggest_lateral_movement() { let args = json!({"evidence_type": "lateral_movement"}); let result = suggest_techniques(&args).unwrap(); assert!(result.success); @@ -491,7 +491,7 @@ mod tests { } #[test] - fn test_suggest_with_hyphens() { + fn suggest_with_hyphens() { let args = json!({"evidence_type": "lateral-movement"}); let result = suggest_techniques(&args).unwrap(); assert!(result.success); @@ -499,7 +499,7 @@ mod tests { } #[test] - fn test_suggest_unknown_type() { + fn suggest_unknown_type() { let args = json!({"evidence_type": "nonexistent"}); let result = suggest_techniques(&args).unwrap(); assert!(!result.success); @@ -508,7 +508,7 @@ mod tests { } #[test] - fn test_missing_required_arg() { + fn missing_required_arg() { let args = json!({}); let result = lookup_technique(&args); assert!(result.is_err()); diff --git a/ares-tools/src/blue/persistence.rs b/ares-tools/src/blue/persistence.rs index 43c0dddb..961bcce3 100644 --- a/ares-tools/src/blue/persistence.rs +++ b/ares-tools/src/blue/persistence.rs @@ -477,7 +477,7 @@ mod tests { } #[test] - fn test_store_and_find() { + fn store_and_find() { let dir = tempfile::tempdir().unwrap(); let path = dir.path().join("test_investigations.json"); let store = InvestigationStore::open(path); @@ -492,7 +492,7 @@ mod tests { } #[test] - fn test_statistics() { + fn statistics() { let dir = tempfile::tempdir().unwrap(); let path = dir.path().join("test_stats.json"); let store = InvestigationStore::open(path); @@ -506,7 +506,7 @@ mod tests { } #[test] - fn test_label_investigation() { + fn labels_investigation() { let dir = tempfile::tempdir().unwrap(); let path = dir.path().join("test_label.json"); let store = InvestigationStore::open(path); @@ -521,7 +521,7 @@ mod tests { } #[test] - fn test_query_effectiveness() { + fn query_effectiveness() { let dir = tempfile::tempdir().unwrap(); let path = dir.path().join("test_qe.json"); let store = InvestigationStore::open(path); diff --git a/ares-tools/src/credential_access/mod.rs b/ares-tools/src/credential_access/mod.rs index 9ef90a16..5e9d003c 100644 --- a/ares-tools/src/credential_access/mod.rs +++ b/ares-tools/src/credential_access/mod.rs @@ -22,7 +22,7 @@ mod tests { /// Verify that the base_dn builder produces correct LDAP distinguished names. #[test] - fn test_base_dn_from_domain() { + fn base_dn_from_domain() { let domain = "contoso.local"; let dn: String = domain .split('.') @@ -34,7 +34,7 @@ mod tests { /// Verify that the base_dn builder handles a deeper domain. #[test] - fn test_base_dn_from_child_domain() { + fn base_dn_from_child_domain() { let domain = "north.contoso.local"; let dn: String = domain .split('.') @@ -46,7 +46,7 @@ mod tests { /// Verify password_spray builds args for jitter correctly (presence only). #[test] - fn test_password_spray_args_shape() { + fn password_spray_args_shape() { // We can't fully execute without the binary, but we can verify // the required_str / optional helpers parse correctly. let args = json!({ @@ -62,7 +62,7 @@ mod tests { /// Verify username_as_password parses required fields. #[test] - fn test_username_as_password_args() { + fn username_as_password_args() { let args = json!({ "target": "192.168.58.10", "users_file": "/tmp/users.txt", diff --git a/ares-tools/src/credentials.rs b/ares-tools/src/credentials.rs index 9a5e4a86..c382be34 100644 --- a/ares-tools/src/credentials.rs +++ b/ares-tools/src/credentials.rs @@ -93,3 +93,141 @@ pub fn impacket_auth( pub fn kerberos_env(ticket_path: &str) -> (String, String) { ("KRB5CCNAME".to_string(), ticket_path.to_string()) } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn impacket_target_with_domain_and_password() { + let result = impacket_target(Some("CONTOSO"), "admin", Some("P@ss"), "10.0.0.1"); + assert_eq!(result, "CONTOSO/admin:P@ss@10.0.0.1"); + } + + #[test] + fn impacket_target_no_domain() { + let result = impacket_target(None, "admin", Some("pass"), "dc01"); + assert_eq!(result, "admin:pass@dc01"); + } + + #[test] + fn impacket_target_empty_domain() { + let result = impacket_target(Some(""), "admin", Some("pass"), "dc01"); + assert_eq!(result, "admin:pass@dc01"); + } + + #[test] + fn impacket_target_no_password() { + let result = impacket_target(Some("CONTOSO"), "admin", None, "dc01"); + assert_eq!(result, "CONTOSO/admin@dc01"); + } + + #[test] + fn impacket_target_no_domain_no_password() { + let result = impacket_target(None, "user", None, "target"); + assert_eq!(result, "user@target"); + } + + #[test] + fn hash_args_plain_nthash() { + let args = hash_args("aabbccdd"); + assert_eq!(args, vec!["-hashes", ":aabbccdd"]); + } + + #[test] + fn hash_args_lm_nt_pair() { + let args = hash_args("aad3b435:aabbccdd"); + assert_eq!(args, vec!["-hashes", "aad3b435:aabbccdd"]); + } + + #[test] + fn netexec_creds_password_auth() { + let args = netexec_creds(Some("admin"), Some("P@ss"), None, Some("CONTOSO")); + assert_eq!(args, vec!["-u", "admin", "-p", "P@ss", "-d", "CONTOSO"]); + } + + #[test] + fn netexec_creds_hash_auth() { + let args = netexec_creds( + Some("admin"), + Some("ignored"), + Some("aabbccdd"), + Some("CONTOSO"), + ); + // Hash takes priority over password + assert_eq!( + args, + vec!["-u", "admin", "-H", ":aabbccdd", "-d", "CONTOSO"] + ); + } + + #[test] + fn netexec_creds_hash_with_colon() { + let args = netexec_creds(Some("admin"), None, Some("lm:nt"), None); + assert_eq!(args, vec!["-u", "admin", "-H", "lm:nt"]); + } + + #[test] + fn netexec_creds_no_username() { + let args = netexec_creds(None, Some("pass"), None, None); + assert_eq!(args, vec!["-p", "pass"]); + } + + #[test] + fn netexec_creds_empty() { + let args = netexec_creds(None, None, None, None); + assert!(args.is_empty()); + } + + #[test] + fn bloodyad_creds_builds_correct_args() { + let args = bloodyad_creds("contoso.local", "admin", "P@ssw0rd", "10.0.0.1"); + assert_eq!( + args, + vec![ + "-d", + "contoso.local", + "-u", + "admin", + "-p", + "P@ssw0rd", + "--host", + "10.0.0.1", + ] + ); + } + + #[test] + fn impacket_auth_with_hash() { + let (target, extra) = impacket_auth( + Some("CONTOSO"), + "admin", + Some("ignored"), + Some("aabbccdd"), + "dc01", + ); + assert_eq!(target, "CONTOSO/admin@dc01"); + assert_eq!(extra, vec!["-hashes", ":aabbccdd"]); + } + + #[test] + fn impacket_auth_with_password() { + let (target, extra) = impacket_auth(Some("CONTOSO"), "admin", Some("P@ss"), None, "dc01"); + assert_eq!(target, "CONTOSO/admin:P@ss@dc01"); + assert!(extra.is_empty()); + } + + #[test] + fn impacket_auth_no_creds() { + let (target, extra) = impacket_auth(None, "user", None, None, "host"); + assert_eq!(target, "user@host"); + assert!(extra.is_empty()); + } + + #[test] + fn kerberos_env_builds_tuple() { + let (key, val) = kerberos_env("/tmp/krb5cc_admin"); + assert_eq!(key, "KRB5CCNAME"); + assert_eq!(val, "/tmp/krb5cc_admin"); + } +} diff --git a/ares-tools/src/parsers/certipy.rs b/ares-tools/src/parsers/certipy.rs index 19c508d5..eb6b59d9 100644 --- a/ares-tools/src/parsers/certipy.rs +++ b/ares-tools/src/parsers/certipy.rs @@ -132,7 +132,7 @@ mod tests { use serde_json::json; #[test] - fn test_parse_certipy_esc1() { + fn parse_certipy_esc1() { let output = "[!] Vulnerabilities\nESC1: Template allows enrollment with low-priv"; let params = json!({"target": "192.168.58.10", "domain": "contoso.local"}); let vulns = parse_certipy_find(output, ¶ms); @@ -143,7 +143,7 @@ mod tests { } #[test] - fn test_parse_certipy_multiple_esc_types() { + fn parse_certipy_multiple_esc_types() { let output = "[!] Vulnerabilities\nESC1: ...\nESC4: Template is misconfigured\nESC8: Web enrollment"; let params = json!({"target_ip": "192.168.58.10"}); @@ -159,7 +159,7 @@ mod tests { } #[test] - fn test_parse_certipy_no_vulnerabilities_keyword() { + fn parse_certipy_no_vulnerabilities_keyword() { // Without [!] Vulnerabilities header, only "ESCn :" pattern matches let output = "ESC1 : Template allows enrollment"; let vulns = parse_certipy_find(output, &json!({"target": "192.168.58.10"})); @@ -167,20 +167,20 @@ mod tests { } #[test] - fn test_parse_certipy_no_esc_types() { + fn parse_certipy_no_esc_types() { let output = "[!] Vulnerabilities\nNo vulnerable templates found"; let vulns = parse_certipy_find(output, &json!({"target": "192.168.58.10"})); assert!(vulns.is_empty()); } #[test] - fn test_parse_certipy_empty_output() { + fn parse_certipy_empty_output() { let vulns = parse_certipy_find("", &json!({})); assert!(vulns.is_empty()); } #[test] - fn test_parse_certipy_vuln_id_format() { + fn parse_certipy_vuln_id_format() { let output = "[!] Vulnerabilities\nESC4: misconfigured template"; let params = json!({"target": "192.168.58.20"}); let vulns = parse_certipy_find(output, ¶ms); @@ -188,7 +188,7 @@ mod tests { } #[test] - fn test_parse_certipy_extended_esc_types() { + fn parse_certipy_extended_esc_types() { let output = "[!] Vulnerabilities\nESC1: ...\nESC6: EDITF flag\nESC9: UPN spoof\nESC13: issuance policy"; let params = json!({"target_ip": "192.168.58.10"}); let vulns = parse_certipy_find(output, ¶ms); @@ -203,7 +203,7 @@ mod tests { } #[test] - fn test_parse_certipy_with_ca_name() { + fn parse_certipy_with_ca_name() { let output = "CA Name : ESSOS-CA\n[!] Vulnerabilities\nESC1: enrollee supplies subject"; let params = json!({"target": "192.168.58.10", "domain": "essos.local"}); let vulns = parse_certipy_find(output, ¶ms); @@ -213,7 +213,7 @@ mod tests { } #[test] - fn test_parse_certipy_inline_pattern() { + fn parse_certipy_inline_pattern() { // certipy find -vulnerable output format let output = " ESC1 : 'ESSOS.LOCAL\\Domain Users' can enroll, enrollee supplies subject"; let params = json!({"target": "192.168.58.10"}); @@ -223,13 +223,13 @@ mod tests { } #[test] - fn test_esc_priority_ordering() { + fn esc_priority_ordering() { assert!(esc_priority("esc1") < esc_priority("esc4")); assert!(esc_priority("esc4") < esc_priority("esc5")); } #[test] - fn test_esc_priority_all_values() { + fn esc_priority_all_values() { assert_eq!(esc_priority("esc1"), 1); assert_eq!(esc_priority("esc6"), 1); assert_eq!(esc_priority("esc4"), 2); @@ -246,31 +246,31 @@ mod tests { } #[test] - fn test_extract_ca_name_standard() { + fn extract_ca_name_standard() { let output = "CA Name : CONTOSO-CA\nDNS Name : ca01.contoso.local"; assert_eq!(extract_ca_name(output), Some("CONTOSO-CA".to_string())); } #[test] - fn test_extract_ca_name_no_spaces() { + fn extract_ca_name_no_spaces() { let output = "CA Name:MYCA\nother line"; assert_eq!(extract_ca_name(output), Some("MYCA".to_string())); } #[test] - fn test_extract_ca_name_missing() { + fn extract_ca_name_missing() { assert_eq!(extract_ca_name("No CA info here"), None); assert_eq!(extract_ca_name(""), None); } #[test] - fn test_extract_ca_name_empty_value() { + fn extract_ca_name_empty_value() { assert_eq!(extract_ca_name("CA Name : "), None); } #[test] - fn test_extract_template_for_esc() { + fn extract_template_for_esc_basic() { let output = "Template Name : VulnTemplate\n Permissions\n ESC1 : 'DOMAIN\\Users' can enroll"; assert_eq!( extract_template_for_esc(output, "esc1"), @@ -279,13 +279,13 @@ mod tests { } #[test] - fn test_extract_template_for_esc_not_found() { + fn extract_template_for_esc_not_found() { let output = "ESC1 : 'DOMAIN\\Users' can enroll"; assert_eq!(extract_template_for_esc(output, "esc1"), None); } #[test] - fn test_extract_template_for_esc_multiple_templates() { + fn extract_template_for_esc_multiple_templates() { let output = "Template Name : Template1\n ESC4 : misconfigured\nTemplate Name : Template2\n ESC1 : enrollable"; // ESC4 should get Template1 assert_eq!( @@ -300,7 +300,7 @@ mod tests { } #[test] - fn test_esc_types_constant() { + fn esc_types_constant() { assert_eq!(ESC_TYPES.len(), 14); assert!(ESC_TYPES.contains(&"esc1")); assert!(ESC_TYPES.contains(&"esc8")); @@ -311,7 +311,7 @@ mod tests { } #[test] - fn test_parse_certipy_with_template_name() { + fn parse_certipy_with_template_name() { let output = "Template Name : ESC1-Vuln\n [!] Vulnerabilities\n ESC1 : 'CONTOSO\\Users' can enroll"; let params = json!({"target": "192.168.58.10", "domain": "contoso.local"}); let vulns = parse_certipy_find(output, ¶ms); @@ -320,7 +320,7 @@ mod tests { } #[test] - fn test_parse_certipy_vulnerability_inline_keyword() { + fn parse_certipy_vulnerability_inline_keyword() { // "vulnerab" keyword present alongside ESC type but no [!] Vulnerabilities header let output = "Certificate template is vulnerable to ESC1 attack"; let params = json!({"target": "192.168.58.10"}); @@ -329,7 +329,7 @@ mod tests { } #[test] - fn test_parse_certipy_colon_format() { + fn parse_certipy_colon_format() { // "ESC8:" format without spaces let output = "ESC8:web enrollment enabled"; let params = json!({"target": "192.168.58.10"}); diff --git a/ares-tools/src/parsers/cracker.rs b/ares-tools/src/parsers/cracker.rs index f75d3246..728b41db 100644 --- a/ares-tools/src/parsers/cracker.rs +++ b/ares-tools/src/parsers/cracker.rs @@ -203,7 +203,7 @@ mod tests { use serde_json::json; #[test] - fn test_parse_hashcat_tgs_cracked() { + fn parse_hashcat_tgs_cracked() { let output = r#"hashcat (v6.2.6) starting... Session....: hashcat Status......: Cracked @@ -221,7 +221,7 @@ $krb5tgs$23$*sarah.connor$CHILD.CONTOSO.LOCAL$child.contoso.local/sarah.connor*$ } #[test] - fn test_parse_hashcat_asrep_cracked() { + fn parse_hashcat_asrep_cracked() { let output = r#"--- hashcat --show --- $krb5asrep$23$michelle@FABRIKAM.LOCAL:8a7a0b3264590ef6:fr3edom "#; @@ -234,7 +234,7 @@ $krb5asrep$23$michelle@FABRIKAM.LOCAL:8a7a0b3264590ef6:fr3edom } #[test] - fn test_parse_hashcat_ntlm_cracked() { + fn parse_hashcat_ntlm_cracked() { let output = "--- hashcat --show ---\ne19ccf75ee54e06b06a5907af13cef42:Summer2024!\n"; let params = json!({"domain": "contoso.local", "username": "Administrator"}); let creds = parse_cracker_output(output, ¶ms); @@ -244,7 +244,7 @@ $krb5asrep$23$michelle@FABRIKAM.LOCAL:8a7a0b3264590ef6:fr3edom } #[test] - fn test_parse_john_show_cracked() { + fn parse_john_show_cracked() { let output = "Using default input encoding: UTF-8\n\ Loaded 1 password hash\n\ sarah.connor:MyPassword1:1234:::\n\ @@ -258,7 +258,7 @@ $krb5asrep$23$michelle@FABRIKAM.LOCAL:8a7a0b3264590ef6:fr3edom } #[test] - fn test_no_cracked_output() { + fn no_cracked_output() { let output = "hashcat (v6.2.6) starting...\nExhausted\n--- hashcat --show ---\n"; let params = json!({"domain": "contoso.local"}); let creds = parse_cracker_output(output, ¶ms); @@ -266,7 +266,7 @@ $krb5asrep$23$michelle@FABRIKAM.LOCAL:8a7a0b3264590ef6:fr3edom } #[test] - fn test_john_show_asrep_no_hex_section() { + fn john_show_asrep_no_hex_section() { // John --show for AS-REP omits the hex hash — just user@REALM:password let output = "--- john --show ---\n\ $krb5asrep$23$brian.davis@CHILD.CONTOSO.LOCAL:letmein2025\n\n\ @@ -282,7 +282,7 @@ $krb5asrep$23$michelle@FABRIKAM.LOCAL:8a7a0b3264590ef6:fr3edom } #[test] - fn test_john_show_tgs_unknown_user() { + fn john_show_tgs_unknown_user() { // John --show for TGS shows ?:password (can't determine username) let output = "--- john --show ---\n\ ?:iknownothing\n\n\ @@ -299,7 +299,7 @@ $krb5asrep$23$michelle@FABRIKAM.LOCAL:8a7a0b3264590ef6:fr3edom } #[test] - fn test_john_show_tgs_unknown_user_no_hash_param() { + fn john_show_tgs_unknown_user_no_hash_param() { // Without hash_value param, ?:password is skipped let output = "--- john --show ---\n\ ?:iknownothing\n\n\ diff --git a/ares-tools/src/parsers/delegation.rs b/ares-tools/src/parsers/delegation.rs index b473806f..489e3135 100644 --- a/ares-tools/src/parsers/delegation.rs +++ b/ares-tools/src/parsers/delegation.rs @@ -131,7 +131,7 @@ mod tests { use serde_json::json; #[test] - fn test_parse_delegation_constrained() { + fn parse_delegation_constrained() { let output = "\ AccountName AccountType DelegationType DelegationRightsTo svc_sql$ Computer Constrained CIFS/dc01.contoso.local"; @@ -150,7 +150,7 @@ svc_sql$ Computer Constrained CIFS/dc01.conto } #[test] - fn test_parse_delegation_unconstrained() { + fn parse_delegation_unconstrained() { let output = "DC01$ Computer Unconstrained N/A"; let params = json!({"domain": "contoso.local", "target": "192.168.58.10"}); let vulns = parse_delegation(output, ¶ms); @@ -160,7 +160,7 @@ svc_sql$ Computer Constrained CIFS/dc01.conto } #[test] - fn test_parse_delegation_mixed() { + fn parse_delegation_mixed() { let output = "\ AccountName AccountType DelegationType DelegationRightsTo svc_sql$ Computer Constrained CIFS/dc01.contoso.local @@ -173,13 +173,13 @@ DC01$ Computer Unconstrained N/A"; } #[test] - fn test_parse_delegation_no_results() { + fn parse_delegation_no_results() { let vulns = parse_delegation("[*] No delegation found", &json!({})); assert!(vulns.is_empty()); } #[test] - fn test_extract_delegation_account_with_domain_prefix() { + fn extract_delegation_account_with_domain_prefix() { assert_eq!( extract_delegation_account("CONTOSO/svc_sql$ Computer Constrained"), "svc_sql$" @@ -187,7 +187,7 @@ DC01$ Computer Unconstrained N/A"; } #[test] - fn test_extract_delegation_account_without_prefix() { + fn extract_delegation_account_without_prefix() { assert_eq!( extract_delegation_account("svc_sql$ Computer Constrained"), "svc_sql$" @@ -195,14 +195,14 @@ DC01$ Computer Unconstrained N/A"; } #[test] - fn test_extract_delegation_account_empty() { + fn extract_delegation_account_empty() { assert_eq!(extract_delegation_account(""), ""); } /// Test with "SPN Exists" column and multi-word DelegationType /// like "Constrained w/ Protocol Transition". #[test] - fn test_parse_delegation_extended_format() { + fn parse_delegation_extended_format() { let output = "\ Impacket v0.13.0.dev0+20251022.125034.d843881f - Copyright Fortra, LLC and its affiliated companies diff --git a/ares-tools/src/parsers/mod.rs b/ares-tools/src/parsers/mod.rs index 534952b9..036c49c2 100644 --- a/ares-tools/src/parsers/mod.rs +++ b/ares-tools/src/parsers/mod.rs @@ -374,7 +374,7 @@ mod tests { use serde_json::json; #[test] - fn test_parse_nmap_with_services() { + fn parse_nmap_with_services() { let output = r#"Starting Nmap 7.98 ( https://nmap.org ) at 2026-04-08 11:12 UTC Nmap scan report for dc01.contoso.local (192.168.58.210) Host is up (0.0010s latency). @@ -407,7 +407,7 @@ Nmap done: 1 IP address (1 host up) scanned in 4.32 seconds"#; } #[test] - fn test_parse_nmap_with_stderr_separator() { + fn parse_nmap_with_stderr_separator() { // combined() output includes stderr let output = "Starting Nmap 7.98 ( https://nmap.org ) at 2026-04-08 11:12 UTC\n\ Nmap scan report for dc01.contoso.local (192.168.58.210)\n\ @@ -437,7 +437,7 @@ Warning: some warning here"; } #[test] - fn test_parse_nmap_fallback_no_output() { + fn parse_nmap_fallback_no_output() { let output = ""; let params = json!({"target": "192.168.58.210"}); let hosts = parse_nmap_output(output, ¶ms); @@ -448,7 +448,7 @@ Warning: some warning here"; } #[test] - fn test_parse_nmap_multiple_hosts() { + fn parse_nmap_multiple_hosts() { let output = "Nmap scan report for dc01.contoso.local (192.168.58.210)\n\ PORT STATE SERVICE\n\ 88/tcp open kerberos-sec\n\ @@ -470,7 +470,7 @@ PORT STATE SERVICE\n\ } #[test] - fn test_parse_netexec_users_table_format() { + fn parse_netexec_users_table_format() { let output = r#"SMB 192.168.58.121 445 DC01 [*] Windows 10 / Server 2019 Build 17763 x64 (name:DC01) (domain:north.contoso.local) (signing:True) (SMBv1:False) SMB 192.168.58.121 445 DC01 [+] north.contoso.local\: SMB 192.168.58.121 445 DC01 -Username- -Last PW Set- -BadPW- -Description- @@ -517,7 +517,7 @@ SMB 192.168.58.121 445 DC01 [*] Enumerated 10 local users: CHI } #[test] - fn test_parse_netexec_users_rid_brute_format() { + fn parse_netexec_users_rid_brute_format() { let output = r#"SMB 192.168.58.121 445 DC01 [+] north.contoso.local\: SMB 192.168.58.121 445 DC01 CHILD\alice.johnson (SidTypeUser) SMB 192.168.58.121 445 DC01 CHILD\bob.smith (SidTypeUser)"#; @@ -533,7 +533,7 @@ SMB 192.168.58.121 445 DC01 CHILD\bob.smith (SidTypeUser)"#; } #[test] - fn test_parse_tool_output_enumerate_users_extracts_creds() { + fn parse_tool_output_enumerate_users_extracts_creds() { let output = r#"SMB 192.168.58.121 445 DC01 [*] Windows 10 (name:DC01) (domain:contoso.local) (signing:True) SMB 192.168.58.121 445 DC01 [+] contoso.local\: SMB 192.168.58.121 445 DC01 -Username- -Last PW Set- -BadPW- -Description- @@ -557,7 +557,7 @@ SMB 192.168.58.121 445 DC01 bob 2026-03-25 23:21:09 0 Bob"#; // --- looks_like_ip --- #[test] - fn test_looks_like_ip_valid() { + fn looks_like_ip_valid() { assert!(looks_like_ip("192.168.58.10")); assert!(looks_like_ip("192.168.58.10")); assert!(looks_like_ip("0.0.0.0")); @@ -565,7 +565,7 @@ SMB 192.168.58.121 445 DC01 bob 2026-03-25 23:21:09 0 Bob"#; } #[test] - fn test_looks_like_ip_invalid() { + fn looks_like_ip_invalid() { assert!(!looks_like_ip("not-an-ip")); assert!(!looks_like_ip("192.168.58")); assert!(!looks_like_ip("192.168.58.10.5")); @@ -577,7 +577,7 @@ SMB 192.168.58.121 445 DC01 bob 2026-03-25 23:21:09 0 Bob"#; // --- merge_discoveries --- #[test] - fn test_merge_discoveries_combines_arrays() { + fn merge_discoveries_combines_arrays() { let d1 = json!({ "hosts": [{"ip": "192.168.58.10"}], "credentials": [{"username": "admin"}], @@ -593,7 +593,7 @@ SMB 192.168.58.121 445 DC01 bob 2026-03-25 23:21:09 0 Bob"#; } #[test] - fn test_merge_discoveries_dedup_hosts_by_ip() { + fn merge_discoveries_dedup_hosts_by_ip() { let d1 = json!({ "hosts": [ {"ip": "192.168.58.10", "is_dc": false, "services": ["445/tcp"]}, @@ -613,14 +613,14 @@ SMB 192.168.58.121 445 DC01 bob 2026-03-25 23:21:09 0 Bob"#; } #[test] - fn test_merge_discoveries_empty_input() { + fn merge_discoveries_empty_input() { let merged = merge_discoveries(&[]); assert!(merged["hosts"].is_null()); assert!(merged["credentials"].is_null()); } #[test] - fn test_merge_discoveries_single_input() { + fn merge_discoveries_single_input() { let d = json!({"vulnerabilities": [{"vuln_id": "v1"}]}); let merged = merge_discoveries(&[d]); assert_eq!(merged["vulnerabilities"].as_array().unwrap().len(), 1); @@ -629,7 +629,7 @@ SMB 192.168.58.121 445 DC01 bob 2026-03-25 23:21:09 0 Bob"#; // --- parse_tool_output routing --- #[test] - fn test_parse_tool_output_secretsdump() { + fn parse_tool_output_secretsdump() { let output = "Administrator:500:aad3b435b51404eeaad3b435b51404ee:e19ccf75ee54e06b06a5907af13cef42:::"; let params = json!({"domain": "contoso.local"}); let disc = parse_tool_output("secretsdump", output, ¶ms); @@ -637,7 +637,7 @@ SMB 192.168.58.121 445 DC01 bob 2026-03-25 23:21:09 0 Bob"#; } #[test] - fn test_parse_tool_output_kerberoast() { + fn parse_tool_output_kerberoast() { let output = "$krb5tgs$23$*svc_sql$CONTOSO$contoso.local/svc_sql*$abc"; let params = json!({"domain": "contoso.local"}); let disc = parse_tool_output("kerberoast", output, ¶ms); @@ -645,13 +645,13 @@ SMB 192.168.58.121 445 DC01 bob 2026-03-25 23:21:09 0 Bob"#; } #[test] - fn test_parse_tool_output_unknown_tool() { + fn parse_tool_output_unknown_tool() { let disc = parse_tool_output("unknown_tool", "output", &json!({})); assert_eq!(disc, json!({})); } #[test] - fn test_parse_tool_output_find_delegation() { + fn parse_tool_output_find_delegation() { let output = "svc_sql$ Computer Constrained CIFS/dc01.contoso.local"; let params = json!({"domain": "contoso.local", "target_ip": "192.168.58.10"}); let disc = parse_tool_output("find_delegation", output, ¶ms); @@ -659,7 +659,7 @@ SMB 192.168.58.121 445 DC01 bob 2026-03-25 23:21:09 0 Bob"#; } #[test] - fn test_kerberos_user_enum_all_variants() { + fn kerberos_user_enum_all_variants() { // Test all three output variants from impacket-GetNPUsers let output = r#"Impacket v0.12.0 - Copyright Fortra, LLC and its affiliated companies @@ -691,7 +691,7 @@ SMB 192.168.58.121 445 DC01 bob 2026-03-25 23:21:09 0 Bob"#; } #[test] - fn test_parse_tool_output_username_as_password_filters() { + fn parse_tool_output_username_as_password_filters() { // Only creds where password == username should be kept let output = "[+] 192.168.1.1 CONTOSO\\alice:alice (Pwn3d!)\n\ [+] 192.168.1.1 CONTOSO\\bob:Password1 (Pwn3d!)"; @@ -703,7 +703,7 @@ SMB 192.168.58.121 445 DC01 bob 2026-03-25 23:21:09 0 Bob"#; } #[test] - fn test_parse_tool_output_adidnsdump() { + fn parse_tool_output_adidnsdump() { let output = "dc01 A 192.168.1.10\nweb01 A 192.168.1.20"; let disc = parse_tool_output("adidnsdump", output, &json!({})); let hosts = disc["hosts"].as_array().unwrap(); @@ -711,7 +711,7 @@ SMB 192.168.58.121 445 DC01 bob 2026-03-25 23:21:09 0 Bob"#; } #[test] - fn test_merge_discoveries_trusted_domains_dedup() { + fn merge_discoveries_trusted_domains_dedup() { let d1 = json!({"trusted_domains": [{"domain": "child.contoso.local", "type": "ParentChild"}]}); let d2 = @@ -722,7 +722,7 @@ SMB 192.168.58.121 445 DC01 bob 2026-03-25 23:21:09 0 Bob"#; } #[test] - fn test_merge_discoveries_host_more_services_wins() { + fn merge_discoveries_host_more_services_wins() { let d1 = json!({"hosts": [{"ip": "10.0.0.1", "services": ["445/tcp"]}]}); let d2 = json!({"hosts": [{"ip": "10.0.0.1", "services": ["80/tcp", "443/tcp", "445/tcp"]}]}); diff --git a/ares-tools/src/parsers/mssql.rs b/ares-tools/src/parsers/mssql.rs index 1f72b8ee..ce47fbb8 100644 --- a/ares-tools/src/parsers/mssql.rs +++ b/ares-tools/src/parsers/mssql.rs @@ -156,7 +156,7 @@ mod tests { use serde_json::json; #[test] - fn test_parse_impersonation_found() { + fn parse_impersonation_found() { let output = r#"Impacket v0.12.0 - Copyright Fortra, LLC [*] Encryption required, switching to TLS [*] ENVCHANGE(DATABASE): Old Value: master, New Value: master @@ -174,7 +174,7 @@ class class_desc major_id minor_id grantee_principal_id grantor_princi } #[test] - fn test_parse_impersonation_none() { + fn parse_impersonation_none() { let output = r#"Impacket v0.12.0 SQL> SELECT * FROM sys.server_permissions WHERE type = 'IM'; class class_desc major_id minor_id grantee_principal_id grantor_principal_id type permission_name state state_desc @@ -186,7 +186,7 @@ class class_desc major_id minor_id grantee_principal_id grantor_princi } #[test] - fn test_parse_impersonation_login_failed() { + fn parse_impersonation_login_failed() { let output = "[-] ERROR(SQL01): Login failed for user 'test'"; let params = json!({"target": "192.168.58.12", "username": "test"}); let vulns = parse_mssql_impersonation(output, ¶ms); @@ -194,7 +194,7 @@ class class_desc major_id minor_id grantee_principal_id grantor_princi } #[test] - fn test_parse_linked_servers_found() { + fn parse_linked_servers_found() { let output = r#"Impacket v0.12.0 SQL> EXEC sp_linkedservers; SRV_NAME SRV_PROVIDERNAME SRV_PRODUCT SRV_DATASOURCE @@ -210,7 +210,7 @@ SRV01 SQLNCLI SQL Server SRV01\SQLEXPRESS } #[test] - fn test_parse_linked_servers_self_only() { + fn parse_linked_servers_self_only() { let output = r#"SQL> EXEC sp_linkedservers; SRV_NAME SRV_PROVIDERNAME -------- ---------------- diff --git a/ares-tools/src/parsers/nmap.rs b/ares-tools/src/parsers/nmap.rs index bf03cd0e..c1c83755 100644 --- a/ares-tools/src/parsers/nmap.rs +++ b/ares-tools/src/parsers/nmap.rs @@ -200,7 +200,7 @@ mod tests { use serde_json::json; #[test] - fn test_parse_nmap_single_host_with_services() { + fn parse_nmap_single_host_with_services() { let output = "\ Nmap scan report for dc01.contoso.local (192.168.58.10) Host is up (0.0010s latency). @@ -223,7 +223,7 @@ PORT STATE SERVICE } #[test] - fn test_parse_nmap_ip_only_no_hostname() { + fn parse_nmap_ip_only_no_hostname() { let output = "\ Nmap scan report for 192.168.58.20 PORT STATE SERVICE @@ -241,7 +241,7 @@ PORT STATE SERVICE } #[test] - fn test_parse_nmap_multiple_hosts() { + fn parse_nmap_multiple_hosts() { let output = "\ Nmap scan report for dc01.contoso.local (192.168.58.10) PORT STATE SERVICE @@ -262,7 +262,7 @@ PORT STATE SERVICE } #[test] - fn test_parse_nmap_os_detection() { + fn parse_nmap_os_detection() { let output = "\ Nmap scan report for 192.168.58.10 PORT STATE SERVICE @@ -274,7 +274,7 @@ OS details: Microsoft Windows Server 2019"; } #[test] - fn test_parse_nmap_empty_output_with_target() { + fn parse_nmap_empty_output_with_target() { let params = json!({"target": "192.168.58.10"}); let hosts = parse_nmap_output("Starting Nmap 7.94 ...\nNmap done: 0 hosts up", ¶ms); assert_eq!(hosts.len(), 1); @@ -283,13 +283,13 @@ OS details: Microsoft Windows Server 2019"; } #[test] - fn test_parse_nmap_empty_output_no_target() { + fn parse_nmap_empty_output_no_target() { let hosts = parse_nmap_output("", &json!({})); assert!(hosts.is_empty()); } #[test] - fn test_parse_nmap_dc_hostname_detection() { + fn parse_nmap_dc_hostname_detection() { let output = "Nmap scan report for DC02 (192.168.58.11)\nPORT STATE SERVICE\n445/tcp open microsoft-ds"; let params = json!({"target": "192.168.58.11"}); let hosts = parse_nmap_output(output, ¶ms); @@ -297,7 +297,7 @@ OS details: Microsoft Windows Server 2019"; } #[test] - fn test_parse_nmap_fqdn_from_script_output() { + fn parse_nmap_fqdn_from_script_output() { let output = "\ Nmap scan report for 192.168.58.10 PORT STATE SERVICE @@ -316,7 +316,7 @@ PORT STATE SERVICE } #[test] - fn test_parse_nmap_fqdn_does_not_override_existing() { + fn parse_nmap_fqdn_does_not_override_existing() { // When nmap header already has the FQDN, script output shouldn't override let output = "\ Nmap scan report for dc01.contoso.local (192.168.58.10) @@ -329,7 +329,7 @@ PORT STATE SERVICE } #[test] - fn test_parse_nmap_fqdn_from_dns_host_name() { + fn parse_nmap_fqdn_from_dns_host_name() { let output = "\ Nmap scan report for 192.168.58.11 PORT STATE SERVICE @@ -342,7 +342,7 @@ PORT STATE SERVICE } #[test] - fn test_parse_nmap_aws_internal_hostname_replaced_by_fqdn() { + fn parse_nmap_aws_internal_hostname_replaced_by_fqdn() { // AWS internal hostnames should be discarded, allowing FQDN extraction let output = "\ Nmap scan report for ip-192-168-58-10.us-west-2.compute.internal (192.168.58.10) @@ -360,7 +360,7 @@ PORT STATE SERVICE } #[test] - fn test_parse_nmap_fqdn_from_ssl_cert_commonname() { + fn parse_nmap_fqdn_from_ssl_cert_commonname() { let output = "\ Nmap scan report for 192.168.58.22 PORT STATE SERVICE @@ -373,7 +373,7 @@ PORT STATE SERVICE } #[test] - fn test_parse_nmap_fqdn_from_ssl_cert_san_dns() { + fn parse_nmap_fqdn_from_ssl_cert_san_dns() { let output = "\ Nmap scan report for 192.168.58.23 PORT STATE SERVICE @@ -385,7 +385,7 @@ PORT STATE SERVICE } #[test] - fn test_parse_nmap_version_info_stripped_from_services() { + fn parse_nmap_version_info_stripped_from_services() { // nmap -sV output includes version/product info after the service name. // We should only capture the service name, not the version string. let output = "\ @@ -413,14 +413,14 @@ PORT STATE SERVICE VERSION } #[test] - fn test_flush_nmap_host_empty_ip() { + fn flush_nmap_host_empty_ip() { let mut hosts = Vec::new(); flush_nmap_host("", "host", "Windows", &[], &mut hosts); assert!(hosts.is_empty()); } #[test] - fn test_flush_nmap_host_winrm_role() { + fn flush_nmap_host_winrm_role() { let mut hosts = Vec::new(); let services = vec!["5985/tcp (wsman)".to_string()]; flush_nmap_host("192.168.58.30", "", "", &services, &mut hosts); diff --git a/ares-tools/src/parsers/secrets.rs b/ares-tools/src/parsers/secrets.rs index 55b723f1..4b5f2080 100644 --- a/ares-tools/src/parsers/secrets.rs +++ b/ares-tools/src/parsers/secrets.rs @@ -153,7 +153,7 @@ mod tests { use serde_json::json; #[test] - fn test_parse_secretsdump_ntlm_hashes() { + fn parse_secretsdump_ntlm_hashes() { let output = "\ [*] Dumping local SAM hashes (uid:rid:lmhash:nthash) Administrator:500:aad3b435b51404eeaad3b435b51404ee:e19ccf75ee54e06b06a5907af13cef42::: @@ -177,7 +177,7 @@ svc_sql:1001:aad3b435b51404eeaad3b435b51404ee:abcdef1234567890abcdef1234567890:: } #[test] - fn test_parse_secretsdump_domain_prefix() { + fn parse_secretsdump_domain_prefix() { let output = "CONTOSO\\Administrator:500:aad3b435b51404eeaad3b435b51404ee:e19ccf75ee54e06b06a5907af13cef42:::"; let params = json!({"domain": "contoso.local"}); let (hashes, _) = parse_secretsdump(output, ¶ms); @@ -188,7 +188,7 @@ svc_sql:1001:aad3b435b51404eeaad3b435b51404ee:abcdef1234567890abcdef1234567890:: } #[test] - fn test_parse_secretsdump_netbios_resolved_to_fqdn() { + fn parse_secretsdump_netbios_resolved_to_fqdn() { // NetBIOS prefix should be resolved to FQDN via target_domain let output = "\ FABRIKAM\\alice:1103:aad3b435b51404eeaad3b435b51404ee:abcdef1234567890abcdef1234567890::: @@ -203,7 +203,7 @@ FABRIKAM\\bob:1104:aad3b435b51404eeaad3b435b51404ee:1234567890abcdef1234567890ab } #[test] - fn test_parse_secretsdump_target_domain_preferred() { + fn parse_secretsdump_target_domain_preferred() { // target_domain should take precedence over domain for attribution let output = "FABRIKAM\\svc_sql:1105:aad3b435b51404eeaad3b435b51404ee:abcdef1234567890abcdef1234567890:::"; let params = json!({"domain": "contoso.local", "target_domain": "fabrikam.local"}); @@ -213,7 +213,7 @@ FABRIKAM\\bob:1104:aad3b435b51404eeaad3b435b51404ee:1234567890abcdef1234567890ab } #[test] - fn test_parse_secretsdump_mismatched_netbios_kept() { + fn parse_secretsdump_mismatched_netbios_kept() { // If NetBIOS doesn't match target_domain's first label, keep it raw let output = "CHILD\\jsmith:1001:aad3b435b51404eeaad3b435b51404ee:abcdef1234567890abcdef1234567890:::"; let params = json!({"target_domain": "fabrikam.local"}); @@ -225,7 +225,7 @@ FABRIKAM\\bob:1104:aad3b435b51404eeaad3b435b51404ee:1234567890abcdef1234567890ab } #[test] - fn test_resolve_netbios_to_fqdn() { + fn resolves_netbios_to_fqdn() { assert_eq!( resolve_netbios_to_fqdn("FABRIKAM", "fabrikam.local"), "fabrikam.local" @@ -244,7 +244,7 @@ FABRIKAM\\bob:1104:aad3b435b51404eeaad3b435b51404ee:1234567890abcdef1234567890ab } #[test] - fn test_parse_secretsdump_skips_comments_and_brackets() { + fn parse_secretsdump_skips_comments_and_brackets() { let output = "\ [*] Service RemoteRegistry is in stopped state # This is a comment @@ -255,14 +255,14 @@ FABRIKAM\\bob:1104:aad3b435b51404eeaad3b435b51404ee:1234567890abcdef1234567890ab } #[test] - fn test_parse_secretsdump_empty_output() { + fn parse_secretsdump_empty_output() { let (hashes, creds) = parse_secretsdump("", &json!({})); assert!(hashes.is_empty()); assert!(creds.is_empty()); } #[test] - fn test_parse_kerberoast_hashes() { + fn parse_kerberoast_hashes() { let output = "\ [*] Getting TGS for SPN accounts $krb5tgs$23$*svc_sql$CONTOSO.LOCAL$contoso.local/svc_sql*$abc123def456 @@ -282,13 +282,13 @@ $krb5tgs$23$*svc_http$CONTOSO.LOCAL$contoso.local/svc_http*$789xyz } #[test] - fn test_parse_kerberoast_no_hashes() { + fn parse_kerberoast_no_hashes() { let hashes = parse_kerberoast("[*] No SPN accounts found", &json!({})); assert!(hashes.is_empty()); } #[test] - fn test_parse_asrep_roast() { + fn parses_asrep_roast() { let output = "\ $krb5asrep$23$jdoe@CONTOSO.LOCAL:abc123def456 $krb5asrep$23$svc_backup@CONTOSO.LOCAL:789xyz"; @@ -302,7 +302,7 @@ $krb5asrep$23$svc_backup@CONTOSO.LOCAL:789xyz"; } #[test] - fn test_parse_asrep_roast_empty() { + fn parse_asrep_roast_empty() { let hashes = parse_asrep_roast("[-] No AS-REP roastable accounts", &json!({})); assert!(hashes.is_empty()); } diff --git a/ares-tools/src/parsers/smb.rs b/ares-tools/src/parsers/smb.rs index 411841f0..a18bed5a 100644 --- a/ares-tools/src/parsers/smb.rs +++ b/ares-tools/src/parsers/smb.rs @@ -130,7 +130,7 @@ mod tests { use serde_json::json; #[test] - fn test_parse_smb_signing_disabled() { + fn parse_smb_signing_disabled() { let output = "SMB signing: disabled"; let params = json!({"target_ip": "192.168.58.10"}); let hosts = parse_smb_signing(output, ¶ms); @@ -141,7 +141,7 @@ mod tests { } #[test] - fn test_parse_smb_signing_enabled() { + fn parse_smb_signing_enabled() { let output = "SMB signing: required"; let params = json!({"target": "192.168.58.10"}); let hosts = parse_smb_signing(output, ¶ms); @@ -151,7 +151,7 @@ mod tests { } #[test] - fn test_parse_smb_signing_not_required() { + fn parse_smb_signing_not_required() { let output = "message_signing: not required"; let params = json!({"target_ip": "192.168.58.20"}); let hosts = parse_smb_signing(output, ¶ms); @@ -160,13 +160,13 @@ mod tests { } #[test] - fn test_parse_smb_signing_no_target() { + fn parse_smb_signing_no_target() { let hosts = parse_smb_signing("signing: disabled", &json!({})); assert!(hosts.is_empty()); } #[test] - fn test_parse_netexec_smb_with_fqdn() { + fn parse_netexec_smb_with_fqdn() { let output = "\ SMB 192.168.58.10 445 DC01 [*] Windows Server 2019 Build 17763 x64 (name:DC01) (domain:contoso.local) (signing:True) SMB 192.168.58.20 445 SRV01 [*] Windows Server 2016 Build 14393 x64 (name:SRV01) (domain:contoso.local) (signing:False)"; @@ -179,7 +179,7 @@ SMB 192.168.58.20 445 SRV01 [*] Windows Server 2016 Build 14393 x64 (name:SR } #[test] - fn test_parse_netexec_smb_without_domain() { + fn parse_netexec_smb_without_domain() { // Fallback: no (name:...) (domain:...) → bare NetBIOS name let output = "SMB 192.168.58.10 445 DC01 [*] Windows Server 2019 Build 17763 x64"; let hosts = parse_netexec_smb(output); @@ -188,19 +188,19 @@ SMB 192.168.58.20 445 SRV01 [*] Windows Server 2016 Build 14393 x64 (name:SR } #[test] - fn test_parse_netexec_smb_empty() { + fn parse_netexec_smb_empty() { let hosts = parse_netexec_smb("No SMB hosts found"); assert!(hosts.is_empty()); } #[test] - fn test_extract_fqdn_from_line() { + fn extracts_fqdn_from_line() { let line = "SMB 192.168.58.12 445 DC01 [*] Windows 10 / Server 2019 Build 17763 x64 (name:DC01) (domain:contoso.local) (signing:True)"; assert_eq!(extract_fqdn_from_line(line, "DC01"), "dc01.contoso.local"); } #[test] - fn test_extract_fqdn_trailing_zero() { + fn extract_fqdn_trailing_zero() { let line = "SMB 192.168.58.22 445 SRV01 [*] ... (name:SRV01) (domain:child.contoso.local0.) (signing:False)"; assert_eq!( extract_fqdn_from_line(line, "SRV01"), @@ -209,7 +209,7 @@ SMB 192.168.58.20 445 SRV01 [*] Windows Server 2016 Build 14393 x64 (name:SR } #[test] - fn test_extract_fqdn_no_domain() { + fn extract_fqdn_no_domain() { let line = "SMB 192.168.58.12 445 DC01 [*] Windows Server 2019"; assert_eq!(extract_fqdn_from_line(line, "DC01"), "DC01"); } diff --git a/ares-tools/src/parsers/spider.rs b/ares-tools/src/parsers/spider.rs index 0ee43339..f990fd39 100644 --- a/ares-tools/src/parsers/spider.rs +++ b/ares-tools/src/parsers/spider.rs @@ -225,7 +225,7 @@ mod tests { use serde_json::json; #[test] - fn test_powershell_variable_assignments() { + fn powershell_variable_assignments() { let output = r#" === Downloaded File Contents === @@ -249,7 +249,7 @@ $password = "_S3cur3P@ss_" } #[test] - fn test_net_use_command() { + fn net_use_command() { let output = r#" --- SYSVOL/scripts/map_drive.bat --- net use \\dc02\share /user:CHILD\jeff.morgan _S3cur3P@ss_ @@ -264,7 +264,7 @@ net use \\dc02\share /user:CHILD\jeff.morgan _S3cur3P@ss_ } #[test] - fn test_powershell_params() { + fn powershell_params() { let output = r#" --- scripts/setup.ps1 --- New-SmbMapping -RemotePath "\\dc01\share" -UserName "svc_sql" -Password "SqlP@ss123" @@ -278,7 +278,7 @@ New-SmbMapping -RemotePath "\\dc01\share" -UserName "svc_sql" -Password "SqlP@ss } #[test] - fn test_skips_variable_refs() { + fn skips_variable_refs() { let output = r#" --- scripts/template.ps1 --- $user = "admin" @@ -291,7 +291,7 @@ $pass = $env:SECRET_KEY } #[test] - fn test_multiple_files() { + fn multiple_files() { let output = r#" === Downloaded File Contents === @@ -310,7 +310,7 @@ $pass = "P@ssw0rd" } #[test] - fn test_empty_output() { + fn empty_output() { let creds = parse_spider_credentials("", &json!({})); assert!(creds.is_empty()); } diff --git a/ares-tools/src/parsers/trust.rs b/ares-tools/src/parsers/trust.rs index b3b7b425..8eb523b0 100644 --- a/ares-tools/src/parsers/trust.rs +++ b/ares-tools/src/parsers/trust.rs @@ -136,7 +136,7 @@ mod tests { use super::*; #[test] - fn test_parse_cross_forest_trust() { + fn parse_cross_forest_trust() { let output = r#"dn: CN=fabrikam.local,CN=System,DC=contoso,DC=local cn: fabrikam.local trustDirection: 3 @@ -154,7 +154,7 @@ flatName: FABRIKAM } #[test] - fn test_parse_parent_child_trust() { + fn parse_parent_child_trust() { let output = r#"dn: CN=north.contoso.local,CN=System,DC=contoso,DC=local cn: north.contoso.local trustDirection: 3 @@ -170,7 +170,7 @@ flatName: CHILD } #[test] - fn test_parse_multiple_trusts() { + fn parse_multiple_trusts() { let output = r#"dn: CN=fabrikam.local,CN=System,DC=contoso,DC=local cn: fabrikam.local trustDirection: 3 @@ -192,7 +192,7 @@ flatName: CHILD } #[test] - fn test_parse_inbound_trust() { + fn parse_inbound_trust() { let output = "cn: partner.com\ntrustDirection: 1\ntrustType: 3\ntrustAttributes: 0\nflatName: PARTNER\n"; let trusts = parse_domain_trusts(output); @@ -202,20 +202,20 @@ flatName: CHILD } #[test] - fn test_parse_empty_output() { + fn parse_empty_output() { let trusts = parse_domain_trusts(""); assert!(trusts.is_empty()); } #[test] - fn test_parse_no_trusts_search_result() { + fn parse_no_trusts_search_result() { let output = "# search result\nsearch: 2\nresult: 0 Success\n"; let trusts = parse_domain_trusts(output); assert!(trusts.is_empty()); } #[test] - fn test_parse_outbound_trust() { + fn parse_outbound_trust() { let output = "cn: external.com\ntrustDirection: 2\ntrustType: 3\ntrustAttributes: 0\nflatName: EXTERNAL\n"; let trusts = parse_domain_trusts(output); assert_eq!(trusts.len(), 1); @@ -225,7 +225,7 @@ flatName: CHILD } #[test] - fn test_parse_trust_unknown_direction() { + fn parse_trust_unknown_direction() { let output = "cn: mystery.local\ntrustDirection: 99\ntrustType: 1\ntrustAttributes: 0\nflatName: MYSTERY\n"; let trusts = parse_domain_trusts(output); assert_eq!(trusts.len(), 1); @@ -233,7 +233,7 @@ flatName: CHILD } #[test] - fn test_parse_trust_tree_root_short_domain() { + fn parse_trust_tree_root_short_domain() { // trustType=2 with short domain (< 3 labels) → forest let output = "cn: fabrikam.com\ntrustDirection: 3\ntrustType: 2\ntrustAttributes: 0\nflatName: FABRIKAM\n"; let trusts = parse_domain_trusts(output); @@ -242,7 +242,7 @@ flatName: CHILD } #[test] - fn test_parse_trust_tree_root_long_domain() { + fn parse_trust_tree_root_long_domain() { // trustType=2 with 3+ labels → parent_child let output = "cn: child.contoso.local\ntrustDirection: 3\ntrustType: 2\ntrustAttributes: 0\nflatName: CHILD\n"; let trusts = parse_domain_trusts(output); @@ -251,7 +251,7 @@ flatName: CHILD } #[test] - fn test_parse_trust_domain_lowercased() { + fn parse_trust_domain_lowercased() { let output = "cn: FABRIKAM.LOCAL\ntrustDirection: 3\ntrustType: 2\ntrustAttributes: 8\nflatName: FABRIKAM\n"; let trusts = parse_domain_trusts(output); assert_eq!(trusts[0]["domain"], "fabrikam.local"); diff --git a/ares-tools/src/parsers/users_shares.rs b/ares-tools/src/parsers/users_shares.rs index 711c0c55..b5493311 100644 --- a/ares-tools/src/parsers/users_shares.rs +++ b/ares-tools/src/parsers/users_shares.rs @@ -204,7 +204,7 @@ mod tests { use super::*; #[test] - fn test_parse_netexec_users_rid_brute() { + fn parse_netexec_users_rid_brute() { let output = "\ SMB 192.168.58.10 445 DC01 [*] Enumerating users CONTOSO\\Administrator (SidTypeUser) @@ -219,7 +219,7 @@ CONTOSO\\svc_sql (SidTypeUser)"; } #[test] - fn test_parse_netexec_users_table_format() { + fn parse_netexec_users_table_format() { let output = "\ SMB 192.168.58.10 445 DC01 [*] (domain:contoso.local) Enumerated SMB 192.168.58.10 445 DC01 -Username- -Last PW Set- -BadPW- -Description- @@ -233,7 +233,7 @@ SMB 192.168.58.10 445 DC01 bob.s 2026-03-20 10:00:00 0 Bob Smith"; } #[test] - fn test_parse_netexec_users_with_password_leak() { + fn parse_netexec_users_with_password_leak() { let output = "\ SMB 192.168.58.10 445 DC01 [*] (domain:contoso.local) Enumerated SMB 192.168.58.10 445 DC01 -Username- -Last PW Set- -BadPW- -Description- @@ -250,7 +250,7 @@ SMB 192.168.58.10 445 DC01 svc_test 2026-01-01 00:00:00 0 Service (Passwo } #[test] - fn test_parse_netexec_users_dedup() { + fn parse_netexec_users_dedup() { let output = "\ CONTOSO\\jdoe (SidTypeUser) CONTOSO\\jdoe (SidTypeUser) @@ -260,13 +260,13 @@ CONTOSO\\JDOE (SidTypeUser)"; } #[test] - fn test_parse_netexec_users_empty() { + fn parse_netexec_users_empty() { let users = parse_netexec_users("[*] No users found"); assert!(users.is_empty()); } #[test] - fn test_parse_netexec_shares() { + fn parses_netexec_shares() { let output = "\ SMB 192.168.58.10 445 DC01 Share Permissions Remark SMB 192.168.58.10 445 DC01 ------ ----------- ------ @@ -286,7 +286,7 @@ SMB 192.168.58.10 445 DC01 IT_Share READ,WRITE"; } #[test] - fn test_parse_netexec_shares_empty() { + fn parse_netexec_shares_empty() { let shares = parse_netexec_shares("[*] No shares enumerated"); assert!(shares.is_empty()); } diff --git a/ares-tools/src/privesc/delegation.rs b/ares-tools/src/privesc/delegation.rs index 23229043..2cf78d93 100644 --- a/ares-tools/src/privesc/delegation.rs +++ b/ares-tools/src/privesc/delegation.rs @@ -224,18 +224,14 @@ pub async fn raise_child(args: &Value) -> Result { cmd.timeout_secs(300).execute().await } -// ─── Tests ────────────────────────────────────────────────────────────────── - #[cfg(test)] mod tests { use crate::args::{optional_str, required_str}; use crate::credentials; use serde_json::json; - // ── find_delegation arg validation ────────────────────────────────── - #[test] - fn test_find_delegation_requires_domain() { + fn find_delegation_requires_domain() { let args = json!({ "username": "admin", "dc_ip": "192.168.58.10", @@ -245,7 +241,7 @@ mod tests { } #[test] - fn test_find_delegation_requires_username() { + fn find_delegation_requires_username() { let args = json!({ "domain": "contoso.local", "dc_ip": "192.168.58.10", @@ -255,7 +251,7 @@ mod tests { } #[test] - fn test_find_delegation_requires_dc_ip() { + fn find_delegation_requires_dc_ip() { let args = json!({ "domain": "contoso.local", "username": "admin", @@ -265,7 +261,7 @@ mod tests { } #[test] - fn test_find_delegation_with_password() { + fn find_delegation_with_password() { let args = json!({ "domain": "contoso.local", "username": "admin", @@ -281,7 +277,7 @@ mod tests { } #[test] - fn test_find_delegation_with_hash() { + fn find_delegation_with_hash() { let args = json!({ "domain": "contoso.local", "username": "admin", @@ -296,7 +292,7 @@ mod tests { } #[test] - fn test_find_delegation_requires_password_or_hash() { + fn find_delegation_requires_password_or_hash() { let args = json!({ "domain": "contoso.local", "username": "admin", @@ -308,10 +304,8 @@ mod tests { assert!(hash.is_none()); } - // ── find_delegation integration error ────────────────────────────── - #[test] - fn test_find_delegation_no_auth_errors() { + fn find_delegation_no_auth_errors() { let args = json!({ "domain": "contoso.local", "username": "admin", @@ -324,10 +318,8 @@ mod tests { assert!(result.unwrap_err().to_string().contains("password or hash")); } - // ── s4u_attack arg validation ────────────────────────────────────── - #[test] - fn test_s4u_attack_requires_target_spn() { + fn s4u_attack_requires_target_spn() { let args = json!({ "domain": "contoso.local", "username": "svc_web$", @@ -338,7 +330,7 @@ mod tests { } #[test] - fn test_s4u_attack_requires_impersonate() { + fn s4u_attack_requires_impersonate() { let args = json!({ "domain": "contoso.local", "username": "svc_web$", @@ -349,7 +341,7 @@ mod tests { } #[test] - fn test_s4u_attack_all_args() { + fn s4u_attack_all_args() { let args = json!({ "domain": "contoso.local", "username": "svc_web$", @@ -368,7 +360,7 @@ mod tests { } #[test] - fn test_s4u_attack_no_auth_errors() { + fn s4u_attack_no_auth_errors() { let args = json!({ "domain": "contoso.local", "username": "svc_web$", @@ -381,10 +373,8 @@ mod tests { assert!(result.unwrap_err().to_string().contains("password or hash")); } - // ── generate_golden_ticket arg validation ────────────────────────── - #[test] - fn test_golden_ticket_requires_krbtgt_hash() { + fn golden_ticket_requires_krbtgt_hash() { let args = json!({ "domain_sid": "S-1-5-21-1234567890-987654321-1122334455", "domain": "contoso.local" @@ -393,7 +383,7 @@ mod tests { } #[test] - fn test_golden_ticket_requires_domain_sid() { + fn golden_ticket_requires_domain_sid() { let args = json!({ "krbtgt_hash": "31d6cfe0d16ae931b73c59d7e0c089c0", "domain": "contoso.local" @@ -402,7 +392,7 @@ mod tests { } #[test] - fn test_golden_ticket_default_username() { + fn golden_ticket_default_username() { let args = json!({ "krbtgt_hash": "31d6cfe0d16ae931b73c59d7e0c089c0", "domain_sid": "S-1-5-21-1234567890-987654321-1122334455", @@ -413,7 +403,7 @@ mod tests { } #[test] - fn test_golden_ticket_custom_username() { + fn golden_ticket_custom_username() { let args = json!({ "krbtgt_hash": "31d6cfe0d16ae931b73c59d7e0c089c0", "domain_sid": "S-1-5-21-1234567890-987654321-1122334455", @@ -425,7 +415,7 @@ mod tests { } #[test] - fn test_golden_ticket_extra_sid_optional() { + fn golden_ticket_extra_sid_optional() { let args = json!({ "krbtgt_hash": "31d6cfe0d16ae931b73c59d7e0c089c0", "domain_sid": "S-1-5-21-1234567890-987654321-1122334455", @@ -439,7 +429,7 @@ mod tests { } #[test] - fn test_golden_ticket_extra_sid_absent() { + fn golden_ticket_extra_sid_absent() { let args = json!({ "krbtgt_hash": "31d6cfe0d16ae931b73c59d7e0c089c0", "domain_sid": "S-1-5-21-1234567890-987654321-1122334455", @@ -448,10 +438,8 @@ mod tests { assert!(optional_str(&args, "extra_sid").is_none()); } - // ── add_computer arg validation ──────────────────────────────────── - #[test] - fn test_add_computer_all_required_args() { + fn add_computer_all_required_args() { let args = json!({ "domain": "contoso.local", "username": "jsmith", @@ -474,7 +462,7 @@ mod tests { } #[test] - fn test_add_computer_missing_computer_name() { + fn add_computer_missing_computer_name() { let args = json!({ "domain": "contoso.local", "username": "jsmith", @@ -485,10 +473,8 @@ mod tests { assert!(required_str(&args, "computer_name").is_err()); } - // ── addspn arg validation ────────────────────────────────────────── - #[test] - fn test_addspn_all_required_args() { + fn addspn_all_required_args() { let args = json!({ "domain": "contoso.local", "username": "admin", @@ -507,7 +493,7 @@ mod tests { } #[test] - fn test_addspn_missing_spn() { + fn addspn_missing_spn() { let args = json!({ "domain": "contoso.local", "username": "admin", @@ -519,10 +505,8 @@ mod tests { assert!(required_str(&args, "spn").is_err()); } - // ── rbcd_write arg validation ────────────────────────────────────── - #[test] - fn test_rbcd_write_all_args() { + fn rbcd_write_all_args() { let args = json!({ "domain": "contoso.local", "username": "admin", @@ -545,7 +529,7 @@ mod tests { } #[test] - fn test_rbcd_write_missing_attacker_sid() { + fn rbcd_write_missing_attacker_sid() { let args = json!({ "domain": "contoso.local", "username": "admin", @@ -556,10 +540,8 @@ mod tests { assert!(required_str(&args, "attacker_sid").is_err()); } - // ── krbrelayup arg validation ────────────────────────────────────── - #[test] - fn test_krbrelayup_required_args_only() { + fn krbrelayup_required_args_only() { let args = json!({ "domain": "contoso.local", "dc_ip": "192.168.58.10" @@ -572,7 +554,7 @@ mod tests { } #[test] - fn test_krbrelayup_with_optional_args() { + fn krbrelayup_with_optional_args() { let args = json!({ "domain": "contoso.local", "dc_ip": "192.168.58.10", @@ -584,10 +566,8 @@ mod tests { assert_eq!(optional_str(&args, "create_user"), Some("eviluser")); } - // ── raise_child arg validation ───────────────────────────────────── - #[test] - fn test_raise_child_requires_child_domain() { + fn raise_child_requires_child_domain() { let args = json!({ "username": "admin", "password": "P@ssw0rd!" @@ -596,7 +576,7 @@ mod tests { } #[test] - fn test_raise_child_no_auth_errors() { + fn raise_child_no_auth_errors() { let args = json!({ "child_domain": "child.contoso.local", "username": "admin" @@ -611,7 +591,7 @@ mod tests { } #[test] - fn test_raise_child_with_password_target_format() { + fn raise_child_with_password_target_format() { let args = json!({ "child_domain": "child.contoso.local", "username": "admin", @@ -625,7 +605,7 @@ mod tests { } #[test] - fn test_raise_child_with_hash_target_format() { + fn raise_child_with_hash_target_format() { let args = json!({ "child_domain": "child.contoso.local", "username": "admin", @@ -644,7 +624,7 @@ mod tests { } #[test] - fn test_raise_child_target_domain_optional() { + fn raise_child_target_domain_optional() { let args = json!({ "child_domain": "child.contoso.local", "username": "admin", @@ -654,17 +634,15 @@ mod tests { assert_eq!(optional_str(&args, "target_domain"), Some("contoso.local")); } - // ── credential helper tests ──────────────────────────────────────── - #[test] - fn test_hash_args_with_nt_only() { + fn hash_args_with_nt_only() { let hash_args = credentials::hash_args("31d6cfe0d16ae931b73c59d7e0c089c0"); assert_eq!(hash_args[0], "-hashes"); assert_eq!(hash_args[1], ":31d6cfe0d16ae931b73c59d7e0c089c0"); } #[test] - fn test_hash_args_with_lm_nt() { + fn hash_args_with_lm_nt() { let hash_args = credentials::hash_args( "aad3b435b51404eeaad3b435b51404ee:31d6cfe0d16ae931b73c59d7e0c089c0", ); @@ -676,7 +654,7 @@ mod tests { } #[test] - fn test_impacket_auth_with_hash() { + fn impacket_auth_with_hash() { let (target, extra) = credentials::impacket_auth( Some("contoso.local"), "admin", @@ -689,7 +667,7 @@ mod tests { } #[test] - fn test_impacket_auth_with_password() { + fn impacket_auth_with_password() { let (target, extra) = credentials::impacket_auth( Some("contoso.local"), "admin", @@ -702,7 +680,7 @@ mod tests { } #[test] - fn test_kerberos_env() { + fn kerberos_env() { let (key, val) = credentials::kerberos_env("/tmp/admin.ccache"); assert_eq!(key, "KRB5CCNAME"); assert_eq!(val, "/tmp/admin.ccache"); diff --git a/ares-tools/src/privesc/mod.rs b/ares-tools/src/privesc/mod.rs index e25a3fe6..2ed1c83e 100644 --- a/ares-tools/src/privesc/mod.rs +++ b/ares-tools/src/privesc/mod.rs @@ -26,7 +26,7 @@ mod tests { use serde_json::json; #[test] - fn test_certipy_find_requires_username() { + fn certipy_find_requires_username() { let args = json!({}); let rt = tokio::runtime::Runtime::new().unwrap(); let result = rt.block_on(certipy_find(&args)); @@ -35,7 +35,7 @@ mod tests { } #[test] - fn test_generate_golden_ticket_requires_hash() { + fn generate_golden_ticket_requires_hash() { let args = json!({}); let rt = tokio::runtime::Runtime::new().unwrap(); let result = rt.block_on(generate_golden_ticket(&args)); @@ -44,7 +44,7 @@ mod tests { } #[test] - fn test_petitpotam_requires_listener() { + fn petitpotam_requires_listener() { let args = json!({}); let rt = tokio::runtime::Runtime::new().unwrap(); let result = rt.block_on(petitpotam_unauth(&args)); diff --git a/ares-tools/src/recon.rs b/ares-tools/src/recon.rs index 4f463ef1..10974bb8 100644 --- a/ares-tools/src/recon.rs +++ b/ares-tools/src/recon.rs @@ -590,12 +590,12 @@ mod tests { use super::*; #[test] - fn test_domain_to_base_dn_simple() { + fn domain_to_base_dn_simple() { assert_eq!(domain_to_base_dn("contoso.local"), "DC=contoso,DC=local"); } #[test] - fn test_domain_to_base_dn_nested() { + fn domain_to_base_dn_nested() { assert_eq!( domain_to_base_dn("north.contoso.local"), "DC=north,DC=contoso,DC=local" @@ -603,7 +603,7 @@ mod tests { } #[test] - fn test_domain_to_base_dn_single() { + fn domain_to_base_dn_single() { assert_eq!(domain_to_base_dn("local"), "DC=local"); } }