Auditing Detection Rules You Did Not Write
A 2026 maturity assessment found 4,712 rules in production. 312 had fired.
That ratio is the normal starting position, not an outlier. This is the audit that establishes yours: one API call to inventory what is deployed, three queries to find what fires, the step that separates cannot-fire from has-not-fired, and the metrics to replace rule count with.
1. Inventory what is deployed
Sentinel analytics rules are ARM resources, so they are not queryable from Log Analytics. List them from the management API. The response carries everything the audit needs per rule: displayName, enabled, severity, tactics, query, queryFrequency, queryPeriod, triggerOperator, triggerThreshold and lastModifiedUtc.
GET https://management.azure.com/subscriptions/{subscriptionId}
/resourceGroups/{resourceGroup}
/providers/Microsoft.OperationalInsights/workspaces/{workspace}
/providers/Microsoft.SecurityInsights/alertRules
?api-version=2025-09-01The Az.SecurityInsights PowerShell module wraps the same endpoint if you would rather not handle tokens. For a no-code route, Sentinel ships a Workspace Usage Report workbook whose regular-checks tab lists active rules directly.
lastModifiedUtc as you go. A rule untouched since the workspace was built is a different problem from one someone tuned last month, and the difference decides whether you fix it or retire it.2. Find what actually fires
Alerts land in SecurityAlert, incidents in SecurityIncident. Ninety days is the shortest window that survives one quiet month.
SecurityAlert
| where TimeGenerated > ago(90d)
| summarize Alerts = count(), LastSeen = max(TimeGenerated)
by AlertName, ProductName
| order by Alerts descDiff those AlertName values against the displayName list from step 1. Anything enabled but absent has not fired in ninety days. That is your never-fired set, and on first run it is usually most of the estate.
SecurityIncident
| where TimeGenerated > ago(90d)
| summarize Incidents = count() by Title, Severity
| order by Incidents descA rule with many alerts and no incidents is firing into a void. A rule with many incidents that all close benign is worse than one that never fires: it consumes triage capacity and teaches the team to distrust the queue.
3. Separate cannot-fire from has-not-fired
This is the step that turns a list into a finding. A never-fired rule is either watching for something that has not happened, which is fine, or reading a table with no data in it, which is a false assurance of coverage.
Usage
| where TimeGenerated > ago(7d)
| summarize GB = round(sum(Quantity) / 1024, 2) by DataType
| order by GB descRead each never-fired rule's query property from step 1, note the tables it references, and check them here. A rule referencing a table with no volume, or absent entirely, cannot fire. That is a connector problem, and no amount of rule tuning touches it.
The published 2026 assessment that found 312 of 4,712 rules firing is instructive less for the ratio than for what the ratio was made of. A body of content accumulates when detections are a side project for whoever has bandwidth: nobody owns it, nobody tests it, nobody retires it.
Which means the audit's value is not the never-fired count. It is splitting that count three ways: rules that cannot fire (fix the connector or retire the rule), rules watching for something genuinely absent (keep, and write down what would trip them), and rules whose threshold or scope was copied from a template and never matched your volume (read the query). Only the third group rewards tuning.
4. Classify every rule
One row per rule. The action column is the point: an audit that produces a list without decisions gets filed and changes nothing.
| Class | Signature | Action |
|---|---|---|
| Cannot fire | References a table with no ingestion. | Fix the connector or retire the rule. Leaving it enabled reports coverage you do not have. |
| Never fired, plausible | Data present, behaviour not seen. | Keep, and record what would make it fire so the next reviewer does not re-litigate it. |
| Never fired, implausible | Data present, behaviour common. | Read the query. Usually a threshold from a template that does not match your volume. |
| Fires, no incidents | Alerts exist, nothing correlates. | Check incident creation and grouping before touching the query. |
| Fires, all benign | Incidents close as false positive. | Tune the scope, not the severity. Suppressing the noisy field usually removes the signal too. |
| Fires usefully | Produces incidents that get worked. | Record the entity mapping. It is the template for the next rule. |
| Superseded by a control | A platform control now blocks the behaviour outright. | Retire it. If a control covers the behaviour, the rule is paying compute to observe something prevented. |
A technique that gave a reliable signal in 2022 may be evaded trivially by 2026. Rules are not permanent assets, and a body of content with no deprecation policy grows broader rather than sharper. Mature programmes treat retirement as an outcome, not an admission.
Metrics that count, and the one that does not
Rule count is the metric to abandon first. Mature programmes treat detection content as software with a backlog, a release process, a test suite and a deprecation policy, and measure output rather than inventory.
| Measure | Why it is the right one |
|---|---|
| Incidents detected | The actual output. Rules deployed is inventory, not outcome. |
| Mean time to detection | Moves when detection genuinely improves; unmoved by adding content. |
| False positive rate, tracked as a trend | Published maturity models treat a sustained quarterly reduction as the signal, with roughly a quarter off in a defined programme and half or better in a managed one. |
| Rules retired per quarter | A programme that never deprecates is accumulating, not maturing. |
| Proportion of rules with a test case | Distinguishes content you can vouch for from content you inherited. |
What to do after the audit, in order
| # | Do | Not yet |
|---|---|---|
| 1 | Fix or retire everything in cannot-fire. | Adding rules. The pile is already unreliable. |
| 2 | Read the query of every implausible never-fired rule. | Importing another rule pack. |
| 3 | Tune the all-benign set by scope. | Automating response on rules you do not trust. |
| 4 | Put what survives in version control with a test case each. | A coverage heatmap. It counts rules you cannot vouch for. |
| 5 | Write the deprecation policy before the next intake. | Threat intel feeds. Indicators age; behaviour outlasts them. |
Every step turns on reading a rule's query and saying whether it would catch what it claims to. That is also where the durable value sits now: the detection work most exposed to automation is drafting rules from scratch, while reviewing a generated rule, finding the evasion gap and explaining it in a peer review is not.
Build the capability, not the rule count
The SOC and Detection Engineer path builds it in the order this audit assumes: query fluency, then authoring, then tuning against real noise, then coverage measurement. Hands-on against a realistic enterprise dataset, so the person doing it tunes real rules before they touch yours. Business seats are $324 per seat per year, any number of seats, one invoice.
See Business pricingWeekly security engineering insights
Detection techniques, architecture patterns, and operational judgment, every Tuesday.
No spam. Unsubscribe anytime.