Skip to content

feat(iec104): detect timed control command TypeIDs 58–64 (STORY-180, BC-2.19.029/030) - #437

Merged
Zious11 merged 7 commits into
developfrom
feature/STORY-180-iec104-timed-cmd-detection
Jul 24, 2026
Merged

feat(iec104): detect timed control command TypeIDs 58–64 (STORY-180, BC-2.19.029/030)#437
Zious11 merged 7 commits into
developfrom
feature/STORY-180-iec104-timed-cmd-detection

Conversation

@Zious11

@Zious11 Zious11 commented Jul 24, 2026

Copy link
Copy Markdown
Owner

[STORY-180] IEC-104 Timed Control Command Detection: TypeIDs 58–64

Epic: E-22 — IEC-104 Passive Analyzer
Mode: feature (feature-iec104, wave-85)
Convergence: CONVERGED after 4 adversarial passes (BC-5.39.001: 3 consecutive clean — P2/P3/P4)

Tests
Coverage
Mutation
Holdout

Adds two new detection arms to detect_iec104_threats in src/analyzer/iec104.rs to close
the evasion gap (IEC104-TIMED-CMD-GAP-001, CONFIRMED / HIGH confidence) where CP56Time2a
time-tagged control command TypeIDs 58–64 fell silently through the _ catch-all arm without
emitting any finding. TypeIDs 58–60 (C_SC_TA_1 / C_DC_TA_1 / C_RC_TA_1 — timed switching
commands) now emit exactly one T1692.001 "Unauthorized Message: Command Message" Possible
finding with CASDU and conditional first_ioa evidence, mirroring the untimed arm 45..=47
(BC-2.19.029). TypeIDs 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) now emit both T1692.001 Possible and T0836 "Modify
Parameter" Possible findings, mirroring the untimed arm 48..=51 (BC-2.19.030). The
silent-range code comment is narrowed from "52–99" to "{52–57, 65–99}" per BC-2.19.022 v1.1
(AC-180-007). 27 new unit tests verified the full AC-180-001..008 contract; full iec104 suite
passes 248/248 with 0 regressions.


Architecture Changes

graph TD
    ParseAsdu["parse_asdu()<br/>(pure-core — unchanged)"] -->|returns Option&lt;Asdu&gt;| DetectThreats["detect_iec104_threats()<br/>(effectful-shell)"]
    DetectThreats --> Arm4547["arm 45..=47<br/>T1692.001 only<br/>(BC-2.19.019, existing)"]
    DetectThreats --> Arm4851["arm 48..=51<br/>T1692.001 + T0836<br/>(BC-2.19.019, existing)"]
    DetectThreats --> Arm5860["arm 58..=60<br/>T1692.001 only<br/>(BC-2.19.029, NEW)"]
    DetectThreats --> Arm6164["arm 61..=64<br/>T1692.001 + T0836<br/>(BC-2.19.030, NEW)"]
    DetectThreats --> CatchAll["_ catch-all<br/>{52–57, 65–99} silent<br/>(BC-2.19.022 v1.1, narrowed)"]
    Arm5860 --> Findings["&mut Vec&lt;Finding&gt;"]
    Arm6164 --> Findings
    style Arm5860 fill:#90EE90
    style Arm6164 fill:#90EE90
    style CatchAll fill:#FFE4B5
Loading
Architecture Decision Record

ADR: Match-arm slot order (ADR-013 Decision 3) — slotting timed arms ahead of _ catch-all

Context: TypeIDs 58–64 fell through the _ catch-all arm in detect_iec104_threats
because no explicit arm existed for the CP56Time2a time-tagged variants of the control
command TypeIDs already handled by arms 45..=47 and 48..=51. ADR-013 Decision 3 mandates
that new detection arms be slotted in TypeID ascending order.

Decision: Add arm 58..=60 (T1692.001 only) and arm 61..=64 (T1692.001 + T0836)
between the existing 48..=51 arm and the 100/101/103 arm, ahead of the _ catch-all.
Emit logic mirrors the untimed twins exactly (same evidence shape: CASDU + conditional
first_ioa; same verdict/confidence/category values: Possible/Medium/Impact). Summary
strings are distinct with "time-tagged" qualifier and timed mnemonics so analysts can
distinguish timed from untimed findings.

Rationale: Parity with untimed arms is the explicit contract of BC-2.19.029/030.
The post-emission [TEST] loop (lines 924–928) covers the new arms automatically — no
extra wiring. No new crate dependencies; no unsafe needed (immutable borrow only).

Alternatives Considered:

  1. Generalize existing arms 45..=47 and 48..=51 with a combined range pattern — rejected
    because ADR-013 Decision 3 requires ascending slot order; combining the ranges would
    conflate two distinct behavioral contracts and break the BC-per-arm traceability model.
  2. Handle in a separate function — rejected because ADR-013 Decision 8 requires all
    finding emission to occur in detect_iec104_threats or the effectful shell.

Consequences:

  • TypeIDs 58–64 are now detected and attributed; the evasion gap is closed.
  • The _ catch-all arm silent range narrows from "52–99" to "{52–57, 65–99}" — existing
    BC-2.19.022 v1.1 regression guard tests enforce this boundary.

Story Dependencies

graph LR
    S174["STORY-174<br/>✅ MERGED PR #409<br/>(wave-83)"] --> S180["STORY-180<br/>🟡 this PR"]
    S180 --> Unblocked["(no blocked stories)"]
    style S174 fill:#90EE90
    style S180 fill:#FFD700
    style Unblocked fill:#E0E0E0
Loading

Dependency status: STORY-174 (IEC-104 VP-044/045/046/047 formal hardening) — merged to develop
as PR #409 (wave-83). No downstream stories are blocked on STORY-180 in the current wave scope.


Spec Traceability

