All notable changes to wirerust are documented in this file.
The format follows Keep a Changelog. Version numbers follow Semantic Versioning.
0.13.2 - 2026-07-25
-
ENIP
on_dataPDU dispatch loop: unsafe*mut EnipFlowStatesplit-borrow replaced with safe take-remove-reinsert (STORY-181, SEC-001, wave-85).The PDU dispatch loop in
src/analyzer/enip.rson_datapreviously acquired a raw*mut EnipFlowStatepointer viaself.flows.get_mut(&flow_key)and calledself.process_pdu(unsafe { &mut *flow_ptr }, ...), relying on a multi-line SAFETY comment to guarantee thatprocess_pdunever accessesself.flows. This pattern was sound but fragile — any future change toprocess_pdutouchingself.flowswould silently break soundness.The fix removes the raw pointer entirely. Before the dispatch loop,
self.flows.remove(&flow_key)produces an ownedEnipFlowState;process_pdu(&mut self, &mut flow, ...)is called with this local variable (structurally disjoint fromself.flows); after the loop,self.flows.insert(flow_key, flow)re-inserts the flow. The compiler enforces disjointness — no convention required. Nounsafeblock, no#[allow(clippy::ptr_as_ptr)], no raw-pointer cast remains inon_data. Behavior is identical; all 2667 tests pass unchanged.Resolves SEC-001 from
.factory/tech-debt-register.md(MEDIUM, carry-forward since PR #334).
-
IEC-104 timed control command detection: TypeIDs 58–64 emit T1692.001 and T0836 (STORY-180, BC-2.19.029 + BC-2.19.030, wave-85).
The IEC-104 passive analyzer detects the CP56Time2a time-tagged variants of control command TypeIDs, closing the evasion gap documented in IEC104-TIMED-CMD-GAP-001 where TypeIDs 58–64 fell silently through the
_catch-all arm.Two new match arms in
detect_iec104_threats(src/analyzer/iec104.rs):-
58..=60(C_SC_TA_1 / C_DC_TA_1 / C_RC_TA_1 — timed switching commands): emits one T1692.001 "Unauthorized Message: Command Message" Possible / Medium / Impact finding with CASDU and conditional first_ioa evidence. No T0836 (binary switching control, not parameter writes). Parity with untimed arm 45..=47 (BC-2.19.019); summary wording distinguishes timed from untimed with "time-tagged" qualifier and C_SC_TA/C_DC_TA/C_RC_TA mnemonics (BC-2.19.029). -
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): emits T1692.001 Possible then T0836 "Modify Parameter" Possible, both with CASDU and conditional first_ioa evidence. T0836 is co-emitted because set-point and bitstring TypeIDs modify ICS control parameters. Parity with untimed arm 48..=51 (BC-2.19.019); summaries name C_SE_TA/C_SE_TB/C_SE_TC/C_BO_TA mnemonics (BC-2.19.030).
The catch-all arm comment at
detect_iec104_threatsis narrowed from "52–99" to "{52–57, 65–99}", noting that TypeIDs 58–64 are now handled by BC-2.19.029 and BC-2.19.030 (AC-180-007; BC-2.19.022 v1.1). The existing post-emission[TEST]loop covers the new arms automatically — no extra wiring required (BC-2.19.017 invariant 1).cargo test --test iec104_analyzer_tests: 248 passed (221 prior + 27 new STORY-180 tests). -
0.13.1 - 2026-07-21
-
bin/check-green-doc-tense: four phrase-level stub-era patterns added (STORY-176, AC-176-001, wave-84, PG-GATE-VOCAB-BLINDSPOT).The green-doc-tense gate gains four patterns (26-29) that catch stub-era vocabulary not covered by the original gate:
- Pattern 26
\bskeleton compiles?\b: flags "harness skeleton compiles" / "VP-044 Kani skeleton compiles" -- compile-only harness scaffolding that has no real proof assertions yet. Bare-label forms ("proof skeleton", "VP-024 Sub-D skeleton") and past-tense forms ("skeleton originated") are not matched by specificity. Trailing\bword boundary now also excludes the past-tense form "skeleton compiled" (F-S176P1-002); leading\bexcludes compound-word prefixes such as "exoskeleton" and "microskeleton". - Pattern 27
(exposes|is a|are) compile-only seam(s): flags present-tense assertions that a module or harness exposes compile-only seams. Requires an explicit present-tense verb so "as a compile-only seam" (past-tense narrative) and bare seam idioms ("Test seam accessors", "VP-047 seam") are not matched. - Pattern 28
\b(are|is) (currently) compile-only: flags present-tense predicate claims ("are currently compile-only", "is compile-only at the red-gate boundary"). "was compile-only" and forms without a preceding are/is are not matched. - Pattern 29
until … wired: flags CI-wiring-incomplete prose. Re-narrowed (F-S176P1-001): the originaluntil.*is wiredrequirement was replaced byuntil.*wiredwith a negative lookahead that excludes object pronouns/articles immediately after "wired" (it,the,a,that,this,them). This catches bare "fails until wired" in addition to the original "until … is wired" form, while still excluding past-tense verb-object forms like "wired it" and "wired the handler". - Docstring TOKEN LIST entries 23-25 added (F-S176P1-005): the three patterns added by STORY-174 (AC-174-008) were implemented in the code but never documented in the TOKEN LIST or allowlist notes. Entries 23 (All tests … MUST FAIL with interposed words), 24 (FAIL(S) Red Gate), and 25 (are/is todo!() stub(s)) are now fully documented.
Zero false positives verified across the tracked Rust tree. Self-tested by
bin/test_check_green_doc_tense.py(all known-bad patterns flagged, all known-good allowlist forms not). - Pattern 26
-
.gitignoremutants.out*/glob +bin/test_gitignore_mutants_glob.pyregression guard (STORY-176, AC-176-003, wave-84)..gitignoregainsmutants.out*/under the cargo-mutants section, covering the default cargo-mutants output dirsmutants.out/andmutants.out.j4-invalid/(complements the existingmutants-f6*/glob). A new self-testbin/test_gitignore_mutants_glob.pyasserts both dirs are git-ignored via 2git check-ignoreassertions. The self-test is wired into CI'sbin-selftestjob and was green on merge. -
bin/validate-citations: opt-inpath:line:anchorsymbol-at-line assertion (STORY-166, AC-166-001, wave-84/wave-75, PG-W75-VALIDATE-CITATIONS-SYMBOL-GAP).The citation grammar gains an optional third
:anchorfield --path:line:anchororpath:line-line:anchor(a range's anchor applies to the start line only). When present, the tool reads the cited line and asserts the anchor token appears -- either as adef/async def/fn/classdeclaration prefix, or (minimal acceptable form) as a bare substring. The anchor isre.escape()'d before matching, so regex-special characters (e.g.arr[0]) are treated literally. On mismatch, the tool exits 1 with a new failure class:SYMBOL NOT AT LINE: path:line (expected anchor '<anchor>', found '<line-text>'), where<line-text>is the cited line's stripped content truncated to 80 characters. Barepath:lineandpath:line-linecitations are unchanged -- fully backward compatible. stdlibreonly, no ctags or other external binary dependency. Self-tested bybin/test_validate_citations.py(five tests, T23–T27, added to the existing 22 tests).
-
src/cli.rsENIP write-burst doc-comment unit format (DOC-005, maint-2026-07-21).The
--enip-write-burst-thresholdarg doc-comment atsrc/cli.rs:259used "within any 1-second window" (hyphenated, spelled out). Changed to "within any 1s window" to match the adjacent Modbus write-burst arg atsrc/cli.rs:185(fixed in maint-2026-07-11 as UNIT-FMT-5-20S-001). No behavior change; doc-comment only. -
bin/check-green-doc-tensepattern leading-\btightening + test coverage + subprocess timeout (wave-84 gate code-review CR-002/CR-005/CR-006/SEC-003).Gate-hardening fixes applied after wave-84 code review: pattern 26 (
\bskeleton compiles?\b) gains a leading word boundary so "exoskeleton compiles" is no longer a false-positive; pattern 28 (\b(?:are|is) … compile-only) gains a leading word boundary for consistency; two new GOOD test cases cover these boundaries (exoskeleton false-positive + "until wired the handler" negative-lookahead exercise);bin/test_gitignore_mutants_glob.py'sgit check-ignorecall gainstimeout=30with a clearAssertionErroron timeout (SEC-003).
-
ROUTE-W74-DEFERRED bin/ tooling housekeeping (STORY-166, AC-166-001(g)). Removed the dead
_run()helper frombin/test_validate_citations.py(superseded by_run_with_real_files(), never called); moved inlineos/stat/tempfileimports from individual test bodies to module-level imports; removed an unnecessaryf-prefix on a placeholder-free string literal intest_T21_directory_target_not_a_file; documentedparse_line()'s regex-mismatchNonereturn path. No behavior change. -
CI:
bin-selfteststep names are count-free (W75 NIT-1, ratified by STORY-166 AC-166-001(g)). Removed the hardcoded(22 tests)/(10 tests)parentheticals from thebin-selftestjob's comment and step names in.github/workflows/ci.yml-- they silently stale as the suites grow. Follows the same count-free step-naming pattern already used by thegreen-doc-tense-gatejob.
0.13.0 - 2026-07-18
IEC 60870-5-104 (IEC-104) passive analyzer: full eight-story feature tree (STORY-167..174) delivering APCI parsing, frame classification, U-frame session state machine, ASDU threat detection, N(S)/N(R) sequence tracking, carry buffers + frame-walk loop, dispatcher integration with --iec104 CLI flag, and four real-world E2E pcap/pcapng fixture captures. Plus four fix stories (FIX-P4-001, FIX-F5-001..004) enriching IEC-104 findings with direction, source_ip, and timestamp JSON keys and correcting demo-evidence accuracy.
-
IEC-104 E2E pcap/pcapng corpus fixtures (STORY-167..174 coverage).
Four IEC 60870-5-104 real-world captures added to the E2E smoke-test corpus (
bin/fetch-e2e-pcaps+tests/fixtures/E2E-PCAPS.md+ pinned expectations intests/e2e_corpus_smoke_tests.rs):iec104.pcap(Wireshark Foundation; 10 KB) — canonical IEC-104 reference: U-frames (STARTDT/STOPDT/TESTFR) + I-frame ASDUs + C_IC general interrogation lifecycle. Analyzer produces 66 findings (T1692.001 ×42, T0836 ×24).iec104-sq.pcapng(Wireshark Foundation; 584 B) — native pcapng; SQ-bit ASDU (sequence-of-information-objects encoding). Exercises the pcapng reader with IEC-104. 0 findings (benign link-management only).iec104-iti-diverse.pcap(ITI/ICS-Security-Tools, CC-BY-4.0; 14 KB) — diverse ASDU Type ID mix. Analyzer produces 31 findings (T1692.001 ×21, T0836 ×10).iec104-iti-dissect.pcap(ITI/ICS-Security-Tools, CC-BY-4.0; 11 KB) — broad Type ID / COT coverage including control commands. Analyzer produces 11 findings (T1692.001 ×9, T0814 ×2).
All four captures parse without panics; zero parse_errors on every run. Corpus grows from 36 to 40 files; smoke test now verifies 39 pinned entries.
Companion analyzer-level e2e test added in
tests/iec104_e2e_real_pcaps_tests.rs(4 tests, modeled onenip_e2e_real_pcaps_tests.rs) pinning per-technique finding counts and category/verdict/confidence distributions as regression guards against the IEC-104 analyzer pipeline (BC-2.19, STORY-167..174).
-
CHANGELOG accuracy corrections: carry-overflow Example 3
mitre_techniquesand FIX-F5-001 emit-site count prose (FIX-F5-004, F-B2 from F5 Round-4 adversarial review).Two inaccuracies corrected (CHANGELOG.md only; no source or test code changed):
-
The FIX-F5-003 entry for Example 3 (carry overflow, pre-enrichment baseline) incorrectly stated
mitre_techniques: []. The corrected demo artifact (docs/demo-evidence/FIX-P4-001/demo-json-serialization.rsline 85) and the real emit site (src/analyzer/iec104.rsline 1214) both carrymitre_techniques: ["T0814"]. Fixed:mitre_techniques: []→mitre_techniques: ["T0814"]. -
The FIX-F5-001 introductory paragraph stated "all 12 Finding constructors — 10 via function parameters and 2 inline". The Emit sites section in the same entry correctly counts 8 function + 2 inline = 10 total (confirmed against iec104.rs). Fixed: "12" → "10" and "10 via function" → "8 via function".
Comprehensive cross-check of all other concrete CHANGELOG claims in Unreleased against ground truth (iec104.rs emit sites, demo artifacts) found no additional mismatches: category/verdict/confidence/MITRE for all three example findings and DNP3/EtherNet/IP direction:None parity claim all confirmed accurate.
-
-
Comprehensive demo-evidence JSON accuracy sweep across IEC-104 feature artifacts (FIX-F5-003).
Corrects fabricated enum variants and misattributed MITRE techniques found across FIX-P4-001 demo artifacts. No source or test code changed; docs and CHANGELOG only.
FIX-P4-001 artifacts corrected (
docs/demo-evidence/FIX-P4-001/):-
demo-json-serialization.rs: All three examples used non-existent Rust enum variants (ThreatCategory::Protocol,Verdict::Anomaly) and incorrect JSON casing (Confidence::High→ serde gives "High" but real serde output is "high"). Additionally, Example 1 attributed T0881 (STOPDT-act) to the N(S) desync finding, which is actually T1692.001 (seetrack_ns_desync, iec104.rs). Fixed to use real variants from real emit sites:- Example 1 (N(S) desync, C2S):
ThreatCategory::Impact,Verdict::Possible,Confidence::Medium,mitre_techniques: ["T1692.001"] - Example 2 (malformed LEN, S2C):
ThreatCategory::Anomaly,Verdict::Possible,Confidence::Medium,mitre_techniques: ["T0814"] - Example 3 (carry overflow, no direction): same category/verdict/confidence as
Example 2;
mitre_techniques: ["T0814"] - Corrected
Directionimport path towirerust::reassembly::handler::Direction.
- Example 1 (N(S) desync, C2S):
-
evidence-report.md: "Before/After" JSON blocks contained the same fabricated "Protocol"/"Anomaly"/"High"/"T0881" values. Replaced with real serde output derived from the actual emit sites: "impact"/"possible"/"medium"/"T1692.001" for the C2S N(S) desync example; "anomaly"/"possible"/"medium"/"T0814" for the S2C malformed-LEN example. -
AC-P4-001-test-results.txt: Inline JSON examples used the same fabricated tokens. Replaced with real field values and annotated with the originating emit function.
CHANGELOG correction: The FIX-F5-002 entry previously claimed it corrected "FIX-F5-001 and FIX-P4-001 evidence artifacts". FIX-F5-002 only corrected FIX-F5-001 artifacts (wrong provenance, fabricated JSON, wrong year). FIX-P4-001 artifacts were not touched by FIX-F5-002 and are corrected here by FIX-F5-003. Entry updated to "FIX-F5-001 evidence artifacts only".
-
-
IEC-104 findings now carry
source_ipandtimestampJSON keys (FIX-F5-001, BC-2.19.011 PC-3).All IEC-104
Findingemit sites previously leftsource_ip: Noneandtimestamp: None, causing those keys to be absent from IEC-104 JSON output. The fix threads the initiator IP (resolved from the 5-tupleFlowKeyby direction, mirroring the DNP3/EtherNet/IP house pattern) and the packet timestamp (tsparameter) through all 10Findingconstructors — 8 via function parameters and 2 inline inon_data.This is an additive, backward-compatible JSON change: the two keys now appear on IEC-104 findings where they were previously absent. JSON consumers that tolerate unknown keys or use subset/contains assertions are unaffected.
Emit sites enriched (8 function + 2 inline = 10 total):
process_u_frame: STOPDT-act T0881 + non-canonical U-frame T0814.detect_iec104_threats: TypeIDs 45–47 T1692.001, TypeIDs 48–51 T1692.001 + T0836, TypeID 105 T0827, TypeIDs 0/128–255 T0814.track_ns_desync: N(S) desync T1692.001.on_datainline: carry-overflow T0814 + malformed-LEN T0814.
Signature changes:
process_u_frame,detect_iec104_threats, andtrack_ns_desynceach gainsource_ip: Option<IpAddr>andtimestamp: Option<chrono::DateTime<chrono::Utc>>parameters (callers updated). -
Documentation accuracy corrections for FIX-F5-001 evidence artifacts only (FIX-F5-002).
Corrects three categories of inaccuracy introduced during demo-evidence authoring; no source or test code is changed:
-
Wrong provenance for sibling source_ip/timestamp enrichment: The FIX-F5-001 evidence report (
docs/demo-evidence/FIX-F5-001/evidence-report.md) incorrectly cited STORY-172 and STORY-173 as the origin of DNP3/EtherNet/IP source_ip enrichment. Those stories implement IEC-104 carry buffers and the IEC-104 dispatcher respectively; the DNP3/EtherNet/IP house pattern for source_ip+timestamp originates from the S-139/S-140 lineage (PR #328). Corrected to cite S-139/S-140 (PR #328) and to note that IEC-104 additionally populatesdirection: Some(direction), which DNP3 and EtherNet/IP do not. -
Fabricated JSON in Before/After block: The evidence-report JSON examples contained incorrect field values (
category: "anomaly",confidence: "high", fabricated summary and evidence strings,direction: "client_to_server"with wrong casing). Replaced with the actual T0881 STOPDT-act finding values fromsrc/analyzer/iec104.rslines 382–396:category: "impact",confidence: "medium", real summary string,evidence: ["CF1=0x13 (STOPDT-act)"],direction: "ClientToServer"(serde default, norename_all). -
Wrong year in example timestamps: Example timestamps using
2025-07-17corrected to2026-07-17.
-
-
IEC-104 findings now carry the
directionJSON key (FIX-P4-001, IEC104-FINDING-DIRECTION-001).All IEC-104
Findingemit sites previously leftdirection: None, causing thedirectionkey to be absent from IEC-104 JSON output (the field uses#[serde(skip_serializing_if = "Option::is_none")]). This is an additive, backward-compatible JSON change — JSON consumers that tolerate unknown keys or use subset/contains assertions are unaffected.The fix brings IEC-104 direction enrichment into conformance with the TLS / Modbus / HTTP analyzers, which already set
direction: Some(direction)on every emitted finding. Note: DNP3 and EtherNet/IP analyzers setdirection: None; IEC-104's direction population therefore exceeds the DNP3/EtherNet/IP baseline (which provides onlysource_ip+timestampparity, not direction).Emit sites fixed (10 total):
process_u_frame: STOPDT-act T0881 finding + non-canonical U-frame T0814 finding.detect_iec104_threats: TypeIDs 45–47 T1692.001, TypeIDs 48–51 T1692.001 + T0836, TypeID 105 T0827, TypeIDs 0/128–255 T0814.track_ns_desync: N(S) desync T1692.001; redundantformat!("direction=…")evidence line dropped (structured field carries the same information).on_datainline: carry-overflow T0814 + malformed-LEN T0814.
Signature changes:
process_u_frameanddetect_iec104_threatseach gain adirection: Directionparameter (callers updated).
-
bin/check-green-doc-tensegreen-doc-tense gate extended with three new IEC-104 phrasings (STORY-174, AC-174-008, PG-REDGREEN-COMMENT-CLEANUP).Adds patterns 23–25 to the
_VIOLATION_PATTERNStoken list to catch stale Red-Gate section headers that slipped through the original gate across STORY-167..173 because the existing patterns required exact token adjacency:- Pattern 23 (
All tests\b.*\bMUST FAIL, case-insensitive): catches module/section headers with interposed qualifiers such as "All tests in this module MUST FAIL" or "All tests in this section MUST FAIL". Subsumes the original pattern 1 for these phrasings. - Pattern 24 (
FAILS?\s+Red Gate, case-insensitive): catches compile-only-seam assertions like "FAILS Red Gate" or "FAIL Red Gate". Past-tense "failed Red Gate" is exempt (the 'ed' suffix prevents the\s+from matching after "fail"). - Pattern 25 (
(?:are|is)\s+todo!\(\)\s+stub, case-insensitive): catches present-tense stub-state assertions like "are todo!() stubs" and "is todo!() stub". Past-tense "were" and provenance "originated as" are exempt.
Three baseline stale headers in
tests/iec104_analyzer_tests.rs(~L662-663, ~L1498, ~L1544) scrubbed to GREEN-accurate prose. Self-test passes at 72/72 cases; tree-wide scan finds 0 violations after the scrub. No new CI job; extends the existinggreen-doc-tense-gate. - Pattern 23 (
-
IEC-104 dispatcher integration:
DispatchTarget::Iec104,--iec104flag, T0881 catalog entry, port 2404 inSUPPORTED_PORTS, andMAX_IEC104_FINDINGScap (STORY-173, wave-82, BC-2.05.012, BC-2.10.010, BC-2.12.025, BC-2.18.003, BC-2.18.004, BC-2.19.028, ADR-013 Decisions 1/9/10).Wires the IEC-104 passive analyzer into the full wirerust pipeline across five subsystems:
-
Dispatcher wiring (SS-05, AC-173-008):
StreamDispatchergainsiec104: Option<Iec104Analyzer>field, a 6-parameternew(),set_iec104_analyzer()setter, andiec104_analyzer()/take_iec104_analyzer()accessors.on_dataIec104 arm routes port-2404 flow data toIec104Analyzer::on_data;on_flow_closeIec104 arm forwards toIec104Analyzer::on_flow_close. Early-exit guard extended with&& self.iec104.is_none()so--iec104-only invocations are not silently dropped (ADR-013 Decision 9 steps 4–5). -
MITRE catalog — T0881 six-part atomic (SS-10, AC-173-002):
SEEDED_TECHNIQUE_IDSgains"T0881"(28→29 entries);SEEDED_TECHNIQUE_ID_COUNTbumped to 29;EMITTED_IDSupdated;technique_info("T0881")arm returns("Service Stop", MitreTactic::IcsInhibitResponseFunction)(TA0107);vp007_catalog_drift_guardandverify_all_seeded_ids_resolvepass at count=29 (ADR-013 Decision 10). -
CLI flag (SS-12, AC-173-003):
--iec104boolean flag added toCliArgs;main.rsconstructs and registersIec104Analyzerwhen the flag is present (default-off opt-in model per BC-2.12.025). -
Protocol catalog (SS-18, AC-173-004/005): port 2404 added to
SUPPORTED_PORTS(count 8→9);supported_protocols()count 7→8; VP-041 partition proptest verifies supported_protocols() ∪ unsupported_protocols() partitions KNOWN_PROTOCOLS (disjoint, complete coverage) after port 2404 addition. -
Findings cap (SS-19, AC-173-007 / BC-2.19.028):
const MAX_IEC104_FINDINGS: usize = 10_000added tosrc/analyzer/iec104.rs;Iec104Analyzergainsdropped_findings: u64field; cap enforced at theon_dataextend step by truncatinglocal_findingsto the remaining capacity and accumulating the discarded count intodropped_findings; surfaced insummarize()as detail key"dropped_findings". Mirrors the DNP3/EtherNet/IPMAX_FINDINGSpattern (BC-2.15.022 / BC-2.17.022). Per-flow state continues updating regardless of the cap. -
Real
flows_analyzedcounter (SS-19, STORY-173 LOW#1 / BC-2.19.028 observability):Iec104Analyzergainsflows_analyzed: u64field (initialized 0);on_flow_closeincrements it whenHashMap::removereturnsSome(closed-flow count).summarize()now computesdetail["flows_analyzed"]asself.flows_analyzed + self.flows.len()— closed flows plus still-open flows — replacing the previousself.flows.len()-only value that returned 0 after both flows closed. Mirrors the ENIPflows_analyzedand DNP3closed_flows_countpatterns. -
Real
packets_analyzedcounter (SS-19, STORY-173 LOW#2 / BC-2.19.028 observability):Iec104FlowStategainsframe_count: u64(initialized 0 viaDefault); incremented once per successfulparse_apci_headercall in theon_dataframe-walk loop (valid start-byte + LEN in [4,253] + full frame available; bad-start-byte skips and malformed-LEN stubs are not counted).Iec104Analyzergainstotal_frames_closed: u64;on_flow_closefolds the removed flow'sframe_countinto it.summarize()now returnspackets_analyzed = self.total_frames_closed + Σ open-flow.frame_count— replacing the previousall_findings.len()proxy that returned 0 for finding-free frames (e.g. TESTFR-act). Mirrors the DNP3total_frames_closed+ open-flow sum pattern. -
SEC-001 doc correction (SS-19, STORY-173 SEC-001):
is_valid_iec104_framedoc rewritten to accurately describe it as a standalone pure predicate and VP-047 fuzz seam — not wired as a dispatch gate by design. Its equivalent validation is performed inline in theon_dataframe-walk loop (start-byte check + LEN-range check) per walk-first residual-bound anti-evasion semantics (ADR-013 Decisions 1/2). Module-doc updated to match.
-
-
IEC-104 carry buffers + frame-walk loop + flow lifecycle (STORY-172, wave-81, BC-2.19.025–027, ADR-013 Decision 3).
Implements the outer processing infrastructure for the IEC-104 passive analyzer in
src/analyzer/iec104.rs:-
Iec104Analyzer::on_data(flow_key, data, ts, direction): effectful shell that prepends the directional carry buffer to the delivery, walks the combined buffer processing every complete APCI frame, and stashes any incomplete tail back into the directional carry. WALK-FIRST-RESIDUAL-BOUND carry-overflow guard (F-172-001): the directional carry alone is checked against MAX_IEC104_CARRY_BYTES=255 (not the aggregate carry+delivery); if carry.len() > 255 (adversarial state injection; unreachable from conformant traffic), the carry is cleared and ONE T0814Anomaly/Possible/Mediumemitted on the first overflow per direction via per-direction dedup flagscarry_overflow_reported_c2s/carry_overflow_reported_s2c(BC-2.19.025 v1.3 invariants 4–5; SEC-001-S168 defense-in-depth). The delivery is always walked regardless — no delivery is ever discarded before frame extraction (anti-evasion per F-172-001 and Ptacek/Newsham 1998; BC-2.19.025 invariant 2). Malformed-LEN frames (valid 0x68 + LEN outside [4, 253]) advance 2 bytes and emit ONE T0814 on the first occurrence per direction via per-direction dedup flagsmalformed_len_reported_c2s/malformed_len_reported_s2c(BC-2.19.026 invariant 5; EMIT-WITH-DEDUP). Bad start bytes advance 1 byte with no finding. Complete valid frames are dispatched toprocess_u_frame,parse_asdu+detect_iec104_threats, ortrack_ns_desyncper frame format. VP-047 fuzz target (fuzz_iec104_parser). -
Iec104Analyzer::on_flow_close(flow_key): removes theIec104FlowStateentry from the flow map; carry bytes are silently discarded (dropped with the state); no finding emitted; unknown flow keys are a no-op (BC-2.19.027). -
Iec104FlowStatenow fully wired with all 9 fields:carry_c2s,carry_s2c,session_started,last_ns_c2s,last_ns_s2c,malformed_len_reported_c2s,malformed_len_reported_s2c,carry_overflow_reported_c2s,carry_overflow_reported_s2c. -
VP-045 proptest skeletons
proptest_vp045_direction_isolationandproptest_vp045_independent_run_equivalenceintests/iec104_analyzer_tests.rsverify carry direction isolation (full execution in STORY-174).
-
-
IEC-104 N(S)/N(R) extraction +
Option<u16>first-frame-baseline desync detection (STORY-171, wave-80, BC-2.19.023–024, ADR-013 Decision 6).Implements N(S) sequence-number tracking and desynchronization detection in
src/analyzer/iec104.rs:-
extract_ns(cf1, cf2) -> u16: pure-core free function extracting the 15-bit send sequence number from I-format CF1/CF2 bytes via((cf1 as u16) >> 1) | ((cf2 as u16) << 7)— range [0, 32767] (BC-2.19.023 postcondition 1). -
extract_nr(cf3, cf4) -> u16: pure-core free function extracting the 15-bit receive sequence number from I/S-format CF3/CF4 bytes via the symmetric formula. N(R) is transient — not stored inIec104FlowState(BC-2.19.023 postcondition 4). -
track_ns_desync(state, current_ns, direction) -> Option<Finding>: effectful function implementing the three-pathOption<u16>first-frame guard and k=12 window check:- Path A (state
None): setsSome(current_ns)baseline; NO finding unconditionally. Prevents false positives on mid-capture starts where first N(S) is arbitrary (BC-2.19.024 postcondition A; ADR-013 Decision 6 invariant 3). - Path B (state
Some(prev), 15-bit gap ≤ 12): updates state; no finding (BC-2.19.024 postcondition B). - Path C (state
Some(prev), 15-bit gap > 12): updates state and emits T1692.001 "Unauthorized Message: Command Message" withVerdict::Possible,ThreatCategory::Impact— sequence desynchronization or replay injection detected (BC-2.19.024 postcondition C). - Gap uses
current_ns.wrapping_sub(prev) & 0x7FFF— the& 0x7FFFmask is mandatory to collapsewrapping_sub's 2^16 wrap to the 15-bit N(S) range (BC-2.19.024 invariant 1). Direction::ClientToServerselectslast_ns_c2s;Direction::ServerToClientselectslast_ns_s2c— directional fields updated independently (AC-171-007).
- Path A (state
-
-
IEC-104 control command detection:
detect_iec104_threats(STORY-170, wave-79, BC-2.19.017/019–022, ADR-013 Decision 8).Implements TypeID dispatch for the IEC-104 passive analyzer in
src/analyzer/iec104.rs:-
TypeIDs 45–47 (C_SC_NA_1, C_DC_NA_1, C_RC_NA_1 — switching commands): emit T1692.001 "Unauthorized Message: Command Message" with
Verdict::Possible,ThreatCategory::Impact(BC-2.19.019 postcondition 1; invariant 2). -
TypeIDs 48–51 (C_SE_NA_1, C_SE_NB_1, C_SE_NC_1, C_BO_NA_1 — set-point/bitstring writes): emit T1692.001 Possible AND T0836 "Modify Parameter" Possible — two findings per ASDU (BC-2.19.019 postconditions 1–2).
-
TypeID 105 (C_RP_NA_1 — Reset Process Command): emit T0827 "Loss of Control" with
Verdict::Likely(BC-2.19.020; v1.1 correction: Likely, not Possible). -
TypeIDs 100, 101, 103 (C_IC_NA_1, C_CI_NA_1, C_CS_NA_1 — interrogation/clock-sync): no finding emitted — benign administrative commands (BC-2.19.021 postcondition 1).
-
TypeID=0 or TypeID in [128, 255] (undefined/private-use/reserved): emit T0814 "Denial of Service" with
Verdict::Possible,ThreatCategory::Anomaly(BC-2.19.022 postcondition 1). TypeIDs in [1, 127] not in any detection set are silently logged with no finding (BC-2.19.022 invariant 1). -
cot_test=truetagging: whenasdu.cot_test == true,[TEST]is appended to every emitted finding'ssummaryfield for analyst noise reduction (BC-2.19.017 invariant 1; AC-170-007). -
CASDU and first_ioa target-address context in findings (Pass-1 adversarial remediation, F-170-001; BC-2.19.019 postcondition 3; BC-2.19.020 postcondition 2). Every finding emitted by
detect_iec104_threatsnow includes"CASDU=<value>"as an evidence entry (always present) and"first_ioa=<decimal>"whenasdu.first_ioaisSome— enabling analysts to identify which RTU/IED and IO address was targeted by the control command. Applied to all four finding-emitting arms: TypeIDs 45–47 (T1692.001), 48–51 (T1692.001 + T0836, both findings), 105 (T0827), and 0/128–255 (T0814). The[TEST]tagging path operates onsummaryonly and is unaffected by the evidence additions.
45 new tests in
tests/iec104_analyzer_tests.rs(modstory_170); combined IEC-104 suite is now 136 tests (story_167: 30, story_168: 34, story_169: 27, story_170: 45). Pass-1 adversarial additions (F-170-001):test_F_170_001_casdu_appears_in_finding_evidence_for_control_type,test_F_170_001_first_ioa_appears_in_finding_evidence_when_some,test_BC_2_19_017_start_idx_guard_preexisting_finding_not_tagged. -
-
IEC-104 ASDU DUI header extraction:
parse_asdu+Asdustruct (STORY-169, wave-78, BC-2.19.015–018, ADR-013 Decision 8).Adds pure-core ASDU header extraction to
src/analyzer/iec104:-
Asdustruct with nine broken-out DUI fields:type_id(u8),sq(bool),count(u8),cot_cause(u8),cot_pn(bool),cot_test(bool),cot_originator(u8),casdu(u16),first_ioa: Option<u32>. No packedvsq: u8orcot: u16fields (ADR-013 Decision 3). -
parse_asdu(asdu_body: &[u8]) -> Option<Asdu>: pure-core free function. ReturnsNonewhenasdu_body.len() < 6(6-byte DUI minimum guard; BC-2.19.015; caller emits T0814). On the accept path, extracts all nine fields: TypeID verbatim from byte 0; SQ and count from VSQ byte 1 (BC-2.19.016); COT cause/P-N/T/originator from bytes 2–3 (BC-2.19.017); CASDU as 16-bit LE from bytes 4–5 (BC-2.19.018).first_ioaisSome(24-bit LE zero-extended to u32)whencount > 0ANDlen >= 9;Noneotherwise (BC-2.19.018). No panic for any input (VP-047 fuzz seam).
-
-
IEC-104 frame format discrimination + U-format session state machine (STORY-168, wave-77, BC-2.19.007–014, ADR-013 Decisions 4/5; T0881/T0814 emission).
Extends
src/analyzer/iec104with pure-core frame classification and an effectful U-frame session state machine:-
classify_frame_format(cf1: u8) -> FrameFormat: pure-core free function; total over all 256 u8 CF1 values; no panic (BC-2.19.007–009; VP-046 proptest; ADR-013 Decision 4). Classifies by low 2 bits of CF1: bit 0 = 0 → IFormat, bits1:0 = 0b01 → SFormat, bits1:0 = 0b11 → UFormat. -
process_u_frame(state: &mut Iec104FlowState, cf1: u8) -> Option<Finding>: effectful session state machine for STARTDT/STOPDT/TESTFR U-frames (ADR-013 Decision 5; BC-2.19.010–014). Dispatch table:- STARTDT-act (0x07) / STARTDT-con (0x0B):
session_started = true; no finding. Idempotent (BC-2.19.010). - STOPDT-act (0x13): emits T0881 "Service Stop"
Impact/Possible(if session active) orImpact/Likely(if no prior STARTDT — anomalous stop); setssession_started = false(BC-2.19.011/012). - STOPDT-con (0x23):
session_started = false; no finding (ACT-only MVP; BC-2.19.012). - TESTFR-act (0x43) / TESTFR-con (0x83): no finding; session state unchanged (BC-2.19.013).
- Non-canonical U CF1 (any other value with bits1:0 = 0b11): emits T0814
Anomaly/Possible(CVE-2026-1773 fail-closed; BC-2.19.014). Session state NOT advanced.
- STARTDT-act (0x07) / STARTDT-con (0x0B):
-
Iec104FlowState::session_started: boolfield: initializedfalseviaDefault; governs T0881 confidence escalation (BC-2.19.010–012). -
VP-046 proptest skeleton
proptest_vp046_frame_format_totalityexercising all 256 CF1 values (AC-168-009; full proof run in STORY-174).
34 new tests in
tests/iec104_analyzer_tests.rs(mod story_168) covering all BC-2.19.007–014 postconditions, edge cases, and VP-046 proptest. All 64 IEC-104 tests (30 STORY-167 + 34 STORY-168) pass; no pre-existing regressions.Pass-1 adversarial remediation (STORY-168 wave-77): T0881 Likely-path finding (STOPDT-act without prior STARTDT) now includes a distinguishing evidence entry "STOPDT received without prior STARTDT on this flow" (BC-2.19.012 postcondition 3). This makes the cold-start anomaly self-describing without requiring session-timeline correlation by the analyst.
-
-
IEC-104 APCI core parser:
parse_apci_headerpure-core free function + VP-044 Kani skeleton (STORY-167, wave-76, BC-2.19.001–006, ADR-013 Decisions 1/3/8).New
src/analyzer/iec104module implementing the IEC 60870-5-104 (IEC-104) APCI header parser as a pure-core free function with zero external dependencies (ADR-013 Decision 7 licensing constraint — noiec60870-5, Wireshark, or lib60870 code):-
parse_apci_header(data: &[u8]) -> Option<ApciHeader>: returns None for input shorter than 6 bytes (BC-2.19.001), start byte ≠ 0x68 (BC-2.19.002), LEN < 4 (BC-2.19.003), or LEN > 253 (BC-2.19.004); returnsSome(ApciHeader)with CF1–CF4 extracted verbatim from bytes [2..6] for valid input (BC-2.19.005). Overflow-safe:len + 2≤ 255 for all valid LEN values. VP-044 Kani formal verification target (full proof run: STORY-174). -
is_valid_iec104_frame(data: &[u8]) -> bool: lightweight post-classification gate for port-2404-dispatched flows (BC-2.19.006). Returns true iffdata.len() >= 2,data[0] == 0x68, and4 <= data[1] <= 253. Consistent withparse_apci_header: gate-true ∧ data.len() >= 6 ⟹ parse returns Some (BC-2.19.006 invariant 2). -
ApciHeaderstruct (start,len,cf1–cf4; allu8;#[derive(Debug, Clone, PartialEq, Eq)]). -
Iec104ParseErrorerror enum skeleton (extended in STORY-168). -
VP-044 Kani harness skeleton under
#[cfg(kani)](ADR-013 Decision 8; full proof: STORY-174).
30 new tests in
tests/iec104_analyzer_tests.rscovering all BC-2.19.001–006 postconditions, boundary values, and cross-function invariants (no pre-existing test regressions). -
0.12.1 - 2026-07-13
-
bin/validate-citations: mechanical citation preflight validator (STORY-164, AC-164-002, wave-74, PG-W73-CITATION-VALIDATOR; F-S164P1-002/004 + F-S164P2-002/003/004 remediation).New Python 3.10+ stdlib tool that reads a citations table (file argument or stdin) and verifies each
path:LINE/path:LINE-LINEanchor against the filesystem. Exits 0 when all citations are valid, 1 on any failure (FILE NOT FOUND / INVALID LINE / INVALID RANGE / LINE OUT OF RANGE / MALFORMED / NOT A FILE / OUTSIDE REPO / UNREADABLE), 2 on usage error. Paths are resolved relative to the repo root (WIRERUST_REPO_ROOT or upward walk). Self-tested bybin/test_validate_citations.py(22 tests).F-S164P1-002: non-blank, non-comment lines that do not match the citation regex are now reported as
MALFORMED: <line>and cause exit 1 rather than being silently skipped (false PASS). F-S164P1-004: line numbers less than 1 (e.g.file.md:0,file.md:0-5) are now rejected asINVALID LINErather than being silently accepted as in-bounds.F-S164P2-002: the
FAIL: K of Ndenominator N now counts every non-blank, non-comment line (valid citations and MALFORMED lines alike), so a malformed- only input correctly reportsFAIL: 1 of 1rather thanFAIL: 1 of 0.F-S164P2-003 (CWE-22): absolute paths and parent-directory escapes are now rejected with
OUTSIDE REPO: <path>. Python's pathlib/operator discards the left side for absolute right-hand values, sorepo_root / '/etc/passwd'silently became/etc/passwd;.resolve()+.is_relative_to()containment catches both absolute and../traversal forms. Parity with the same class identified forbin/compute-input-hashin GitHub #392 (not fixed here; deferred to the #392 issue).F-S164P2-004 / F-S164P3-003: unreadable or non-UTF-8 citations files now produce a documented exit-2 usage error rather than an uncaught traceback.
UnicodeDecodeError(non-UTF-8 bytes) andOSError(PermissionError, IsADirectoryError, etc.) are both caught; each emits a descriptiveError:message to stderr and exits 2. Test T19 covers thechmod 000path, with a skip guard when the process can read mode-0 files (root environments).F-S164P6-001: the stdin branch (
sys.stdin.read()) diverged from the file-argument path — non-UTF-8 bytes on stdin raised an uncaughtUnicodeDecodeErrortraceback and exited 1. Fixed by readingsys.stdin.buffer(raw bytes) and decoding explicitly; the sameUnicodeDecodeErrorcatch now applies, emitting aError: stdin is not valid UTF-8:message and exiting 2. Test T20 covers this path.F-S164P8-001: cited target files were validated for existence but not for being a regular file or being readable. A citation to a directory (e.g.
docs:5) passedexists()then crashed withIsADirectoryErrortraceback incount_lines(). A citation to a chmod-000 file produced aPermissionErrortraceback. Fixed by adding anis_file()check (→NOT A FILE: <path>, exit 1) betweenexists()and the INVALID LINE guards, and wrapping thecount_lines()call intry/except OSError(→UNREADABLE: <path>, exit 1). Tests T21 (directory) and T22 (chmod 000, root-skipped) cover both paths.Addresses PG-W73-CITATION-VALIDATOR: the wave-73 gate adversarial review found CRITICAL-severity fabricated citations in STORY-163's own evidence artifact. This tool provides a mechanical preflight gate so such errors are caught before dispatch rather than by the adversary.
-
**
bin/changelog-gate-check: extracted changelog-gate content assertion (STORY-164, AC-164-003, wave-74, PG-W73-CHANGELOG-GATE-CONTENT; F-S164P1-001- F-S164P2-001 remediation).**
The changelog-gate content-assertion logic is extracted from
.github/workflows/ ci.ymlintobin/changelog-gate-check(a standalone bash script invoked by ci.yml). This enables the gate logic to be directly exercised by behavioral tests.F-S164P1-001 (HIGH): the original inline CONTENT_LINES pipeline lacked
|| trueon its terminal grep, causingset -euo pipefailto kill the CI step before the-eq 0diagnostic branch could run — making the blank-only-touch FAIL path dead code. The fix wraps the grep chain in{ ... || true; }so an empty selection reliably resolves toCONTENT_LINES=0and the diagnostic message prints.bin/test_changelog_gate_content.pygains five behavioral tests (B01–B05) that execute the gate script against crafted diff fixtures (real content, blank-only, header-only, deletions-only, direct-path exec-bit guard) and were confirmed FAIL against the broken logic, PASS after the fix.F-S164P2-001: test B05 added to invoke the script via its direct path (no
bashprefix), verifying the committed git file mode is 100755 and the shebang is valid. ci.yml uses the bare pathbin/changelog-gate-check; a missing exec bit would fail CI with exit 126 while all bash-prefixed tests stayed green. The script was already committed at 100755; B05 is the guard.
-
bin/check-green-doc-tense: extract_find_repo_roothelper + add hermetic main()-guard self-tests (STORY-162, wave-73, F-W72G-P2-OBS-001).The repo-root sentinel walk in
main()is extracted into a standalone_find_repo_root(start: Path) -> Path | Nonehelper that walks upward up to 6 levels looking for a.gitentry (file or directory) or a.factory/subdirectory.main()now delegates to this helper, enabling hermetic monkey- patching in tests without relying on the live.gitor.factory/of the develop checkout.bin/test_check_green_doc_tense.pygains five new hermetic self-tests (AC-162-003 / AC-162-004, F-W72G-P2-OBS-001):- Three
_find_repo_rootunit tests verifying the.factory/OR-sentinel,.gitdirectory sentinel, and.gitfile (worktree) sentinel arms. - One no-sentinel regression guard asserting
_find_repo_rootreturnsNoneor an ancestor outside the temp tree when neither.gitnor.factory/is present. - One precision exit-code test asserting
main()returns exactly1(zero-file guard) rather than2(root-not-found guard) when_collect_rust_filesreturns[]and a repo root is reliably found via a hermetic temp fixture.
Codifies wave-72 process-gap F-W72G-P2-OBS-001 per S-7.02 cycle-close obligation (F-S161P1-001 / VP-INDEX LMR-003 template-conformance exemption on factory side).
- Three
-
maint-2026-07-11 cleanup — CR-001/002/003 + doc drift + dnp3 lint hygiene.
bin/test_check_green_doc_tense.pyAC-158-005 test: add hermetic_find_repo_rootpatch (mirroring AC-162-003 pattern); tighten assertion fromexit_code != 0toexit_code == 1; restore both patched helpers infinally(CR-001).bin/check-green-doc-tense_find_repo_rootdocstring/comment: reword to "at most 6 candidates (start inclusive)" to matchrange(6)behavior (CR-002).bin/test_check_green_doc_tense.pytest (c): replacestr.startswithwithPath.is_relative_to()for correct filesystem-hierarchy containment check (CR-003).- README.md
--arp-storm-rateoption description: add>=directional semantics and calibration note (README-OPTIONS-L117-NEUTRAL-001). - README.md ARP JSON schema note: correct
arp_summarykey claim — ARP counters are flat inanalyzers[i].detail, not nested underarp_summary(PG-W-README-JSON-SCHEMA). - README.md DNP3 threshold-tuning note: add bidirectional-flow assumption and mirror-tap guidance (DNP3-TUNING-BIDIR-001).
docs/adr/0002-modular-protocol-analyzers.md: correct tech-debt item IDPC-023→PC-020forEnipAnalyzerStreamHandlerdeviation (DOC-NEW-001).docs/adr/0001-content-first-stream-dispatch.md: addunclassified_port_countsandcoverage_gaps_enabledfields to theStreamDispatcherstruct snippet (NEW-003).CHANGELOG.mdv0.7.0 D3 ARP-storm entry: add inline errata notingmitre_techniques: []per DF-VALIDATION-001 / BC-2.16.008 Invariant 3 (CHANGELOG-D3-T0830-DRIFT-001).src/cli.rsModbus arg doc-comments: harmonize "1-second window" → "1s window" for consistency with adjacent arg format (UNIT-FMT-5-20S-001).src/analyzer/arp.rsdetect_stormdoc-comment: note integer truncation in rate formula (ARP-RATE-INTDIV-DOC-001).src/analyzer/dnp3.rs: remove 9 spurious#[allow(unused)]attributes from actively-usedpub constitems (PC-NEW-001); add rationale comments to 3 of the 6#[allow(clippy::too_many_arguments)]suppressions — the 3 that lacked them; the remaining 3 carried pre-existing// N args: …rationale (PC-NEW-002).src/analyzer/dnp3.rsDnp3FlowStatedoc-comment: reword stale present-tense "are stubs … contain no logic yet" to past-tense provenance "were stubs through STORY-107 and are fully implemented as of STORY-108/109" (F-P1-001).src/analyzer/arp.rstwo test doc-comments: remove stale "RED GATE: these two new keys are absent from the current summarize() implementation" — both keys are fully implemented and the tests are GREEN (F-P1-001 sibling sweep, DF-GREEN-DOC-TENSE-SWEEP).src/analyzer/arp.rsdoc-comment count sweep: correct five remaining "eleven" → "thirteen" occurrences (module doc,summarize()API doc, section comment); addbindings_evictedandstorm_counters_evictedto thesummarize()key-contract enumeration (F-P2-001, DF-SIBLING-SWEEP-001).
0.12.0 - 2026-07-10
-
verdict,confidence, andcategoryJSON field values aligned to lowercase/snake_case (STORY-160, wave-72, BC-2.11.036, issue #255).BREAKING CHANGE (JSON surface only — v0.12.0). JSON schema changes are outside
cargo-semver-checksscope; this entry and theschema_versionenvelope field (see### Addedbelow) are the authoritative change notices.-
verdict,confidence, andcategoryJSON field values are now lowercase / snake_case (Suricata EVE / ECS / OCSF convention). Full mapping (BC-2.11.036):Enum Variant Pre-v0.12.0 JSON v0.12.0+ JSON VerdictLikely"Likely""likely"VerdictUnlikely"Unlikely""unlikely"VerdictInconclusive"Inconclusive""inconclusive"VerdictPossible"Possible""possible"ConfidenceHigh"High""high"ConfidenceMedium"Medium""medium"ConfidenceLow"Low""low"ThreatCategoryLateralMovement"LateralMovement""lateral_movement"ThreatCategoryCredentialAccess"CredentialAccess""credential_access"ThreatCategoryC2"C2""c2"ThreatCategoryReconnaissance"Reconnaissance""reconnaissance"ThreatCategoryExfiltration"Exfiltration""exfiltration"ThreatCategoryPersistence"Persistence""persistence"ThreatCategoryExecution"Execution""execution"ThreatCategoryAnomaly"Anomaly""anomaly"ThreatCategorySuspicious"Suspicious""suspicious"ThreatCategoryImpact"Impact""impact" -
Terminal Display tokens (
"LIKELY","HIGH") and CSV output are UNCHANGED. Thefmt::Displayimplementations forVerdict,Confidence, andThreatCategoryare not modified;serde::Serializeandfmt::Displayare independent surfaces. -
JSON schema changes are outside
cargo-semver-checksscope. Consumers that pattern-match exact enum string values in JSON output (e.g.,verdict == "Likely",category == "LateralMovement") must update to the new lowercase/snake_case forms. -
Known heterogeneity: The
Directionenum (ClientToServer/ServerToClient) retains PascalCase JSON serialization in v0.12.0. Casing alignment is scoped toverdict,confidence, andcategoryonly (BC-2.11.036 scope carve-out).
-
-
"schema_version": "2"envelope field in every JSON report (STORY-160, BC-2.11.037). Absence of this field signals the pre-v0.12.0 format (implicit schema v1, PascalCase enum values). The value is a JSON string (not an integer) to remain forward-compatible with minor revision suffixes. -
CHANGELOG CI gate,
bin/lint-cycle-artifact, andbin/check-green-doc-tensezero-file-guard hardening (STORY-158, wave-72) [process-gap]. Four wave-71 process gaps codified as durable project artifacts: (1)changelog-gateCI job (pull_request only) fails whensrc/,Cargo.toml, orbin/are modified without a correspondingCHANGELOG.mdupdate, enforcing the CHANGELOG obligation that wave-71 PRs missed (PG-W71-CHANGELOG). (2)bin/lint-cycle-artifact(Python 3, stdlib-only) validates cycle artifact identity fields (story_id:andbcs:frontmatter) against the parent story and on-disk BC files, catching fabricated or borrowed BC IDs before adversarial review (PG-W71-CYCLE-ARTIFACT-IDENTITY). (3)bin/check-green-doc-tensenow exits non-zero when no tracked Rust files are found, preventing a silent false-CI-PASS if the scan target moves (PG-W71-CI-SCAN-GUARDS). (4)trust-boundaryCI job gains atest -d src/guard before the grep scan, mirroring the SEC-001 pattern inhelp-provenance-gate(PG-W71-CI-SCAN-GUARDS). -
Fragmented-handshake Criterion benchmark
tls_fragmented/3-record-carry-drain+[[bench]]target (STORY-149, PR #374, closes #360). A new Criterion benchmark exercises the TLS carry-path under realistic 3-record fragmented-handshake conditions, providing a regression fixture for the restructured carry path. CI-gated bounded-borrow source-inspection tests (tests/bc_149_single_borrow_invariant_tests.rs) verify the single-borrow invariant holds across the restructured code paths.
-
TLS carry-path restructured for single-borrow HashMap access (STORY-149, PR #374).
try_parse_recordsrefactored into three cooperating functions (prepare_record_step,RecordStep,process_handshake_carry) to eliminate a double-borrow on the per-flow carry buffer. Thereassembly/tls.pcapCriterion benchmark recovered −7.88% regression (23.841 µs measured, +2.41% vs May-19 anchor — within the ±5 % recovery target). Zero behavior change: 8-pass adversarial convergence and holdout re-evaluation score 0.920 unchanged. -
TLS handshake drain-loop DRY unification in
process_handshake_carry(STORY-150, PR #379). Singlemsg_bytesextraction and singleparse_tls_message_handshakecall site with direction-guarded dispatch arms replace two duplicated extraction+parse sequences (defense-in-depth refactor). Behavior-preserving: Kani VP-039 3/3 proofs re-verified, zero new mutation survivors. -
Bumped
indicatif0.18.5 → 0.18.6 (Windows dumb-terminal detection fix, indicatif#818, dependabot #386).
-
Absolute host paths scrubbed from 193 committed demo-evidence files (PR #376, F-W70P2-002). All absolute host filesystem paths in
docs/demo-evidence/have been replaced with<REPO-ROOT>and<HOME>placeholder tokens. These are scrub markers — not environment variables — indicating where former machine-specific paths appeared. Seedocs/DEMO-EVIDENCE.mdfor the placeholder convention. -
Factory input-hash tool edge cases: empty inputs and inline comment stripping (STORY-157, PR #380).
bin/compute-input-hash:inputs: [](empty inputs list) now derives hashd41d8cd(MD5 of empty bytes) instead of raising an error; inline# commentsuffixes are stripped from input path entries before file resolution.CLAUDE.mddocuments the canonical-tool/hook divergence (PG-HASH-HOOK-DIVERGENCE), edge cases, and Python 3.10+ floor.
- BC-2.16.016 ARP unbounded-findings coverage (STORY-156, PR #378). Standalone
summarize()no-dropped_findingsregression pin closes the coverage gap for BC-2.16.016 unbounded-findings behavior; docstring anchor corrected. CLI--arplong_helpunbounded-findings documentation coverage pinned.
-
Governance codification: multi-file
proof_file_hashmini-Merkle algorithm (STORY-161, wave-72) [governance]. Multi-fileproof_file_hashmini-Merkle algorithm codified in VP-INDEX v2.39; VP-024 v2.5 proof anchor populated andkani_versionrecorded (factory-artifacts branch).CLAUDE.mdgains "Two Hash Disciplines" note distinguishinginput-hash(MD5-first-7, advisory) fromproof_file_hash(SHA-256 mini-Merkle, integrity anchor). -
Public ADR-012 authored for protocols catalog and coverage-gaps system (STORY-159, wave-72) [doc-drift].
docs/adr/0012-protocols-catalog-and-coverage-gaps.mdcreated, resolving a maintenance-sweep finding (NEW-001, HIGH) that identified 38 lines across six source and test files citing ADR-012 with no corresponding public document. The new ADR covers all ten design decisions from thefeature-protocol-coveragecycle (v0.11.2, PRs #351–#357): hand-curated static array, tri-state Suricata-derived vocabulary, port-detection caveats, catalog scope, supported-set derivation, TCP+UDP dynamic detection (including Decision 6 Clarification on increment-site semantics), category tagging,--coverage-gapsexplicit flag,CoverageGapsSummaryreport section, and UDP gap classification decoupled fromenable_dns. Format follows the ADR-0009 precedent: markdown headings, no YAML frontmatter, all internal factory IDs stripped.CLAUDE.mdProject References table updated to include the new entry. Inline comment attests/integration_tests.rs:1166normalized fromADR-012 Dec 10to the canonicalADR-012 Decision 10form, closing the one abbreviated citation in the codebase. -
Wave-72 integration-gate hardening:
action-pin-gateexistence guard + positive coverage assertion; STORY-159 tape path scrub (F-W72G-P1-001, SEC-W72-001, wave-72). (1)action-pin-gateCI job gains a scan-target existence guard (test -d .github/workflows/+ zero-file check) that mirrors the SEC-001 pattern fromtrust-boundaryandhelp-provenance-gate(PG-W71-CI-SCAN-GUARDS): a renamed or emptied scan target now fails loudly instead of trivially PASSing. A positive-coverage assertion (VALIDATEDcounter) ensures the gate processed at least one remote action ref; the PASS line now reports the validated count (e.g., "PASS: N remote action ref(s) validated, 0 mutable"). (2) Five STORY-159 VHS tape scripts indocs/demo-evidence/STORY-159/had~/Documents/GITHUB/wirerustabsolute host paths in theirType "cd …"lines; scrubbed to<REPO-ROOT>matching the STORY-160 tape convention (SEC-W72-001, CWE-200). Binary.gif/.webmartifacts are historical evidence and not re-rendered. Note: the demo-evidence scrub-gate doc (.factory/maintenance/demo-evidence-scrub-gate.md) needs a~/tilde-expansion pattern extension — that file lives on factory-artifacts and is routed to the orchestrator separately.
0.11.5 - 2026-07-06
-
Three DNP3 observability counters surfacing previously silent analyzer state (PR #370, BC-2.15.016/020/022). Three new monotonic counters expose DNP3 resource-cap events that were previously invisible to operators. All counters are purely additive — new keys appear in
summarize()JSON output only; detection logic, Finding emission, and all behavioral invariants are unchanged.-
dropped_findings(DNP3 summary) — incremented at each of 11MAX_FINDINGScap-check sites when a finding is suppressed.MAX_FINDINGS = 10_000; the counter makes finding-cap pressure observable. (BC-2.15.022) -
master_addrs_dropped(DNP3 summary) — incremented when a new-unique master address is silently ignored because theMAX_MASTER_ADDRS = 64cap is full. Existing-address hits do not increment the counter. (BC-2.15.016 v2.1 PC-6) -
pending_requests_evicted(DNP3 summary) — incremented wheninsert_pending_requestLRU-evicts an entry. Follows theinsert_binding_lru-returns-bool pattern established in PR #366 (ARP). (BC-2.15.016 v2.1 PC-10)
Direct precedent: PR #365 / #366 added the equivalent counter pattern for ARP (
bindings_evicted,storm_counters_evicted), Modbus (dropped_transactions), and HTTP/TLS (dropped_map_entries). The DNP3 counters close the remaining observability gap in the silent-limit audit. -
- Bumped
crossbeam-epoch0.9.18 → 0.9.20 to clear RUSTSEC-2026-0204 (invalid pointer dereference in thefmt::Pointerimplementation ofcrossbeam-epoch0.9.18, fixed in 0.9.20). This is a dev-dependency-only transitive dependency (criterion→crossbeam-epoch) with no exposure in production builds. (PR #371)
-
Doc-drift fixes from maint-2026-07-06 sweep (PR #369). Closes all HIGH and MEDIUM documentation-drift findings from the maintenance sweep:
-
README — added the
protocolssubcommand to the CLI reference section (was absent despite being a live, shipped command since v0.11.2). -
ADR-0001 (stream dispatch) and ADR-0002 (modular analyzers) — added EtherNet/IP (ENIP) to both ADRs, which previously omitted it despite ENIP being a full Rule 7 dispatcher entry since v0.11.0.
src/lib.rspublic module docs updated to match. -
Observability counter documentation — added user-facing documentation for the
--counters/ observability counter surface introduced in v0.11.3.
-
0.11.4 - 2026-07-06
-
Four observability counters surfacing previously silent analyzer state (PR #365, BC-INDEX v2.18). The silent-limit observability audit is now closed. Four counter fields are newly emitted in
summarize()JSON output and terminal output across four analyzers:-
bindings_evicted(ARP summary) — cumulative count of LRU-evicted ARP binding-table entries since analysis start. The ARP analyzer's binding table is capped at 65 536 entries; this counter exposes how many entries have been silently dropped when the cap is reached. ARP summary key count: 11 → 13 (also addsstorm_counters_evicted). -
storm_counters_evicted(ARP summary) — cumulative count of LRU-evicted ARP storm-counter entries since analysis start. The storm-counter LRU table is capped at 4 096 entries; this counter exposes how many entries have been silently dropped. -
dropped_transactions(Modbus summary) — cumulative count of Modbus pending-transaction map entries dropped when the per-flow cap is reached. Previously the cap silently discarded new pending entries; the counter makes the drop event visible in output. Modbus summary key count: 6 → 7. -
dropped_map_entries(TLS summary and HTTP summary) — cumulative count of entries dropped from the TLS SNI/fingerprint maps and the HTTP host/path maps when per-map caps are reached. Previously cap-triggered drops were silent; the counter makes them visible. HTTP summary key count: 9 → 10; TLS summary gains one additional field.
-
- Negative regression tests for eviction/drop no-Finding invariants + HTTP existing-key
AC-008 (PR #366). Test module
bc_silent_resource_caps_testsadds negative regression coverage asserting that ARP binding-table and storm-counter eviction, and Modbus dropped-transaction-map overflows, do not emit spurious Findings — the counters increment but the finding list remains clean. HTTP AC-008 coverage (existing-key update does not create a duplicate map entry) is also covered. Includes a cosmetic refactor of the ARP analyzer'sinsert_binding_lrupath for clarity.
0.11.3 - 2026-07-06
- Fixed unbounded per-flow memory growth in the DNP3 and EtherNet/IP (ENIP) analyzers (issue #342, CWE-401/CWE-770). The stream dispatcher now purges DNP3/ENIP per-flow state on flow close (mirroring Modbus/HTTP/TLS), bounding analyzer memory to live flows; closed-flow state is folded into aggregates so findings/summary output are unchanged. (PR #362)
0.11.2 - 2026-07-05
-
protocolssubcommand — coverage catalog table + JSON output (STORY-152, PR #353). A new top-level subcommandwirerust protocolsprints a formatted table of every protocol in theKNOWN_PROTOCOLScatalog alongside its classification (analyzed, gap, or unclassified) and associated CLI flag. Pass--json [FILE]to emit the catalog as structured JSON suitable for downstream tooling.--csvis explicitly rejected with a clear error. The subcommand is available without any input file. -
analyze --coverage-gapsflag — tri-state CoverageGapsSummary report (STORY-154, PR #355). When--coverage-gapsis passed towirerust analyze, the analysis output includes aCoverageGapsSummarysection that classifies each protocol in the capture into one of three states:covered(traffic observed and an analyzer was enabled),gap(traffic observed but no analyzer enabled), orunclassified(traffic observed for a protocol not inKNOWN_PROTOCOLS). The tri-state report is emitted in both terminal and JSON output and is designed for gap-driven coverage workflows. -
KNOWN_PROTOCOLScatalog + partition functions — SS-18 (STORY-151, PR #351). A new static catalogKNOWN_PROTOCOLSenumerates all protocols wirerust is aware of, together with their default port(s), CLI flag, and category. Partition functions (is_covered,is_gap,is_unclassified) operate over capture traffic against the catalog, forming the data backbone for the protocols subcommand and coverage-gaps report. VP-041 formally verified via Kani. -
Dispatcher unclassified-protocol gap counters for TCP + UDP (STORY-153, PR #352). The
StreamDispatchernow accumulates per-port counters for TCP and UDP traffic that does not match any known protocol rule. These counters feed theunclassifiedbucket in theCoverageGapsSummary, giving operators visibility into novel or undocumented protocols in a capture.
protocols --json=PATHpath argument honored;--csvrejected (PR #354, wave-68 F-W68-01). Theprotocolssubcommand now correctly writes JSON output to the path supplied via--json=PATH(previously ignored, always writing to stdout). Passing--csvtoprotocolsnow returns a clear validation error — CSV output is not defined for the protocols catalog.
- E-21 formal hardening — VP-041/042/043 proven (PR #357). Kani proof harnesses Sub-A through Sub-C verify partition-function totality (VP-041), coverage-gap classification correctness (VP-042), and unclassified-counter monotonicity (VP-043). A cargo-fuzz target and mutation-testing pass cover the new dispatcher counter paths, achieving a 100 % effective kill rate on the E-21 detection delta.
0.11.1 - 2026-07-01
- TLS handshake-message reassembly across TLS records (TLS-CLIENTHELLO-FRAG-001, HIGH). The TLS analyzer previously parsed ClientHello and ServerHello records only when the full handshake message arrived in a single TLS record. A TLS peer that fragments a handshake message across record boundaries (valid per RFC 8446 §5.1 / RFC 5246 §6.2.1) caused wirerust to miss the SNI extension, JA3 fingerprint, and JA3S fingerprint entirely — a trivially exploitable evasion path. The analyzer now maintains a per-direction carry buffer that accumulates record payloads across records until a complete handshake message is available, then parses it. Carry bounds are enforced per-direction: per-message cap 65 536 bytes (the maximum TLS handshake message size), per-record cap 18 432 bytes (maximum TLS record payload). On overflow the carry buffer is cleared and recovery continues from the next record (clear-and-recover policy). Closes silent SNI/JA3/JA3S fingerprint-evasion via fragmented handshakes. [STORY-144 #341, STORY-145 #343, STORY-146 #344, BC-2.07.038–042]
- TLS buffer-saturation telemetry. A new
buffer_saturation_dropscounter is included in the TLS analyzer summary. It increments each time the per-direction carry buffer reaches its cap and is cleared (overflow-and-recover event). Exposes carry-overflow frequency for threat-hunting and capacity tuning without changing the existing wire format of other summary fields. [STORY-146, PR #344, BC-2.07.043]
-
TLS reassembly path formally hardened — Kani VP-039 proof harnesses (3 non-vacuous), a cargo-fuzz target, and 12 mutation-gap tests added for the new reassembly path, closing the formal-verification obligation for the carry-buffer bounds and clear-and-recover semantics. [PR #345]
-
Bumped
anyhow1.0.102 → 1.0.103 to clear RUSTSEC-2026-0190 (advisory against 1.0.102 only; no behavior change). [PR #346]
0.11.0 - 2026-06-29
-
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
Unknowncatch-all. [STORY-130, PR #317, BC-2.17.004] - Parses Common Packet Format (CPF) item lists from
SendRRData(0x006F) andSendUnitData(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]
-
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) runsbin/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]
-
ENIP source-IP attribution corrected. The per-direction source-IP resolution in
on_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;ServerToClientmaps 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]
0.10.0 - 2026-06-24
-
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"]withdetail["parse_errors"]in your consumer. Forjqusers:jq '.[] | .detail.total_parse_errors'→jq '.[] | .detail.parse_errors'.
0.9.4 - 2026-06-23
- 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.
- 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), andIcsCommandAndControl(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).
- 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.
0.9.3 - 2026-06-22
-
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 magic
0x0A0D0D0A), 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. The
if_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's
if_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 — use
mergecaporeditcapto 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.)
-
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.
-
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_optionsfunction 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, soif_tsresoland other IDB options are decoded correctly from big-endian pcapng files.
- 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).
0.9.2 - 2026-06-19
-
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()byFlowKeybeforeenumerate(), 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.
0.9.1 - 2026-06-19
--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.
0.9.0 - 2026-06-19
-
TerminalReporterfindings-render mode: two bools →FindingsRenderenum →FindingsRenderstruct 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: boolpublic fields onTerminalReporterwere removed and replaced by a singlerender: FindingsRenderfield 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 }. TheGroupingenum has variantsGroupedandFlat; theCollapseenum has variantsCollapsedandExpanded. 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(inwirerust::reporter::terminal) are now marked#[non_exhaustive], allowing future variants or fields to be added without a semver-breaking change. BecauseFindingsRenderis#[non_exhaustive], external crates must construct it via the newFindingsRender::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).
-
--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-collapsesuppressed 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.
0.8.0 - 2026-06-17
--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.
- 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 was unchanged in 0.8.0; grouped-mode collapse shipped in 0.9.0. Pass--no-collapseto disable. Governed by ADR-0003 Display-Layer Aggregation.
0.7.1 - 2026-06-17
- Regression test coverage for VLAN / QinQ (802.1ad double-tag) / MACsec link-extension ARP
offset handling — 10 tests across
tests/bc_2_16_qinq_macsec_offset_tests.rsandtests/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.
- 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).
0.7.0 - 2026-06-16
-
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.(Corrected: D3 findings emitmitre_techniques: []— T0814 attribution withheld per DF-VALIDATION-001 / BC-2.16.008 Invariant 3. See v0.7.0 shipping state vs. current behavior.) - 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
(configurable
- 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).
- 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.
0.6.0 - 2026-06-12
-
DNP3 TCP protocol analyzer for ICS/OT network forensics (Feature #8, PRs #219–#231). Analyzes TCP streams on port 20000 per IEEE Std 1815-2012 (DNP3); dispatched as Rule 6 in the stream dispatcher after content-signature rules (TLS record, HTTP prefix) and port rules for TLS, HTTP, and Modbus — it never misclassifies TLS or HTTP traffic (BC-2.15.021 INV-2, ADR-007 Decision 1).
Parses the 10-byte DNP3 data-link layer header: sync bytes, LENGTH, CONTROL, DEST/SRC link addresses (little-endian per IEEE 1815-2012 §8.2). Classifies application-layer function codes into six classes: Read, Write, Control, Restart, Management, Response. Per-flow state with a 292-byte carry-buffer frame-walk handles fragmented TCP delivery and desync detection.
Emits findings mapped to 5 MITRE ATT&CK for ICS techniques:
- T1692.001 Unauthorized Message: Command Message — direct-operate burst (Control-class FCs exceed the per-flow threshold within a 60-second detection window), unexpected master source (Control FC from a source address not in the established master set), and broadcast control command (Control FC to a DNP3 broadcast destination address)
- T1691.001 Block Operational Technology Message: Command Message — Control-class requests that receive no matching RESPONSE (FC 0x81) within 10 seconds contribute to a block-event counter; fires when >= 3 block events accumulate within the 300-second correlation window
- T0827 Loss of Control — fires when the combined count of restart events and block-command events reaches >= 3 within the 300-second correlation window (co-emitted after T0814 or T1691.001)
- T0814 Denial of Service — emitted per cold/warm restart command (FC 0x0D / FC 0x0E), and as a malformed-frame anomaly when >= 3 parse-invalid frames are observed within the 300-second correlation window
- T0836 Modify Parameter — emitted per WRITE command (FC 0x02)
Additional T0814 trigger sources (Inhibit Response Function):
- DISABLE_UNSOLICITED (FC 0x15): verdict Likely / confidence Medium — alarm suppression / event-blinding primitive; emitted per occurrence.
- ENABLE_UNSOLICITED (FC 0x14): verdict Possible / confidence Low — unsolicited reporting control; emitted per occurrence; also sets the per-flow context flag that suppresses the unsolicited-response anomaly.
- Unsolicited-response anomaly: UNSOLICITED_RESPONSE (FC 0x82) arrives on a flow where ENABLE_UNSOLICITED was never observed and no solicited exchange has been seen; verdict Possible / confidence Low; one-shot per flow (T0814).
Bounded-resource design: per-flow state capped at 64 tracked master addresses, 256 pending requests, and 10,000 total findings; 300-second correlation window with six windowed counters reset together (ADR-007 Decision 4).
-
CLI flags for the DNP3 analyzer:
--dnp3— enable DNP3 TCP analysis (also included in-a/--all; default-off, BC-2.15.021)--dnp3-direct-operate-threshold N— per-flow direct-operate burst threshold; fires T1692.001 when Control-class FC count exceeds N within the 60-second detection window (default: 10, BC-2.15.017)
-
Dispatcher Rule 6 — Port-20000 classification added to the stream dispatcher as Rule 6 (STORY-110, ADR-007 Decision 1). Fires after content-signature rules (Rules 1–2) and port rules for TLS/HTTP/Modbus (Rules 3–5), preserving the VP-004 port-precedence invariant.
-
MitreTactic::IcsImpacttactic variant — new variant added to theMitreTacticenum (STORY-109, VP-007 obligation). Maps to the MITRE ATT&CK for ICS "Impact" tactic (TA0105). Used exclusively by T0827 "Loss of Control". Added atomically with the T0827 emission branch and thetechnique_info("T0827")catalog entry. -
T1691.001andT0827catalog entries — two new technique IDs seeded in the static MITRE catalog (technique_info): T1691.001 "Block Operational Technology Message: Command Message" (IcsInhibitResponseFunction) and T0827 "Loss of Control" (IcsImpact). Total catalog size: 23 technique IDs (STORY-109, VP-007). -
Formal verification and quality assurance for the DNP3 analyzer:
- VP-023 (Kani): parse safety sub-properties A–D: all-input range, FC totality, frame-length bounds, carry-buffer progress.
- Fuzz testing:
fuzz_dnp3_parsetarget added (PR #229). - Mutation testing: 100% effective kill rate on the detection core including edge cases for window-seeding (PR #231).
-
T0814 full detection surface documented (DRIFT-DNP3-DOC-T0814-COMPLETENESS-001). The DNP3 T0814 "Denial of Service / Inhibit Response Function" technique is emitted from five trigger sources: cold/warm restart command (FC 0x0D/0x0E; verdict Likely/High), DISABLE_UNSOLICITED (FC 0x15; verdict Likely/Medium), ENABLE_UNSOLICITED (FC 0x14; verdict Possible/Low), unsolicited-response anomaly (FC 0x82 on a flow with no prior ENABLE_UNSOLICITED; verdict Possible/Low), and malformed-frame anomaly (>= 3 parse-invalid frames in the 300s window; verdict Possible/Low). README and CHANGELOG now enumerate all five sources.
0.5.0 - 2026-06-10
- Behavioral change — emitted output: Remapped revoked MITRE ATT&CK-ICS techniques to their
replacement IDs in the pinned ics-attack-19.1 catalog (issue #222):
T0855"Unauthorized Command Message" →T1692.001"Unauthorized Message: Command Message" (ICS sub-technique under parent T1692 "Unauthorized Message"). Behavioral change: Modbus findings now emitT1692.001instead ofT0855in themitre_techniquesfield of all JSON, terminal, and CSV output. Tactic (IcsImpairProcessControl) and co-emission ordering are unchanged.T0856"Spoof Reporting Message" →T1692.002"Unauthorized Message: Reporting Message" (ICS sub-technique under T1692). Catalog-only (seeded, never emitted); no emitted output affected.
0.4.0 - 2026-06-10
-
Modbus TCP protocol analyzer for ICS/OT network forensics (Feature #7, issue #7, PRs #211–#218). Detects Modbus traffic on port 502; parses the MBAP header (transaction ID, protocol ID, length, unit ID) and function code; per-flow transaction correlation with bounded pending-table (request / response matching). Emits findings mapped to 7 MITRE ATT&CK for ICS techniques:
- T0855 Unauthorized Command Message (write-class function codes) (→ remapped to T1692.001 in v0.5.0)
- T0836 Modify Parameter (write-register / write-coil)
- T0835 Manipulate I/O Image (force-listen-only, write-multiple coils)
- T0831 Manipulation of Control (mask write register, write file record)
- T0806 Brute Force I/O (sustained coil/register write flooding)
- T0814 Denial of Service (exception-burst flooding pattern)
- T0888 Remote System Information Discovery (FC-scanning / register-map enumeration via exception burst on recon function codes 0x01/0x02)
Multi-tag co-emission: one finding per write PDU carrying the union of applicable techniques. Dual-window write-rate detection: burst threshold (>20 writes/1 s, configurable) + sustained threshold (>10 writes/s over ≥2 s, configurable). Exception-burst anomaly detection triggers T0888 on recon-code exception runs. Per-analyzer summary reports function-code distribution, write count, exception count, and PDU count.
-
CLI flags for the Modbus analyzer:
--modbus— enable Modbus TCP analysis (also included in-a/--all)--modbus-write-burst-threshold N— burst detection threshold (default 20 writes/1 s)--modbus-write-sustained-threshold N— sustained-rate threshold (default 10 writes/s over ≥2 s)
-
Dispatcher port-502 classification — Rule 5 in the stream dispatcher classifies port-502 flows for Modbus after content-signature rules and the 443/8443/80/8080 port rules; it never steals HTTP or TLS traffic (VP-004 port-precedence invariant preserved, PR #214).
-
Formal verification and quality assurance for the Modbus analyzer:
- VP-022 (Kani): MBAP parse safety, function-code classification totality, exception-code biconditional invariant.
- Fuzz testing: 3.7 M executions, 0 crashes (PR #216).
- Mutation testing: 100 % effective kill rate on the detection core (PR #216).
- E2E integration: pcap fixture + end-to-end flow tests (PR #217).
- T0888 blemish fix: exception-burst correctly emits T0888 for recon function codes 0x01/0x02 (PR #218, BC-2.14.019).
-
Architecture records:
- ADR-005 — Binary ICS protocol integration strategy.
- ADR-006 — Multi-technique Finding attribution model.
0.3.0 - 2026-06-09
-
Finding MITRE attribution: scalar → array (ECS-aligned).
Finding.mitre_technique: Option<String>has been renamed tomitre_techniques: Vec<String>. In JSON output the field is now"mitre_techniques"(an array); it is omitted entirely when empty. Downstream JSON consumers must update to read an array instead of a scalar. In CSV output the column is renamedmitre_techniques; multiple values are semicolon-joined (e.g.T0855;T0836); a single value is written without a separator; an empty value is an empty string. The terminal reporter now rendersMITRE: T0855, T0836for multi-technique findings and groups by the first technique's tactic. This aligns the schema with Elastic ECSthreat.technique.id(PR #209, STORY-100/101). -
JSON report envelope: new fields. Every JSON report now includes two top-level envelope fields:
"mitre_domain": "ics-attack"and"mitre_attack_version": "ics-attack-19.1". The domain is constant (wirerust targets the ATT&CK for ICS matrix). The version is pinned to ATT&CK for ICS v19.1 (released 2026-04-28), which covers all 21 seeded technique IDs including the 6 staged ICS entries (STORY-101, PR #209).
Downstream consumers of wirerust JSON or CSV output must update for this release:
- JSON: The finding attribute changed from
"mitre_technique": "T1027"(string, may be absent) to"mitre_techniques": ["T1027"](array, omitted when empty). Update any field reads toobj["mitre_techniques"][0]for single-technique findings or iterate the array for multi-technique ones. - CSV: Column 6 changed from
mitre_techniquetomitre_techniques. Multi-value cells are semicolon-joined; split on";"to get individual technique IDs. - JSON envelope: Two new top-level keys (
mitre_domain,mitre_attack_version) are now always present. If your parser requires a strict fixed key set, add these two keys to your allowlist.
- MITRE ICS catalog expanded. The technique catalog grew from 15 to 21 seeded entries. Six new ICS technique IDs are staged for the upcoming Modbus analyzer (STORY-104): T0836 (Modify Parameter), T0814 (Deny Control), T0806 (Brute Force I/O), T0835 (Manipulate I/O Image), T0831 (Manipulation of Control), T0888 (Remote System Information Discovery). T0855 (Unauthorized Command Message) is now emitted by the TLS analyzer. Total emitted count: 13 (6 Enterprise + 7 ICS), up from 6 emitted in v0.2.0 (PR #209, STORY-100/101).
0.2.0 - 2026-06-09
- Finding timestamp provenance — every
Findingnow carries acapture_tsfield populated with the pcap capture-relative timestamp of the packet that triggered the finding. The timestamp is threaded from the pcap reader throughStreamHandler::on_dataall the way to each Finding emission site in the TLS and HTTP analyzers. It is surfaced as an RFC 3339 string in JSON output and as a newtimestampcolumn in CSV output (#100; PRs #197, #198, #199; BC-2.04.055, BC-2.09.007, VP-021). Segment-limit summary findings intentionally carry no timestamp (correct by design).
- SNI control-byte summary now correctly surfaces control bytes in the human-readable finding for mixed control + non-ASCII values (#104, PR #194).
- Weak-cipher evidence vector is capped at 64 entries with an elision marker to prevent unbounded growth on adversarial captures (#102, PR #195).
- Migrated release workflow actions from Node 20 to Node 24 with fresh
SHA-pinned refs (
upload-artifactv7.0.1,download-artifactv8.0.1,softprops/action-gh-releasev3.0.0); added Dependabot tracking for workflow actions (PR #192). - SHA-pinned all remaining CI actions (
actions/checkout,rust-cache,cargo-deny,amannn/action-semantic-pull-request) and added the action-pin-gate enforcement job that fails CI if any action ref is not a 40-char hex SHA (PR #196). - Test and spec hardening for timestamp provenance: exact-value assertions replacing approximate checks, stale doc-comment corrections (PRs #200, #201).
0.1.0 - 2026-06-08
Core pipeline
- PCAP reader supporting five link types: Ethernet (1), Raw IP (101), Linux
Cooked / SLL (113), IPv4 (228), and IPv6 (229). Snaplen-truncated captures
(e.g.
tcpdump -s 96) are accepted via the unvalidated raw-record path. pcapng is not supported. - Zero-copy L2–L4 packet decoding via
etherparse. The full capture is loaded into memory as aVec<RawPacket>before analysis; available RAM determines the practical file-size limit. - Single-pass analysis pipeline: Reader → Decoder → Analyzers → Reporter, producing host/service/protocol summaries and threat findings in one pass.
- Directory expansion: pass a directory path and wirerust processes every
.pcapfile found within it (.pcapngfiles are excluded).
TCP stream reassembly engine
- Forensic-grade TCP stream reassembly with a first-wins overlap policy (earlier-arriving data wins on byte conflicts).
- Configurable per-direction depth limit (
--reassembly-depth, default 10 MB) and global memory cap (--reassembly-memcap, default 1024 MB). - Evasion and anomaly detection: overlapping-segment counting
(
--overlap-threshold, default 50 per flow direction), consecutive small-segment detection (--small-segment-threshold, default 100 run length;--small-segment-max-bytes, default 16 B), and out-of-window segment counting (--out-of-window-threshold, default 100). - Interactive-protocol port exemption from small-segment detection (default:
ports 23 and 513; overridable via
--small-segment-ignore-ports). - Idle-flow expiry: flows silent longer than
--flow-timeoutseconds (default 300) are evicted from the flow table. - Reassembly statistics surfaced in all output formats: bytes reassembled, segment-limit drops, overlap count, out-of-window count, and small-segment count.
Protocol analyzers
- DNS analyzer: traffic statistics including query/response counts, top queried hostnames, and query-type distribution.
- HTTP/1.x analyzer (requires TCP reassembly): stream-level request and response parsing with detection for path traversal sequences, web-shell indicators, unusual HTTP methods, missing or empty Host headers, and other header anomalies. Parse-error isolation prevents one poisoned stream from affecting other flows.
- TLS analyzer: ClientHello and ServerHello parsing; SNI extraction and classification (clean ASCII, ASCII control bytes C0/DEL, valid non-ASCII UTF-8, non-UTF-8 bytes); JA3 and JA3S fingerprinting with GREASE value filtering; weak cipher detection; deprecated SSL 2.0 and 3.0 detection.
- Stream dispatcher: content-first protocol classification (TLS record
signature, HTTP prefix, then port-based fallback) with classification
caching and a configurable retry budget (
max_classification_attempts).
Threat detection and MITRE ATT&CK
- Finding system with verdict, confidence score, source IP, direction tag, and optional MITRE ATT&CK technique ID.
- Static MITRE ATT&CK catalog mapping technique IDs (T-format) to tactic and
technique name, consumed by the terminal reporter when
--mitreis passed. --mitreflag groups terminal output by ATT&CK tactic with technique names displayed alongside each finding.
Output formats and CLI
- Colored terminal reporter with MITRE tactic grouping, top-SNI and top-host tables, reassembly statistics section, and skipped-packet accounting. Deterministic tie-ordering for top-SNI and top-host tables.
- JSON reporter: structured output with deterministic field ordering,
skipped_packetscounter, anddropped_findingscounter.#[non_exhaustive]on public enums for forward compatibility. - CSV reporter: 9-column findings table (tactic, verdict, confidence, source IP, destination IP, port, protocol, description, MITRE technique). CSV-injection neutralization applied to all string fields. Evidence strings joined with a pipe separator.
- Output routing:
--output-format json|csvwrites to stdout;--json [FILE]and--csv [FILE]write to a file (or stdout if no path is given).--jsonand--csvare mutually exclusive. analyzesubcommand with--dns,--http,--tls,--mitre, and-a/--allflags. HTTP analysis automatically enables TCP reassembly.summarysubcommand with optional--hostsflag for a per-host IP breakdown. Outputs total packets, bytes, protocol distribution, and service-hint counts.--no-colorflag disables ANSI color globally.- Zero, non-integer, or out-of-range values for
--reassembly-depthand--reassembly-memcapare rejected at argument-parse time.
Observability
dropped_findingscounter tracks findings discarded when the per-analyzer cap is reached; surfaced in JSON output.skipped_packetscounter tracks packets skipped during decode; surfaced in all output formats.truncated_recordscounter tracks snaplen-truncated records; surfaced in JSON output.- Criterion micro-benchmarks for hot paths in the decoder and reassembly engine.
- Bumped
indicatiffrom 0.17 to 0.18 to transitively drop the unmaintainednumber_prefixcrate (RUSTSEC-2025-0119). cargo auditandcargo denysupply-chain checks added to CI.- Release profile enables
overflow-checks = trueso integer overflows are caught in release builds. - Output sanitization in the terminal reporter guards against C1 control bytes in packet-derived strings.