diff --git a/docs/demo-evidence/STORY-129/AC-001-mitre-attack-json-enrichment.gif b/docs/demo-evidence/STORY-129/AC-001-mitre-attack-json-enrichment.gif index f379abef..ee4d9de4 100644 Binary files a/docs/demo-evidence/STORY-129/AC-001-mitre-attack-json-enrichment.gif and b/docs/demo-evidence/STORY-129/AC-001-mitre-attack-json-enrichment.gif differ diff --git a/docs/demo-evidence/STORY-129/AC-001-mitre-attack-json-enrichment.tape b/docs/demo-evidence/STORY-129/AC-001-mitre-attack-json-enrichment.tape index f8e4c4a6..e65babd5 100644 --- a/docs/demo-evidence/STORY-129/AC-001-mitre-attack-json-enrichment.tape +++ b/docs/demo-evidence/STORY-129/AC-001-mitre-attack-json-enrichment.tape @@ -1,11 +1,13 @@ # STORY-129 AC-1/AC-4/AC-7/AC-8: per-finding mitre_attack array in JSON output # Demonstrates: known techniques produce fully-resolved 5-field objects; -# multi-tag preserves order; ICS tactic_id correct; mitre_techniques unchanged. +# multi-tag preserves order; ICS tactic_id correct (T0888->TA0102, T0836->TA0106); +# mitre_techniques unchanged. # Input: tests/fixtures/modbus-write.pcap (3 findings with mitre_attack populated) # Tool: wirerust CLI (release build) +# Re-recorded 2026-06-23 on fix/ics-tactic-ids: T0888 now resolves to TA0102 "Discovery (ICS)" -Output AC-001-mitre-attack-json-enrichment.gif -Output AC-001-mitre-attack-json-enrichment.webm +Output docs/demo-evidence/STORY-129/AC-001-mitre-attack-json-enrichment.gif +Output docs/demo-evidence/STORY-129/AC-001-mitre-attack-json-enrichment.webm Set FontFamily "Menlo" Set FontSize 14 @@ -15,8 +17,10 @@ Set Theme "Dracula" Set Padding 20 Set PlaybackSpeed 1.0 +Require wirerust + Hide -Type "cd /Users/zious/Documents/GITHUB/wirerust/.worktrees/STORY-129" +Type "cd /Users/zious/Documents/GITHUB/wirerust/.worktrees/ics-tactic-ids" Enter Sleep 1s Show diff --git a/docs/demo-evidence/STORY-129/AC-001-mitre-attack-json-enrichment.webm b/docs/demo-evidence/STORY-129/AC-001-mitre-attack-json-enrichment.webm index 1bd4e0cf..930a5ebf 100644 Binary files a/docs/demo-evidence/STORY-129/AC-001-mitre-attack-json-enrichment.webm and b/docs/demo-evidence/STORY-129/AC-001-mitre-attack-json-enrichment.webm differ diff --git a/docs/demo-evidence/STORY-129/evidence-report.md b/docs/demo-evidence/STORY-129/evidence-report.md index 6d2e6177..9e14576d 100644 --- a/docs/demo-evidence/STORY-129/evidence-report.md +++ b/docs/demo-evidence/STORY-129/evidence-report.md @@ -3,9 +3,9 @@ **Feature:** Per-finding `mitre_attack` array in JSON output (BC-2.11.035) **Issue:** #64 **Story ID:** STORY-129 -**Recorded:** 2026-06-22 +**Recorded:** 2026-06-23 (re-recorded on fix/ics-tactic-ids; original 2026-06-22 showed incorrect TA0007) **Tool:** VHS 0.11.0 -**Binary:** `target/release/wirerust` (v0.9.3, built from STORY-129 branch) +**Binary:** `target/release/wirerust` (v0.9.3, built from fix/ics-tactic-ids branch) **Fixture:** `tests/fixtures/modbus-write.pcap` --- @@ -14,7 +14,7 @@ | AC | Description | Recording | Path | |----|-------------|-----------|------| -| AC-1, AC-4, AC-7, AC-8 | Known techniques produce fully-resolved 5-field objects; multi-tag order preserved; ICS tactic_id correct; `mitre_techniques` unchanged | AC-001-mitre-attack-json-enrichment.gif / .webm | `docs/demo-evidence/STORY-129/` | +| AC-1, AC-4, AC-7, AC-8 | Known techniques produce fully-resolved 5-field objects; multi-tag order preserved; ICS tactic_id correct (T0888→TA0102, T0836→TA0106); `mitre_techniques` unchanged | AC-001-mitre-attack-json-enrichment.gif / .webm | `docs/demo-evidence/STORY-129/` | | AC-9 | CSV output is additive-non-breaking: no `mitre_attack` column appears | AC-009-csv-unaffected.gif / .webm | `docs/demo-evidence/STORY-129/` | --- @@ -32,7 +32,7 @@ **What it shows:** - 3 findings are emitted from `modbus-write.pcap` - Each finding's `mitre_attack` array is populated with fully-resolved technique objects -- Finding 1+2 (Modbus recon): T0888 → `name: "Remote System Information Discovery"`, `tactic_id: "TA0007"`, `tactic_name: "Discovery"` +- Finding 1+2 (Modbus recon): T0888 → `name: "Remote System Information Discovery"`, `tactic_id: "TA0102"`, `tactic_name: "Discovery (ICS)"` (corrected from TA0007 "Discovery" which mapped to the Enterprise matrix) - Finding 3 (write command): two entries — T1692.001 and T0836 — in declaration order (AC-4), both resolving to `tactic_id: "TA0106"` / `tactic_name: "Impair Process Control"` (AC-7, ICS matrix) - The `reference` URL uses the verbatim technique ID (preserving dot separator for sub-techniques) - The raw `mitre_techniques` array remains unchanged alongside `mitre_attack` (AC-8) @@ -65,7 +65,34 @@ ## Real mitre_attack JSON Snippet -Captured from a live run; excerpt of findings[2] (multi-technique, 2 entries): +Captured from a live run on fix/ics-tactic-ids (2026-06-23). All three findings shown. + +### findings[0] — T0888 Discovery (ICS) — CORRECTED (was TA0007, now TA0102) + +```json +{ + "category": "Anomaly", + "confidence": "Medium", + "direction": "ClientToServer", + "evidence": ["FC=0x11 TxnID=0x0001 UnitID=1"], + "mitre_attack": [ + { + "id": "T0888", + "name": "Remote System Information Discovery", + "reference": "https://attack.mitre.org/techniques/T0888/", + "tactic_id": "TA0102", + "tactic_name": "Discovery (ICS)" + } + ], + "mitre_techniques": ["T0888"], + "source_ip": "192.168.1.10", + "summary": "Modbus recon: Report Server ID (FC 0x11) from unit 1", + "timestamp": "2024-05-29T16:26:43Z", + "verdict": "Inconclusive" +} +``` + +### findings[2] — Multi-technique Impair Process Control (unchanged) ```json { @@ -101,6 +128,11 @@ Envelope fields (top-level): - `"mitre_attack_version": "ics-attack-19.1"` - `"mitre_domain": "ics-attack"` +**Note — ARP (T0830):** No ARP pcap fixture exists in `tests/fixtures/`. The expected output +for T0830 would be `tactic_id: "TA0100"` / `tactic_name: "Collection (ICS)"`. This is +verified by unit tests in `tests/reporter_json_tests.rs` but cannot be demonstrated via a +live pcap recording without an ARP fixture. + --- ## Acceptance Criteria Coverage @@ -113,7 +145,7 @@ Envelope fields (top-level): | AC-4: multi-tag order preserved | DEMONSTRATED | AC-001 shows T1692.001 at index 0 and T0836 at index 1 | | AC-5: duplicates not deduplicated | NOT RECORDED (unit-test covered) | Covered by `test_BC_2_11_035_duplicate_ids_not_deduplicated` | | AC-6: sub-technique dot preserved | DEMONSTRATED | AC-001 shows `T1692.001` with dot in id and reference URL | -| AC-7: ICS tactic_id resolved | DEMONSTRATED | AC-001 shows `tactic_id: "TA0106"` (ICS matrix, not Enterprise) | +| AC-7: ICS tactic_id resolved | DEMONSTRATED | AC-001 shows `tactic_id: "TA0102"` for T0888 (Discovery ICS) and `tactic_id: "TA0106"` for T0836/T1692.001 (Impair Process Control); all ICS-matrix IDs, not Enterprise | | AC-8: mitre_techniques unchanged | DEMONSTRATED | AC-001 output shows both `mitre_techniques` and `mitre_attack` coexisting | | AC-9: CSV unaffected | DEMONSTRATED | AC-009 recording shows no mitre_attack column in CSV | | AC-10: terminal unaffected | NOT RECORDED (unit-test covered) | Covered by `test_BC_2_11_035_terminal_unaffected` | diff --git a/src/mitre.rs b/src/mitre.rs index bd8d7fe9..3d5b3d7a 100644 --- a/src/mitre.rs +++ b/src/mitre.rs @@ -67,6 +67,18 @@ pub enum MitreTactic { /// impact-category findings. Distinct from the Enterprise `Impact` tactic. /// Added atomically with T0827 emission (STORY-109, VP-007 obligation). IcsImpact, + /// ICS Discovery tactic (TA0102) — T0846 "Remote System Discovery" and + /// T0888 "Remote System Information Discovery". Distinct from Enterprise + /// Discovery (TA0007). Added in F5 to emit the authoritative ICS-matrix TA-id. + IcsDiscovery, + /// ICS Collection tactic (TA0100) — T0830 "Adversary-in-the-Middle". + /// Distinct from Enterprise Collection (TA0009) and LateralMovement (TA0008). + /// Added in F5 to emit the authoritative ICS-matrix TA-id. + IcsCollection, + /// ICS Command and Control tactic (TA0101) — T0885 "Commonly Used Port". + /// Distinct from Enterprise CommandAndControl (TA0011). Added in F5 to emit + /// the authoritative ICS-matrix TA-id. + IcsCommandAndControl, } impl fmt::Display for MitreTactic { @@ -89,6 +101,9 @@ impl fmt::Display for MitreTactic { MitreTactic::IcsInhibitResponseFunction => "Inhibit Response Function", MitreTactic::IcsImpairProcessControl => "Impair Process Control", MitreTactic::IcsImpact => "Impact (ICS)", + MitreTactic::IcsDiscovery => "Discovery (ICS)", + MitreTactic::IcsCollection => "Collection (ICS)", + MitreTactic::IcsCommandAndControl => "Command and Control (ICS)", }; f.write_str(name) } @@ -116,6 +131,9 @@ pub fn all_tactics_in_report_order() -> &'static [MitreTactic] { MitreTactic::IcsInhibitResponseFunction, MitreTactic::IcsImpairProcessControl, MitreTactic::IcsImpact, + MitreTactic::IcsDiscovery, + MitreTactic::IcsCollection, + MitreTactic::IcsCommandAndControl, ] } @@ -143,10 +161,11 @@ pub fn technique_info(id: &str) -> Option<(&'static str, MitreTactic)> { "T1505.003" => ("Web Shell", MitreTactic::Persistence), "T1573" => ("Encrypted Channel", MitreTactic::CommandAndControl), // ICS. MITRE assigns distinct TA-IDs per matrix (e.g., Enterprise - // Discovery TA0007 vs ICS Discovery TA0111); we intentionally - // merge by name so a single grouped report has one section per - // tactic name regardless of source matrix. - "T0846" => ("Remote System Discovery", MitreTactic::Discovery), + // Discovery TA0007 vs ICS Discovery TA0102, Enterprise Collection + // TA0009 vs ICS Collection TA0100, Enterprise C2 TA0011 vs ICS C2 + // TA0101). F5 uses dedicated ICS variants so the reporter emits the + // authoritative ICS-matrix TA-id for each technique (f5-ics-technique-tactic-authoritative.md). + "T0846" => ("Remote System Discovery", MitreTactic::IcsDiscovery), "T1692.001" => ( "Unauthorized Message: Command Message", MitreTactic::IcsImpairProcessControl, @@ -155,19 +174,16 @@ pub fn technique_info(id: &str) -> Option<(&'static str, MitreTactic)> { "Unauthorized Message: Reporting Message", MitreTactic::IcsImpairProcessControl, ), - "T0885" => ("Commonly Used Port", MitreTactic::CommandAndControl), + "T0885" => ("Commonly Used Port", MitreTactic::IcsCommandAndControl), // ICS — NEW F2 (STORY-100 / BC-2.10.005). Seeded for Modbus/DNP3 analyzers. "T0836" => ("Modify Parameter", MitreTactic::IcsImpairProcessControl), "T0814" => ("Denial of Service", MitreTactic::IcsInhibitResponseFunction), "T0806" => ("Brute Force I/O", MitreTactic::IcsImpairProcessControl), "T0835" => ("Manipulate I/O Image", MitreTactic::IcsImpairProcessControl), - "T0831" => ( - "Manipulation of Control", - MitreTactic::IcsImpairProcessControl, - ), + "T0831" => ("Manipulation of Control", MitreTactic::IcsImpact), "T0888" => ( "Remote System Information Discovery", - MitreTactic::Discovery, + MitreTactic::IcsDiscovery, ), // STORY-109 / VP-007 atomic obligation — seeded together with the // T1691.001 and T0827 emission branches (BC-2.15.014 / BC-2.15.015). @@ -178,7 +194,7 @@ pub fn technique_info(id: &str) -> Option<(&'static str, MitreTactic)> { "T0827" => ("Loss of Control", MitreTactic::IcsImpact), // STORY-114 / VP-007 atomic obligation — seeded together with the // T0830 and T1557.002 emission branches (D1/D12/GARP-conflict ARP spoof). - "T0830" => ("Adversary-in-the-Middle", MitreTactic::LateralMovement), + "T0830" => ("Adversary-in-the-Middle", MitreTactic::IcsCollection), "T1557.002" => ( "Adversary-in-the-Middle: ARP Cache Poisoning", MitreTactic::CredentialAccess, @@ -234,6 +250,9 @@ pub fn technique_tactic_id(id: &str) -> Option<&'static str> { MitreTactic::IcsInhibitResponseFunction => "TA0107", MitreTactic::IcsImpairProcessControl => "TA0106", MitreTactic::IcsImpact => "TA0105", + MitreTactic::IcsDiscovery => "TA0102", + MitreTactic::IcsCollection => "TA0100", + MitreTactic::IcsCommandAndControl => "TA0101", }; Some(ta_id) } @@ -283,7 +302,7 @@ mod kani_proofs { "T1691.001", // Block OT Message: Command Message (BC-2.15.014; IcsInhibitResponseFunction) "T0827", // Loss of Control (BC-2.15.015; IcsImpact) // STORY-114 (2) — VP-007 atomic obligation; ARP D1/D12/GARP-conflict spoof detection. - "T0830", // Adversary-in-the-Middle (BC-2.16.004; LateralMovement) + "T0830", // Adversary-in-the-Middle (BC-2.16.004; IcsCollection/TA0100) "T1557.002", // ARP Cache Poisoning (BC-2.16.004; CredentialAccess) ]; @@ -349,7 +368,7 @@ mod kani_proofs { /// /// Count history: Post-F2 (STORY-100) 11 Enterprise + 10 ICS = 21 total (pre-STORY-109 subtotal). /// STORY-109 (VP-007 atomic obligation) +2 ICS (T1691.001, T0827) = 23 total. -/// STORY-114 (VP-007 ARP obligation) +2 ARP (T0830 ICS LateralMovement, T1557.002 Enterprise CredentialAccess) +/// STORY-114 (VP-007 ARP obligation) +2 ARP (T0830 ICS IcsCollection/TA0100, T1557.002 Enterprise CredentialAccess) /// = 25 total (12 Enterprise + 13 ICS; normative split per VP-007 §CC-003). /// ICS v19 remap (issue #222): T0855→T1692.001, T0856→T1692.002. #[cfg(any(kani, test))] diff --git a/tests/bc_2_09_100_multitag_tests.rs b/tests/bc_2_09_100_multitag_tests.rs index 12721cba..c31d0382 100644 --- a/tests/bc_2_09_100_multitag_tests.rs +++ b/tests/bc_2_09_100_multitag_tests.rs @@ -393,10 +393,11 @@ fn test_BC_2_10_005_seeded_technique_id_count_is_25() { /// BC-2.10.007 postcondition 2, AC-006 (STORY-100): /// All 21 STORY-100-era seeded IDs return the correct MitreTactic. This test /// covers the original 21-ID subset; the current catalog contains 25 seeded IDs. -/// All 21 IDs resolve in the current green catalog. +/// F5 correctness fix applied: ICS techniques use correct ICS-matrix variants. #[test] fn test_BC_2_10_007_technique_tactic_correct_for_all_21_seeded_ids() { // BC-2.10.007 postcondition 2: exhaustive tactic table for the 21 STORY-100-era seeded IDs. + // F5 fix: T0846/T0888 → IcsDiscovery, T0885 → IcsCommandAndControl, T0831 → IcsImpact. let assignments: &[(&str, MitreTactic)] = &[ // Enterprise (11) — STORY-100 era ("T1027", MitreTactic::DefenseEvasion), @@ -411,17 +412,22 @@ fn test_BC_2_10_007_technique_tactic_correct_for_all_21_seeded_ids() { ("T1505.003", MitreTactic::Persistence), ("T1573", MitreTactic::CommandAndControl), // ICS pre-F2 (4) - ("T0846", MitreTactic::Discovery), + // T0846: F5 fix — IcsDiscovery (ICS TA0102), not Enterprise Discovery (TA0007) + ("T0846", MitreTactic::IcsDiscovery), ("T1692.001", MitreTactic::IcsImpairProcessControl), ("T1692.002", MitreTactic::IcsImpairProcessControl), - ("T0885", MitreTactic::CommandAndControl), - // ICS F2 additions (6): all resolve in the current catalog (GREEN) + // T0885: F5 fix — IcsCommandAndControl (ICS TA0101), not Enterprise C2 (TA0011) + ("T0885", MitreTactic::IcsCommandAndControl), + // ICS F2 additions (6) ("T0836", MitreTactic::IcsImpairProcessControl), ("T0814", MitreTactic::IcsInhibitResponseFunction), ("T0806", MitreTactic::IcsImpairProcessControl), + // T0835: no change — still IcsImpairProcessControl (TA0106) — confirmed correct ("T0835", MitreTactic::IcsImpairProcessControl), - ("T0831", MitreTactic::IcsImpairProcessControl), - ("T0888", MitreTactic::Discovery), + // T0831: F5 fix — IcsImpact (ICS TA0105), not IcsImpairProcessControl (TA0106) + ("T0831", MitreTactic::IcsImpact), + // T0888: F5 fix — IcsDiscovery (ICS TA0102), not Enterprise Discovery (TA0007) + ("T0888", MitreTactic::IcsDiscovery), ]; assert_eq!( assignments.len(), @@ -437,14 +443,16 @@ fn test_BC_2_10_007_technique_tactic_correct_for_all_21_seeded_ids() { } } -/// BC-2.10.007 EC-004 (T0888 → Discovery): -/// `technique_tactic("T0888")` returns `Some(Discovery)`. +/// BC-2.10.007 EC-004 (T0888 → IcsDiscovery): +/// `technique_tactic("T0888")` returns `Some(IcsDiscovery)`. +/// F5 fix: ICS ATT&CK places T0888 under Discovery (ICS TA0102), not Enterprise Discovery. #[test] fn test_BC_2_10_007_t0888_maps_to_discovery_tactic() { assert_eq!( technique_tactic("T0888"), - Some(MitreTactic::Discovery), - "BC-2.10.007 EC-004: T0888 must map to Discovery (Remote System Information Discovery)" + Some(MitreTactic::IcsDiscovery), + "BC-2.10.007 EC-004: T0888 must map to IcsDiscovery (ICS TA0102), \ + NOT Enterprise Discovery (TA0007) — F5 correctness fix" ); } diff --git a/tests/bc_2_16_story114_arp_tests.rs b/tests/bc_2_16_story114_arp_tests.rs index 0dc9a656..ae364959 100644 --- a/tests/bc_2_16_story114_arp_tests.rs +++ b/tests/bc_2_16_story114_arp_tests.rs @@ -21,17 +21,21 @@ mod story_114_mitre { use wirerust::mitre::{MitreTactic, technique_name, technique_tactic}; /// AC-011 (BC-2.16.004 Invariant 4 / VP-007): technique_info("T0830") returns - /// ("Adversary-in-the-Middle", MitreTactic::LateralMovement) and + /// ("Adversary-in-the-Middle", MitreTactic::IcsCollection) and /// technique_info("T1557.002") returns ("Adversary-in-the-Middle: ARP Cache Poisoning", /// MitreTactic::CredentialAccess). Both resolve via technique_name and technique_tactic. /// /// Verifies that T0830 and T1557.002 are seeded in the MITRE catalog following the /// VP-007 5-part atomic update (SEEDED=25, EMITTED=17; originally absent at SEEDED=23). /// EC-012: T0830 and T1557.002 resolve to Some after the 5-part update. + /// + /// F5 correctness fix: T0830 maps to MitreTactic::IcsCollection (ICS TA0100), + /// not MitreTactic::LateralMovement (Enterprise TA0008). The ICS ATT&CK matrix + /// places "Adversary-in-the-Middle" under the Collection tactic (TA0100). #[test] fn test_t0830_and_t1557_002_resolves_in_catalog() { - // T0830: "Adversary-in-the-Middle", MitreTactic::LateralMovement - // (ADR-008 Decision 6 tactic anchor; ICS ATT&CK v19.1) + // T0830: "Adversary-in-the-Middle", MitreTactic::IcsCollection + // F5 fix: ICS ATT&CK v19.1 places T0830 under Collection (TA0100), not Lateral Movement. let t0830_name = technique_name("T0830"); assert!( t0830_name.is_some(), @@ -56,9 +60,10 @@ mod story_114_mitre { ); assert_eq!( t0830_tactic, - Some(MitreTactic::LateralMovement), - "AC-011 / BC-2.16.004 Invariant 4 (tactic anchor — ADR-008 Decision 6): T0830 \ - must map to MitreTactic::LateralMovement. Got: {:?}", + Some(MitreTactic::IcsCollection), + "AC-011 / BC-2.16.004 Invariant 4 (tactic anchor — F5 correctness fix): T0830 \ + must map to MitreTactic::IcsCollection (ICS TA0100), NOT LateralMovement (Enterprise TA0008). \ + ICS ATT&CK places T0830 under Collection (TA0100). Got: {:?}", t0830_tactic ); @@ -138,7 +143,12 @@ mod story_114_mitre { ("T1505.003", "Web Shell", MitreTactic::Persistence), ("T1573", "Encrypted Channel", MitreTactic::CommandAndControl), // ICS pre-F2 (4) - ("T0846", "Remote System Discovery", MitreTactic::Discovery), + // T0846: F5 fix — IcsDiscovery (ICS TA0102), not Enterprise Discovery (TA0007) + ( + "T0846", + "Remote System Discovery", + MitreTactic::IcsDiscovery, + ), ( "T1692.001", "Unauthorized Message: Command Message", @@ -149,10 +159,11 @@ mod story_114_mitre { "Unauthorized Message: Reporting Message", MitreTactic::IcsImpairProcessControl, ), + // T0885: F5 fix — IcsCommandAndControl (ICS TA0101), not Enterprise C2 (TA0011) ( "T0885", "Commonly Used Port", - MitreTactic::CommandAndControl, + MitreTactic::IcsCommandAndControl, ), // ICS new F2 — STORY-100 (6) ( @@ -175,15 +186,13 @@ mod story_114_mitre { "Manipulate I/O Image", MitreTactic::IcsImpairProcessControl, ), - ( - "T0831", - "Manipulation of Control", - MitreTactic::IcsImpairProcessControl, - ), + // T0831: F5 fix — IcsImpact (ICS TA0105), not IcsImpairProcessControl (TA0106) + ("T0831", "Manipulation of Control", MitreTactic::IcsImpact), + // T0888: F5 fix — IcsDiscovery (ICS TA0102), not Enterprise Discovery (TA0007) ( "T0888", "Remote System Information Discovery", - MitreTactic::Discovery, + MitreTactic::IcsDiscovery, ), // STORY-109 (2) ( @@ -192,11 +201,12 @@ mod story_114_mitre { MitreTactic::IcsInhibitResponseFunction, ), ("T0827", "Loss of Control", MitreTactic::IcsImpact), - // STORY-114 ARP (2) — the additions that make this test RED until impl + // STORY-114 ARP (2) + // T0830: F5 fix — IcsCollection (ICS TA0100), not LateralMovement (Enterprise TA0008) ( "T0830", "Adversary-in-the-Middle", - MitreTactic::LateralMovement, + MitreTactic::IcsCollection, ), ( "T1557.002", diff --git a/tests/mitre_tests.rs b/tests/mitre_tests.rs index ad868d2f..3946084a 100644 --- a/tests/mitre_tests.rs +++ b/tests/mitre_tests.rs @@ -8,7 +8,9 @@ use std::collections::HashSet; -use wirerust::mitre::{MitreTactic, all_tactics_in_report_order, technique_name, technique_tactic}; +use wirerust::mitre::{ + MitreTactic, all_tactics_in_report_order, technique_name, technique_tactic, technique_tactic_id, +}; // --------------------------------------------------------------------------- // AC-001 | BC-2.10.001 postcondition 1 @@ -97,18 +99,18 @@ fn test_ics_impair_process_control_display() { // --------------------------------------------------------------------------- // AC-005 | BC-2.10.003 postcondition 1 -// all_tactics_in_report_order().len() equals 17 (14 Enterprise + 3 ICS). -// STORY-109 adds MitreTactic::IcsImpact for T0827 (VP-007 atomic obligation), -// bringing the ICS-unique count from 2 to 3 and the total from 16 to 17. +// all_tactics_in_report_order().len() equals 20 (14 Enterprise + 6 ICS). +// F5 adds MitreTactic::IcsDiscovery, IcsCollection, IcsCommandAndControl, +// bringing the ICS-unique count from 3 to 6 and the total from 17 to 20. // --------------------------------------------------------------------------- #[test] -fn test_all_tactics_length_is_16() { - // BC-2.10.003 postcondition 1 / invariant 2 (updated STORY-109): - // 14 Enterprise + 3 ICS-unique = 17 variants. +fn test_all_tactics_length_is_20() { + // BC-2.10.003 postcondition 1 / invariant 2 (updated F5): + // 14 Enterprise + 6 ICS-unique = 20 variants. assert_eq!( all_tactics_in_report_order().len(), - 17, - "expected 14 Enterprise + 3 ICS-unique = 17 variants (STORY-109 adds IcsImpact)" + 20, + "expected 14 Enterprise + 6 ICS-unique = 20 variants (F5 adds IcsDiscovery, IcsCollection, IcsCommandAndControl)" ); } @@ -142,13 +144,14 @@ fn test_all_tactics_enterprise_kill_chain_order() { } // --------------------------------------------------------------------------- -// AC-007 | BC-2.10.003 postcondition 3 -// Elements [14] and [15] are IcsInhibitResponseFunction and IcsImpairProcessControl. +// AC-007 | BC-2.10.003 postconditions 3 and 4 +// Elements [14]-[16] are the first three ICS variants; [17]-[19] are the +// three new F5 ICS variants: IcsDiscovery, IcsCollection, IcsCommandAndControl. // --------------------------------------------------------------------------- #[test] fn test_all_tactics_ics_at_end() { - // BC-2.10.003 postcondition 3: ICS tactics appear after all 14 Enterprise - // tactics at positions [14] and [15]. + // BC-2.10.003 postcondition 3: ICS tactics at positions [14]-[16]. + // BC-2.10.003 postcondition 4: F5 ICS tactics at positions [17]-[19]. let tactics = all_tactics_in_report_order(); assert_eq!( tactics[14], @@ -160,12 +163,33 @@ fn test_all_tactics_ics_at_end() { MitreTactic::IcsImpairProcessControl, "position [15] must be IcsImpairProcessControl" ); + assert_eq!( + tactics[16], + MitreTactic::IcsImpact, + "position [16] must be IcsImpact (added F2 DNP3, STORY-109)" + ); + // BC-2.10.003 postcondition 4 / EC-007/EC-008/EC-009 (F5 additions): + assert_eq!( + tactics[17], + MitreTactic::IcsDiscovery, + "position [17] must be IcsDiscovery (added F5)" + ); + assert_eq!( + tactics[18], + MitreTactic::IcsCollection, + "position [18] must be IcsCollection (added F5)" + ); + assert_eq!( + tactics[19], + MitreTactic::IcsCommandAndControl, + "position [19] must be IcsCommandAndControl (added F5)" + ); } // --------------------------------------------------------------------------- // AC-008 | BC-2.10.004 postcondition 1 & 2 -// Collecting all_tactics_in_report_order() into a HashSet gives size 17. -// STORY-109 adds MitreTactic::IcsImpact → total 17. +// Collecting all_tactics_in_report_order() into a HashSet gives size 20. +// F5 adds IcsDiscovery, IcsCollection, IcsCommandAndControl → total 20. // --------------------------------------------------------------------------- #[test] fn test_all_tactics_no_duplicates() { @@ -179,14 +203,14 @@ fn test_all_tactics_no_duplicates() { tactics.len(), "duplicate variant detected in all_tactics_in_report_order()" ); - // STORY-109: IcsImpact added → 17 total (was 16 post-F2). - assert_eq!(unique.len(), 17); + // F5: IcsDiscovery + IcsCollection + IcsCommandAndControl added → 20 total (was 17). + assert_eq!(unique.len(), 20); } // --------------------------------------------------------------------------- // AC-009 | BC-2.10.004 postcondition 3 -// No variant omitted — all 17 variants appear in the slice. -// STORY-109 adds MitreTactic::IcsImpact → total 17. +// No variant omitted — all 20 variants appear in the slice. +// F5 adds IcsDiscovery, IcsCollection, IcsCommandAndControl → total 20. // --------------------------------------------------------------------------- #[test] fn test_all_tactics_all_variants_present() { @@ -213,6 +237,10 @@ fn test_all_tactics_all_variants_present() { MitreTactic::IcsImpairProcessControl, // STORY-109 (VP-007 atomic obligation) — IcsImpact for T0827 MitreTactic::IcsImpact, + // F5 — 3 new ICS variants for correct ICS-matrix TA-IDs + MitreTactic::IcsDiscovery, + MitreTactic::IcsCollection, + MitreTactic::IcsCommandAndControl, ] .into_iter() .collect(); @@ -315,8 +343,10 @@ fn test_technique_name_returns_none_for_unknown_ids() { // --------------------------------------------------------------------------- // AC-013 + AC-014 | BC-2.10.007 postcondition 2 -// All 21 seeded technique-to-tactic assignments are correct. +// All seeded technique-to-tactic assignments are correct per MITRE ICS ATT&CK. // AC-013 spot-checks T1027 => DefenseEvasion; AC-014 is exhaustive. +// F5 correctness fix: T0846/T0888 → IcsDiscovery, T0885 → IcsCommandAndControl, +// T0830 → IcsCollection, T0831 → IcsImpact. // --------------------------------------------------------------------------- #[test] fn test_technique_tactic_correct_assignments() { @@ -329,7 +359,7 @@ fn test_technique_tactic_correct_assignments() { ); // AC-014: exhaustive table per BC-2.10.007 postcondition 2. - // Includes all 21 seeded IDs (11 Enterprise + 10 ICS, post-F2 / STORY-100). + // F5 correctness fix applied: ICS techniques now use correct ICS-matrix variants. let assignments: &[(&str, MitreTactic)] = &[ // Enterprise (11) ("T1027", MitreTactic::DefenseEvasion), @@ -343,18 +373,30 @@ fn test_technique_tactic_correct_assignments() { ("T1499.002", MitreTactic::Impact), ("T1505.003", MitreTactic::Persistence), ("T1573", MitreTactic::CommandAndControl), - // ICS pre-F2 (4) — T0855/T0856 remapped to T1692.001/T1692.002 per v19 (issue #222) - ("T0846", MitreTactic::Discovery), + // ICS pre-F2 — T0855/T0856 remapped to T1692.001/T1692.002 per v19 (issue #222) + // T0846: F5 fix — was Discovery (Enterprise TA0007), now IcsDiscovery (ICS TA0102) + ("T0846", MitreTactic::IcsDiscovery), ("T1692.001", MitreTactic::IcsImpairProcessControl), ("T1692.002", MitreTactic::IcsImpairProcessControl), - ("T0885", MitreTactic::CommandAndControl), + // T0885: F5 fix — was CommandAndControl (Enterprise TA0011), now IcsCommandAndControl (ICS TA0101) + ("T0885", MitreTactic::IcsCommandAndControl), // ICS new F2 — STORY-100 additions (6) ("T0836", MitreTactic::IcsImpairProcessControl), ("T0806", MitreTactic::IcsImpairProcessControl), + // T0835: no change — still IcsImpairProcessControl (TA0106) — confirmed correct ("T0835", MitreTactic::IcsImpairProcessControl), - ("T0831", MitreTactic::IcsImpairProcessControl), + // T0831: F5 fix — was IcsImpairProcessControl (TA0106), now IcsImpact (ICS TA0105) + ("T0831", MitreTactic::IcsImpact), ("T0814", MitreTactic::IcsInhibitResponseFunction), - ("T0888", MitreTactic::Discovery), + // T0888: F5 fix — was Discovery (Enterprise TA0007), now IcsDiscovery (ICS TA0102) + ("T0888", MitreTactic::IcsDiscovery), + // STORY-109 additions + ("T1691.001", MitreTactic::IcsInhibitResponseFunction), + ("T0827", MitreTactic::IcsImpact), + // STORY-114 ARP additions + // T0830: F5 fix — was LateralMovement (Enterprise TA0008), now IcsCollection (ICS TA0100) + ("T0830", MitreTactic::IcsCollection), + ("T1557.002", MitreTactic::CredentialAccess), ]; for (id, expected_tactic) in assignments { @@ -364,6 +406,38 @@ fn test_technique_tactic_correct_assignments() { "technique_tactic({id:?}) returned unexpected tactic" ); } + + // Explicit technique_tactic_id assertions for the F5-corrected techniques: + // T0888 → IcsDiscovery → TA0102 + assert_eq!( + technique_tactic_id("T0888"), + Some("TA0102"), + "T0888 must resolve to tactic_id TA0102 (IcsDiscovery, ICS ATT&CK)" + ); + // T0846 → IcsDiscovery → TA0102 + assert_eq!( + technique_tactic_id("T0846"), + Some("TA0102"), + "T0846 must resolve to tactic_id TA0102 (IcsDiscovery, ICS ATT&CK)" + ); + // T0885 → IcsCommandAndControl → TA0101 + assert_eq!( + technique_tactic_id("T0885"), + Some("TA0101"), + "T0885 must resolve to tactic_id TA0101 (IcsCommandAndControl, ICS ATT&CK)" + ); + // T0830 → IcsCollection → TA0100 + assert_eq!( + technique_tactic_id("T0830"), + Some("TA0100"), + "T0830 must resolve to tactic_id TA0100 (IcsCollection, ICS ATT&CK)" + ); + // T0831 → IcsImpact → TA0105 + assert_eq!( + technique_tactic_id("T0831"), + Some("TA0105"), + "T0831 must resolve to tactic_id TA0105 (IcsImpact, ICS ATT&CK)" + ); } // --------------------------------------------------------------------------- @@ -554,3 +628,74 @@ fn test_ec_005_first_tactic_is_reconnaissance() { "first element of kill-chain order must be Reconnaissance" ); } + +// --------------------------------------------------------------------------- +// F5 Pass-1 process-gap hardening | authoritative ICS TA-id table +// +// Consolidated value-correctness pin for every ICS technique → +// technique_tactic_id() mapping, sourced from the F5 authoritative table: +// .factory/cycles/feature-mitre-json-names/f5-ics-technique-tactic-authoritative.md +// (MITRE ATT&CK for ICS v19 / ics-attack-19.1, released 2026-04-20). +// +// Process gap from Pass-1: prior tests asserted MitreTactic variants but not +// the final TA-id string emitted by technique_tactic_id(). A future edit that +// swaps two valid ICS variants (e.g. IcsCollection↔IcsDiscovery) would produce +// the correct tactic name but the WRONG TA-id, silently passing tactic-variant +// tests while breaking TA-id correctness. This test closes that gap by asserting +// the exact (technique_id → tactic_id string) pair for every ICS technique. +// --------------------------------------------------------------------------- +#[test] +fn test_ics_techniques_resolve_authoritative_tactic_ids() { + // Authoritative ICS technique → tactic-id table (MITRE ATT&CK for ICS v19). + // Source: f5-ics-technique-tactic-authoritative.md (verified against + // attack.mitre.org technique pages via WebFetch during F5 research pass). + // + // ICS Discovery (TA0102): + // T0888 Remote System Information Discovery → TA0102 + // T0846 Remote System Discovery → TA0102 + // ICS Command and Control (TA0101): + // T0885 Commonly Used Port → TA0101 + // ICS Collection (TA0100): + // T0830 Adversary-in-the-Middle → TA0100 + // ICS Impact (TA0105): + // T0831 Manipulation of Control → TA0105 (F5 fix: was TA0106) + // T0827 Loss of Control → TA0105 + // ICS Impair Process Control (TA0106): + // T0836 Modify Parameter → TA0106 + // T0806 Brute Force I/O → TA0106 + // T0835 Manipulate I/O Image → TA0106 + // T1692.001 Unauthorized Message: Command → TA0106 + // ICS Inhibit Response Function (TA0107): + // T0814 Denial of Service → TA0107 + // T1691.001 Block OT Message: Command → TA0107 + let authoritative: &[(&str, &str)] = &[ + // ICS Discovery — TA0102 + ("T0888", "TA0102"), + ("T0846", "TA0102"), + // ICS Command and Control — TA0101 + ("T0885", "TA0101"), + // ICS Collection — TA0100 (F5 remap: T0830 moved from LateralMovement/TA0008) + ("T0830", "TA0100"), + // ICS Impact — TA0105 + ("T0831", "TA0105"), // F5 remap: moved from ImpairProcessControl/TA0106 + ("T0827", "TA0105"), + // ICS Impair Process Control — TA0106 + ("T0836", "TA0106"), + ("T0806", "TA0106"), + ("T0835", "TA0106"), + ("T1692.001", "TA0106"), + // ICS Inhibit Response Function — TA0107 + ("T0814", "TA0107"), + ("T1691.001", "TA0107"), + ]; + + for (id, expected_ta_id) in authoritative { + assert_eq!( + technique_tactic_id(id), + Some(*expected_ta_id), + "technique_tactic_id({id:?}) must return {expected_ta_id:?} \ + per MITRE ATT&CK for ICS v19 authoritative table \ + (f5-ics-technique-tactic-authoritative.md)" + ); + } +} diff --git a/tests/reporter_json_tests.rs b/tests/reporter_json_tests.rs index ff0c8568..74c4714d 100644 --- a/tests/reporter_json_tests.rs +++ b/tests/reporter_json_tests.rs @@ -1060,15 +1060,15 @@ fn test_BC_2_11_035_ec009_enterprise_subtechnique() { } /// BC-2.11.035 EC-010: ICS technique T0830 (Adversary-in-the-Middle) resolves -/// to tactic Lateral Movement (TA0008). +/// to tactic Collection (ICS) (TA0100), not Lateral Movement. /// -/// Verifies that an ICS technique maps to its correct ICS-matrix tactic through -/// FindingJsonDto: tactic_id is TA0008, and tactic_name is the exact Display -/// string for MitreTactic::LateralMovement. -/// Catalog confirmed: T0830 → "Adversary-in-the-Middle", -/// MitreTactic::LateralMovement → TA0008 → "Lateral Movement" (STORY-114). +/// F5 correctness fix: T0830 maps to MitreTactic::IcsCollection (ICS TA0100), +/// not MitreTactic::LateralMovement (Enterprise TA0008). The ICS ATT&CK matrix +/// places "Adversary-in-the-Middle" under the Collection tactic (TA0100). +/// Verifies that FindingJsonDto emits tactic_id "TA0100" and tactic_name +/// "Collection (ICS)" for T0830. #[test] -fn test_BC_2_11_035_ec010_ics_lateral_movement() { +fn test_BC_2_11_035_ec010_ics_collection() { let mut finding = make_finding("test finding"); finding.mitre_techniques = vec!["T0830".to_string()]; let json_str = render(&[finding]); @@ -1090,12 +1090,14 @@ fn test_BC_2_11_035_ec010_ics_lateral_movement() { "BC-2.11.035 EC-010: name must match catalog entry for T0830" ); assert_eq!( - entry["tactic_id"], "TA0008", - "BC-2.11.035 EC-010: tactic_id must be TA0008 (Lateral Movement)" + entry["tactic_id"], "TA0100", + "BC-2.11.035 EC-010: tactic_id must be TA0100 (IcsCollection / Collection ICS), \ + NOT TA0008 (Enterprise Lateral Movement) — F5 correctness fix" ); assert_eq!( - entry["tactic_name"], "Lateral Movement", - "BC-2.11.035 EC-010: tactic_name must be Lateral Movement" + entry["tactic_name"], "Collection (ICS)", + "BC-2.11.035 EC-010: tactic_name must be Collection (ICS), \ + NOT Lateral Movement — F5 correctness fix" ); assert_eq!( entry["reference"], "https://attack.mitre.org/techniques/T0830/",