flowchart LR
    BC029["BC-2.19.029 v1.3<br/>Timed Switching Cmds<br/>TypeIDs 58–60"] --> AC001["AC-180-001<br/>T1692.001 only"]
    BC029 --> AC002["AC-180-002<br/>No T0836"]
    BC029 --> AC004["AC-180-004<br/>timed-variant wording"]
    BC029 --> AC005["AC-180-005<br/>cot_test [TEST] suffix"]
    BC030["BC-2.19.030 v1.2<br/>Timed Set-Point Cmds<br/>TypeIDs 61–64"] --> AC003["AC-180-003<br/>T1692.001 + T0836"]
    BC030 --> AC004
    BC030 --> AC005
    BC022["BC-2.19.022 v1.1<br/>Reserved/Invalid TypeID<br/>regression guard"] --> AC006["AC-180-006<br/>52–57, 65–99 silent"]
    BC022 --> AC007["AC-180-007<br/>comment narrowed"]
    BC029 --> AC008["AC-180-008<br/>count-independent"]
    BC030 --> AC008
    AC001 --> T58["test_BC_2_19_029_type_id_58_emits_t1692_001_only<br/>line 7017"]
    AC003 --> T61["test_BC_2_19_030_type_id_61_emits_two_findings<br/>line 7368"]
    AC006 --> T52["test_BC_2_19_022_v1_1_type_id_52_no_finding<br/>line 7806"]
    T58 --> IEC104["src/analyzer/iec104.rs<br/>arm 58..=60"]
    T61 --> IEC104
    T52 --> IEC104
Loading

Test Evidence

PG-W74-PRDESC-ROW-VERIFY — Row-Verification Record

Row-verified 4 entries from the per-test table below against
tests/iec104_analyzer_tests.rs on branch feature/STORY-180-iec104-timed-cmd-detection:

  • Row 1: test_BC_2_19_029_type_id_58_emits_t1692_001_only — confirmed at line 7017
  • Row 2: test_BC_2_19_029_casdu_first_ioa_evidence — confirmed at line 7155
  • Row 3: test_BC_2_19_030_type_id_61_emits_two_findings — confirmed at line 7368
  • Row 4: test_BC_2_19_022_v1_1_type_id_52_no_finding — confirmed at line 7806

Aggregate count cross-check:

  • Claimed: 27 STORY-180 tests — matches actual cargo test story_180 output: "27 passed; 0 failed" ✓
  • Claimed: 248 total iec104 tests — matches actual cargo test --test iec104_analyzer_tests output: "248 passed; 0 failed" ✓

Coverage Summary

Metric Value Threshold Status
IEC-104 suite tests 248/248 pass 100% PASS
STORY-180 new tests 27/27 pass 100% PASS
Coverage % N/A >80% N/A
Mutation kill rate N/A >90% N/A (VP-047 fuzz covers new arms)
Holdout satisfaction N/A — wave gate >0.85 N/A — evaluated at wave gate

Test Flow

graph LR
    Unit["248 IEC-104 Unit Tests<br/>(27 new STORY-180)"]
    Adversarial["4 Adversarial Passes<br/>BC-5.39.001 SATISFIED"]
    RedGate["Red Gate<br/>21 red / 227 green"]
    Formal["VP-047 fuzz harness<br/>(auto-covers 58–64)"]

    RedGate -->|TDD verified| Unit
    Unit -->|248/248 PASS| Pass1["PASS"]
    Adversarial -->|3/3 clean streak| Pass2["PASS"]
    Formal -->|covers new arms| Pass3["N/A — no new VP"]

    style Pass1 fill:#90EE90
    style Pass2 fill:#90EE90
    style Pass3 fill:#E0E0E0
Loading
Metric Value
New tests 27 added (story_180 module), 0 modified
Total iec104 suite 248 tests PASS (221 prior + 27 new)
Coverage delta N/A (line coverage not instrumented in this story)
Mutation kill rate N/A
Regressions 0 — all 221 prior tests pass; untimed twins (TypeIDs 45/51) verified by regression guard tests
Detailed Test Results — STORY-180 (27 tests)

New Tests (This PR — story_180 module)

Test Name AC Result
test_BC_2_19_029_type_id_58_emits_t1692_001_only AC-180-001/002 PASS
test_BC_2_19_029_type_id_59_emits_t1692_001_only AC-180-001/002 PASS
test_BC_2_19_029_type_id_60_emits_t1692_001_only AC-180-001/002 PASS
test_BC_2_19_029_type_id_58_verdict_confidence_category AC-180-001 PASS
test_BC_2_19_029_casdu_first_ioa_evidence AC-180-001 PASS
test_BC_2_19_029_type_id_59_first_ioa_none_no_first_ioa_evidence AC-180-001 PASS
test_BC_2_19_029_timed_summary_contains_time_tagged_qualifier AC-180-004 PASS
test_BC_2_19_029_timed_summary_differs_from_untimed_twin AC-180-004 PASS
test_BC_2_19_029_type_id_60_cot_test_suffix AC-180-005 PASS
test_BC_2_19_029_type_id_58_count_zero_still_emits AC-180-008 PASS
test_BC_2_19_030_type_id_61_emits_two_findings AC-180-003 PASS
test_BC_2_19_030_type_id_62_emits_two_findings AC-180-003 PASS
test_BC_2_19_030_type_id_63_emits_two_findings AC-180-003 PASS
test_BC_2_19_030_type_id_64_emits_two_findings AC-180-003 PASS
test_BC_2_19_030_type_id_61_verdict_confidence_category_both_findings AC-180-003 PASS
test_BC_2_19_030_type_id_61_casdu_first_ioa_evidence_both_findings AC-180-003 PASS
test_BC_2_19_030_type_id_62_first_ioa_none_no_first_ioa_evidence AC-180-003 PASS
test_BC_2_19_030_timed_summaries_contain_time_tagged_and_mnemonics AC-180-004 PASS
test_BC_2_19_030_timed_summaries_differ_from_untimed_twin AC-180-004 PASS
test_BC_2_19_030_type_id_64_cot_test_both_findings_tagged AC-180-005 PASS
test_BC_2_19_030_type_id_61_count_zero_still_emits_two_findings AC-180-008 PASS
test_BC_2_19_022_v1_1_type_id_52_no_finding AC-180-006 PASS
test_BC_2_19_022_v1_1_type_id_57_no_finding AC-180-006 PASS
test_BC_2_19_022_v1_1_type_id_65_no_finding AC-180-006 PASS
test_BC_2_19_022_v1_1_type_id_99_no_finding AC-180-006 PASS
test_BC_2_19_019_v1_1_regression_type_id_45_still_one_finding AC-180-006 PASS
test_BC_2_19_019_v1_1_regression_type_id_51_still_two_findings AC-180-006 PASS

