The Problem#
Attack Surface Reduction rules are one of the most effective endpoint security controls in Microsoft Defender for Endpoint. There are 19 of them as of this writing (Microsoft adds new ones periodically – verify the current list against the official documentation). They cover everything from blocking Office macro API calls to preventing credential theft from LSASS. They’re also one of the fastest ways to break your environment if you enable them in block mode without understanding what they’ll catch.
The recommended approach is simple in theory: enable rules in audit mode first, review the data, then move to block. In practice, getting that data out of Defender at scale – across all rules, across thousands of devices, scoped to specific machine groups – is a non-trivial engineering problem. The Defender portal’s built-in ASR report gives you a summary, but not the detailed per-event data you need to make rule-by-rule decisions.
Prerequisites#
- Defender for Endpoint P2 (Advanced Hunting access)
- PowerShell 7.x with
Microsoft.Graph.AuthenticationandImportExcelmodules - Interactive sign-in via
Connect-MgGraphwithThreatHunting.Read.Alldelegated scope - An account with access to Advanced Hunting in the Defender portal
Implementation#
The ASR Rule Registry#
The scripts define all current ASR rules with their GUIDs. This is your source of truth – Microsoft doesn’t expose a “list all ASR rules” API, so you maintain this table yourself:
$AsrRules = @(
[PSCustomObject]@{ RuleName = "Block abuse of exploited vulnerable signed drivers"
RuleId = "56a863a9-875e-4185-98a7-b882c64b5ce5" }
[PSCustomObject]@{ RuleName = "Block Adobe Reader from creating child processes"
RuleId = "7674ba52-37eb-4a4f-a9a1-f0f9a1619a2c" }
[PSCustomObject]@{ RuleName = "Block credential stealing from the Windows local security authority subsystem (lsass.exe)"
RuleId = "9e6c4e1f-7d60-472f-ba1a-a39ef669e4b2" }
# ... all 19 rules defined in the script
[PSCustomObject]@{ RuleName = "Use advanced protection against ransomware"
RuleId = "c1db55ab-c21a-4637-bb3f-a12568109d35" }
)The full list with all 19 GUIDs is in the script. The snippet above is abbreviated – don’t copy-paste it, use the script directly.
The Advanced Hunting Query#
For each rule, the script runs a KQL query against DeviceEvents, joining with DeviceInfo to get machine group context. The query includes pagination via serialize and row_number() for offset-based batching:
DeviceEvents
| where Timestamp >= fromTime and Timestamp < toTime
| where ActionType startswith "ASR"
| where ActionType contains "Audited"
| extend AF = todynamic(AdditionalFields)
| where tostring(AF["RuleId"]) == ruleId
| join kind=leftouter (
DeviceInfo
| summarize arg_max(Timestamp, MachineGroup) by DeviceId
) on DeviceId
| order by Timestamp desc
| serialize RowNum = row_number()
| where RowNum > offset and RowNum <= upperThe blocked report version swaps "Audited" for "Blocked". The DeviceInfo join gives you machine group membership, which is critical for scoping rollouts – you want to know which rules are noisy in which parts of your environment.
Adaptive Chunking – The Hard Part#
The Defender Advanced Hunting API returns a maximum of 10,000 rows per query, enforces query execution time limits, and will throttle you with 429 responses under load. For a busy ASR rule across 30 days, you might have hundreds of thousands of events.
The script handles this with a batched, chunked, recursive approach:
$DetailBatchSize = 10000 # Rows per query page
$DetailChunkDays = 2 # Time window per chunk
$ApiRetryMax = 6 # Max retries on throttling
$MinAdaptiveChunkMinutes = 60 # Minimum window before giving up- Time chunking: The query period (e.g., 30 days) is split into 2-day windows. Each window is queried independently.
- Row pagination: Within each window, results are paginated using
row_number()and offset-based filtering. - Retry with exponential backoff: 429 responses trigger up to 6 retries with jittered exponential backoff.
- Adaptive window splitting: If a time chunk fails entirely after all retries, the script splits it in half and tries each sub-window recursively. This continues until the window reaches the minimum threshold (60 minutes).
if (-not $result.Succeeded) {
$windowMinutes = [int][Math]::Floor(($WindowEnd - $WindowStart).TotalMinutes)
if ($windowMinutes -gt $MinAdaptiveChunkMinutes) {
$midpoint = $WindowStart.AddMinutes(
[Math]::Floor(($WindowEnd - $WindowStart).TotalMinutes / 2))
$left = Get-WindowDetails -WindowStart $WindowStart -WindowEnd $midpoint -WindowIndent ($WindowIndent + 1)
$right = Get-WindowDetails -WindowStart $midpoint -WindowEnd $WindowEnd -WindowIndent ($WindowIndent + 1)
}
}This recursive splitting is what makes the script viable for noisy rules in large environments. A rule like “Block execution of potentially obfuscated scripts” in audit mode can generate enormous volumes. Fixed-size queries will either miss data or fail entirely.
Machine Group Filtering#
You can scope the report to specific Defender machine groups (comma or semicolon separated):
.\Get-ASR-Audit-Report.ps1 -QueryDays 14 -MachineGroupFilter "Pilot Group 1;Pilot Group 2"No app registration, no secrets – Connect-MgGraph handles the interactive auth and token refresh automatically.
This injects a KQL filter clause into the query. Essential for staged rollouts – audit a rule against your pilot group, review the data, then expand scope.
Data Integrity#
The script tracks query success status per rule and per chunk. If any chunk fails completely (even after adaptive splitting), the rule is marked with a Partial or Failed status:
$RequireCompleteData = $true
if ($RequireCompleteData -and -not $DetailQueryResult.Succeeded) {
throw "Incomplete data for ASR rule '$RuleName' ($RuleId). Stopping to avoid incorrect report."
}With $RequireCompleteData = $true (the default), the script halts entirely if any rule returns incomplete data. A failed run is better than a quiet lie – you don’t want to make blocking decisions based on a report that silently dropped 40% of the events.
Excel Output#
The output is a multi-sheet Excel file: a Summary sheet sorted by event count (descending), plus one detail sheet per rule. Each detail row includes device name, timestamp, file paths, initiating process, and machine group.
What to Look For#
When reviewing the report, here’s the decision framework:
| Audit Event Count (30 days) | Recommended Action |
|---|---|
| 0 events | Safe to move to Block – no legitimate activity triggered this rule |
| 1-50 events | Review the file paths and processes. Usually a handful of known tools that need exclusions |
| 50+ events | Investigate before blocking. Scope exclusions first, re-audit, then decide |
Rules that are commonly noisy in enterprise environments:
- “Block execution of potentially obfuscated scripts” – catches legitimate PowerShell tooling, deployment agents, monitoring scripts
- “Block credential stealing from LSASS” – security scanners and some backup agents trigger this
- “Block process creations originating from PSExec and WMI commands” – SCCM, remote management tools, and admin scripts
Security Considerations#
ThreatHunting.Read.Allgrants read access to all Advanced Hunting data, not just ASR events. This is audit tooling – the account running it should have appropriate access.- The fallback query (used when the primary query with
DeviceInfojoin fails) omits machine group data. Some rows may lack group context, but you still get the event data. - Report files contain device names, user accounts, file paths, and command lines. Treat them as sensitive security data.