Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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
Expand Down
Binary file not shown.
44 changes: 38 additions & 6 deletions docs/demo-evidence/STORY-129/evidence-report.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`

---
Expand All @@ -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/` |

---
Expand All @@ -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)
Expand Down Expand Up @@ -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
{
Expand Down Expand Up @@ -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
Expand All @@ -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` |
Expand Down
45 changes: 32 additions & 13 deletions src/mitre.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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)
}
Expand Down Expand Up @@ -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,
]
}

Expand Down Expand Up @@ -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,
Expand All @@ -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).
Expand All @@ -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,
Expand Down Expand Up @@ -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)
}
Expand Down Expand Up @@ -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)
];

Expand Down Expand Up @@ -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))]
Expand Down
28 changes: 18 additions & 10 deletions tests/bc_2_09_100_multitag_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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),
Expand All @@ -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(),
Expand All @@ -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"
);
}

Expand Down
Loading