Source: cargo test --test iec104_analyzer_tests story_180 — "27 passed; 0 failed"
(feature branch ccec171 / tests/iec104_analyzer_tests.rs)


Holdout Evaluation

N/A — evaluated at wave gate per factory process (E-22 epic, wave-85).


Adversarial Review

Pass Code Tip Findings Critical High Medium Low Status
P1 d64d660 3 0 0 3 0 Fixed (a008703)
P2 a008703 3 0 0 0 3 Swept (e40955f) — streak 1/3
P3 e40955f 1 0 0 0 1 Fixed (0502c64) — streak 2/3
P4 0502c64 1 0 0 0 1 Fixed (BC label) — streak 3/3

Convergence: CONVERGED — BC-5.39.001 SATISFIED (3 consecutive clean passes P2/P3/P4).
Adversary forced to hallucinate after pass P4. No open HIGH or CRITICAL findings.

Medium-Severity Findings & Resolutions

F-180-P1-001 (MEDIUM): dispatch-table doc comment drift

  • Location: src/analyzer/iec104.rs — match-arm inline comments
  • Category: code-quality / spec-fidelity
  • Problem: Inline comments enumerated only untimed TypeIDs 45–51 and omitted the new
    timed-variant detection arms 58–64.
  • Resolution: Comments updated to enumerate full TypeID coverage in a0087033.
  • Test added: None (comment-only fix).

F-180-P1-002 (MEDIUM): CHANGELOG count mismatch

  • Location: CHANGELOG.md — [Unreleased] entry
  • Category: documentation
  • Problem: Entry claimed "21 red assertion-shaped tests" but actual new-test count was 27.
  • Resolution: Count corrected to 27 in a0087033.

F-180-P1-003 (MEDIUM): stale present-tense RED docstrings (9 sites)

  • Location: tests/iec104_analyzer_tests.rs — 9 doc sites
  • Category: documentation / PG-W85-003
  • Problem: 9 sites retained currently asserts, is expected to, and similar RED-phase
    phrasing — the exact class that bin/check-green-doc-tense is designed to catch.
  • Resolution: 9 sites reframed to past-tense GREEN-phase prose in a0087033.

Security Review

Result: CLEAN — 0 Critical, 0 High, 0 Medium, 0 Low (new findings introduced by STORY-180).

graph LR
    Critical["Critical: 0"]
    High["High: 0"]
    Medium["Medium: 0"]
    Low["Low: 0 (new)"]

    style Critical fill:#90EE90
    style High fill:#90EE90
    style Medium fill:#90EE90
    style Low fill:#90EE90
Loading
Security Scan Details

Analysis Summary

The diff consists of two plain match arms reading immutable &Asdu fields (type_id,
casdu, first_ioa, cot_test — all pre-parsed, typed Rust fields) and pushing to
&mut Vec<Finding>. No raw user input, no string interpolation with untrusted data,
no network I/O, no file I/O, no unsafe code, no external crate dependencies.

Input Validation: Asdu struct fields are pre-validated by parse_asdu upstream.
The new arms perform O(1) pattern matching on a u8 TypeID value.

Injection Risks: None. Evidence strings use format!() with typed u16/u32
integer values (CASDU, first_ioa). No dynamic dispatch, no SQL, no command execution.

Authentication / Authorization: Passive analyzer operating on already-captured
network traffic. No authentication boundaries crossed.

Crypto / Secrets: None applicable.

Data Exposure: CASDU and first_ioa are ICS metadata already present in the network
capture; not PII; not credentials. Same exposure level as existing untimed arms 45–51.

SAST

  • Critical: 0 | High: 0 | Medium: 0 | Low: 0 (new)

Pre-existing Finding (not introduced by STORY-180)

  • SEC-001 CWE-22 (LOW) — pre-existing, unchanged by this PR.

ADR-013 Decision 7 Compliance

No ICS parsing libraries (iec60870-5, wireshark, lib60870, nom) introduced.


Risk Assessment & Deployment

Blast Radius

  • Systems affected: src/analyzer/iec104.rs (detect_iec104_threats function only),
    tests/iec104_analyzer_tests.rs (test file only). No Cargo.toml changes; no new
    crate dependencies; no public API surface changes.
  • User impact: Additive — previously undetected TypeIDs 58–64 now produce findings.
    No existing findings are modified or removed. No breaking change.
  • Data impact: None. In-memory finding emission only; no persistent storage.
  • Risk Level: LOW — additive detection arms in a pure read path; no behavioral
    regression risk (221 prior tests pass; PG-W72-BREAKING-HOLDOUT-SWEEP does NOT trigger
    for this story — additive detection, not a BREAKING or output-format-change story).

Performance Impact

Metric Before After Delta Status
Match arm eval per ASDU N arms N+2 arms +2 arms (additive) OK
Memory per finding same same 0 OK
Throughput unchanged unchanged N/A OK

Note: The two new match arms add negligible cost — they are O(1) pattern comparisons in
a single-level match over a u8 TypeID.

Rollback Instructions

Immediate rollback (< 2 min):

git revert ccec1711  # demo evidence commit (top of stack)
git revert 0502c642  # P3 close
git revert e40955f1  # P2 sweep
git revert a0087033  # P1 remediation
git revert d64d660b  # CHANGELOG + fmt
git revert 18d0a91d  # detection arms implementation
git push origin develop

Or revert the squash-merge commit directly after merge.

Verification after rollback:

  • Run cargo test --test iec104_analyzer_tests — should return to 221 tests passing
  • Run cargo test --all-targets — 0 failures expected

Feature Flags

Flag Controls Default
N/A No feature flags used — ICS detection arms are always-on N/A

Traceability

