Releases: Zious11/wirerust
v0.11.0
[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 anUnknowncatch-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-zerogeneral_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-thresholdwithin 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-thresholdwithin 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::IcsExecutionenum 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_headerall-input
safety,classify_enip_commandtotality,is_valid_enip_framebiconditional,
classify_cip_servicetotality. [STORY-130/132, BC-2.17.001–004/007] fuzz_enip_cip_parsecargo-fuzz harness coveringparse_cpf_items,
parse_cip_header,parse_cip_request_path, andparse_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]
- Parses the 24-byte ENIP encapsulation header (all fields, little-endian per ODVA
Changed
-
ENIP session summary wire format cleaned up. The
enip_summaryJSON 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-tenseon 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
inon_datawas 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::ClientToServermaps to the TCP initiator;ServerToClient
maps to the TCP responder), mirroring the Modbus pattern. Findingsource_ipfields
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 undercountingtotal_pdu_count,
flows_analyzed,parse_errors, andcommand_distributionwhenever flows were still
open at capture end. The summary now folds all still-openEnipFlowStateentries 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
(cross-direction carry-buffer contamination). The carry buffer is now split into two
independent fields keyed by direction, eliminating the splice. [STORY-141, PR #336,
BC-2.14.EC-X1] -
Modbus EC-X2:
saturating_subfor clock-backwards window reset.
A non-monotonic timestamp (e.g. packet re-ordering or NTP step) caused the time-delta
computation in the Modbus window-reset path to underflow (wrapping subtraction on an unsigned
value). The subtraction now usessaturating_sub, preventing the underflow and keeping the
window-reset logic correct when clocks move backwards. [STORY-141, PR #336, BC-2.14.EC-X2] -
DNP3 EC-X1: per-direction carry buffer split (
carry_c2s/carry_s2c).
Same cross-direction carry-buffer splice fix applied to the DNP3 analyzer. [STORY-140,
PR #335, BC-2.15.EC-X1] -
DNP3 EC-X2:
saturating_subfor clock-backwards window reset.
Same saturating subtraction fix applied to the DNP3 window-reset path. [STORY-140, PR #335,
BC-2.15.EC-X2] -
DNP3 desync-latch: complete-predicate gated on
frame_count == 0.
The DNP3 desync-latch complete-predicate fired unconditionally, which could produce a spurious
desync event on the very first frame of a session before any real desync had occurred. The
predicate is now gated onframe_count == 0so it only triggers after at least one valid
frame has been observed. [STORY-142, PR #336, BC-2.15.DESYNC] -
ENIP EC-X1: per-direction carry buffer split (
carry_c2s/carry_s2c).
Same cross-direction carry-buffer splice fix applied to the EtherNet/IP analyzer. [STORY-139,
PR #334, BC-2.17.EC-X1] -
ENIP EC-X2:
saturating_subfor clock-backwards window reset.
Same saturating subtraction fix applied to the ENIP window-reset path. [STORY-139, PR #334,
BC-2.17.EC-X2]
v0.10.0
Breaking Changes
-
DNP3 analyzer output: renamed summary key
total_parse_errors→parse_errors.
Thedetailmap produced by the DNP3 analyzer now uses the key"parse_errors"instead of
"total_parse_errors", aligning DNP3 with sibling analyzers (HTTP, TLS, Modbus) that already
use"parse_errors". JSON consumers reading DNP3 summary output must migrate the key name.
[PC-014, BC-2.15.020 v1.4, STORY-108 AC-010]Migration: Replace any lookup of
detail["total_parse_errors"]with
detail["parse_errors"]in your consumer. Forjqusers:
jq '.[] | .detail.total_parse_errors'→jq '.[] | .detail.parse_errors'.
v0.9.4
Added
- Per-finding
mitre_attackJSON array for SIEM consumers (issue #64). Each finding in JSON
output now carries amitre_attackarray. Every element is an object with the fieldsid,
name,tactic_id,tactic_name, andreference, resolved from the static MITRE catalog at
report time. Downstream SIEM ingestion pipelines can consume structured technique metadata
directly without maintaining a separate ID-to-name lookup.
Fixed
- ICS-matrix tactic IDs corrected for ICS techniques. ICS techniques previously emitted
Enterprise-matrix tactic IDs; they now emit the correct ICS-matrix tactic IDs. Three new ICS
tactic variants were added:IcsDiscovery(TA0102),IcsCollection(TA0100), and
IcsCommandAndControl(TA0101). Two technique-to-tactic mappings were corrected:- T0830 Adversary-in-the-Middle reclassified from its previous tactic to Collection
(TA0100). - T0831 Manipulation of Control reclassified from its previous tactic to Impact
(TA0105).
- T0830 Adversary-in-the-Middle reclassified from its previous tactic to Collection
Docs
- Corrected the ARP tactic column in README to reflect the updated ICS-matrix tactic assignments.
- Superseded the stale MITRE mapping design doc; current behavior is authoritative.
v0.9.3
Added
-
pcapng capture-format reader. wirerust now reads pcapng files in addition to classic
pcap. Format is detected by a magic-byte probe on the first four bytes of the file
(pcapng SHB magic0x0A0D0D0A), so pcapng files are accepted regardless of file
extension — including when passing a directory, where the file list is now built by
magic-byte detection rather than by extension filter alone (.pcapngfiles were
previously excluded from directory expansion).The reader parses four block types:
- SHB (Section Header Block) — both big- and little-endian byte orders.
- IDB (Interface Description Block) — up to 65,535 interfaces per file; all
interfaces in a single file must share the same link type. Theif_tsresolIDB
option (code 9) is parsed to determine timestamp resolution; nanosecond captures
(e.g.if_tsresol = 0x09) are converted correctly to microseconds for analysis. - EPB (Enhanced Packet Block) — packet data, interface ID lookup, and per-packet
timestamp reconstruction using the interface'sif_tsresol. - SPB (Simple Packet Block) — parsed and yielded as packets with no timestamp
(SPB carries no timestamp field).
The following block types are silently skipped: NRB (Name Resolution Block), ISB
(Interface Statistics Block), DSB (Decryption Secrets Block), OPB (Obsolete Packet
Block), and any unrecognized block type. Multi-section files (a second SHB) are
rejected — usemergecaporeditcapto re-save as a single-section file.The same five link types supported for classic pcap (Ethernet 1, Raw IP 101, Linux
Cooked/SLL 113, IPv4 228, IPv6 229) are supported for pcapng.A 4 GiB per-file size cap (E-INP-014) is enforced via
fstaton the already-open
file descriptor before the full file is loaded into memory. -
PcapSource::is_pcapngdiscriminant field. ThePcapSourcestruct now carries
a publicis_pcapng: boolfield that istruewhen the file was identified as pcapng
by magic-byte detection. Used internally for the zero-packet notice wording
("pcapng file" vs. "pcap file"). -
Per-file error isolation for batch analysis. When analyzing a directory, a parse
error or read failure on one file is reported to stderr and skipped; remaining files
in the batch continue to be processed. Files that parse successfully but contain zero
packets emit a notice to stderr: "notice: <path>: 0 packets read from <pcap|pcapng>
file", with the OPB-clause appended when the file contained Obsolete Packet Blocks
that were skipped. -
New input-validation error codes (pcapng-specific guards):
Code Condition E-INP-010 pcapng block framing rejection — crate-level framing error (btl misaligned, EOF mid-block, zero-advance forward-progress stall) or EPB interface ID out of range on a non-empty interface table. E-INP-011 Multi-IDB link-type conflict — a subsequent Interface Description Block declares a link type that differs from the first interface's link type. E-INP-012 Second Section Header Block — multi-section pcapng files are not supported. E-INP-013 IDB after first packet block — an Interface Description Block appears after the first EPB or SPB has already been emitted, an ordering not supported by wirerust. E-INP-014 File too large — pcapng file exceeds the 4 GiB in-memory limit; message instructs the user to split the capture or use a streaming tool. E-INP-015 Interface table cap exceeded — pcapng file declares more than 65,535 Interface Description Blocks. (Codes E-INP-008 and E-INP-009 — SHB/IDB/EPB body-too-short and empty interface
table, respectively — were also introduced in this delta as part of the pcapng reader
but do not appear in the above table as they describe internal structural failures
rather than user-actionable input constraints.)
Fixed
-
TCP reassembly CWE-407 null-eviction storm (PR #298). When the flow table reached
max_flowsand a new flow arrived, the eviction loop's break condition (<= max_flows)
fired immediately on the first iteration, causing an O(F log F) sort with zero flows
actually evicted. On captures with frozen or duplicate timestamps — where the
time-based idle expiry never fires — every new flow beyond the cap triggered a full
sort with no eviction, producing quadratic behavior. On a 120,000-flow
frozen-timestamp capture the wall time was ~75 s before this fix.Three mitigations were applied:
- R1 (CWE-401 zombie segments): Segments whose end offset lies strictly below the
reassembly flush cursor are now rejected instead of being inserted into the gap map,
preventing unbounded zombie segment accumulation. - R2 (null-eviction storm fix): The break condition changed from
<= max_flowsto
< max_flows, ensuring at least one flow is evicted on each eviction call. - R3 (batch eviction to headroom):
max_flows-triggered eviction now evicts down
to 90% ofmax_flowsin one call (headroom target =max(1, max_flows * 9 / 10)),
amortizing the O(F log F) sort across the next ~10% of new-flow admissions. The same
120,000-flow frozen-timestamp scenario completes in ~0.76 s after these fixes.
- R1 (CWE-401 zombie segments): Segments whose end offset lies strictly below the
-
R4 packet-index cadence expiry (defense-in-depth for frozen timestamps). A
packet-index sweep runs every N packets (expiry_sweep_interval, configurable) and
expires flows idle for more thanidle_packet_thresholdpackets, independent of
capture timestamps. This ensures idle flows are reclaimed even on captures where all
packet timestamps are identical or otherwise frozen. -
read_magicshort-read race eliminated. The magic-byte probe used by directory
expansion previously calledread()and accepted a short read as a valid result, meaning
a file with exactly 4 bytes might not return all four bytes on a singleread()call.
Changed toread_exact(), which either fills the buffer or returns an error, so files
shorter than 4 bytes correctly returnNoneand files of exactly 4 bytes are read
reliably. -
pcapng block-walk forward-progress guard (CWE-835). The block-walk loop now
checks that the parser advances after each block; a zero-advance result is treated as a
framing anomaly (E-INP-010) rather than looping indefinitely. -
pcapng file-size gate uses
fstaton the open fd (CWE-367 advisory). The size
check now callsmetadata()on the already-open file descriptor rather than a second
path-basedstat()call, closing the TOCTOU window between magic-byte detection and
size enforcement. -
pcapng IDB options TLV parsed with section endianness. The
parse_idb_options
function previously read option TLV fields as fixed little-endian. It now uses the
section endianness (big or little) detected from the SHB byte-order magic, so
if_tsresoland other IDB options are decoded correctly from big-endian pcapng files.
Security
- CWE-407 + CWE-401 mitigated in the TCP reassembly engine (see Fixed — PR #298).
- CWE-835 forward-progress guard added to the pcapng block-walk loop.
- CWE-367 TOCTOU window for pcapng file-size gate closed by switching to
fstaton
the open file descriptor. - Block sequence counter in the pcapng block-walk uses
saturating_addto prevent
wraparound (SEC-005).
v0.9.2
Fixed
-
DNP3
control_operation_countswas non-deterministic across process runs.
Dnp3Analyzer::summarize()previously calledself.flows.values().enumerate()
over aHashMap<FlowKey, Dnp3FlowState>. BecauseHashMapuses a per-process
random seed (HashBrown), the iteration order changed each run, causing the
flow index assigned byenumerate()to map to a different flow on every
invocation. TheBTreeMapkey-sort masked the issue at the key level (keys
"0".."N-1"were always sorted), but the VALUE at each key was
non-deterministic. Runningwirerust analyze <dnp3-capture> --alltwice on the
same file produced differentcontrol_operation_countsoutput (confirmed on a
real 26K-packet DNP3 capture in post-release e2e testing).Fix: derive
Ord+PartialOrdonFlowKey(lexicographic order on
(lower_ip, lower_port, upper_ip, upper_port);IpAddrandu16both
implementOrd). Insummarize(), sortflows.iter()byFlowKeybefore
enumerate(), so index→value assignment is stable across all process runs.
JSON schema is unchanged — keys remain"0".."N-1"strings in a BTreeMap.
Traces to BC-2.15.020 postcondition 1.
v0.9.1
Fixed
--no-collapsehelp text and README referenced non-existent flags
--output json/--output csv. There is no--outputflag in wirerust;
the real flags are--json <FILE>,--csv <FILE>, and
--output-format <fmt>. The doc-comment insrc/cli.rsand the corresponding
line inREADME.mdboth said "Has no effect on --output json or --output csv."
Corrected to "Has no effect on --json, --csv, or --output-format json|csv
output." Behavior is unchanged — JSON and CSV output were already
collapse-invariant; only the help text wording was wrong.
v0.9.0
Changed (BREAKING)
-
TerminalReporterfindings-render mode: two bools →FindingsRenderenum →FindingsRender
struct of two orthogonal enums (STORY-120 PR #266, STORY-122/A PR #268).
This entry supersedes the three-variant enum description that shipped in an earlier 0.9.0
pre-release entry.Phase 1 (STORY-120, PR #266): The
show_mitre_grouping: boolandcollapse_findings: bool
public fields onTerminalReporterwere removed and replaced by a singlerender: FindingsRender
field typed as a three-variant enum (Grouped,FlatCollapsed,FlatExpanded).Phase 2 (STORY-122/A, PR #268):
FindingsRenderwas reshaped from a three-variant enum into
a struct of two orthogonal enums:{ grouping: Grouping, collapse: Collapse }. The
Groupingenum has variantsGroupedandFlat; theCollapseenum has variantsCollapsed
andExpanded. All four combinations are valid. The three named enum variants (Grouped,
FlatCollapsed,FlatExpanded) no longer exist. Per RFC 1105 this is an additional breaking
change: any code that matched or constructed the three-variant enum must migrate to the
two-field struct. The 0.8.x → 0.9.0 minor bump covers both phases.Forward-compatibility (F7-R2):
Grouping,Collapse, andFindingsRender(in
wirerust::reporter::terminal) are now marked#[non_exhaustive], allowing future
variants or fields to be added without a semver-breaking change. BecauseFindingsRender
is#[non_exhaustive], external crates must construct it via the new
FindingsRender::new(grouping, collapse)constructor rather than a struct literal
(struct-literal construction of a#[non_exhaustive]struct is rejected by the compiler
outside the defining crate).
Changed
-
--mitrenow collapses identical findings within each MITRE tactic bucket by default
(STORY-119/B, PR #269). When--mitreis passed,wirerust analyzeroutes output through
the newrender_findings_grouped_collapsedpath, which groups identical findings (same category,
verdict, confidence, summary) within each tactic bucket into a single line with a(xN)count
suffix and up to K=3 representative evidence samples. Singletons render without a count suffix.
Terminal output for--mitreis no longer byte-identical to the pre-0.9.0 grouped output.
JSON and CSV output are unaffected. -
--no-collapseis now dual-scope (STORY-119/B, PR #269). Previously--no-collapse
suppressed collapse only in flat (non---mitre) mode. It now suppresses collapse in both flat
and grouped (--mitre) modes. Passing--no-collapserestores one-line-per-finding output
regardless of whether--mitreis also passed.
v0.8.0
Added
--no-collapseflag forwirerust analyzeto opt out of terminal finding-collapse (closes
#259, STORY-118). Pass--no-collapseto restore the pre-v0.8.0 one-line-per-finding output.
Changed
- Terminal
analyzeoutput now collapses repeated findings by default. Findings that share
the same (category, verdict, confidence, summary) are collapsed into a single line with a
(xN)count suffix and up to 3 representative evidence samples (K=3). This is a
display-layer-only behavioral change: JSON and CSV output are unaffected, and
--mitre-grouped mode is also unchanged (grouped-mode collapse is deferred to a future
release). Pass--no-collapseto disable. Governed by ADR-0003 Display-Layer Aggregation.
v0.7.1
Added
- Regression test coverage for VLAN / QinQ (802.1ad double-tag) / MACsec link-extension ARP
offset handling — 10 tests acrosstests/bc_2_16_qinq_macsec_offset_tests.rsand
tests/bc_2_16_e17_macsec_offset_tests.rs(issue #253, STORY-116/117). Includes an
off-by-8 SCI-accounting guard for MACsec-tagged ARP.
Notes
- No runtime behavior change: the VLAN/QinQ/MACsec offset handling itself shipped in 0.7.0;
this release adds regression guards. MACsec-over-ARP offset correctness is proven by
etherparse source + upstream proptests + synthetic tests and is documented as an
evidence-backed limitation (no public on-wire MACsec+ARP capture exists).
v0.7.0
Added
-
ARP Security Analyzer (issue #9, epic E-16) for link-layer and OT network forensics.
Detects five threat classes with MITRE ATT&CK attribution:- D1 ARP spoofing — binding-conflict detection with MEDIUM→HIGH severity escalation
(configurable--arp-spoof-threshold, default 3 conflicts). Attributed to T0830
Adversary-in-the-Middle and T1557.002 ARP Cache Poisoning. - D2 Gratuitous ARP (GARP) — unsolicited GARP frames flagged as Possible; binding-conflict
GARP (GARP where the announced MAC differs from the established binding) escalated to Likely. - D3 ARP storms — high-rate ARP flood detection (configurable
--arp-storm-rate, default
50 frames/window). Attributed to T0830. - D11 Malformed ARP frames — strict + lax/snaplen-truncated ARP parsing; frames that fail
both passes are flagged as malformed-protocol anomalies. - D12 L2/L3 MAC mismatch — Ethernet source MAC vs. ARP sender hardware address mismatch
detection, flagging potential header spoofing.
New CLI flags:
--arp(enable; also included in-a/--all),--arp-spoof-threshold N,
--arp-storm-rate N. Binding-table LRU cap: 65 536 entries; storm-counter LRU cap: 4 096
entries.Implemented across STORY-111..115 (PRs #236, #238, #239, #240, #241) with formal hardening
in PRs #242–#251. - D1 ARP spoofing — binding-conflict detection with MEDIUM→HIGH severity escalation
Changed
- Migrated the packet decoder from etherparse 0.16 to 0.20 (
DecodedFrame{Ip,Arp}model).
Strict and lax/snaplen-truncated ARP parsing added; VLAN/QinQ/MACsec link-extension offset
handling included. - Bumped chrono 0.4.44 → 0.4.45 (#237).
Verified
- VP-024 ARP parse-safety and binding-cap formally verified: 5 Kani proof harnesses proven
correct, cargo-fuzz 16.2 M executions / 0 crashes, cargo-mutants 98.9 % kill rate on the
ARP delta.