Skip to content

Report detections the guard did not block, not only the ones it did - #156

Merged
patchstackdave merged 1 commit into
mainfrom
eng-3586-detection-reporting
Aug 20, 2026
Merged

Report detections the guard did not block, not only the ones it did#156
patchstackdave merged 1 commit into
mainfrom
eng-3586-detection-reporting

Conversation

@patchstackdave

@patchstackdave patchstackdave commented Aug 20, 2026

Copy link
Copy Markdown
Contributor

A rule that blocks nothing reports nothing.

firewall-log.js posts enforced blocks in the WordPress-compatible shape and answers "what did we stop". There was no way to see that a rule carrying enforcement: dry-run matched traffic it would otherwise have blocked — so a rule that is quietly wrong and a rule that is protecting look identical from the outside.

This adds a second, deliberately narrow channel for that.

The payload

sent not sent
rule id the matched value
route — request path, query string removed the request body
parameters — what the rule reads headers
phase, enforced, rules_etag, detected_at query-string values, IP, user-agent

A channel that counts detections is a different thing from a copy of an application's traffic, and the difference is one careless field. Once values are collected, every question about retention, access and jurisdiction arrives with them — so anything value-level belongs behind its own explicit opt-in with its own controls, not as a side effect of counting.

Two details worth review:

The regression guard is a scan of the serialized payload, not of the object being built. A field added later — message, value, headers — passes every structural assertion and only fails that scan, which is the direction this has to fail in.

parameters is the set the rule reads, from its own definition, not the condition that matched. The engine reports a rule, not which of its conditions fired, and threading that out would mean changing evaluation for the sake of a reporting field. A rule with a single parameter — the common case for a narrowly scoped rule — gives the same answer either way.

Off by default

Enabling it adds an outbound POST to every guard configured with a site UUID. That is a change in what an installed app does on the network, and it belongs in the shipped docs before it becomes a default rather than after. reportDetections: true switches it on; detectionFlushMs tunes the batch window.

Wiring it default-on also broke 32 existing tests — the same fact, stated by the suite.

Bounded (500 events, oldest dropped) and fail-open: an unreachable endpoint is silent and never retries into a loop. The drop count is sent with the batch, because a consumer computing a rate from these needs to know its denominator is short, and nobody infers that from a gap.

Verification

1249 tests, typecheck clean. Four guarantees mutation-checked — keeping the query string, adding the block message to the payload, removing the queue cap, and deriving enforced from the site mode rather than the rule each fail the assertion that names them.

@coderbuds

coderbuds Bot commented Aug 20, 2026

Copy link
Copy Markdown

Detection reporter reliably logs dry-run events without exposing sensitive data.

🎯 Quality: 100% Elite · 📦 Size: Large — consider splitting if possible

🛡️ Standards: no pre-flight fit check ran for this change — wire assess-change-fit into your coding agents to catch size before opening.

📈 This month: Your 100th PR — above team average · Averaging Excellent

See how your team is trending →

@patchstackdave
patchstackdave force-pushed the eng-3586-detection-reporting branch from 0ddebc2 to dd328fd Compare August 20, 2026 10:29
@patchstackdave patchstackdave changed the title Report what a rule would have stopped, not only what it stopped Report detections the guard did not block, not only the ones it did Aug 20, 2026
A rule that blocks nothing reports nothing. `firewall-log.js` posts enforced blocks in the
WordPress-compatible shape and answers "what did we stop"; there was no way to see that a rule
carrying `enforcement: dry-run` matched traffic it would otherwise have blocked. Without that, a
rule which is quietly wrong and a rule which is protecting look identical from the outside.

## The payload is small on purpose

Per detection: the rule id, the request PATH with the query string removed, the parameters the rule
reads, the phase, whether it was enforced, the rule-bundle ETag, and a timestamp.

It never carries the matched value, the request body, headers, or query-string values. A channel that
counts detections is a different thing from a copy of an application's traffic, and the difference is
one careless field: once values are collected, every question about retention, access and
jurisdiction arrives with them. Anything value-level belongs behind its own explicit opt-in with its
own controls, not as a side effect of counting.

The route drops the query because `?token=…` is a value, and the guard against regression is a scan
of the SERIALIZED payload rather than of the object being built: a field added later (`message`,
`value`, `headers`) passes every structural assertion and fails that one.

`parameters` is the set the rule READS, from its own definition — not the condition that matched. The
engine reports a rule, not which of its conditions fired, and threading that out would mean changing
evaluation for the sake of a reporting field.

## Off by default

Enabling it adds an outbound POST to every guard configured with a site UUID, which is a change in
what an installed app does on the network — that belongs in the shipped docs before it becomes a
default rather than after. `reportDetections: true` switches it on.

Bounded (500 events, oldest dropped) and fail-open: an unreachable endpoint is silent and never
retries into a loop. The drop count is sent WITH the batch, because a consumer computing a rate from
these needs to know its denominator is short, and nobody infers that from a gap.

1249 tests, typecheck clean. Four guarantees are mutation-checked: keeping the query string, adding
the block message to the payload, removing the queue cap, and deriving `enforced` from the site mode
rather than the rule each fail the assertion that names them.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@patchstackdave
patchstackdave force-pushed the eng-3586-detection-reporting branch from dd328fd to 5e91e0a Compare August 20, 2026 10:30
@patchstackdave

Copy link
Copy Markdown
Contributor Author

/review

@patchstackdave
patchstackdave merged commit 51c4fc4 into main Aug 20, 2026
6 checks passed
@patchstackdave
patchstackdave deleted the eng-3586-detection-reporting branch August 20, 2026 10:37
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.

2 participants