Requirement Story AC Test Verification Status
BC-2.19.029 PC1 AC-180-001 test_BC_2_19_029_type_id_58_emits_t1692_001_only unit PASS
BC-2.19.029 PC2/inv2 AC-180-002 test_BC_2_19_029_type_id_58_emits_t1692_001_only (count=1) unit PASS
BC-2.19.029 PC3 AC-180-001 test_BC_2_19_029_casdu_first_ioa_evidence unit PASS
BC-2.19.029 PC4 AC-180-004 test_BC_2_19_029_timed_summary_contains_time_tagged_qualifier unit PASS
BC-2.19.029 PC6/inv1 AC-180-005 test_BC_2_19_029_type_id_60_cot_test_suffix unit PASS
BC-2.19.030 PC1-PC3 AC-180-003 test_BC_2_19_030_type_id_61_emits_two_findings unit PASS
BC-2.19.030 PC4-PC5 AC-180-004 test_BC_2_19_030_timed_summaries_contain_time_tagged_and_mnemonics unit PASS
BC-2.19.030 PC7/inv1 AC-180-005 test_BC_2_19_030_type_id_64_cot_test_both_findings_tagged unit PASS
BC-2.19.022 v1.1 inv1 AC-180-006 test_BC_2_19_022_v1_1_type_id_52_no_finding unit PASS
BC-2.19.022 v1.1 arch anchor AC-180-007 source-level (grep) manual verify PASS
BC-2.19.029 inv3 AC-180-008 test_BC_2_19_029_type_id_58_count_zero_still_emits unit PASS
BC-2.19.030 inv3 AC-180-008 test_BC_2_19_030_type_id_61_count_zero_still_emits_two_findings unit PASS
Full VSDD Contract Chain
BC-2.19.029 -> AC-180-001 -> test_BC_2_19_029_type_id_58_emits_t1692_001_only -> src/analyzer/iec104.rs arm 58..=60 -> ADV-P4-CONVERGED -> unit-PASS
BC-2.19.029 -> AC-180-002 -> test_BC_2_19_029_type_id_58_emits_t1692_001_only (count=1) -> src/analyzer/iec104.rs arm 58..=60 -> ADV-P4-CONVERGED -> unit-PASS
BC-2.19.030 -> AC-180-003 -> test_BC_2_19_030_type_id_61_emits_two_findings -> src/analyzer/iec104.rs arm 61..=64 -> ADV-P4-CONVERGED -> unit-PASS
BC-2.19.022 v1.1 -> AC-180-006 -> test_BC_2_19_022_v1_1_type_id_52_no_finding -> src/analyzer/iec104.rs catch-all comment narrowed -> ADV-P4-CONVERGED -> unit-PASS
BC-2.19.022 v1.1 -> AC-180-007 -> source-level grep -> src/analyzer/iec104.rs lines 912-914 comment -> ADV-P4-CONVERGED -> manual-PASS
VP-047 -> fuzz_iec104_parser -> auto-covers TypeIDs 58-64 once arms added -> src/analyzer/iec104.rs -> STORY-174 formal hardening

Demo Evidence

Demo evidence committed at ccec1711 to feature branch
feature/STORY-180-iec104-timed-cmd-detection.

Path: docs/demo-evidence/STORY-180/ (8 artifacts)

File AC Coverage
AC-001-002-typeid-58-60-timed-switching.md AC-180-001 (BC-2.19.029 PC1+PC3), AC-180-002 (BC-2.19.029 inv2)
AC-003-typeid-61-64-timed-setpoint.md AC-180-003 (BC-2.19.030 PC1-PC3)
AC-004-timed-summary-wording.md AC-180-004 (BC-2.19.029 PC4; BC-2.19.030 PC4-PC5)
AC-005-cot-test-tagging.md AC-180-005 (BC-2.19.017 inv1; BC-2.19.029 PC6; BC-2.19.030 PC7)
AC-006-silence-regression-guard.md AC-180-006 (BC-2.19.022 v1.1 inv1)
AC-007-silent-range-comment.md AC-180-007 (BC-2.19.022 v1.1 arch anchor; source-level)
AC-008-count-independent-emission.md AC-180-008 (BC-2.19.029 inv3; BC-2.19.030 inv3)
evidence-report.md Index (full test run transcripts, coverage map, edge-case table)

Coverage: 8 ACs covered × ≥1 artifact each — PG-W70-DEMO-SCRUB gate PASSED.


AI Pipeline Metadata

Pipeline Details
ai-generated: true
pipeline-mode: feature (feature-iec104, wave-85)
factory-version: "1.0.0-rc.23"
pipeline-stages:
  spec-crystallization: completed (BC-2.19.029 v1.3 + BC-2.19.030 v1.2 + BC-2.19.022 v1.1)
  story-decomposition: completed (STORY-180 v1.1, D-505 human story-approval PASSED)
  tdd-implementation: completed (Red Gate: 21 red/227 green; Green: 248/248)
  holdout-evaluation: N/A — evaluated at wave gate
  adversarial-review: completed (CONVERGED — 4 passes, BC-5.39.001 SATISFIED)
  formal-verification: N/A — VP-047 fuzz auto-covers new arms
  convergence: achieved
convergence-metrics:
  clean_streak: "P2/P3/P4 = 3/3"
  last_classification: NITPICK_ONLY
  open_high_critical: 0
adversarial-passes: 4
models-used:
  builder: claude-sonnet-4-6
  adversary: claude-sonnet-4-6 (per-story adversarial, wave-85 pattern)
generated-at: "2026-07-24T00:00:00Z"
wave: 85
story-version: "1.1"
branch-head: "ccec1711"

Pre-Merge Checklist

  • All CI status checks passing
  • Coverage delta is positive or neutral (additive story — 27 new tests, 0 regressions)
  • No critical/high security findings unresolved (0 C/0 H from adversarial; security reviewer TBD)
  • Rollback procedure validated (see Risk Assessment section)
  • Feature flag: N/A — no feature flags (always-on detection arms)
  • Human review completed (per DF-MERGE-AUTH-CLASSIFIER-001 / recent-wave pattern: step 8 merge halted for human execution)
  • PG-W72-BREAKING-HOLDOUT-SWEEP: does NOT apply — additive detection story, not a BREAKING or output-format-change story
  • CHANGELOG [Unreleased] entry present (AC-158-001 / changelog-gate CI job)
  • Demo evidence: 8 artifacts × 8 ACs, PG-W70-DEMO-SCRUB PASSED
  • Adversarial convergence: BC-5.39.001 SATISFIED (DF-CONVERGENCE-BEFORE-MERGE-001)
  • Dependency STORY-174: PR test: STORY-174 IEC-104 formal hardening — VP-044 Kani + VP-045/046 proptests + VP-047 fuzz + cargo-mutants + green-doc-tense gate extension (wave-83) #409 merged to develop

