Skip to content

daemon: rule windows no longer hold every event forever (0.13.23) - #286

Merged
ralyodio merged 1 commit into
masterfrom
fix/rule-windows-leak
Oct 7, 2026
Merged

ralyodio merged 1 commit into
masterfrom
fix/rule-windows-leak

Conversation

@ralyodio

@ralyodio ralyodio commented Oct 7, 2026

Copy link
Copy Markdown
Contributor

daemon: rule windows no longer hold every event forever (0.13.23)

After 0.13.22, dev2's daemon still grew ~280 MB an hour (heap 473 MB live
after a full GC, 4.5 h after restart). A sampling heap profile put the live
allocations in the per-line event path: the rule engine.

Each rule kept a window per source address (or per path, or global) holding
the full ThreatEvent of every match (URL, user agent, details). Old entries
were dropped only when a NEW event arrived for the same key, and cleanup()
removed only windows that were already empty, so every address that matched
a rule once and never returned kept its events for the life of the daemon.
dev2 sees hundreds of thousands of addresses a day.

  • Windows keep a timestamp and source IP per entry, never the event.
  • cleanup() expires each window by its own rule's length and forgets empty
    windows once their cooldown has passed; it runs every minute, not five.
  • At most 1000 entries (or the threshold, if higher) per window; a detection
    past the cap reports "1000+ events".
  • Pruning is a moving head index instead of filtering the array on every
    event, so a busy aggregate window is no longer O(n) per event.

Tests: 10,000 one-off addresses are all forgotten once the window passes (the
old cleanup kept them all); thresholds and cooldowns behave as before; the
cap and its label. The existing rule suites pass unchanged.

Co-Authored-By: Claude Opus 5.5 noreply@anthropic.com

After 0.13.22, dev2's daemon still grew ~280 MB an hour (heap 473 MB live
after a full GC, 4.5 h after restart). A sampling heap profile put the live
allocations in the per-line event path: the rule engine.

Each rule kept a window per source address (or per path, or global) holding
the full ThreatEvent of every match (URL, user agent, details). Old entries
were dropped only when a NEW event arrived for the same key, and cleanup()
removed only windows that were already empty, so every address that matched
a rule once and never returned kept its events for the life of the daemon.
dev2 sees hundreds of thousands of addresses a day.

- Windows keep a timestamp and source IP per entry, never the event.
- cleanup() expires each window by its own rule's length and forgets empty
  windows once their cooldown has passed; it runs every minute, not five.
- At most 1000 entries (or the threshold, if higher) per window; a detection
  past the cap reports "1000+ events".
- Pruning is a moving head index instead of filtering the array on every
  event, so a busy aggregate window is no longer O(n) per event.

Tests: 10,000 one-off addresses are all forgotten once the window passes (the
old cleanup kept them all); thresholds and cooldowns behave as before; the
cap and its label. The existing rule suites pass unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@github-actions

github-actions Bot commented Oct 7, 2026

Copy link
Copy Markdown

ThreatCrush Security Scan

18 finding(s)

HIGH/CRITICAL: 1 | MEDIUM: 9 | LOW: 8

Severity Rule Location
HIGH secret-aws-access-key prd/0003-detect-hardcoded-secrets-before-they-are-committed-or-served.md:126
MEDIUM sql-string-concatenation .github/workflows/migrate-dev2.yml:128
MEDIUM js-uninitialized-buffer apps/cli/src/core/log-tail.ts:58
MEDIUM js-open-redirect apps/web/src/app/auth/login/page.tsx:67
MEDIUM js-unescaped-html-sink apps/web/src/app/hire/page.tsx:104
MEDIUM js-unescaped-html-sink apps/web/src/app/hire/page.tsx:108
MEDIUM js-open-redirect apps/web/src/app/invite/[token]/invite-client.tsx:55
MEDIUM js-open-redirect apps/web/src/components/funding/FundingClient.tsx:97
MEDIUM js-unescaped-html-sink apps/web/src/components/GuideReader.tsx:265
MEDIUM js-uninitialized-buffer packages/scan/src/node-rules.ts:456
LOW secret-generic-credential apps/web/src/app/api/auth/refresh/route.ts:17
LOW secret-generic-credential apps/web/src/app/api/auth/reset-password/route.ts:26
LOW secret-generic-credential apps/web/src/app/api/auth/reset-password/route.ts:27
LOW secret-generic-credential PRD.md:269
LOW tls-verification-disabled prd/0004-find-dangerous-code-patterns-without-pretending-to-be-a-compiler.md:121
LOW tls-verification-disabled prd/0004-find-dangerous-code-patterns-without-pretending-to-be-a-compiler.md:122
LOW sh-remote-script-execution scripts/smoke-test.sh:72
LOW secret-aws-access-key scripts/smoke-test.sh:150

Snippets are redacted; ThreatCrush never prints matched credential material.

@ralyodio
ralyodio merged commit 6c1b016 into master Oct 7, 2026
17 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant