fix(daemon): flush the aggregator by the longest window — hourly tiers regain their history (#610) - #618
Conversation
…to the aggregator flush Adds the FlushOnce harness seam (runFlush body factored into flushAggregates, unchanged behaviour) and an nginx hit helper. The scenario paces one wp-login attempt every 5 minutes with the 10-minute flush tick in between; the 10th attempt within the hour must be banned by http_wp_probe_sustained. Fails on dev: the flush cutoff is the LAST configured window (300 s), not the longest. Refs #610 Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KCu1p3UrUGSzWVwhNuJ47k
… configured one (#610) runFlush evicted everything older than Windows()[last]; Windows() is in rule order and the embedded set ends with the mail rules' 300 s window, so every 10 minutes the in-memory history was cut to 5 minutes and no hourly *_sustained rule could ever see more than one flush interval: an attacker pacing one attempt every 5 minutes was invisible to the tier built for that pacing. Aggregator.MaxWindow() is now the flush horizon; the benchmark replays the flush on the daemon's cadence so this class is covered there too. Closes #610 Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KCu1p3UrUGSzWVwhNuJ47k
|
Strix is installed on this repository, but we couldn't run this PR security review because this workspace's trial has ended. Add a card to resume code reviews here. So far, Strix has reviewed 49 pull requests, surfaced 8 security issues (1 critical/high) and blocked 3 risky merges across this workspace. |
There was a problem hiding this comment.
The fix correctly addresses issue #610 by ensuring the aggregator flush uses the longest configured window rather than the last window in rule order. The implementation is sound with comprehensive test coverage including unit tests, E2E tests, and benchmark updates. The changes properly restore the intended behavior where hourly detection rules can see their full hour of history instead of being limited to 5-10 minutes.
You can now have the agent implement changes and create commits directly on your pull request's source branch. Simply comment with /q followed by your request in natural language to ask the agent to make changes.
…ollow-ups in INVARIANTS (#610) The bench builds its aggregator from every rule window (no long-window split), so MaxWindow() is 7 days and a flush at that horizon evicts nothing — it proved no coverage. Real benchmark coverage of the flush is #623. INVARIANTS C3 now lists #621–#624. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KCu1p3UrUGSzWVwhNuJ47k
What & why
Closes #610 (Phase 2 of #605, invariant C3).
runFlushevicted every in-memory event older thanWindows()[last].Windows()is in rule order, and the embedded rules end with the mail rules' 300 s window, so every 10-minute flush cut the history to 5 minutes. Every hourly*_sustainedrule (http_wp_probe_sustained10/1h,http_xmlrpc_sustained,http_scanner_sustained,ssh_bruteforce_sustained, the mail/vaultwarden/nextcloud/keycloak sustained tiers) could see at most one flush interval — an attacker pacing one attempt every 5 minutes was invisible to the tier that exists for that pacing.Acceptance criteria restated:
wp-login.phphits one every 5 min with the 10-min flush tick in between, 10th at +45 min →http_wp_probe_sustainedbans (TestDetection_HourlyTierSurvivesFlush; fails ondevbefore the fix, commit 1 of this PR).MaxWindow()(7 days) is inert there; the earlier commit that added it was removed after the adversarial review (F3).Changes
internal/aggregate:MaxWindow()(the valueNewalready computed).internal/daemon/daemon.go:runFlushbody factored intoflushAggregates(ctx, now); cutoff isnow - MaxWindow().internal/daemon/harness.go:FlushOnceseam;internal/e2e:httpHithelper + the scenario.internal/daemon/flush_test.go: unit regression (skips if the rule order no longer has the bug shape).docs/internal/INVARIANTS.md: C3 gap marked fixed.Tests
make lint testgreen locally (-race):go test -race ./...ok;go test -tags bench ./internal/bench/ok;gofmt -l .empty;go vet ./...ok;golangci-lint run ./...(v2 image) 0 issues; ip-hygiene gate clean.Blast radius (per docs/internal/INVARIANTS.md)
Aggregator.Addeviction already usedmaxWindow(unchanged,aggregate_test);evaluateRules/Aggregate(ip, w, now)per window unchanged (lowslow_test,httplowslow_test, e2e A scenarios);PruneEventCountshorizon unchanged (long windows,lowslow_test).DefaultMaxSamplesbounds the read-side sample only, not memory.internal/e2e.TestDetection_HourlyTierSurvivesFlush.Security review (per docs/internal/SECURITY-REVIEW.md)
flushAggregatespure refactor plus the horizon change).Self-assessment: no step toward lock-out, injection, privilege escalation, secret exfiltration or evasion — it removes an evasion (pacing under the hourly tier).
Checklist
🤖 Generated with Claude Code
https://claude.ai/code/session_01KCu1p3UrUGSzWVwhNuJ47k