Zious11 added 7 commits July 24, 2026 09:50
…v1.1 regression guard

Red Gate verified: 21 RED (TypeIDs 58-64 new behavioral tests), 6 GREEN (silence
guards 52/57/65/99 + untimed twin regression guards 45/51). 227 total passing.
…60 and 61..=64

Add match arm 58..=60 (BC-2.19.029): C_SC_TA_1/C_DC_TA_1/C_RC_TA_1 — emits one
T1692.001 Possible/Medium/Impact finding with CASDU+conditional first_ioa evidence
and "time-tagged" qualifier in summary; no T0836 (binary switching commands, parity
with untimed arm 45..=47).

Add match arm 61..=64 (BC-2.19.030): C_SE_TA_1/C_SE_TB_1/C_SE_TC_1/C_BO_TA_1 —
emits T1692.001 Possible then T0836 Possible, both with CASDU+conditional first_ioa
evidence and "time-tagged" + C_SE_TA/C_SE_TB/C_SE_TC/C_BO_TA mnemonics in summaries
(parity with untimed arm 48..=51).

Narrow silent-range comment at detect_iec104_threats catch-all arm from "52–99" to
"{52–57, 65–99}" with note that TypeIDs 58–64 were moved to BC-2.19.029/030 arms
(AC-180-007; BC-2.19.022 v1.1).

All 27 story_180 tests pass (21 previously failing now green; 6 regression guards
remain green). ADR-013 Decision 3 slot order maintained (58..=60 then 61..=64,
after 48..=51, before 105). No new dependencies; no unsafe; no changes to parse_asdu.
CHANGELOG: add [Unreleased] entry documenting IEC-104 timed control command detection
for TypeIDs 58-64 (BC-2.19.029 arms 58-60, BC-2.19.030 arms 61-64, BC-2.19.022 v1.1
comment narrowing). Follows existing entry style; present-tense green-doc-tense discipline.

tests/iec104_analyzer_tests.rs: rustfmt normalization only (whitespace/line-wrapping in
test_BC_2_19_029_type_id_58_verdict_confidence_category,
test_BC_2_19_029_timed_summary_differs_from_untimed_twin,
test_BC_2_19_030_timed_summaries_differ_from_untimed_twin — zero logic changes).
… docstrings (F-180-P1-001/002/003)

F-180-P1-001 (src/analyzer/iec104.rs): added rows for 58-60 (T1692.001,
BC-2.19.029) and 61-64 (T1692.001 + T0836, BC-2.19.030) to the TypeID
dispatch table doc-comment; corrected unhandled-range row from
"1-127 (unhandled)" to "{52-57, 65-99, ...} (unhandled)" to align with
the narrowed catch-all comment (AC-180-007; BC-2.19.022 v1.1).

F-180-P1-002 (CHANGELOG.md): corrected test-count claim from
"227 prior + 21 new" to "221 prior + 27 new story_180 tests; 21 flipped
red->green at the Green step, 6 regression guards green throughout".

F-180-P1-003 (tests/iec104_analyzer_tests.rs): converted 9 present-tense
"Expected RED: ... falls through catch-all" docstrings to past-tense
provenance per DF-GREEN-DOC-TENSE-SWEEP; also converted 2 mixed-state
"Expected RED for TypeID=X assertion" docstrings (7241, 7616) for sibling-
sweep completeness. Docstrings using "no findings before implementation"
left as-is (acceptable per task spec).

Sibling sweeps clean: no "currently falls through" hits; silently-logged
mentions consistent with narrowed set; "52-99" only in historical
change-description prose; "21 new"/"227 prior" claims gone; story_180
#[test] count = 27 confirmed.
…nce parity, EC-008 citation (F-180-P2-001/002/003)

F-180-P2-001: add 106 to story_170 docstring set {1,30,44,52,99,102,104,127} → {…,106,…}
  to match the literal array [1,30,44,52,99,102,104,106,127] at line 3218.

F-180-P2-002: convert all 10 terse "Expected RED: no findings before implementation."
  docstrings in mod story_180 to the enriched past-tense provenance form used by their
  11 siblings, e.g. "At the RED gate, TypeID=N fell through the `_` catch-all (0 findings);
  now handled by the <58..=60 | 61..=64> arm." — correct TypeID and arm per test.

F-180-P2-003: reword the EC-008 citation in test_BC_2_19_029_type_id_59_first_ioa_none_no_first_ioa_evidence
  docstring from "EC-008 (STORY-180)" (which specifies TypeID=58) to
  "EC-008 analog (STORY-180 EC-008 specifies TypeID=58; the conditional
  first_ioa-omission property is TypeID-independent within the 58..=60 arm —
  TypeID=59 exercised here)."

Sibling sweeps (DF-SIBLING-SWEEP-001):
  1. grep "Expected RED" → 0 hits anywhere in file (all 10 converted)
  2. grep "104, 127" → 0 hits (docstring set now includes 106)
  3. grep "EC-008" → 12 hits in other-story modules (167/169/170/172) — out of
     story scope, TypeID claims are story-specific and accurate; no edit needed.
     In story_180: line 7179 (analog citation, fixed), line 7201 (assertion string,
     untouched per instructions), line 7695 (scoped to BC-2.19.030, accurate),
     line 7812 (scoped to BC-2.19.022, accurate).

Tests: 248/0. Clippy: clean. Fmt: clean. No assertions, production code, or CHANGELOG touched.
…030 canonical vectors (F-180-P3-001)

