Skip to content
Merged
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
Output AC-146-001-002-004-006-test-suite.gif
Output AC-146-001-002-004-006-test-suite.webm

Require cargo

Set Shell "bash"
Set FontFamily "Menlo"
Set FontSize 13
Set Width 1200
Set Height 700
Set Theme "Dracula"
Set Padding 20
Set WaitTimeout 120s

# AC-146-001 (fill_buf_for_testing seam), AC-146-002 (counter increments on tail-drop),
# AC-146-004 (accessor buffer_saturation_drop_count), AC-146-006 (both directions,
# same aggregate counter) — all covered by the story_146 test module.
#
# The saturation path requires the buffer to hit MAX_BUF=65536 which is only
# reachable via fill_buf_for_testing; the 8-test suite exercises:
# partial-drop, full-drop, no-drop boundary, exact-fit boundary,
# both-directions, counter persistence across flow close, and summarize value equality.
#
# Success path: all 8 story_146 tests pass.
# Error path: misspelled module name -> 0 tests run (demonstrates test filter precision).

Hide
Type "cd /Users/zious/Documents/GITHUB/wirerust/.worktrees/story-146-tls-buffer-saturation-telemetry"
Enter
Sleep 300ms
Show

Type "echo '=== STORY-146: buffer_saturation_drops counter AC-001/002/004/006 ==='"
Enter
Sleep 600ms

Type "echo '--- Success path: all 8 story_146 tests pass ---'"
Enter
Sleep 300ms

Type "cargo test --test tls_analyzer_tests story_146 2>&1"
Enter
Wait+Screen /test result/
Sleep 2s

Type "echo '--- Error path: misspelled module -> 0 tests run ---'"
Enter
Sleep 300ms

Type "cargo test --test tls_analyzer_tests story_146_typo 2>&1"
Enter
Wait+Screen /test result/
Sleep 2s
Binary file not shown.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
47 changes: 47 additions & 0 deletions docs/demo-evidence/STORY-146/AC-146-005-summarize-key-present.tape
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
Output AC-146-005-summarize-key-present.gif
Output AC-146-005-summarize-key-present.webm

Require cargo

Set Shell "bash"
Set FontFamily "Menlo"
Set FontSize 13
Set Width 1200
Set Height 700
Set Theme "Dracula"
Set Padding 20
Set WaitTimeout 60s

# AC-146-005: summarize() exposes "buffer_saturation_drops" in the detail map.
# Success path: wirerust --tls analyze on a real TLS 1.2 pcap shows the new
# "buffer_saturation_drops: 0" key in ANALYZER: TLS section.
# Error path: running without --tls omits the TLS analyzer section entirely,
# demonstrating the key is exclusively from the TLS analyzer.

Hide
Type "cd /Users/zious/Documents/GITHUB/wirerust/.worktrees/story-146-tls-buffer-saturation-telemetry"
Enter
Sleep 300ms
Show

Type "echo '=== STORY-146: buffer_saturation_drops key in summarize() ==='"
Enter
Sleep 600ms

Type "echo '--- Success path: --tls flag shows buffer_saturation_drops ---'"
Enter
Sleep 300ms

Type "./target/release/wirerust --no-color analyze --tls tests/fixtures/tls12-aes256gcm.pcap"
Enter
Wait+Screen /buffer_saturation_drops/
Sleep 2s

Type "echo '--- Error path: without --tls, no TLS analyzer section ---'"
Enter
Sleep 300ms

Type "./target/release/wirerust --no-color analyze tests/fixtures/tls12-aes256gcm.pcap"
Enter
Wait+Screen /WIRERUST TRIAGE/
Sleep 2s
Binary file not shown.
56 changes: 56 additions & 0 deletions docs/demo-evidence/STORY-146/evidence-report.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
# Demo Evidence Report — STORY-146

**Story:** TLS buffer-saturation telemetry (`buffer_saturation_drops` counter)
**Story ID:** STORY-146
**Recorded:** 2026-06-30
**Toolchain:** VHS 0.11.0 · wirerust release binary · cargo test

---

## Coverage Map

| Acceptance Criteria | Demo Artifact | What It Shows |
|---------------------|--------------|---------------|
| AC-146-005 (summarize key present, value=0 when no saturation) | `AC-146-005-summarize-key-present.webm` / `.gif` | `wirerust --no-color analyze --tls tests/fixtures/tls12-aes256gcm.pcap` — terminal output includes `ANALYZER: TLS` section with `buffer_saturation_drops: 0`; without `--tls` the section is absent (error path). |
| AC-146-001 (`fill_buf_for_testing` seam), AC-146-002 (counter increments on tail-drop), AC-146-004 (accessor `buffer_saturation_drop_count`), AC-146-006 (both directions, same aggregate counter) | `AC-146-001-002-004-006-test-suite.webm` / `.gif` | `cargo test --test tls_analyzer_tests story_146` — all 8 tests pass, including `test_BC_2_07_043_buffer_saturation_observable`, `test_BC_2_07_043_buffer_saturation_full_drop`, `test_BC_2_07_043_both_directions_increment_same_counter`, `test_BC_2_07_043_counter_persists_across_flows`, and `test_BC_2_07_043_summarize_value_equals_drop_count`; misspelled module name shows 0 tests run (error path). |

