diff --git a/CHANGELOG.md b/CHANGELOG.md index ad4fc844..a3c0dc74 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,8 +9,119 @@ Version numbers follow [Semantic Versioning](https://semver.org/). ## [0.11.0] - 2026-06-29 +### Added + +- **EtherNet/IP (ENIP) + CIP protocol analyzer** — the headline feature of this release + (Feature #316, STORY-130..139, PRs #317–#334, ADR-010). wirerust now analyzes TCP/44818 + flows using the ODVA EtherNet/IP + Common Industrial Protocol (CIP) stack. The analyzer + is enabled with `--enip` (also covered by `--all`) and requires stream reassembly. + + **Protocol coverage:** + + - Parses the 24-byte ENIP encapsulation header (all fields, little-endian per ODVA + specification): command, length, session_handle, status, sender_context, options. + [STORY-130, PR #317, BC-2.17.001/002] + - Classifies all 65,536 possible u16 command values into the 9 ODVA known commands + (ListServices, ListIdentity, ListInterfaces, RegisterSession, UnRegisterSession, + SendRRData, SendUnitData, IndicateStatus, Cancel) plus an `Unknown` catch-all. + [STORY-130, PR #317, BC-2.17.004] + - Parses Common Packet Format (CPF) item lists from `SendRRData` (0x006F) and + `SendUnitData` (0x0070) payloads: bounded item-count walk, type_id recognition for + Null Address (0x0000), Connected Address (0x00A1), Connected Data (0x00B1), and + Unconnected Data (0x00B2) items. CIP service extraction and request-path segment + parse apply to Unconnected Data Items (0x00B2) only in this release. + [STORY-132, PR #319, BC-2.17.005/006/007/009] + - Dispatched as Rule 7 in the `StreamDispatcher` — port-44818 fallback after the + existing TLS, HTTP, Modbus (port 502), and DNP3 (port 20000) rules. Content-signature + rules (TLS record, HTTP prefix) take priority. [STORY-131, PR #318, ADR-010 Decision 1] + - Per-flow state (`EnipFlowState`) with a 600-byte per-direction carry buffer + (`carry_c2s` / `carry_s2c`), frame-walk loop, and session summary folded at + capture end. [STORY-136/137/138, PRs #326–#329, BC-2.17.016/017/021] + + **CLI flags:** + + - `--enip` — enable EtherNet/IP TCP analysis (default-off; included by `--all`) + - `--enip-write-burst-threshold N` — T0836 write-burst threshold: fires when more than + N CIP write-class service requests (SetAttributesAll, SetAttributeList, + SetAttributeSingle) are observed in any 1-second window per flow (default: 50) + - `--enip-error-burst-threshold M` — T0888 error-burst threshold: fires when more than + M CIP error responses (non-zero `general_status`) are observed in any 10-second window + per flow (default: 5; strict `>` semantics) + + **MITRE ATT&CK for ICS detections (ics-attack-19.1):** + + - **T0846 Remote System Discovery** — emitted per flow on the first ENIP ListIdentity + (command 0x0063) frame; one-shot guard per flow. [STORY-134, PR #323, BC-2.17.010] + - **T0888 Remote System Information Discovery** — two detection patterns: + Pattern A: CIP GetAttribute{All,List,Single} request targeting Identity Object + (Class 0x01) in the request path; Pattern B: CIP error-response burst exceeding + `--enip-error-burst-threshold` within a 10-second window. [STORY-134/135, PRs #323/#324, + BC-2.17.014] + - **T0858 Change Operating Mode** — emitted per CIP Stop service (service code 0x07) + request, indicating a controller run-to-stop transition command. [STORY-135, PR #324, + BC-2.17.011] + - **T0816 Device Restart/Shutdown** — emitted per CIP Reset service (service code 0x05) + request. [STORY-135, PR #324, BC-2.17.013] + - **T0836 Modify Parameter** — emitted when CIP write-class services (SetAttributesAll + 0x02, SetAttributeList 0x04, SetAttributeSingle 0x10) exceed + `--enip-write-burst-threshold` within a 1-second window per flow. [STORY-135, PR #324, + BC-2.17.012] + - **T0814 Denial of Service** — malformed-frame anomaly; fires when 3 or more + structurally invalid ENIP frames accumulate in a 300-second window per flow. Shared + technique ID with the DNP3/Modbus analyzers. [STORY-137, PR #327, BC-2.17.018] + + New `MitreTactic::IcsExecution` enum variant added (TA0104) for T0858 "Change Operating + Mode". MITRE catalog grew from 25 to 28 seeded technique IDs; emitted count grew from + 17 to 20 (T0858, T0816, T0846 added to the emitted set). T0846 promoted from + seeded-only to emitted for the first time via ENIP ListIdentity detection. + [STORY-133, PR #320, BC-2.10.008, VP-007] + + **Session summary (`enip_summary`):** `summarize()` produces a 7-key JSON object — + `command_distribution`, `total_pdu_count`, `parse_errors`, `write_count`, + `error_count`, `flows_analyzed`, `dropped_findings` — folding both closed and + still-open flows at call time. [STORY-138, PR #329, BC-2.17.021] + + **Formal verification and quality assurance:** + + - VP-032 Kani proof harnesses Sub-A through Sub-D: `parse_enip_header` all-input + safety, `classify_enip_command` totality, `is_valid_enip_frame` biconditional, + `classify_cip_service` totality. [STORY-130/132, BC-2.17.001–004/007] + - `fuzz_enip_cip_parse` cargo-fuzz harness covering `parse_cpf_items`, + `parse_cip_header`, `parse_cip_request_path`, and `parse_enip_header` — F-P9-002 + obligation discharged. [PR #332] + - Full-pipeline E2E tests against real ENIP/CIP pcaps: holdout scenarios HS-110 + through HS-122 verified (6 test cases, real-world captures). [PR #333] + +### Changed + +- **ENIP session summary wire format cleaned up.** The `enip_summary` JSON output uses + the canonical key name `"parse_errors"` (not `"total_parse_errors"`) from day one, + consistent with the lesson learned from the DNP3 rename in v0.10.0. The summary wire + format was further cleaned up to ensure consistent field ordering and null-safety. + [PR #331, BC-2.17.021 Invariant 1] + +- **Green-doc-tense CI gate added.** A new CI job (`green-doc-tense-gate`) runs + `bin/check-green-doc-tense` on all tracked source and test files, failing if any + doc-comment or changelog entry uses aspirational tense markers ("will", "planned", + "future") in contexts that assert current behavior. The gate includes a self-test + (`bin/test_check_green_doc_tense.py`) that verifies 10 known-bad and 14 known-good + patterns. [PR #321, b9b2e93] + ### Fixed +- **ENIP source-IP attribution corrected.** The per-direction source-IP resolution + in `on_data` was incorrect: it used a port-44818 heuristic that misidentified the + client when the FlowKey's lower port was 44818. Replaced with direction-based + attribution (`Direction::ClientToServer` maps to the TCP initiator; `ServerToClient` + maps to the TCP responder), mirroring the Modbus pattern. Finding `source_ip` fields + now correctly reflect the sending endpoint. [PR #328, AC-139-002] + +- **ENIP `summarize()` includes still-open flows.** `summarize()` previously reported + only counters accumulated from closed flows, silently undercounting `total_pdu_count`, + `flows_analyzed`, `parse_errors`, and `command_distribution` whenever flows were still + open at capture end. The summary now folds all still-open `EnipFlowState` entries into + the aggregate at call time (RULING-W61-001). [PR #330, BC-2.17.021 Postcondition 1] + - **Modbus EC-X1: per-direction carry buffer split (`carry_c2s` / `carry_s2c`).** The Modbus analyzer previously used a single shared carry buffer for both directions, allowing a response packet's trailing bytes to be spliced into the next request's reassembly window