Strengthen test_BC_2_19_029_casdu_first_ioa_evidence to assert
"time-tagged switching control command (C_SC_TA/C_DC_TA/C_RC_TA)" in
evidence (BC-2.19.029 canonical vector row 1).

Strengthen test_BC_2_19_030_type_id_61_casdu_first_ioa_evidence_both_findings
with per-finding descriptive assertions: ev1 (T1692.001) asserts
"time-tagged set-point/bitstring write command (C_SE_TA/C_SE_TB/C_SE_TC/C_BO_TA)";
ev2 (T0836) asserts "parameter modification (C_SE_TA/C_SE_TB/C_SE_TC/C_BO_TA)"
(BC-2.19.030 postconditions 1–2; F-180-P3-001).
27/27 story_180 tests pass (248/248 total IEC-104 suite). Evidence
files cover all AC-180-001..008: timed switching commands 58-60
(T1692.001 only), timed set-point/bitstring commands 61-64 (T1692.001
+ T0836), summary wording, cot_test tagging, silence regression guard
for {52-57, 65-99}, narrowed catch-all comment (AC-180-007 source
verification), and count-independence. PG-W70-DEMO-SCRUB gate: PASSED.
@Zious11

Zious11 commented Jul 24, 2026

Copy link
Copy Markdown
Owner Author

Review Cycle 1 Triage

Finding Severity Routed To Status
(none)

Verdict: APPROVE — 0 blocking findings, 0 suggestions, 0 nits.

All gates clear: implementation correct (58..=60 T1692.001 only; 61..=64 T1692.001+T0836), evidence parity with untimed twins confirmed, catch-all comment narrowed to {52–57, 65–99}, 27/27 story_180 tests pass, 248/248 total iec104 suite, clippy/fmt clean, CHANGELOG present, demo evidence 8 ACs PG-W70-DEMO-SCRUB PASSED, security CLEAN (0C/0H/0M), adversarial BC-5.39.001 SATISFIED.

PG-W74-PRDESC-ROW-VERIFY: 4 test entries confirmed in tests/iec104_analyzer_tests.rs (lines 7017, 7155, 7368, 7806); aggregate counts 27/248 cross-checked against actual cargo test output.

covered_sha: ccec171