---

## Artifacts

```
demos/story-146/
AC-146-005-summarize-key-present.tape — VHS script source
AC-146-005-summarize-key-present.gif — 379K GIF recording
AC-146-005-summarize-key-present.webm — 288K WebM recording

AC-146-001-002-004-006-test-suite.tape — VHS script source
AC-146-001-002-004-006-test-suite.gif — 219K GIF recording
AC-146-001-002-004-006-test-suite.webm — 256K WebM recording
```

---

## Notes on Coverage Approach

AC-146-002 and AC-146-006 require the per-direction TCP segment buffer to reach
MAX_BUF=65,536 bytes before a drop occurs. This threshold is not reachable with the
existing small pcap fixtures without engineering a synthetic 65K+ TLS capture.
The `fill_buf_for_testing` seam (AC-146-001) exists precisely to make this path
testable: it parks the buffer at capacity so a single subsequent `on_data` call
triggers a drop. The `story_146` test module exercises this seam directly, making
the test suite the correct evidence vehicle for AC-146-001/002/004/006.
AC-146-005 is demonstrated via the live CLI since `buffer_saturation_drops: 0`
is always present in `summarize()` output, even with no saturation events.

---

## AC Coverage Status

| AC | Covered | Vehicle |
|----|---------|---------|
| AC-146-001 | Yes | test suite (8 passing) |
| AC-146-002 | Yes | test suite (`test_BC_2_07_043_buffer_saturation_observable`, `_full_drop`) |
| AC-146-004 | Yes | test suite (`buffer_saturation_drop_count()` accessor calls) |
| AC-146-005 | Yes | live CLI on `tls12-aes256gcm.pcap` |
| AC-146-006 | Yes | test suite (`test_BC_2_07_043_both_directions_increment_same_counter`) |
92 changes: 92 additions & 0 deletions src/analyzer/tls.rs
Original file line number Diff line number Diff line change
Expand Up @@ -347,6 +347,16 @@ pub struct TlsAnalyzer {
/// existing `truncated_records` aggregate counter pattern.
/// AC-144-001 / ADR-011 Decision 1 / BC-2.07.039 Invariant 5.
handshake_reassembly_overflows: u64,
/// Aggregate count of per-direction TCP-segment buffer tail-drop events.
///
/// Incremented each time `on_data` detects that the incoming data length
/// exceeds the remaining capacity of `client_buf` or `server_buf`
/// (`data.len() > remaining` where `remaining = MAX_BUF.saturating_sub(buf.len())`).
/// This is an AGGREGATE counter on `TlsAnalyzer` — NOT on `TlsFlowState` — and is
/// NOT reset at `on_flow_close`. Mirrors the `truncated_records` / `handshake_reassembly_overflows`
/// aggregate counter pattern.
/// AC-146-001 / BC-2.07.043 Invariants 1–3.
buffer_saturation_drops: u64,
all_findings: Vec<Finding>,
}

Expand All @@ -369,6 +379,7 @@ impl TlsAnalyzer {
parse_errors: 0,
truncated_records: 0,
handshake_reassembly_overflows: 0,
buffer_saturation_drops: 0,
all_findings: Vec::new(),
}
}
Expand Down Expand Up @@ -1111,6 +1122,10 @@ impl StreamHandler for TlsAnalyzer {
return;
}

