feat(iec104): detect timed control command TypeIDs 58–64 (STORY-180, BC-2.19.029/030) - #437
Merged
Merged
Conversation
…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.
Owner
Author
Review Cycle 1 Triage
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 covered_sha: ccec171 |
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.
Merged
7 tasks
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     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)
Merged
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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
[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)
Adds two new detection arms to
detect_iec104_threatsinsrc/analyzer/iec104.rsto closethe 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 withoutemitting 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<Asdu>| 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<Finding>"] Arm6164 --> Findings style Arm5860 fill:#90EE90 style Arm6164 fill:#90EE90 style CatchAll fill:#FFE4B5Architecture Decision Record
ADR: Match-arm slot order (ADR-013 Decision 3) — slotting timed arms ahead of
_catch-allContext: TypeIDs 58–64 fell through the
_catch-all arm indetect_iec104_threatsbecause 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 arm61..=64(T1692.001 + T0836)between the existing
48..=51arm and the100/101/103arm, 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 — noextra wiring. No new crate dependencies; no
unsafeneeded (immutable borrow only).Alternatives Considered:
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.
finding emission to occur in
detect_iec104_threatsor the effectful shell.Consequences:
_catch-all arm silent range narrows from "52–99" to "{52–57, 65–99}" — existingBC-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:#E0E0E0Dependency 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 --> IEC104Test Evidence
PG-W74-PRDESC-ROW-VERIFY — Row-Verification Record
Row-verified 4 entries from the per-test table below against
tests/iec104_analyzer_tests.rson branchfeature/STORY-180-iec104-timed-cmd-detection:test_BC_2_19_029_type_id_58_emits_t1692_001_only— confirmed at line 7017 ✓test_BC_2_19_029_casdu_first_ioa_evidence— confirmed at line 7155 ✓test_BC_2_19_030_type_id_61_emits_two_findings— confirmed at line 7368 ✓test_BC_2_19_022_v1_1_type_id_52_no_finding— confirmed at line 7806 ✓Aggregate count cross-check:
cargo test story_180output: "27 passed; 0 failed" ✓cargo test --test iec104_analyzer_testsoutput: "248 passed; 0 failed" ✓Coverage Summary
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:#E0E0E0Detailed Test Results — STORY-180 (27 tests)
New Tests (This PR — story_180 module)
test_BC_2_19_029_type_id_58_emits_t1692_001_onlytest_BC_2_19_029_type_id_59_emits_t1692_001_onlytest_BC_2_19_029_type_id_60_emits_t1692_001_onlytest_BC_2_19_029_type_id_58_verdict_confidence_categorytest_BC_2_19_029_casdu_first_ioa_evidencetest_BC_2_19_029_type_id_59_first_ioa_none_no_first_ioa_evidencetest_BC_2_19_029_timed_summary_contains_time_tagged_qualifiertest_BC_2_19_029_timed_summary_differs_from_untimed_twintest_BC_2_19_029_type_id_60_cot_test_suffixtest_BC_2_19_029_type_id_58_count_zero_still_emitstest_BC_2_19_030_type_id_61_emits_two_findingstest_BC_2_19_030_type_id_62_emits_two_findingstest_BC_2_19_030_type_id_63_emits_two_findingstest_BC_2_19_030_type_id_64_emits_two_findingstest_BC_2_19_030_type_id_61_verdict_confidence_category_both_findingstest_BC_2_19_030_type_id_61_casdu_first_ioa_evidence_both_findingstest_BC_2_19_030_type_id_62_first_ioa_none_no_first_ioa_evidencetest_BC_2_19_030_timed_summaries_contain_time_tagged_and_mnemonicstest_BC_2_19_030_timed_summaries_differ_from_untimed_twintest_BC_2_19_030_type_id_64_cot_test_both_findings_taggedtest_BC_2_19_030_type_id_61_count_zero_still_emits_two_findingstest_BC_2_19_022_v1_1_type_id_52_no_findingtest_BC_2_19_022_v1_1_type_id_57_no_findingtest_BC_2_19_022_v1_1_type_id_65_no_findingtest_BC_2_19_022_v1_1_type_id_99_no_findingtest_BC_2_19_019_v1_1_regression_type_id_45_still_one_findingtest_BC_2_19_019_v1_1_regression_type_id_51_still_two_findingsSource:
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
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
src/analyzer/iec104.rs— match-arm inline commentstimed-variant detection arms 58–64.
a0087033.F-180-P1-002 (MEDIUM): CHANGELOG count mismatch
CHANGELOG.md— [Unreleased] entrya0087033.F-180-P1-003 (MEDIUM): stale present-tense RED docstrings (9 sites)
tests/iec104_analyzer_tests.rs— 9 doc sitescurrently asserts,is expected to, and similar RED-phasephrasing — the exact class that
bin/check-green-doc-tenseis designed to catch.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:#90EE90Security Scan Details
Analysis Summary
The diff consists of two plain match arms reading immutable
&Asdufields (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
unsafecode, no external crate dependencies.Input Validation:
Asdustruct fields are pre-validated byparse_asduupstream.The new arms perform O(1) pattern matching on a
u8TypeID value.Injection Risks: None. Evidence strings use
format!()with typedu16/u32integer 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
Pre-existing Finding (not introduced by STORY-180)
ADR-013 Decision 7 Compliance
No ICS parsing libraries (
iec60870-5,wireshark,lib60870,nom) introduced.Risk Assessment & Deployment
Blast Radius
src/analyzer/iec104.rs(detect_iec104_threatsfunction only),tests/iec104_analyzer_tests.rs(test file only). No Cargo.toml changes; no newcrate dependencies; no public API surface changes.
No existing findings are modified or removed. No breaking change.
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
Note: The two new match arms add negligible cost — they are O(1) pattern comparisons in
a single-level
matchover au8TypeID.Rollback Instructions
Immediate rollback (< 2 min):
Or revert the squash-merge commit directly after merge.
Verification after rollback:
cargo test --test iec104_analyzer_tests— should return to 221 tests passingcargo test --all-targets— 0 failures expectedFeature Flags
Traceability
test_BC_2_19_029_type_id_58_emits_t1692_001_onlytest_BC_2_19_029_type_id_58_emits_t1692_001_only(count=1)test_BC_2_19_029_casdu_first_ioa_evidencetest_BC_2_19_029_timed_summary_contains_time_tagged_qualifiertest_BC_2_19_029_type_id_60_cot_test_suffixtest_BC_2_19_030_type_id_61_emits_two_findingstest_BC_2_19_030_timed_summaries_contain_time_tagged_and_mnemonicstest_BC_2_19_030_type_id_64_cot_test_both_findings_taggedtest_BC_2_19_022_v1_1_type_id_52_no_findingtest_BC_2_19_029_type_id_58_count_zero_still_emitstest_BC_2_19_030_type_id_61_count_zero_still_emits_two_findingsFull VSDD Contract Chain
Demo Evidence
Demo evidence committed at
ccec1711to feature branchfeature/STORY-180-iec104-timed-cmd-detection.Path:
docs/demo-evidence/STORY-180/(8 artifacts)AC-001-002-typeid-58-60-timed-switching.mdAC-003-typeid-61-64-timed-setpoint.mdAC-004-timed-summary-wording.mdAC-005-cot-test-tagging.mdAC-006-silence-regression-guard.mdAC-007-silent-range-comment.mdAC-008-count-independent-emission.mdevidence-report.mdCoverage: 8 ACs covered × ≥1 artifact each — PG-W70-DEMO-SCRUB gate PASSED.
AI Pipeline Metadata
Pipeline Details
Pre-Merge Checklist