Exclusions and mutes¶
FreeDR has three ways to stop a rule from alerting on activity you know is expected:
| Mechanism | Scope | Lifetime | Effect on matching events |
|---|---|---|---|
| Built-in exclusion | One rule, part of its definition | Versioned with the rule | No alert |
| User exclusion | One rule, your local state | Until you remove it | No alert |
| Mute | One rule, or one rule plus one entity | 24 hours or 7 days | Alert stored but marked muted: no case, no AI run, no notification |
All three apply to one rule only. FreeDR has no global allowlist.
Exclusions and mutes change only alerting. FreeDR still collects and stores the events, and you can still find them in search and Hunt.
How an exclusion is applied¶
An exclusion is a SQL boolean expression (a predicate) over the columns of the rule's source table. Before each run, the engine adds every built-in exclusion and every active user exclusion to the rule's candidate rows:
… AND ((<predicate>) IS NOT TRUE)
So a row is suppressed only when a predicate is definitely true:
- A predicate that evaluates to
NULLdoes not suppress the row. For example, a test on a column that is unknown for this event does not hide it. - Unknown lineage (a
NULLparent_image_pathoractor_image_path) therefore fails toward alerting.
Exclusions are applied before the rule's LIMIT, and they apply in rule tests and exclusion previews as well. A predicate may read the same relations as the rule query, each bounded by the rule's timeframe:
<source>_context;- other typed sources;
artifact_path_state.
When FreeDR validates a predicate, it wraps it as SELECT … FROM <source> AS typed WHERE (<predicate>), using the same checks as a rule query. Because suppressed rows are removed from the candidate relation, a subquery on the source name inside the rule cannot see them. Use <source>_context if the rule needs the unfiltered rows.
Built-in exclusions¶
Built-in exclusions are the builtin_exclusions list in a rule document. Each item has a predicate (at most 512 bytes) and a reason (at most 256 bytes), and a rule may have at most 16.
- In the shipped pack, 25 rules carry 38 built-in exclusions in total. Most of them exclude stock Windows or Microsoft servicing behaviour.
- The catalog shows the number per rule. The rule detail view in the console shows the predicates and reasons.
- Built-in exclusions of shipped rules cannot be changed or removed. They change only with a new rule version.
- In a custom rule, you edit them in the rule editor (Add built-in exclusion), and they are versioned with the rule.
Example from the shipped pack:
builtin_exclusions:
- predicate: |-
value_type IN (1, 2) AND regexp_full_match(lower(…value_data…), '(wintrust|mso)\.dll|c:\\windows\\(system32|syswow64)\\(…)\.dll')
reason: 'Stock provider DLL values rewritten by Windows servicing, regsvr32 wintrust.dll repair scripts and MSI Office (mso.dll); they cannot load attacker code (Elastic and Sigma 5a2b21ee filters).'
User exclusions¶
User exclusions are your local per-rule exceptions. They work for built-in and custom rules alike, and they are stored separately from the rule definition, so a built-in rule stays unchanged. The console calls them "user exceptions".
Limits¶
| Limit | Value |
|---|---|
| Predicate | Non-empty, at most 1,024 bytes. It must validate against the rule's source and timeframe exactly like a built-in exclusion. |
| Reason | Required (not blank), at most 4,096 bytes |
| Active exclusions per rule | 16 |
| Pending (AI-proposed) exclusions per rule | 16, counted separately |
| Duplicates | A rule cannot hold the same predicate twice, whether pending or active |
Building an exclusion from an alert¶
The usual way to create an exclusion is from a false-positive alert. Open the alert and choose the exclusion option. FreeDR reads the event that triggered the alert and offers its stable fields as an exclusion predicate (POST /api/v1/detections/alerts/{alert_id}/exclusion-preview):
| Triggering event | Fields offered (selected by default: ✓) |
|---|---|
| Process start | Image path ✓, Parent image path ✓, Command line (✓ when the rule's SQL reads the command line) |
| Image load | Loaded image path ✓, Loading process image ✓ |
| Registry | Registry key ✓, Writer image path ✓, Value name |
| File | File path ✓, Writer image path ✓ |
| USN / Artifact | File path ✓ |
| Event Log | Named fields image, parentimage, sourceimage, targetimage, targetobject, targetfilename, imageloaded ✓; commandline (✓ as above); parentcommandline |
How the predicate is built:
- The trigger's event type (
event_type = Norevent_type_id = N) is always part of the predicate. - Every value is compared exactly. Nothing matches by name pattern.
- A path is offered only when it is a full path (a directory and a file name). A bare file name is never used.
- A parent or writer image compares the stamped lineage column (
parent_image_pathoractor_image_path). - Values longer than 160 bytes, or containing control characters, are compared by length and SHA-256:
strlen(column) = N AND sha256(column) = '…'. - Event Log fields compare the whole stored named-field entry with
list_contains(text_list_01, …). - You must select at least one field, and the combined predicate must fit in 1,024 bytes. If it does not, deselect a field.
- If the triggering event is no longer retained, no exclusion can be built from it.
Preview¶
The Preview button ("preview": true) counts what the exclusion would have suppressed. It has a 10-second budget and writes nothing. It reports:
| Field | Meaning |
|---|---|
window_start_us, window_end_us |
The last 7 days, over which the rule is evaluated |
matches_without, matches_with |
Rule matches in that window without and with the candidate predicate. Current active exclusions are applied in both. |
query_limit, hit_query_limit |
The 1,000-row rule limit, and whether matches_without reached it |
alerts_checked, alerts_checked_limit, alerts_matched |
How many of the rule's 2,000 most recent stored alerts were checked, and how many have a retained trigger the predicate matches |
unresolved_alert_ids |
The covered alerts that are not yet resolved |
timed_out |
The budget ran out. Counts it did not reach are null. |
You can also choose Ask AI to draft (if AI is configured). The AI picks among the same offered fields and returns a draft. Nothing is saved until you preview and save it yourself.
Saving¶
Saving calls POST /api/v1/detections/rules/{rule_id}/user-exclusions with:
{"schema_version": 1, "csrf_token": "…", "predicate": "…", "reason": "…", "alert_id": "<optional originating alert>"}
- The exclusion is active immediately and applies from the rule's next run.
- After saving, the console offers to resolve the covered unresolved alerts as false positive in one bulk triage step.
Other operations:
- List a rule's exclusions, pending and active, with their state and provenance:
GET …/rules/{rule_id}/user-exclusions. - Remove an active exclusion:
DELETE …/rules/{rule_id}/user-exclusions/{exclusion_id}(body:schema_version,csrf_token). - You can also type a predicate by hand in the rule's user exceptions. It is validated the same way.
AI-proposed exclusions (pending approval)¶
When the AI investigator decides that an alert in a case is a false positive, it can file an exclusion for that alert's rule. Such a proposal is pending:
- It is validated like any user exclusion.
- It is stored with the case, the alert and the preview counts it was filed with (
matches_without,matches_with,alerts_matched). - It suppresses nothing.
- Detection, rule tests and previews use active exclusions only.
Only you can activate a proposal. The AI has no path to approve one.
| Action | How |
|---|---|
| See pending proposals | The Overview page (count badge with Approve / Reject and links to the alert and case), the rule's user exceptions (marked "pending (AI)"), and the case summary tab. API: GET /api/v1/detections/pending-exclusions[?case_id=<uuid>], newest first, at most 200. |
| Approve | POST /api/v1/detections/rules/{rule_id}/user-exclusions/{exclusion_id}/approve. The predicate is validated again against the rule as it is installed now. Approval fails if the predicate no longer validates or the rule already has 16 active exclusions. |
| Reject | POST …/user-exclusions/{exclusion_id}/reject. Deletes the proposal. |
If you add, by hand, a predicate that the AI already proposed, that proposal is approved.
Mutes¶
A mute snoozes a rule without hiding its matches:
- Duration: exactly
24hor7d. A mute is active until it expires, and no job is needed to end it. - Scope: either the whole rule (no predicate), or the rule plus one entity. An entity predicate is built from the alert's trigger fields exactly like an exclusion and is validated the same way.
- Limit: 16 active mutes per rule.
While a mute is active, matching alerts are still stored with their normal severity and identity, but marked muted. A muted alert:
- never joins a case, so it never triggers an AI investigation;
- never produces a tray notification;
- is not counted as urgent;
- does not change the tray icon state.
A muted alert is listed with a muted marker. The first time an alert is stored decides whether it is muted. A later re-match of the same alert after the mute starts or ends does not change it.
| Action | API |
|---|---|
| Mute | POST /api/v1/detections/rules/{rule_id}/mutes with {"schema_version":1,"csrf_token":"…","duration":"24h"\|"7d","predicate":"…optional…","alert_id":"…optional…"} |
| End early | DELETE /api/v1/detections/rules/{rule_id}/mutes/{mute_id} |
| List active mutes | GET /api/v1/detections/mutes[?rule_id=]. Each mute has its expiry. The Alerts page lists active mutes with the time left and an Unmute button. |
Mutes are local operator state. They are not a tamper protection.
Audit trail¶
Every change to a rule's exclusions and mutes writes one row to an append-only tuning audit:
| Action | Written when |
|---|---|
exclusion_added |
You add a user exclusion |
exclusion_removed |
You remove one, or a custom-rule edit changed the source (detail "rule source changed") |
exclusion_proposed |
The AI files a pending exclusion |
exclusion_approved |
You approve a pending exclusion, or add a predicate the AI had proposed |
exclusion_rejected |
You reject a pending exclusion, or a source change removed it |
muted |
A mute is created (detail 24h or 7d) |
unmuted |
A mute is ended early (detail "ended early"), or removed by a source change |
Each row records:
- the predicate (empty for a whole-rule mute);
- the reason or detail;
- the originating alert, when there is one;
- the time.
To read the audit, open the rule's tuning history in the console or call GET /api/v1/detections/rules/{rule_id}/tuning-audit (the newest 100 entries).