// AC-146-002 / BC-2.07.043 Invariant 4: compute did_drop INSIDE the &mut state
// block (borrow-constraint mandated); increment self.buffer_saturation_drops
// AFTER the block closes (mutable borrow on self.flows released).
let did_drop;
{
let state = self
.flows
Expand All @@ -1122,20 +1137,39 @@ impl StreamHandler for TlsAnalyzer {
match direction {
Direction::ClientToServer => {
let remaining = MAX_BUF.saturating_sub(state.client_buf.len());
// AC-146-002: detect tail-drop condition (strictly greater, NOT >=).
// data.len() > remaining covers both partial-drop (remaining>0, 1+
// bytes truncated) and full-drop (remaining==0) paths. The form
// `to_copy < data.len()` would miss the full-drop case because to_copy
// is computed only inside the `if remaining > 0` arm (ADR-011 C-3).
did_drop = data.len() > remaining;
if remaining > 0 {
let to_copy = data.len().min(remaining);
state.client_buf.extend_from_slice(&data[..to_copy]);
}
}
Direction::ServerToClient => {
let remaining = MAX_BUF.saturating_sub(state.server_buf.len());
// AC-146-002: symmetric drop detection for the ServerToClient arm.
// Same aggregate counter — BC-2.07.043 Postcondition 3.
did_drop = data.len() > remaining;
if remaining > 0 {
let to_copy = data.len().min(remaining);
state.server_buf.extend_from_slice(&data[..to_copy]);
}
}
}
}
// AC-146-002: increment AFTER the &mut state block closes (borrow released).
// Placement is between the buffer-append block and the try_parse_records call
// (ADR-011 Decision 1 / BC-2.07.043 Invariant 4). Byte-drop semantics unchanged.
if did_drop {
// SEC-003: saturating_add mirrors sibling `handshake_reassembly_overflows`
// increments — avoids theoretical overflow-check panic under the release
// profile's `overflow-checks = true`. Counter saturation at u64::MAX is safe
// and intentional for an aggregate diagnostic.
self.buffer_saturation_drops = self.buffer_saturation_drops.saturating_add(1);
}

self.try_parse_records(flow_key, direction);
}
Expand Down Expand Up @@ -1196,6 +1230,13 @@ impl StreamAnalyzer for TlsAnalyzer {
"handshake_reassembly_overflows".to_string(),
serde_json::json!(self.handshake_reassembly_overflows),
);
// AC-146-005: surface `buffer_saturation_drops` in the detail map.
// Key ALWAYS present even when count==0 (EC-008 / BC-2.07.043 Postcondition 4).
// Mirrors `truncated_records` and `handshake_reassembly_overflows` pattern above.
detail.insert(
"buffer_saturation_drops".to_string(),
serde_json::json!(self.buffer_saturation_drops),
);

AnalysisSummary {
analyzer_name: self.name().to_string(),
Expand Down Expand Up @@ -1389,6 +1430,57 @@ impl TlsAnalyzer {
pub fn handshake_reassembly_overflow_count(&self) -> u64 {
self.handshake_reassembly_overflows
}

// ── STORY-146 accessor + test seam (AC-146-001) ───────────────────────────
//
// During the RED gate, `buffer_saturation_drop_count` and `fill_buf_for_testing`
// carried `todo!()` bodies to enforce test failures before implementation.
// Both are now fully implemented: `buffer_saturation_drop_count` reads
// `self.buffer_saturation_drops` directly (mirroring the
// `truncated_record_count` / `handshake_reassembly_overflow_count` pattern),
// and `fill_buf_for_testing` fills the per-direction TCP-segment buffer to an
// exact byte count so tests can drive the full-drop path without live traffic.

/// Public accessor: aggregate count of per-direction buffer saturation tail-drop events.
///
/// Reads `self.buffer_saturation_drops` directly. Mirrors the existing
/// `truncated_record_count()` and `handshake_reassembly_overflow_count()` public
/// accessor pattern (AC-146-001 / BC-2.07.043 Invariants 1–3). Read-only; do not
/// use for drop-prevention decisions.
pub fn buffer_saturation_drop_count(&self) -> u64 {
self.buffer_saturation_drops
}

/// Test-only seam: fill the per-direction TCP-segment buffer to exactly `n` bytes.
///
/// Fills `client_buf` (for `Direction::ClientToServer`) or `server_buf`
/// (for `Direction::ServerToClient`) of the given flow to exactly `n` bytes,
/// creating the flow state entry if it does not yet exist.
///
/// Precondition: `n <= MAX_BUF`. Required to exercise the full-drop path
/// (`remaining==0`, EC-002) since that state is not reachable via the public
/// `on_data` API alone without first filling the buffer with real TLS data.
///
/// Signature uses `flow_key: &FlowKey` by reference, matching the convention
/// of all five sibling TLS test seams. AC-146-001 / BC-2.07.043 Architecture Anchor.
/// MUST NOT be called from production code.
#[doc(hidden)]
pub fn fill_buf_for_testing(&mut self, flow_key: &FlowKey, direction: Direction, n: usize) {
debug_assert!(
n <= MAX_BUF,
"fill_buf_for_testing: n={n} exceeds MAX_BUF={MAX_BUF}"
);
let state = self
.flows
.entry(flow_key.clone())
.or_insert_with(TlsFlowState::new);
let buf = match direction {
Direction::ClientToServer => &mut state.client_buf,
Direction::ServerToClient => &mut state.server_buf,
};
buf.clear();
buf.resize(n, 0u8);
}
}

// ── JA3 / JA3S property tests (LESSON-P2.04) ─────────────────────────────────
Expand Down
Loading