@Zious11
Zious11 merged commit 421bf57 into develop Jul 24, 2026
26 checks passed
@Zious11
Zious11 deleted the feature/STORY-180-iec104-timed-cmd-detection branch July 24, 2026 18:44
Zious11 added a commit that referenced this pull request Jul 24, 2026
PR #437 feat(iec104): detect timed control command TypeIDs 58–64
squash-merged to develop 2026-07-24T18:44:47Z, human-executed
post-classifier-halt. DF-MERGE-AUTH-CLASSIFIER-001 satisfied
(wave-84 pattern #421/#426/#427/#437).

Changes:
- STATE.md: D-507 appended; frontmatter vp_index_version v2.46→v2.47,
  story_index_version v3.91→v3.92, stories_delivered 116→117,
  develop_head updated to 421bf57; EXACT RESUME POINT, Project
  Metadata, Phase Progress wave-85, Convergence Status, Concurrent
  Cycles, CPS last-5 (D-507 added, D-502 rolled off), Decisions Log
  D-507 added, Active Carry-Forwards IEC104-TIMED-CMD-GAP-001 +
  CV-008 RESOLVED and removed, Session Resume Checkpoint updated.
- stories/STORY-180.md: status ready→delivered (3 loci), version
  1.1→1.2, changelog 1.2 row added, input-hash 8ddf419→e87befe
  (canonical Python; BC-2.19.029 v1.3 + BC-2.19.030 v1.2 changed;
  PG-HASH-HOOK-DIVERGENCE advisory-only).
- stories/STORY-INDEX.md: v3.91→v3.92, STORY-180 row ready→delivered,
  wave-85 delivery row 1/2 DELIVERED.
- specs/verification-properties/VP-INDEX.md: v2.46→v2.47, VP-047
  source_bc += BC-2.19.029/030 (CV-008 RESOLVED).
- stories/sprint-state.yaml: STORY-180 status in-progress→done,
  pr: 437, merge_commit: 421bf57, merged_date: 2026-07-24.

Count-propagation sweep (S-7.02): grep for old counts (v3.91,
v2.46, 116 delivered) across STATE.md, STORY-INDEX.md, VP-INDEX.md,
ARCH-INDEX.md, BC-INDEX.md, prd.md — 0 files with stale strings.
Zious11 added a commit that referenced this pull request Jul 24, 2026
…detection (gate fix) (#439)

# F-W85G-GATE1-001: Update ITI diverse e2e expectations for
timed-command detection

**Gate Fix:** F-W85G-GATE1-001 (wave-85 Gate-1)
**Mode:** gate-fix (fix-pr-delivery profile — same rigor as story PRs
minus stubs/Red Gate)
**Source:** Wave-85 integration gate — iti_diverse test failing on
develop (expected 31, got 66)
**Severity:** MEDIUM — test-expectation + doc alignment; no behavioral
regression

![Tests](https://img.shields.io/badge/tests-4%2F4_e2e-brightgreen)
![CI-note](https://img.shields.io/badge/CI-fixture_gitignored-yellow)

![Security](https://img.shields.io/badge/security-test%2Fdoc_only-brightgreen)

![Changelog](https://img.shields.io/badge/changelog--gate-not_triggered-brightgreen)

STORY-180 (BC-2.19.029/030, PR #437) added detection of timed control
command TypeIDs
58–64. The `iec104-iti-diverse.pcap` machine-local fixture (CC-BY-4.0)
contains 25 such
ASDUs that are now correctly detected (+35 findings). The e2e test
expectations were
written before this capture was exercised against the new arms. This PR
updates the
expectations (31→66), adds a time-tagged guard assertion (==35), aligns
the fixture index
doc (`tests/fixtures/E2E-PCAPS.md`), and removes a duplicated scrub-gate
line in
`docs/demo-evidence/STORY-180/evidence-report.md` (gate finding
O-W85G-P1-001).

---

## Architecture Changes

```mermaid
graph TD
    TestFile["tests/iec104_e2e_real_pcaps_tests.rs\n(expectations updated)"]
    FixtureDoc["tests/fixtures/E2E-PCAPS.md\n(count tables aligned)"]
    DemoEvidence["docs/demo-evidence/STORY-180/evidence-report.md\n(duplicate line removed)"]
    Analyzer["src/analyzers/iec104/\n(STORY-180 timed-command arms — unchanged)"]

    TestFile -.->|tests against| Analyzer
    FixtureDoc -.->|documents| TestFile
    DemoEvidence -.->|STORY-180 evidence| Analyzer

    style TestFile fill:#90EE90
    style FixtureDoc fill:#90EE90
    style DemoEvidence fill:#90EE90
```

No architecture changes. No `src/`, `Cargo.toml`, or `bin/`
modifications. The
IEC-104 analyzer implementation from STORY-180 is correct and unchanged;
only
test expectations and documentation are updated to match the actual
analyzer output
against the local fixture.

---

## Story Dependencies

```mermaid
graph LR
    S180["STORY-180 (BC-2.19.029/030)\nPR #437 MERGED\nTimed TypeIDs 58-64 detection"]
    S181["STORY-181 (SEC-001)\nPR #438 MERGED"]
    GATE1["F-W85G-GATE1-001\nthis PR"]

    S180 --> GATE1
    S181 --> GATE1

    style GATE1 fill:#FFD700
    style S180 fill:#90EE90
    style S181 fill:#90EE90
```

Both upstream PRs are merged to develop. This gate fix has no downstream
dependents
beyond the wave-85 gate closure.

---

## Spec Traceability

```mermaid
flowchart LR
    BC["BC-2.19.029/030\nTimed control command\nTypeIDs 58-64"]
    AC["STORY-180 ACs\n(delivered PR #437)"]
    TEST["test_e2e_BC_2_19_iec104_iti_diverse\n_T0836_T1692_001_mixed_asdu"]
    FIX["F-W85G-GATE1-001\nExpectation alignment"]

    BC --> AC
    AC --> TEST
    TEST --> FIX
```

The gate fix closes the expectation gap: STORY-180 delivered the
detection logic;
the fixture in the machine-local corpus confirmed it works correctly;
this PR updates
the test to assert the correct counts.

---

## Test Evidence

### Coverage Summary

| Metric | Value | Threshold | Status |
|--------|-------|-----------|--------|
| E2E tests (local run) | 4/4 pass | 100% | PASS |
| Regressions | 0 | 0 | PASS |
| src/ changes | none | n/a | N/A — test/doc only |
| Changelog gate | not triggered | test/doc exemption | PASS |

**CI note:** The `iec104-iti-diverse.pcap` fixture is in
`tests/fixtures/local-samples/`
(gitignored — CC-BY-4.0 corpus, absent from the CI runner). The updated
`test_e2e_BC_2_19_iec104_iti_diverse_T0836_T1692_001_mixed_asdu` test
will be skipped by
CI (fixture not present). All other tests, lint, and format checks run
normally in CI.
Evidence of the fix passing comes from the local worktree run (4/4 e2e
pass, full suite
0 failed on the fix branch HEAD
`f12c302922d306577be6656349e46f5423ff2bd3`).

### Per-Test Assertion Table (PG-W74-PRDESC-ROW-VERIFY)

Row-verification performed against
`tests/iec104_e2e_real_pcaps_tests.rs` (worktree HEAD `f12c302`).
Fewer than 3 rows in the meaningful assertion set → all rows verified.

| Assertion (row) | Old value | New value | Source line | Verified |
|----------------|-----------|-----------|-------------|---------|
| R1: `iec104.all_findings.len()` == N | 31 | **66** | line 392 | PASS —
exact match |
| R2: `t0836_count` == N | 10 | **20** | line 417 | PASS — exact match |
| R3: `t1692_001_count` == N | 21 | **46** | line 422 | PASS — exact
match |
| R4: `time_tagged_count` == N (new) | (absent) | **35** | line 437 |
PASS — new assertion at line 436–441 |

**Aggregate-count cross-check (PG-W74-PRDESC-ROW-VERIFY §2):**
- Claimed total: 66 = T0836×20 + T1692.001×46. Cross-check: 20+46=66 ✓.
- Derivation confirmed in commit comment:
untimed (31) + timed (+35: x=15 switching TypeIDs 58-59 + 2y=20 setpoint
TypeIDs 61+63) = 66.
  T0836 delta = y = 10 (10→20); T1692.001 delta = x+y = 25 (21→46). ✓
- Aggregate counts are sourced from the local worktree run, NOT from CI
(fixture absent
in CI — explicitly disclosed). Row-verify confirmed values match source
file.

### Test Flow

```mermaid
graph LR
    E2E["4 E2E tests\n(local fixture corpus)"]
    Unit["Unit tests\n(unchanged)"]

    E2E -->|4/4 PASS local| Pass1["PASS"]
    Unit -->|no changes| Pass2["PASS (CI)"]

    style Pass1 fill:#90EE90
    style Pass2 fill:#90EE90
```

<details>
<summary><strong>Detailed Test Results</strong></summary>

### Changed Assertions

| Assertion | File | Line | Old | New | Result |
|-----------|------|------|-----|-----|--------|
| `all_findings.len()` | `tests/iec104_e2e_real_pcaps_tests.rs` | 392 |
31 | 66 | PASS |
| `t0836_count` | same | 417 | 10 | 20 | PASS |
| `t1692_001_count` | same | 422 | 21 | 46 | PASS |
| `time_tagged_count` | same | 437 | (new) | 35 | PASS |
| `detail["total_findings"]` | same | 477 | 31 | 66 | PASS |

### New Assertion Added

`time_tagged_count == 35` at lines 431–441 — guards that exactly x+2y=35
findings
carry a "time-tagged" summary string, decomposing the timed contribution
from STORY-180
(TypeIDs 58-59 → T1692.001 only; TypeIDs 61+63 → T1692.001 + T0836
each).

### Doc Changes

| File | Change | Reason |
|------|--------|--------|
| `tests/fixtures/E2E-PCAPS.md` | iti-diverse row: 31→66, T0836 10→20,
T1692.001 21→46 | Align fixture index with actual analyzer output |
| `docs/demo-evidence/STORY-180/evidence-report.md` | Remove 1 duplicate
scrub-gate line | Gate finding O-W85G-P1-001 |

</details>

---

## Demo Evidence

N/A — this gate fix makes no user-observable behavioral changes. The
IEC-104 analyzer
output is unchanged; only test expectations and documentation are
aligned to the actual
analyzer output against the `iec104-iti-diverse.pcap` local fixture.

STORY-180 demo evidence (which covers the timed-command detection
behavior) is available
at `docs/demo-evidence/STORY-180/evidence-report.md` (PR #437).

| AC | Demo Required | Reason |
|----|---------------|--------|
| F-W85G-GATE1-001 test alignment | No | Transparent expectation update;
no behavioral change |
| O-W85G-P1-001 duplicate line removal | No | Doc cleanup, no behavior |

---

## Holdout Evaluation

N/A — gate fix is a test-expectation alignment, not a behavioral change.
Evaluated at wave gate (wave-85). STORY-180 holdout was completed at PR
#437.

---

## Adversarial Review

N/A — per-story adversarial does not apply to fix-pr-delivery profile.
Wave-level adversarial covers this fix as part of wave-85 gate closure.

---

## Security Review

```mermaid
graph LR
    Critical["Critical: 0"]
    High["High: 0"]
    Medium["Medium: 0"]
    Low["Low: 0"]

    style Critical fill:#90EE90
    style High fill:#90EE90
    style Medium fill:#90EE90
    style Low fill:#90EE90
```

Diff scope: `tests/` and `docs/` only. No `src/`, `Cargo.toml`, or
`bin/` changes.
No new dependencies, no new code paths, no security surface affected.
SAST/cargo audit: N/A for test-expectation update.

---

## Risk Assessment & Deployment

### Blast Radius
- **Systems affected:** Test suite only (local fixture corpus; CI runner
not affected)
- **User impact:** None — no behavioral change in the analyzer
- **Data impact:** None
- **Risk Level:** LOW

### Performance Impact

N/A — test/doc only. No runtime code changes.

<details>
<summary><strong>Rollback Instructions</strong></summary>

**Immediate rollback (< 1 min):**
```bash
git revert f12c302
git push origin develop
```

This would revert test expectations back to 31, re-breaking the wave-85
gate.
Rollback is not expected to be needed — this is a corrective alignment,
not a
speculative change.

</details>

### Feature Flags
None — test/doc only.

---

## Traceability

| Gate Finding | Source | Fix | Test | Status |
|-------------|--------|-----|------|--------|
| Gate-1: iti_diverse expected 31 got 66 | Wave-85 integration gate |
Update assertions 31→66 |
`test_e2e_BC_2_19_iec104_iti_diverse_T0836_T1692_001_mixed_asdu` | PASS
(local) |
| O-W85G-P1-001: duplicate scrub-gate line | Wave-85 gate review |
Remove duplicate line |
`docs/demo-evidence/STORY-180/evidence-report.md` | PASS |

<details>
<summary><strong>Full VSDD Contract Chain</strong></summary>

```
BC-2.19.029/030 → STORY-180 → PR #437 (MERGED) → timed TypeID 58-64 detection
  → iec104-iti-diverse.pcap (local fixture) → 66 findings (was silently dropping 35)
  → F-W85G-GATE1-001 → test expectations aligned → wave-85 gate-1 unblocked
```

</details>

---

## AI Pipeline Metadata

<details>
<summary><strong>Pipeline Details</strong></summary>

```yaml
ai-generated: true
pipeline-mode: gate-fix (fix-pr-delivery)
factory-version: "1.0.0-rc.23"
pipeline-stages:
  gate-finding: F-W85G-GATE1-001 (wave-85 Gate-1 integration run)
  fix-implementation: completed in worktree .worktrees/FIX-W85G
  stubs-red-gate: N/A (not applicable for gate fixes)
  holdout-evaluation: N/A (evaluated at wave gate)
  adversarial-review: N/A (wave-level adversarial covers gate fixes)
  changelog-gate: not triggered (tests/+docs/ only)
generated-at: "2026-07-24"
models-used:
  pr-manager: claude-sonnet-4-6
worktree: .worktrees/FIX-W85G
branch: fix/w85-gate-iti-e2e-expectations
head-sha: f12c302
```

</details>

---

## Pre-Merge Checklist

- [ ] All CI checks passing (fmt, clippy, unit tests, action-pin-gate,
changelog-gate)
- [x] No src/ changes — changelog-gate does not trigger (test/doc
exemption verified)
- [x] No critical/high security findings (test/doc diff only)
- [x] Rollback procedure documented
- [x] Row-verify performed for all assertion rows
(PG-W74-PRDESC-ROW-VERIFY)
- [x] Aggregate counts cross-checked (66=20+46, derivation confirmed)
- [ ] Human merge authorization (DF-MERGE-AUTH-CLASSIFIER-001 — no
wave-85 grant exists; MERGE-AUTH-HALT required)
@Zious11 Zious11 mentioned this pull request Jul 25, 2026
Zious11 added a commit that referenced this pull request Jul 25, 2026
…85 closed

D-513 SESSION WRAP (2026-07-25). Human /wrap at clean post-v0.13.2 milestone.
Session D-504..D-512 (exhaustive): pre-gate consistency audit (D-504);
story gate (D-505); STORY-180 DELIVERED (D-507, PR #437 421bf57);
STORY-181 DELIVERED (D-509, PR #438 5555495; SEC-001 CLOSED);
gate-fix PR #439 0ab6f52; wave-085 gate CONVERGED+CLOSED (D-511, S-7.02 COMPLETE);
v0.13.2 RELEASED (D-512). No in-flight work. Pipeline PAUSED.

STATE.md: status→paused, pipeline→PAUSED, D-513 frontmatter/ERP/CPS/decisions/checkpoint.
D-508 CPS row rolled off (archived to burst-log + session-checkpoints.md).
D-512 checkpoint archived to cycles/wave-085/session-checkpoints.md.
sidecar-learning.md: D-513 session-end marker appended.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant