A false positive is a rule that was right in general and wrong here. This page is where that gets written down — narrowly, with an author and a date, so the rule keeps protecting everywhere else.
It holds two tables. The upper one is what people decided; the lower one is what the engine decided on its own and is waiting to be confirmed.

Almost every exclusion should be born on the Security Events page, not here. Open the block, and the panel offers:
That route is better than writing one by hand for a plain reason: it knows what actually matched. An exclusion written from memory tends to be wider than the problem.
| Column | Meaning |
|---|---|
| RULE | the attack class — sqli, xss, command injection — and, when the exclusion names one, the single managed rule it applies to |
| MATCH CONDITIONS | every condition the exclusion carries: path, method, client IP, user agent, field |
| TENANT | which site it applies to |
| ALLOWED | how many requests it has let through |
| STATUS | enabled or disabled |
| ACTIONS | disable, history, delete |
Every exclusion is a hole somebody opened on purpose. The count is what tells you whether it is still load-bearing.
Has never allowed a request — either the problem went away, or the exclusion never matched what its author meant. Both are reasons to remove it, and until this column existed there was no way to tell either from a working exclusion.Exclusions are not edited in place. If the scope is wrong, write a new one from the event that shows the real behaviour and delete the old one — which leaves a trail of what was believed and when, rather than a row that quietly changed meaning.
As narrow as the evidence. An exclusion can carry all of these at once, and every one you add makes it safer:
| Condition | Example | Why |
|---|---|---|
| A single rule | 942550 |
the other SQLi rules keep inspecting that path |
| A field | HOSTNAME |
the rule keeps firing on every other parameter |
| A path | /graphql, or a prefix, glob or exact URI |
everywhere else is untouched |
| A method | POST |
a GET to the same path is still inspected |
| A client IP | one address or range | everybody else still meets the rule |
| A user agent | a substring | for a specific client of yours |
A worked example from this installation: the wiki's own API was being blocked because a GraphQL query — {pages{single(id:1){…}}} — reads as SQL injection to CRS rule 942550, and a documentation page full of <img src=…> reads as XSS to 941160. Three exclusions were written, each scoped to POST /graphql and to the publishing machine's address. The same path is still fully inspected for everybody else, and every other SQLi and XSS rule still applies to the API.
Start with the narrowest thing that describes what you saw, and widen only when a second event proves you must. An exclusion that names one rule, one path and one method is a scalpel; one that names only a class is a door.

The lower table is not a list of your decisions — it is a list of the engine's.
Some rules match on a parameter's name rather than on its value. A field called HOSTNAME trips the command-injection rule that looks for the HOSTNAME shell variable, although the value is innocent. EdgeGuard notices this and suppresses the match by itself, and every row here is one of those, reported by the agents on their heartbeat.
| Column | Meaning |
|---|---|
| RULE | which managed rule was suppressed |
| FIELD | the parameter whose name caused it, and the section it came from |
| SUPPRESSED BY | which layer decided — for example the parameter-name gate |
| SUPPRESSIONS | how many times, in the window |
| SINCE / LAST SEEN | when it started and when it last happened |
| DECISION | yours, in four states |
Each row is a decision waiting, and every state can be left again — nothing here is one-way.
| State | What it does to traffic |
|---|---|
| Undecided | nothing. The engine keeps suppressing, which is what the row reports |
| Agree | nothing. It records that somebody reviewed it — a signature, not a rule |
| Exclude | writes it down as a real exclusion, scoped to that rule and that field, so it survives the semantic layer being switched off and appears in the upper table with an author |
| Block again | the rule stops being suppressed on this site and decides for itself — those requests start being refused |
Exclude is unavailable until the engine has named the field: an exclusion without one would have to cover the whole path, which silences the rule for every other field there.
Block again is the only one that asks before it acts, and the question names how many requests a week are currently being let through. It takes effect within about 30 seconds, on the agents' next configuration pull.
Why both tables exist: the upper one is policy you wrote, the lower one is a guess the engine made. The text under the table says the alternative out loud — either the rule needs tuning for this site, or the suppression is covering a real attack — and Block again is how you say the second.
1. Reproduce the request that was blocked. The event's detail panel shows the exact URI, method and headers; the EdgeGuard ID on the block page finds it again through the global search.
curl -o /dev/null -w '%{http_code}\n' \
-H 'User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) Chrome/140.0.0.0' \
"https://site.example.com/the/path?field=value"
It should answer whatever the origin answers, instead of 403. Allow about 30 seconds after saving for the agents to pull it.
2. Check the counter. The ALLOWED column should start moving. If it stays at never allowed a request while the block continues, the exclusion is not matching the request — most often because the path scope is an exact URI where a prefix was meant.
3. Check what still applies. Send something that genuinely should be blocked on that same path. If it is refused, the exclusion is as narrow as you intended:
curl -o /dev/null -w '%{http_code}\n' \
"https://site.example.com/the/path?field=%27%20OR%201%3D1--" # expect 403
Exclusions accumulate. Two habits keep them from becoming a quiet second policy nobody reads: