Skip to content

Rules and detection

FreeDR detects threats by running SQL rules over the telemetry it stores in its local DuckDB evidence store. Each rule is a YAML (or JSON) document that holds one DuckDB SELECT over one typed data source, plus metadata such as severity, confidence, ATT&CK techniques and an alert text template. When a rule matches, FreeDR stores an alert. Related alerts are then grouped into cases (see Alerts and cases).

This page covers:

  • the built-in rule pack that ships with the agent;
  • how and when rules run;
  • how to tell whether detection is healthy.

Related pages:

The built-in rule pack

Property Value
Pack core-windows, version 0.3.0 ("Built-in Windows detection content")
Rules 345
Rule schema All rules use schema_version: 6 and evidence_semantics: exact
Rule kinds 342 event_match (one alert per matching event), 3 threshold (one alert per run when enough events match)
Delivery Compiled into the agent binary. Nothing is loaded from disk at runtime, so a missing or edited file on the host cannot change the shipped rules.
Editing Built-in rules are read-only. You can enable or disable them, add your own exclusions, and mute them. You cannot change their SQL or metadata.
Default state Every built-in rule is enabled until you disable it.

Each rule has its own rule_version (from 1 to 11 in the current pack). FreeDR uses the version to tell matches apart: it gives an alert from version 3 of a rule a different identity than an alert from version 2 on the same event. See How alerts are created.

Categories and severities

The category of a built-in rule must be one of the ATT&CK enterprise tactics below, or malware for Microsoft Defender verdicts that have no tactic. Custom rules may use any category token.

Category Rules Critical High Medium Low Informational
defense_evasion 115 1 80 22 12 0
persistence 83 1 34 29 19 0
credential_access 42 3 32 7 0 0
execution 27 0 15 9 1 2
command_and_control 18 0 13 4 1 0
impact 17 0 12 5 0 0
discovery 14 0 3 7 4 0
lateral_movement 9 0 2 4 3 0
privilege_escalation 6 1 4 1 0 0
malware 5 1 2 1 1 0
collection 3 0 3 0 0 0
exfiltration 3 0 0 3 0 0
initial_access 3 0 0 2 1 0
Total 345 7 200 94 42 2

Declared confidence: 66 high, 253 medium, 26 low. Confidence is stored on each alert and you can filter alerts by it. It does not change how alerts are grouped into cases or whether you are notified.

The severity decides what happens after a match:

Severity Creates a case automatically Automatic AI investigation of the case Tray notification Counts as "urgent"
critical yes yes, if AI is configured yes, if enabled for this level yes
high yes yes, if AI is configured yes, if enabled for this level yes
medium yes yes, if AI is configured if your minimum is medium or low no
low yes no (cases with low severity are not auto-investigated) if your minimum is low no
informational no no never no

Informational alerts are stored and listed like any other alert, but they never enter case automation. The two informational built-in rules are "New PowerShell Instance Created" and "Suspicious command shell start". For details see Alerts and cases.

Data sources

Every rule reads exactly one typed source in its FROM clause. Each typed source is one table in the evidence store.

Source table What it holds Built-in rules
kernel_process_etw Process starts, exits and image loads from the kernel process ETW provider, with parent and actor lineage stamped at collection time 159
windows_event_log Windows Event Log records (Security, System, Sysmon, PowerShell, Defender, Task Scheduler and others) 97
registry_etw Registry value sets and deletes, key creates and deletes, and key security changes from the registry ETW provider 59
file_etw File create/open, write, rename, delete, set-info and read operations from the file ETW provider 25
usn NTFS change journal (USN) records 3
artifact Collected file content facts: hash, Authenticode status, signer and PE header fields 2

A few rules also read history from their own source (<source>_context, 7 rules), another typed source (3 rules), or artifact_path_state, which holds the latest collected facts for each file path (7 rules). The catalog shows these in its "Also reads" column.

Each rule also lists the telemetry it depends on in required_telemetry; 339 built-in rules declare it. This is descriptive metadata only. FreeDR does not check that the source is actually collecting. A rule whose source is off simply never matches. The console's Coverage view groups rules by technique and repeats this warning: declared coverage does not prove that an activity would be detected.

MITRE ATT&CK mapping

  • 333 of the 345 rules declare at least one ATT&CK technique in attack_techniques. The pack covers 178 distinct technique and sub-technique IDs, across 87 parent techniques.
  • The 12 rules without a technique are mostly the Microsoft Defender verdict rules (category malware) plus a few task-deletion and PowerShell rules.
  • The most frequent techniques are T1059.001 (PowerShell, 32 rules), T1003.001 (LSASS memory, 26), T1112, T1562.001 and T1547.001 (20 each), T1204.002 (19) and T1105 (17).
  • A rule can list at most 32 technique IDs, each in the form T1234 or T1234.567.

The mapping is metadata. You can use it in the console to search rules and to build the Coverage view. It does not change how FreeDR detects.

Where the rules come from

Most shipped rules are translations of public detection content into FreeDR's typed SQL. Mainly that is SigmaHQ Sigma rules; others come from the mdecrevoisier Sigma collection, Hayabusa, Elastic, Splunk and Velociraptor. Where a rule was translated, a comment at the top of its YAML file names the frozen upstream file, its SHA-256 and its upstream ID. Some rules have no such header; these were written for FreeDR or carry no upstream reference.

Each rule's description explains what the translation keeps and what it cannot evaluate from FreeDR's telemetry. For example, PE OriginalFileName is not retained, so a renamed copy of a binary is outside the scope of name-based process rules.

How detection runs

Schedule

Every rule has a priority, and the priority sets how often the rule runs:

Priority Runs every Built-in rules
P1 2 minutes 114
P2 8 minutes 188
P3 15 minutes 43

The detection engine runs three independent lanes, one per priority. On each tick a lane:

  • runs every enabled built-in rule of that priority;
  • runs the latest version of every enabled custom rule of that priority, after validating it again (a custom rule that no longer validates is skipped and marks detection degraded).

Limits:

  • At most 16 rule runs execute at the same time.
  • Each rule runs one window at a time. If the previous run of a rule is still going, the new tick is recorded as a busy_skip.

Which events a run looks at

Detection is incremental and works on ingest time:

  • Each rule keeps a cursor, which is the ingest time (written_at) up to which it has already evaluated.
  • A run evaluates the rows of its source written after the cursor, up to the current ingest boundary. When the run succeeds, the cursor advances.
  • When a rule has no cursor, it looks back over its timeframe. This happens the first time a rule runs and after its rule_version changes.
  • Each event is therefore evaluated once per rule version, even when ingestion is delayed.

Runs read only the recent ("hot") part of the evidence store and never open the older Parquet files. This keeps the cost of a run independent of how long FreeDR has been running. If a rule falls so far behind that its cursor is older than the oldest hot row (for example after a long failure backoff), it evaluates what hot still holds. It then records the gap (hot_gap_end_us) in its run history and moves on. Hot retention therefore has a minimum of 45 minutes.

Timeframe

A rule's timeframe must be 2m, 8m or 15m. It bounds how much history the rule may correlate with:

  • Context reads. <source>_context and other typed sources contain only rows whose event time lies between the earliest new candidate minus the timeframe and the latest candidate plus 60 seconds.
  • Literals. An INTERVAL literal longer than the timeframe is rejected when the rule is loaded.
  • Threshold rules. A threshold rule's timeframe must equal its run interval, for example 8m for P2. All 3 built-in threshold rules follow this.

SQL over typed tables

A rule query is ordinary DuckDB SQL, with these restrictions:

  • one SELECT;
  • exactly one typed source in FROM;
  • no joins, CTEs or table functions;
  • no other tables.

In the query, the source name (for example kernel_process_etw) means the new rows of this run, after exclusions are applied. <source>_context means the time-bounded history of the same source. The query must return event_id and occurred_at first, then one column for each {{placeholder}} in the alert text. See Writing custom rules for the full contract.

A run may return at most 1,000 rows:

  • The built-in rules end in LIMIT 1000, so further matches in the same window are not alerted.
  • A query without its own limit that returns more than 1,000 rows fails the run.

Late, failed and skipped runs

Each run gets a 5-second budget.

Late runs. A run that overruns its budget is late, not failed. Nothing is recorded as a failure, the cursor does not move, and the next tick retries the same window with a doubled budget, up to 64 times the original (320 seconds). Only one extended-budget retry runs at a time, so slow rules cannot take every database reader.

Run outcomes. Every other run is recorded in the run history with one of these outcomes:

Outcome Meaning Counted as
success The window was evaluated and its alerts were committed successful
evaluation_failure The query failed. Examples: invalid SQL against current data, a NULL value in a placeholder column, or more than 1,000 rows failed
render_failure The alert text could not be rendered. Examples: a control character in a value, or a description over 4 KiB failed
persistence_failure The alerts could not be written to the control store failed
cancelled The service shut down during the run failed
busy_skip The previous run of the same rule was still going skipped
backoff_skip The rule is waiting out a failure backoff skipped

Failure backoff.

  • After a failure, the rule waits one cadence. Each further consecutive failure doubles the wait, up to 32 cadences.
  • A success resets the counter.
  • The cursor is not advanced by a failure, so the window is retried later.
  • A shutdown cancellation is recorded but does not start a backoff.

Detection health

The Alert rules page header shows whether the engine is healthy, plus its successful and failed run counts. Selecting a rule shows its recent runs.

The same data is available from the local API:

  • GET /api/v1/detections/health returns:

    Field Meaning
    rules Built-in plus custom rule count
    successful_runs, failed_runs, skipped_runs Lifetime totals from the run history
    last_completed_at_us When the last run completed
    degraded, last_runtime_error Set when a rule fails, a custom rule is invalid, the database worker is unavailable, or a control-store check fails
    late_runs Evaluations that overran their budget since the service started
    lagging_rules Rules whose current window is still waiting for a late result
    sqlite_health Result of the integrity check of the control store, which runs every 5 minutes
    case_automation Processed count, backlog flag and last error of the alert-to-case worker
  • GET /api/v1/detections/runs?rule_id=<id>&limit=<1-100> returns the latest runs. Each run includes its window, start and end times, outcome, match count, alerts inserted and any hot_gap_end_us.

  • GET /api/v1/detections/rules lists every rule with its enabled state and its last run.

Run history is control data. It expires with the evidence retention period (retention_days, 90 days by default), but the lifetime health totals are kept.

Enabling and disabling rules

You can enable or disable any rule, built-in or custom, from the rule list (including several at once) or with PATCH /api/v1/detections/rules/{rule_id}/state ({"schema_version":1,"csrf_token":"…","enabled":true|false}).

  • The state is stored locally, and every change is kept in an append-only history.
  • A disabled rule is skipped. It does not count as a failure.
  • When you enable it again, it continues from its cursor. If that cursor is older than the hot data, the run records a gap as described above.