Skip to content
32 changes: 32 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,38 @@ Version numbers follow [Semantic Versioning](https://semver.org/).

## [Unreleased]

### Added

- **IEC-104 timed control command detection: TypeIDs 58–64 emit T1692.001 and T0836
(STORY-180, BC-2.19.029 + BC-2.19.030, wave-85).**

The IEC-104 passive analyzer detects the CP56Time2a time-tagged variants of control
command TypeIDs, closing the evasion gap documented in IEC104-TIMED-CMD-GAP-001 where
TypeIDs 58–64 fell silently through the `_` catch-all arm.

Two new match arms in `detect_iec104_threats` (`src/analyzer/iec104.rs`):

- `58..=60` (C_SC_TA_1 / C_DC_TA_1 / C_RC_TA_1 — timed switching commands): emits one
T1692.001 "Unauthorized Message: Command Message" Possible / Medium / Impact finding
with CASDU and conditional first_ioa evidence. No T0836 (binary switching control, not
parameter writes). Parity with untimed arm 45..=47 (BC-2.19.019); summary wording
distinguishes timed from untimed with "time-tagged" qualifier and C_SC_TA/C_DC_TA/C_RC_TA
mnemonics (BC-2.19.029).

- `61..=64` (C_SE_TA_1 / C_SE_TB_1 / C_SE_TC_1 / C_BO_TA_1 — timed set-point and
bitstring write commands): emits T1692.001 Possible then T0836 "Modify Parameter" Possible,
both with CASDU and conditional first_ioa evidence. T0836 is co-emitted because set-point
and bitstring TypeIDs modify ICS control parameters. Parity with untimed arm 48..=51
(BC-2.19.019); summaries name C_SE_TA/C_SE_TB/C_SE_TC/C_BO_TA mnemonics (BC-2.19.030).

The catch-all arm comment at `detect_iec104_threats` is narrowed from "52–99" to
"{52–57, 65–99}", noting that TypeIDs 58–64 are now handled by BC-2.19.029 and
BC-2.19.030 (AC-180-007; BC-2.19.022 v1.1). The existing post-emission `[TEST]` loop
covers the new arms automatically — no extra wiring required (BC-2.19.017 invariant 1).

`cargo test --test iec104_analyzer_tests`: 248 passed (221 prior + 27 new story_180
tests; 21 flipped red→green at the Green step, 6 regression guards green throughout).

## [0.13.1] - 2026-07-21

### Added
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
# AC-180-001 / AC-180-002 — TypeIDs 58–60 (C_SC_TA/C_DC_TA/C_RC_TA): T1692.001 Only, No T0836

**Story:** STORY-180: IEC-104 Timed Control Command Detection: TypeIDs 58–64
**ACs:** AC-180-001, AC-180-002
**Traces to:** BC-2.19.029 postconditions 1–3; invariant 2
**Wave:** 85

---

## Acceptance Criteria

**AC-180-001:** TypeIDs 58–60 emit exactly one T1692.001 Possible finding with CASDU and
first_ioa evidence — identical parity to untimed arm 45..=47.

**AC-180-002:** TypeIDs 58–60 do NOT emit T0836 (switching commands are binary control,
not parameter writes) — mirrors BC-2.19.019 Invariant 2.

---

## Test Suite Execution

Command:
```
cargo test --test iec104_analyzer_tests "BC_2_19_029"
```

Output:
```
Finished `test` profile [unoptimized + debuginfo] target(s) in 0.08s
Running tests/iec104_analyzer_tests.rs (target/debug/deps/iec104_analyzer_tests-09617f6be29af6e9)

running 10 tests
test story_180::test_BC_2_19_029_type_id_58_count_zero_still_emits ... ok
test story_180::test_BC_2_19_029_timed_summary_contains_time_tagged_qualifier ... ok
test story_180::test_BC_2_19_029_type_id_58_emits_t1692_001_only ... ok
test story_180::test_BC_2_19_029_timed_summary_differs_from_untimed_twin ... ok
test story_180::test_BC_2_19_029_casdu_first_ioa_evidence ... ok
test story_180::test_BC_2_19_029_type_id_58_verdict_confidence_category ... ok
test story_180::test_BC_2_19_029_type_id_60_cot_test_suffix ... ok
test story_180::test_BC_2_19_029_type_id_59_first_ioa_none_no_first_ioa_evidence ... ok
test story_180::test_BC_2_19_029_type_id_59_emits_t1692_001_only ... ok
test story_180::test_BC_2_19_029_type_id_60_emits_t1692_001_only ... ok

test result: ok. 10 passed; 0 failed; 0 ignored; 0 measured; 238 filtered out; finished in 0.00s
```

Result: **10/10 PASS**

---

## Test Coverage

### AC-180-001: Exactly One T1692.001 Possible Finding per TypeID (BC-2.19.029 PC1 + PC3)

| Test Name | TypeID / Condition | Assertion | Result |
|-----------|-------------------|-----------|--------|
| `test_BC_2_19_029_type_id_58_emits_t1692_001_only` | TypeID=58 (C_SC_TA_1) | exactly 1 finding; T1692.001 present | PASS |
| `test_BC_2_19_029_type_id_59_emits_t1692_001_only` | TypeID=59 (C_DC_TA_1) | exactly 1 finding; T1692.001 present | PASS |
| `test_BC_2_19_029_type_id_60_emits_t1692_001_only` | TypeID=60 (C_RC_TA_1) | exactly 1 finding; T1692.001 present | PASS |
| `test_BC_2_19_029_type_id_58_verdict_confidence_category` | TypeID=58 | Verdict::Possible, Confidence::Medium, ThreatCategory::Impact | PASS |
| `test_BC_2_19_029_casdu_first_ioa_evidence` | TypeID=58, casdu=1, first_ioa=Some(100) | evidence contains "CASDU=1" and "first_ioa=100" | PASS |
| `test_BC_2_19_029_type_id_59_first_ioa_none_no_first_ioa_evidence` | TypeID=59, first_ioa=None | evidence contains "CASDU=" but NOT "first_ioa=" (EC-008) | PASS |

### AC-180-002: No T0836 for Switching Commands (BC-2.19.029 invariant 2)

The `test_BC_2_19_029_type_id_58_emits_t1692_001_only`,
`test_BC_2_19_029_type_id_59_emits_t1692_001_only`, and
`test_BC_2_19_029_type_id_60_emits_t1692_001_only` tests each assert `findings.len() == 1`,
which proves T0836 is never emitted (exactly one finding, not two).

---

## Dispatch Behavior Summary

| TypeID | IEC-104 Name | Findings | MITRE Techniques | Verdict |
|--------|-------------|----------|-----------------|---------|
| 58 | C_SC_TA_1 (timed single-point switching) | 1 | T1692.001 | Possible |
| 59 | C_DC_TA_1 (timed double-point switching) | 1 | T1692.001 | Possible |
| 60 | C_RC_TA_1 (timed regulating step) | 1 | T1692.001 | Possible |

---

## Verdict

AC-180-001: **PASS** — All three timed switching TypeIDs (58, 59, 60) emit exactly one
T1692.001 Possible finding with CASDU and conditional first_ioa evidence.

AC-180-002: **PASS** — T0836 is never emitted for TypeIDs 58–60; single-finding assertion
in each test is the negative proof.
94 changes: 94 additions & 0 deletions docs/demo-evidence/STORY-180/AC-003-typeid-61-64-timed-setpoint.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
# AC-180-003 — TypeIDs 61–64 (C_SE_TA/C_SE_TB/C_SE_TC/C_BO_TA): T1692.001 + T0836 Both Possible

**Story:** STORY-180: IEC-104 Timed Control Command Detection: TypeIDs 58–64
**AC:** AC-180-003
**Traces to:** BC-2.19.030 postconditions 1–3
**Wave:** 85

---

## Acceptance Criterion

- Given an I-format ASDU with TypeID in {61, 62, 63, 64}
- Then exactly two findings are emitted:
1. T1692.001 "Unauthorized Message: Command Message" with Verdict::Possible, Confidence::Medium, ThreatCategory::Impact
2. T0836 "Modify Parameter" with Verdict::Possible, Confidence::Medium, ThreatCategory::Impact
- Both findings' evidence vectors include CASDU and, when present, first_ioa
- T0836 is co-emitted because TypeIDs 61–64 are ICS parameter writes (set-point and bitstring output register writes)

---

## Test Suite Execution

Command:
```
cargo test --test iec104_analyzer_tests "BC_2_19_030"
```

Output:
```
Finished `test` profile [unoptimized + debuginfo] target(s) in 0.07s
Running tests/iec104_analyzer_tests.rs (target/debug/deps/iec104_analyzer_tests-09617f6be29af6e9)

running 11 tests
test story_180::test_BC_2_19_030_type_id_61_count_zero_still_emits_two_findings ... ok
test story_180::test_BC_2_19_030_timed_summaries_differ_from_untimed_twin ... ok
test story_180::test_BC_2_19_030_timed_summaries_contain_time_tagged_and_mnemonics ... ok
test story_180::test_BC_2_19_030_type_id_61_casdu_first_ioa_evidence_both_findings ... ok
test story_180::test_BC_2_19_030_type_id_64_emits_two_findings ... ok
test story_180::test_BC_2_19_030_type_id_62_emits_two_findings ... ok
test story_180::test_BC_2_19_030_type_id_61_verdict_confidence_category_both_findings ... ok
test story_180::test_BC_2_19_030_type_id_64_cot_test_both_findings_tagged ... ok
test story_180::test_BC_2_19_030_type_id_61_emits_two_findings ... ok
test story_180::test_BC_2_19_030_type_id_62_first_ioa_none_no_first_ioa_evidence ... ok
test story_180::test_BC_2_19_030_type_id_63_emits_two_findings ... ok

test result: ok. 11 passed; 0 failed; 0 ignored; 0 measured; 237 filtered out; finished in 0.00s
```

Result: **11/11 PASS**

---

## Test Coverage

### Exactly Two Findings per TypeID — T1692.001 + T0836

| Test Name | TypeID / Condition | Assertion | Result |
|-----------|-------------------|-----------|--------|
| `test_BC_2_19_030_type_id_61_emits_two_findings` | TypeID=61 (C_SE_TA_1) | exactly 2 findings; T1692.001 + T0836 present | PASS |
| `test_BC_2_19_030_type_id_62_emits_two_findings` | TypeID=62 (C_SE_TB_1) | exactly 2 findings; T1692.001 + T0836 present | PASS |
| `test_BC_2_19_030_type_id_63_emits_two_findings` | TypeID=63 (C_SE_TC_1) | exactly 2 findings; T1692.001 + T0836 present | PASS |
| `test_BC_2_19_030_type_id_64_emits_two_findings` | TypeID=64 (C_BO_TA_1, bitstring) | exactly 2 findings; T1692.001 + T0836 present | PASS |

### Verdict, Confidence, Category (BC-2.19.030 PC1 + PC2)

| Test Name | Condition | Assertion | Result |
|-----------|-----------|-----------|--------|
| `test_BC_2_19_030_type_id_61_verdict_confidence_category_both_findings` | TypeID=61 | Both findings: Verdict::Possible, Confidence::Medium, ThreatCategory::Impact | PASS |

### CASDU / first_ioa Evidence in Both Findings (BC-2.19.030 PC3)

| Test Name | Input | Assertion | Result |
|-----------|-------|-----------|--------|
| `test_BC_2_19_030_type_id_61_casdu_first_ioa_evidence_both_findings` | TypeID=61, casdu=5, first_ioa=Some(200) | Both findings contain "CASDU=5" and "first_ioa=200" | PASS |
| `test_BC_2_19_030_type_id_62_first_ioa_none_no_first_ioa_evidence` | TypeID=62, first_ioa=None | Both findings contain "CASDU=" but NOT "first_ioa=" | PASS |

---

## Dispatch Behavior Summary

| TypeID | IEC-104 Name | Findings | MITRE Techniques | Verdict |
|--------|-------------|----------|-----------------|---------|
| 61 | C_SE_TA_1 (timed set-point normalized value) | 2 | T1692.001 + T0836 | Possible |
| 62 | C_SE_TB_1 (timed set-point scaled value) | 2 | T1692.001 + T0836 | Possible |
| 63 | C_SE_TC_1 (timed set-point short float) | 2 | T1692.001 + T0836 | Possible |
| 64 | C_BO_TA_1 (timed bitstring of 32 bits) | 2 | T1692.001 + T0836 | Possible |

---

## Verdict

AC-180-003: **PASS** — All four timed set-point/bitstring TypeIDs (61, 62, 63, 64) emit
exactly two findings (T1692.001 Possible + T0836 Possible) with CASDU and conditional
first_ioa evidence in both findings, matching BC-2.19.030 postconditions 1–3.
95 changes: 95 additions & 0 deletions docs/demo-evidence/STORY-180/AC-004-timed-summary-wording.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
# AC-180-004 — Timed-Variant Summary Wording Distinguishes from Untimed Twin Summaries

**Story:** STORY-180: IEC-104 Timed Control Command Detection: TypeIDs 58–64
**AC:** AC-180-004
**Traces to:** BC-2.19.029 postcondition 4; BC-2.19.030 postconditions 4 and 5
**Wave:** 85

---

## Acceptance Criterion

- For TypeIDs 58–60: the T1692.001 finding `summary` field uses the "time-tagged" qualifier
and names the timed mnemonics (C_SC_TA/C_DC_TA/C_RC_TA)
- For TypeIDs 61–64: both the T1692.001 and T0836 finding `summary` fields name the timed
mnemonics (C_SE_TA/C_SE_TB/C_SE_TC/C_BO_TA)
- Neither timed summary string is identical to the corresponding untimed arm's summary —
analysts can distinguish timed from untimed findings in output

---

## Test Suite Execution

Command:
```
cargo test --test iec104_analyzer_tests "timed_summary"
```

Output:
```
Finished `test` profile [unoptimized + debuginfo] target(s) in 0.07s
Running tests/iec104_analyzer_tests.rs (target/debug/deps/iec104_analyzer_tests-09617f6be29af6e9)

running 4 tests
test story_180::test_BC_2_19_029_timed_summary_contains_time_tagged_qualifier ... ok
test story_180::test_BC_2_19_029_timed_summary_differs_from_untimed_twin ... ok
test story_180::test_BC_2_19_030_timed_summaries_contain_time_tagged_and_mnemonics ... ok
test story_180::test_BC_2_19_030_timed_summaries_differ_from_untimed_twin ... ok

test result: ok. 4 passed; 0 failed; 0 ignored; 0 measured; 244 filtered out; finished in 0.00s
```

Result: **4/4 PASS**

---

## Test Coverage

| Test Name | Scope | Assertion | Result |
|-----------|-------|-----------|--------|
| `test_BC_2_19_029_timed_summary_contains_time_tagged_qualifier` | TypeID=58 T1692.001 summary | summary contains "time-tagged" and "C_SC_TA/C_DC_TA/C_RC_TA" | PASS |
| `test_BC_2_19_029_timed_summary_differs_from_untimed_twin` | TypeID=58 vs TypeID=45 T1692.001 summary | timed summary != untimed arm-45 summary | PASS |
| `test_BC_2_19_030_timed_summaries_contain_time_tagged_and_mnemonics` | TypeID=61 both findings | both summaries contain "time-tagged" and "C_SE_TA/C_SE_TB/C_SE_TC/C_BO_TA" | PASS |
| `test_BC_2_19_030_timed_summaries_differ_from_untimed_twin` | TypeID=61 vs TypeID=48 summaries | timed T1692.001 summary != untimed arm-48 T1692.001 summary | PASS |

---

## Source-Level Verification

Summary string for arm 58..=60 (src/analyzer/iec104.rs):
```
"IEC-104 time-tagged control command TypeID={type_id} \
(C_SC_TA/C_DC_TA/C_RC_TA): time-tagged switching control command \
observed on passive monitor \
(T1692.001 unauthorized command message; BC-2.19.029)"
```

Summary string for arm 61..=64 T1692.001 (src/analyzer/iec104.rs):
```
"IEC-104 time-tagged control command TypeID={type_id} \
(C_SE_TA/C_SE_TB/C_SE_TC/C_BO_TA): time-tagged set-point or bitstring \
write command observed on passive monitor \
(T1692.001 unauthorized command message; BC-2.19.030)"
```

Summary string for arm 61..=64 T0836 (src/analyzer/iec104.rs):
```
"IEC-104 time-tagged parameter modification TypeID={type_id} \
(C_SE_TA/C_SE_TB/C_SE_TC/C_BO_TA): time-tagged set-point or bitstring \
write modifying ICS control parameter on passive monitor \
(T0836 modify parameter; BC-2.19.030 postcondition 2)"
```

Confirmed by grep:
```
grep -n "C_SC_TA/C_DC_TA/C_RC_TA\|C_SE_TA/C_SE_TB" src/analyzer/iec104.rs
```
Returns lines 857–860 and 904–907, 920–923 — all contain "time-tagged".

---

## Verdict

AC-180-004: **PASS** — All four summary-wording tests pass. Timed summaries include
"time-tagged" qualifier and timed-arm mnemonics; diff tests confirm no string identity
with the untimed twin arm summaries.
Loading