chore: release v0.13.2 - #440
Merged
Merged
Conversation
chore: back-merge v0.13.1 main into develop (true-merge)
…BC-2.19.029/030) (#437) # [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_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 ```mermaid 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:#FFE4B5 ``` <details> <summary><strong>Architecture Decision Record</strong></summary> ### 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. </details> --- ## Story Dependencies ```mermaid 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 ``` **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 ```mermaid 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 ``` --- ## 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 ```mermaid 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 ``` | 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 | <details> <summary><strong>Detailed Test Results — STORY-180 (27 tests)</strong></summary> ### 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`) </details> --- ## 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. <details> <summary><strong>Medium-Severity Findings & Resolutions</strong></summary> ### 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`. </details> --- ## Security Review **Result: CLEAN — 0 Critical, 0 High, 0 Medium, 0 Low (new findings introduced by STORY-180).** ```mermaid 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 ``` <details> <summary><strong>Security Scan Details</strong></summary> ### 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. </details> --- ## 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. <details> <summary><strong>Rollback Instructions</strong></summary> **Immediate rollback (< 2 min):** ```bash git revert ccec171 # demo evidence commit (top of stack) git revert 0502c64 # P3 close git revert e40955f # P2 sweep git revert a008703 # P1 remediation git revert d64d660 # CHANGELOG + fmt git revert 18d0a91 # 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 </details> ### 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 | <details> <summary><strong>Full VSDD Contract Chain</strong></summary> ``` 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 ``` </details> --- ## 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 <details> <summary><strong>Pipeline Details</strong></summary> ```yaml 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" ``` </details> --- ## Pre-Merge Checklist - [ ] All CI status checks passing - [x] Coverage delta is positive or neutral (additive story — 27 new tests, 0 regressions) - [x] No critical/high security findings unresolved (0 C/0 H from adversarial; security reviewer TBD) - [x] Rollback procedure validated (see Risk Assessment section) - [x] 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) - [x] PG-W72-BREAKING-HOLDOUT-SWEEP: does NOT apply — additive detection story, not a BREAKING or output-format-change story - [x] CHANGELOG [Unreleased] entry present (AC-158-001 / changelog-gate CI job) - [x] Demo evidence: 8 artifacts × 8 ACs, PG-W70-DEMO-SCRUB PASSED - [x] Adversarial convergence: BC-5.39.001 SATISFIED (DF-CONVERGENCE-BEFORE-MERGE-001) - [x] Dependency STORY-174: PR #409 merged to develop
…spatch (STORY-181, SEC-001) (#438) # [STORY-181] Fix SEC-001 ENIP Unsafe Split-Borrow in on_data: Eliminate *mut EnipFlowState Raw Pointer in PDU Dispatch Loop (Behavior-Preserving Refactor) **Epic:** E-20 — EtherNet/IP ENIP/CIP Analyzer **Mode:** maintenance (behavior-preserving refactor) **Convergence:** CONVERGED after 3 adversarial passes (P1 NITPICK_ONLY / P2 NITPICK_ONLY / P3 CLEAN — BC-5.39.001 satisfied)     This PR eliminates SEC-001 from the wirerust tech-debt register: the `on_data` PDU dispatch loop in `src/analyzer/enip.rs` previously cast `self.flows.get_mut(&flow_key)` to a raw `*mut EnipFlowState` and called `self.process_pdu(unsafe { &mut *flow_ptr }, ...)`, relying on a multi-line SAFETY comment to guarantee that `process_pdu` never accesses `self.flows`. The fix replaces this with a safe take-remove-reinsert pattern: `self.flows.remove(&flow_key)` produces an owned local flow, `process_pdu(&mut self, &mut flow, ...)` receives a structurally disjoint reference, and `self.flows.insert(flow_key, flow)` re-inserts after the loop. The compiler now enforces the disjointness invariant — no `unsafe` block, no raw-pointer cast, no `#[allow(clippy::ptr_as_ptr)]` remain in `on_data`. The PR also fixes the ROUTE-W74 OBS-1 residual: the `parse_line()` docstring in `bin/validate-citations` now documents the regex-mismatch `None` return path (AC-181-004). All 2667 tests pass unchanged; behavior is identical to pre-refactor. --- ## Architecture Changes ```mermaid graph TD EnipAnalyzer["EnipAnalyzer\n(src/analyzer/enip.rs)"] OnData["on_data()\n[PDU dispatch loop]"] ProcessPdu["process_pdu()\n[per-PDU analysis]"] FlowsMap["self.flows\nHashMap<FlowKey, EnipFlowState>"] EnipAnalyzer --> OnData OnData -->|"BEFORE: unsafe *mut raw ptr\n(aliased self.flows)"| ProcessPdu OnData -->|"AFTER: safe remove→local→insert\n(compiler-enforced disjoint)"| ProcessPdu OnData -->|"remove() / insert()"| FlowsMap ProcessPdu -.->|"NEVER accesses\n(now structurally enforced)"| FlowsMap style OnData fill:#90EE90 ``` <details> <summary><strong>Architecture Decision Record</strong></summary> ### ADR: SEC-001 Take-Remove-Reinsert Pattern for EnipFlowState Split-Borrow **Context:** `on_data` needed simultaneous access to `self` (for `process_pdu(&mut self, ...)`) and to an element of `self.flows` (an `&mut EnipFlowState`). The prior approach used a raw `*mut` pointer to sidestep the borrow checker, relying on a documented invariant that `process_pdu` never accesses `self.flows`. This invariant was verified-by-inspection, not structurally enforced. **Decision:** Replace the raw pointer with a take-remove-reinsert pattern: `flows.remove()` → process loop with local owned value → `flows.insert()`. **Rationale:** The `remove`/`insert` approach gives the compiler full visibility into borrow lifetimes. The flow is absent from `self.flows` during the dispatch loop, so any future `process_pdu` change that accessed `self.flows` would be caught at compile time rather than remaining a latent soundness risk. The behavioral contract (carry buffer accumulation, direction isolation) is preserved because `process_pdu` only mutates fields other than `self.flows`. **Alternatives Considered:** 1. Refactor `process_pdu` to not require `&mut self` — rejected because it would require significant signature changes and introduce a separate borrow splitting complexity. 2. Keep the unsafe block but add more tests — rejected because tests cannot enforce the aliasing invariant; only structural safety can. **Consequences:** - Zero unsafe blocks remain in `src/analyzer/enip.rs` (SEC-001 closed). - The invariant "process_pdu does NOT access self.flows" is now compiler-enforced rather than convention-based — future maintainers get a compile error if this changes. - EC-002 (empty pdu_queue): remove+insert is a no-op that preserves flow state correctly. </details> --- ## Story Dependencies ```mermaid graph LR STORY181["STORY-181\n✅ this PR"] style STORY181 fill:#FFD700 ``` **depends_on:** `[]` — no blocking predecessors. All prior E-20 stories (including STORY-139 ENIP carry buffer, wave 62) are already merged to develop. No dependency hold. --- ## Spec Traceability ```mermaid flowchart LR BC["BC-2.17.016\nENIP Per-Direction\nCarry Buffer"] AC1["AC-181-001\nunsafe eliminated"] AC2["AC-181-002\nbehavior identical"] AC3["AC-181-003\nno API change"] AC4["AC-181-004\nbin docstring"] T1["test_carry_buffer_partial_header\n(regression guard)"] T2["test_carry_buffer_two_frames_one_segment\n(regression guard)"] T3["test_ec_x1_cross_direction_no_splice\n(regression guard)"] S1["src/analyzer/enip.rs\non_data() lines 978–1001"] S2["bin/validate-citations\nparse_line() docstring"] BC --> AC1 BC --> AC2 BC --> AC3 AC2 --> T1 AC2 --> T2 AC2 --> T3 T1 --> S1 T2 --> S1 T3 --> S1 AC4 --> S2 ``` --- ## Test Evidence ### Coverage Summary | Metric | Value | Threshold | Status | |--------|-------|-----------|--------| | Full cargo test --all-targets | 2667 / 2667 pass | 100% | PASS | | ENIP integration suite | 184 / 184 pass | 100% | PASS | | validate-citations tests | 27 / 27 pass | 100% | PASS | | Clippy -D warnings | 0 warnings | 0 | PASS | | Cargo fmt | clean | clean | PASS | | Holdout satisfaction | N/A-BY-DESIGN | N/A | N/A (refactor) | | Mutation kill rate | not run (refactor; behavior-preserving) | advisory | N/A | ### BC-2.17.016 Carry-Path Regression Witnesses (AC-181-002 Mandate) Per AC-181-002, the PR description MUST enumerate at least three existing tests that exercise the carry path. The following three carry-path regression tests confirmed passing at HEAD `0b5ba318`: | Test | Module | Result | |------|--------|--------| | `test_carry_buffer_partial_header` | `frame_walk` | PASS | | `test_carry_buffer_two_frames_one_segment` | `frame_walk` | PASS | | `test_ec_x1_cross_direction_no_splice` | `direction_and_clock` | PASS | **Row-verify (PG-W74-PRDESC-ROW-VERIFY):** These test names are drawn directly from the evidence-report at `docs/demo-evidence/STORY-181/AC-181-002-behavior-identical.md` which confirms each ran and passed. The ENIP test binary confirmed 184/184 at HEAD commit `0b5ba318`. Aggregate count cross-check: `cargo test --all-targets` returned 2667 passing / 0 failed / 5 ignored — consistent with the baseline-identical characterization in the convergence report. ### Test Flow ```mermaid graph LR Unit["2667 Unit/Integration Tests"] ENIP["184 ENIP Suite Tests"] Python["27 Python Tests\n(bin/test_validate_citations.py)"] Clippy["Clippy -D warnings"] Fmt["cargo fmt --check"] Unit -->|"0 failed"| Pass1["PASS"] ENIP -->|"0 failed"| Pass2["PASS"] Python -->|"0 failed"| Pass3["PASS"] Clippy -->|"0 warnings"| Pass4["PASS"] Fmt -->|"clean"| Pass5["PASS"] style Pass1 fill:#90EE90 style Pass2 fill:#90EE90 style Pass3 fill:#90EE90 style Pass4 fill:#90EE90 style Pass5 fill:#90EE90 ``` | Metric | Value | |--------|-------| | **New tests** | 0 added (behavior-preserving refactor; all existing tests serve as regression guard) | | **Total suite** | 2667 tests PASS | | **ENIP suite** | 184 tests PASS | | **Regressions** | 0 | | **Red Gate** | N/A-BY-DESIGN (log: `.factory/cycles/wave-085/STORY-181/implementation/red-gate-log.md`) | <details> <summary><strong>Detailed Test Results — BC-2.17.016 Carry-Path Tests</strong></summary> ### Carry-Path Regression Witnesses | Test | Module | Result | Duration | |------|--------|--------|----------| | `test_carry_buffer_partial_header` | `frame_walk` | PASS | < 1ms | | `test_carry_buffer_two_frames_one_segment` | `frame_walk` | PASS | < 1ms | | `test_ec_x1_cross_direction_no_splice` | `direction_and_clock` | PASS | < 1ms | These three tests cover the BC-2.17.016 postconditions: - Partial-header carry accumulation (c2s and s2c buffers survive the dispatch loop refactor) - Two-frame-per-segment carry drain (multi-PDU dispatch loop iterates correctly over local flow) - Cross-direction isolation (no c2s/s2c carry splice across direction boundary) ### Red Gate Log This story is a behavior-preserving refactor (SEC-001 split-borrow elimination). The TDD Red Gate is N/A-BY-DESIGN: no new behavioral assertions were required (the refactor changes no observable behavior). Baseline at worktree base `421bf572`: 2667 passing / 0 failed / 5 ignored (log commit `e7f76508`). This N/A status was explicitly adjudicated in the implementation plan and is recorded in the red-gate-log above. </details> --- ## Holdout Evaluation | Metric | Value | Notes | |--------|-------|-------| | Result | **N/A — evaluated at wave gate** | Behavior-preserving refactor; no new behavioral surface | This is a maintenance refactor story (SEC-001 tech-debt closure). No new user-facing behavior is introduced. Holdout evaluation applies at the wave gate level, not per-story for behavior-preserving refactors. --- ## Adversarial Review | Pass | Code Tip | Findings | Critical | High | LOW | Status | |------|----------|----------|----------|------|-----|--------| | P1 | e957282 | 2 | 0 | 0 | 2 | SWEPT (294168f) | | P2 | 294168f | 2 | 0 | 0 | 2 | SWEPT (093ff51) | | P3 | 093ff51 | 0 | 0 | 0 | 0 | CLEAN — CONVERGED | **Convergence:** CONVERGED 3/3 (BC-5.39.001 satisfied) — clean streak P1/P2/P3. All findings were LOW severity; zero HIGH or CRITICAL at any pass. Report: `.factory/cycles/wave-085/STORY-181/convergence-report.md` <details> <summary><strong>Adversarial Finding Dispositions</strong></summary> ### F-181-P1-001 (LOW) — False pdu_queue Invariant Comment - **Location:** `src/analyzer/enip.rs` (dispatch-phase inline comment) - **Category:** code-quality (comment precision) - **Problem:** Inline comment over-stated a PDU-queue guarantee that was not fully correct. - **Resolution:** Comment corrected in `294168fa` to accurately reflect the actual invariant. ### F-181-P1-002 (LOW) — Stale process_pdu flow_key Parameter Docstring - **Location:** `src/analyzer/enip.rs` `process_pdu` docstring - **Category:** code-quality (documentation) - **Problem:** Pre-existing stale `flow_key` parameter docstring; adjudicated in-scope. - **Resolution:** Docstring corrected in `294168fa`. ### F-181-P2-001 (LOW) — RULING-137-002 Cross-Ref Missing - **Location:** `src/analyzer/enip.rs` inline comment - **Category:** code-quality (traceability) - **Problem:** Inline comment cited the ruling but omitted the back-reference to the originating architectural decision. - **Resolution:** Cross-reference added in `093ff519`. ### F-181-P2-002 (LOW) — `"line ~1033"` Reference Off by 6 Lines - **Location:** `src/analyzer/enip.rs` inline comment - **Category:** code-quality (precision) - **Problem:** Line reference was 6 lines off after the refactor shifted line numbers. - **Resolution:** Corrected in `093ff519`. ### O-181-P3-001 (theoretical, non-blocking) - **Category:** theoretical-only - **Description:** Panic-unwind flow-drop divergence in a `debug_assert`-only panic path compiled out in release. Explicitly non-blocking; no action required. </details> --- ## 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 ``` **SEC-001 CLOSED:** The primary security finding from PR #334 review (MEDIUM, carried forward to wave-85 as D-493) is resolved by this PR. Zero `unsafe` blocks remain in `src/analyzer/enip.rs`. Adversary-confirmed across all 3 passes. <details> <summary><strong>Security Scan Details</strong></summary> ### Unsafe Block Elimination `grep -n "flow_ptr\|ptr_as_ptr\|\*mut EnipFlowState\|unsafe" src/analyzer/enip.rs` returns **zero matches** at HEAD `0b5ba318`. The four SEC-001 symbols are absent from the file: - `let flow_ptr: *mut EnipFlowState` — removed - `unsafe { &mut *flow_ptr }` — removed - `#[allow(clippy::ptr_as_ptr)]` — removed - The `unsafe` keyword at the former dispatch site — removed ### Dependency Audit No new crate dependencies introduced. `Cargo.toml` unchanged. ### Formal Verification | Property | Method | Status | |----------|--------|--------| | Carry buffer accumulation preserved | BC-2.17.016 test suite (184 tests) | VERIFIED | | Borrow disjointness (structural) | Rust compiler type-checker | VERIFIED at compile time | | `process_pdu` self.flows isolation | Exhaustive grep (×3 adversarial passes) | VERIFIED | </details> --- ## Risk Assessment & Deployment ### Blast Radius - **Systems affected:** `src/analyzer/enip.rs` `on_data` dispatch loop only; no public API changes - **User impact:** None — behavior-preserving refactor; CLI output and finding emission are identical - **Data impact:** None — no storage, no schema changes - **Risk Level:** LOW (behavior-preserving; compiler-enforced correctness improvement) ### Performance Impact | Metric | Before | After | Delta | Status | |--------|--------|-------|-------|--------| | Latency | HashMap::remove + HashMap::insert per dispatch cycle | Same operations, same cost | neutral | OK | | Memory | EnipFlowState stack-local during dispatch | Same; short-lived local | neutral | OK | | Throughput | Identical PDU dispatch rate | Identical | 0 | OK | The `remove` + `insert` operations on a `HashMap` are O(1) amortized — equivalent to the prior `get_mut` + pointer deref. No observable performance difference is expected. <details> <summary><strong>Rollback Instructions</strong></summary> **Immediate rollback (< 2 min):** ```bash git revert 224311a # take-remove-reinsert implementation commit git push origin develop ``` The revert restores the pre-refactor `*mut EnipFlowState` unsafe block. All tests pass in either form — this is a behavior-preserving change. No feature flag; no migration required. **Verification after rollback:** - `cargo test --all-targets` returns 2667 passing / 0 failed - `grep -n "flow_ptr" src/analyzer/enip.rs` shows the raw pointer restored </details> ### Feature Flags None. This is a direct code change with no feature flag. Rollback is via `git revert`. --- ## Traceability | Requirement | Story AC | Test | Verification | Status | |-------------|---------|------|-------------|--------| | BC-2.17.016 carry postconditions preserved | AC-181-002 | `test_carry_buffer_partial_header` | ENIP suite 184/184 | PASS | | BC-2.17.016 carry postconditions preserved | AC-181-002 | `test_carry_buffer_two_frames_one_segment` | ENIP suite 184/184 | PASS | | BC-2.17.016 cross-direction isolation | AC-181-002 | `test_ec_x1_cross_direction_no_splice` | ENIP suite 184/184 | PASS | | SEC-001 unsafe eliminated | AC-181-001 | grep zero-match | compiler type-checker | PASS | | No public API change | AC-181-003 | git diff --stat | Cargo.toml absent from diff | PASS | | ROUTE-W74 OBS-1 docstring | AC-181-004 | `python3 bin/test_validate_citations.py` | 27/27 pass | PASS | <details> <summary><strong>Full VSDD Contract Chain</strong></summary> ``` BC-2.17.016 → AC-181-001 → grep "unsafe" src/analyzer/enip.rs → zero matches → compiler-enforced BC-2.17.016 → AC-181-002 → test_carry_buffer_partial_header → enip.rs on_data carry select → ENIP 184/184 BC-2.17.016 → AC-181-002 → test_carry_buffer_two_frames_one_segment → enip.rs on_data carry select → ENIP 184/184 BC-2.17.016 → AC-181-002 → test_ec_x1_cross_direction_no_splice → enip.rs on_data carry select → ENIP 184/184 BC-2.17.016 → AC-181-003 → git diff --stat → Cargo.toml absent → no API change ROUTE-W74/OBS-1 → AC-181-004 → bin/validate-citations parse_line() → test_validate_citations.py → 27/27 ``` </details> --- ## Demo Evidence Demo evidence at `docs/demo-evidence/STORY-181/` (5 artifacts, scrub PASSED 2026-07-24): | File | AC Coverage | |------|-------------| | `AC-181-001-unsafe-eliminated.md` | AC-181-001: grep zero-match + before/after code excerpt | | `AC-181-002-behavior-identical.md` | AC-181-002: 184/184 ENIP tests + 3 carry-path witnesses + full suite | | `AC-181-003-no-api-change.md` | AC-181-003: process_pdu signature grep + git diff stat | | `AC-181-004-bin-docstring.md` | AC-181-004: docstring excerpt + 27/27 test_validate_citations.py | | `evidence-report.md` | Index — coverage map + scrub gate PASSED | --- ## AI Pipeline Metadata <details> <summary><strong>Pipeline Details</strong></summary> ```yaml ai-generated: true pipeline-mode: maintenance factory-version: "1.0.0-rc.23" pipeline-stages: spec-crystallization: completed (wave-85 story decomposition 2026-07-23) story-decomposition: completed (STORY-181 v1.1, human approval D-505 2026-07-24) tdd-implementation: completed (224311a + 1349135 + e957282; Red Gate N/A-BY-DESIGN) holdout-evaluation: "N/A — evaluated at wave gate (behavior-preserving refactor)" adversarial-review: completed (3 passes, CONVERGED BC-5.39.001) formal-verification: skipped (refactor; compiler type-check is structural verification) convergence: achieved (P1/P2/P3 clean streak) convergence-metrics: adversarial-passes: 3 last-classification: CLEAN clean-streak: "P1/P2/P3 = 3/3" code-tip-at-convergence: "093ff519" open-HIGH-CRIT: 0 models-used: builder: claude-sonnet-4-6 adversary: claude-sonnet-4-6 (step-4.5 adversarial) generated-at: "2026-07-24T22:00:00Z" wave: 85 story-version: "1.1" ``` </details> --- ## Pre-Merge Checklist - [ ] All CI status checks passing - [x] Coverage delta: neutral (2667/0/5 baseline-identical — behavior-preserving) - [x] No critical/high security findings unresolved (SEC-001 CLOSED; 0 HIGH/CRIT adversarial) - [x] Rollback procedure: `git revert 224311a`; all tests pass in both states - [x] No feature flag required (direct code change) - [ ] Human review completed (autonomy level requires human merge authorization — see DF-MERGE-AUTH-CLASSIFIER-001; no wave-85 wave-level grant exists) - [x] Adversarial convergence: CONVERGED 3/3 (BC-5.39.001) - [x] Demo evidence: 5 artifacts per-AC, scrub PASSED - [x] AC-181-002 carry-path witnesses enumerated: test_carry_buffer_partial_header, test_carry_buffer_two_frames_one_segment, test_ec_x1_cross_direction_no_splice - [x] PG-W74-PRDESC-ROW-VERIFY: row-verified 3 carry-path test entries + aggregate count cross-checked - [x] CHANGELOG [Unreleased] entry present (AC-158-001)
…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)
Zious11
added a commit
that referenced
this pull request
Jul 25, 2026
Release PR #440 9601d71 merged to main (human-merged). Tag v0.13.2 (lightweight). GH release 4 assets published 2026-07-25T00:36:46Z. Back-merge PR #441 TRUE-MERGE e8841d7 to develop (human-authorized gh pr merge --merge; ancestry PASS, no DRIFT-BACKMERGE- SQUASH recurrence). Ships wave-85: IEC-104 timed-command detection (TypeIDs 58-64) + SEC-001 ENIP unsafe elimination + gate-fix. Version 0.13.1→0.13.2. CR-004 CHANGELOG trim applied at release cut. STATE.md: release fields updated (released_version v0.13.2, released_at 2026-07-25, release_tag v0.13.2, release_commit 9601d71..., develop_head e8841d7..., cargo_version 0.13.2); EXACT RESUME POINT D-512; Phase Progress v0.13.2 RELEASED row added; Concurrent Cycles wave-085 develop head updated; CPS D-507 rolled off / D-512 on; D-512 Decisions Log entry; Session Resume Checkpoint D-512 replaces D-511; SIZE BUDGET banner corrected to ~315 lines (wc-l). cycles/wave-085/session-checkpoints.md: D-511 checkpoint archived. cycles/wave-085/burst-log.md: D-507 CPS archival + D-512 burst entry. Count-propagation sweep: no numeric BC/VP/story counts changed in this burst. Version string sweep: all v0.13.1 and 0ab6f52 hits are legitimate historical audit-trail entries (prior_released_version, frozen gate SHA, release row). No propagation gaps.
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.
Summary
STORY-180 (PR feat(iec104): detect timed control command TypeIDs 58–64 (STORY-180, BC-2.19.029/030) #437): IEC-104 timed control command detection — TypeIDs 58–64 now emit T1692.001 and T0836 findings, closing the evasion gap (IEC104-TIMED-CMD-GAP-001) where CP56Time2a time-tagged variants of control commands fell silently through the catch-all arm. Two new match arms cover
58..=60(C_SC_TA_1/C_DC_TA_1/C_RC_TA_1, T1692.001 only) and61..=64(C_SE_TA_1/C_SE_TB_1/C_SE_TC_1/C_BO_TA_1, T1692.001 + T0836); catch-all comment narrowed from "52–99" to "{52–57, 65–99}".STORY-181 (PR fix(enip): eliminate unsafe *mut EnipFlowState split-borrow in PDU dispatch (STORY-181, SEC-001) #438) + gate-fix PR test(wave-85): update ITI diverse e2e expectations for timed-command detection (gate fix) #439: ENIP
on_dataunsafe split-borrow eliminated (SEC-001, MEDIUM tech-debt carry-forward since PR fix(enip): per-direction carry isolation + saturating-clock window monotonicity (EC-X1/EC-X2) [STORY-139] #334) — raw*mut EnipFlowStatepointer replaced with safe take-remove-reinsert pattern; compiler enforces disjointness with no convention required. No behavior change; all 2667 tests pass unchanged. PR test(wave-85): update ITI diverse e2e expectations for timed-command detection (gate fix) #439 updated ITI diverse e2e expectations for timed-command detection.Wave gate
Wave-85 gate status: CONVERGED / CLOSED (D-511). All CI checks green on develop tip (
0ab6f52ee3be21687437d29923fadc903ca70387).CI
Pre-release checks run on
release/0.13.2(b33e45f9):cargo test --all-targets: 0 failed across all suitescargo clippy --all-targets -- -D warnings: cleancargo fmt --check: cleancargo build: compiles atwirerust v0.13.2Notes
Do NOT merge via the GitHub UI "Squash and merge" button — use a true merge commit (
--no-ff) per the D-491 precedent to preserve branch history onmain. After merge: tagv0.13.2onmain, create the GitHub Release with 4 binary artifacts (release.yml triggers on tag push), then back-mergemainintodevelopas a true-merge commit.