Lesson 168 — The Detection Looked Good Until We Backtested It
The query was clean. The logic made sense. The signals correlated around the same process, device and time window. On paper, the detection looked ready.
Then we ran it against historical telemetry. Hundreds of results appeared — and most belonged to software deployment, administrator tooling and legitimate automation. Backtesting had done exactly what it was supposed to do: expose the difference between a good detection idea and a production-ready detection.

Your detection problem
A promising multi-signal rule performs badly when exposed to real historical data. Measure the noise, find recurring legitimate patterns, test known suspicious examples and tune without destroying the behaviour the detection was designed to find.
Detection briefing
Detection objective
Use KQL to evaluate a candidate detection against historical telemetry, measure how frequently it fires, identify the users, devices and command patterns creating noise, validate whether known suspicious activity remains detectable and document the tuning decisions made before production deployment.
Detection engineer's rule
Never tune only to make the result count smaller. A quiet rule that no longer detects the behaviour it was designed to find is not an improvement. Every exclusion must preserve the security hypothesis.
Stage 1 — run the candidate across a meaningful historical window
The first test is deliberately simple. Take the core behaviour from Lesson 167 and extend the time range. A one-hour or one-day development window may hide recurring legitimate activity. Thirty days gives us a much better view of prevalence across normal business cycles.
DeviceProcessEvents
| where Timestamp > ago(30d)
| where FileName in~ ("powershell.exe", "pwsh.exe")
| extend BrowserParent =
InitiatingProcessFileName in~ (
"chrome.exe",
"msedge.exe",
"firefox.exe"
)
| extend DownloadBehaviour =
ProcessCommandLine has_any (
"Invoke-WebRequest",
"DownloadString",
"WebClient",
"Start-BitsTransfer"
)
| extend EncodedCommand =
ProcessCommandLine has_any (
"-enc",
"-encodedcommand"
)
| extend SignalCount =
toint(BrowserParent)
+ toint(DownloadBehaviour)
+ toint(EncodedCommand)
| where SignalCount >= 2
| project Timestamp, DeviceName, AccountName,
ProcessCommandLine, InitiatingProcessFileName,
SignalCountThe first surprise
The query may return far more events than expected. That is useful information. Backtesting is not a demonstration that your query works syntactically; it is an experiment designed to discover how the logic behaves in the real environment.
Choose the window deliberately
Include enough history to capture patching, software deployment, month-end activity, administrator maintenance and other recurring patterns. A detection tested only during a quiet afternoon has barely been tested at all.
Stage 2 — measure before reading individual events
Do not manually open hundreds of rows. Summarise the backtest first. We want to know whether the matches are distributed across the estate or concentrated around a few users, devices or parent processes.
let Backtest =
DeviceProcessEvents
| where Timestamp > ago(30d)
| where FileName in~ ("powershell.exe", "pwsh.exe")
| extend BrowserParent =
InitiatingProcessFileName in~ (
"chrome.exe", "msedge.exe", "firefox.exe"
)
| extend DownloadBehaviour =
ProcessCommandLine has_any (
"Invoke-WebRequest", "DownloadString",
"WebClient", "Start-BitsTransfer"
)
| extend EncodedCommand =
ProcessCommandLine has_any (
"-enc", "-encodedcommand"
)
| extend SignalCount =
toint(BrowserParent)
+ toint(DownloadBehaviour)
+ toint(EncodedCommand)
| where SignalCount >= 2;
Backtest
| summarize Matches=count(),
Devices=dcount(DeviceId),
Users=dcount(AccountName)
by InitiatingProcessFileName
| order by Matches descConcentration tells a story
If 80 percent of the results come from three deployment servers or one administrator workflow, you have learned something actionable. The detection may not be fundamentally wrong; it may simply be missing environmental context.
Volume is not the only metric
Record unique devices, users, recurring command patterns and time distribution as well as total matches. Ten events across ten sensitive endpoints may matter more than one hundred repetitions of the same approved automation.
Stage 3 — find the false-positive clusters
Now group the command lines and process context. Normalise command-line values so repeated legitimate workflows become visible instead of appearing as hundreds of unrelated rows.
DeviceProcessEvents
| where Timestamp > ago(30d)
| where FileName in~ ("powershell.exe", "pwsh.exe")
| extend BrowserParent =
InitiatingProcessFileName in~ (
"chrome.exe", "msedge.exe", "firefox.exe"
)
| extend DownloadBehaviour =
ProcessCommandLine has_any (
"Invoke-WebRequest", "DownloadString",
"WebClient", "Start-BitsTransfer"
)
| extend EncodedCommand =
ProcessCommandLine has_any (
"-enc", "-encodedcommand"
)
| extend SignalCount =
toint(BrowserParent)
+ toint(DownloadBehaviour)
+ toint(EncodedCommand)
| where SignalCount >= 2
| summarize Matches=count(),
Devices=make_set(DeviceName, 10),
Users=make_set(AccountName, 10)
by InitiatingProcessFileName,
ProcessCommandLine
| order by Matches descInvestigate before excluding
A frequently occurring command is not automatically benign. Confirm the owner, purpose, deployment mechanism, expected devices and change history before creating an exclusion.
Prefer narrow exclusions
Exclude the verified workflow rather than an entire technology. Removing all PowerShell, all administrator accounts or all management servers may make the rule quiet while creating a large blind spot.
Stage 4 — tune the verified noise without erasing the hypothesis
Suppose investigation confirms that a signed internal deployment script creates a recurring legitimate pattern. Add the narrowest reliable environmental condition available and document why it exists.
let ApprovedDeploymentDevices =
dynamic([
"DEPLOY-01",
"DEPLOY-02"
]);
DeviceProcessEvents
| where Timestamp > ago(30d)
| where FileName in~ ("powershell.exe", "pwsh.exe")
| extend BrowserParent =
InitiatingProcessFileName in~ (
"chrome.exe", "msedge.exe", "firefox.exe"
)
| extend DownloadBehaviour =
ProcessCommandLine has_any (
"Invoke-WebRequest", "DownloadString",
"WebClient", "Start-BitsTransfer"
)
| extend EncodedCommand =
ProcessCommandLine has_any (
"-enc", "-encodedcommand"
)
| extend SignalCount =
toint(BrowserParent)
+ toint(DownloadBehaviour)
+ toint(EncodedCommand)
| where SignalCount >= 2
| where not(
DeviceName in~ (ApprovedDeploymentDevices)
and ProcessCommandLine has
"Approved-Software-Deployment.ps1"
)
| project Timestamp, DeviceName, AccountName,
ProcessCommandLine, SignalCountContext makes exclusions safer
The example requires both an approved device and a specific verified workflow. That is much safer than excluding every event containing a generic word such as “deployment”. Production allowlists should be governed and reviewed as environments change.
Every exclusion is a security decision
Record who validated the behaviour, why it is trusted, when the exclusion was added and what would cause it to be reviewed. An undocumented exclusion can become tomorrow's detection blind spot.
Stage 5 — test whether the detection still catches what matters
Tuning is only half the test. Now verify that suspicious examples still satisfy the detection logic. If you have labelled historical incidents, red-team activity or controlled test events, compare expected detections with actual detections.
let KnownSuspiciousDevices =
dynamic([
"LAB-WIN11-07",
"REDTEAM-CLIENT-02"
]);
DeviceProcessEvents
| where Timestamp > ago(30d)
| where DeviceName in~ (KnownSuspiciousDevices)
| where FileName in~ ("powershell.exe", "pwsh.exe")
| extend BrowserParent =
InitiatingProcessFileName in~ (
"chrome.exe", "msedge.exe", "firefox.exe"
)
| extend DownloadBehaviour =
ProcessCommandLine has_any (
"Invoke-WebRequest", "DownloadString",
"WebClient", "Start-BitsTransfer"
)
| extend EncodedCommand =
ProcessCommandLine has_any (
"-enc", "-encodedcommand"
)
| extend SignalCount =
toint(BrowserParent)
+ toint(DownloadBehaviour)
+ toint(EncodedCommand)
| project Timestamp, DeviceName,
AccountName, ProcessCommandLine,
BrowserParent, DownloadBehaviour,
EncodedCommand, SignalCount,
WouldDetect = SignalCount >= 2
| order by Timestamp descFalse negatives matter too
If a known suspicious event now produces WouldDetect = false, stop and investigate the tuning. Reducing false positives at the cost of obvious false negatives is not successful optimisation.
Known examples are valuable regression tests
Keep a small set of representative test cases where your process allows it. Each time the rule changes, rerun those cases. Detection engineering benefits from regression testing just as software engineering does.
Stage 6 — compare before and after
Finally, measure the effect of tuning. The goal is not zero results. The goal is a meaningful reduction in verified noise while retaining suspicious coverage and enough context for analysts to make decisions.
let BaseCandidates =
DeviceProcessEvents
| where Timestamp > ago(30d)
| where FileName in~ ("powershell.exe", "pwsh.exe")
| extend BrowserParent =
InitiatingProcessFileName in~ (
"chrome.exe", "msedge.exe", "firefox.exe"
)
| extend DownloadBehaviour =
ProcessCommandLine has_any (
"Invoke-WebRequest", "DownloadString",
"WebClient", "Start-BitsTransfer"
)
| extend EncodedCommand =
ProcessCommandLine has_any (
"-enc", "-encodedcommand"
)
| extend SignalCount =
toint(BrowserParent)
+ toint(DownloadBehaviour)
+ toint(EncodedCommand)
| where SignalCount >= 2;
BaseCandidates
| summarize BeforeTuning=count(),
VerifiedNoise=countif(
ProcessCommandLine has
"Approved-Software-Deployment.ps1"
),
RemainingCandidates=countif(
ProcessCommandLine !has
"Approved-Software-Deployment.ps1"
)Measure the trade-off
Record candidate volume before and after tuning, the verified noise removed and the suspicious test cases retained. Those measurements make the change reviewable instead of relying on “the results look better now”.
Backtesting never really ends
Software changes, administrators introduce new workflows and attackers change techniques. A production detection should be reviewed when its result profile changes significantly, not frozen forever after its first successful test.
Agent Foskett's detection backtesting workflow
What a backtest can reveal
| Backtest finding | What it may mean | Detection response |
|---|---|---|
| Very high result volume | The behaviour is common or the logic is too broad. | Group results and identify dominant legitimate patterns. |
| Results concentrated on a few devices | Management or deployment infrastructure may explain the activity. | Investigate the workflow before considering a narrow exclusion. |
| Known suspicious example is missed | The threshold or tuning may be too restrictive. | Review the logic before production deployment. |
| One command dominates results | A repeatable legitimate workflow or widespread suspicious activity may exist. | Validate ownership and purpose; never assume frequency means benign. |
| Noise falls but suspicious tests still match | Tuning may be improving precision without obvious coverage loss. | Document the evidence and continue validation. |
Write the backtest like a detection engineer
Example: The candidate detection was evaluated against 30 days of historical endpoint telemetry. Initial testing produced a high volume of matches concentrated around a small number of approved software deployment workflows. Those patterns were investigated and verified before narrowly scoped exclusions were introduced. The tuned logic was then rerun against the same historical period and tested against known suspicious examples to confirm that the reduction in false positives did not remove the behaviour the detection was designed to identify. Result volume, exclusions and validation outcomes were documented before production promotion.
Lesson 168 key takeaways
- A syntactically correct KQL query is not automatically a production-ready detection.
- Backtest candidate detections across a meaningful historical period.
- Measure result volume, prevalence, affected devices, users and recurring patterns.
- Investigate frequent behaviour before deciding that it is benign.
- Use the narrowest reliable exclusion for verified legitimate activity.
- Never tune solely to reduce the number of alerts.
- Retest known suspicious examples after every material tuning change.
- False negatives are as important to understand as false positives.
- Document exclusions, assumptions, validation evidence and tuning decisions.
- Detection backtesting should continue as the environment and threat landscape change.
Module 14 — now measure whether analysts can live with it
Lesson 168 showed that historical testing can expose noise and blind spots before a detection reaches production. But even a technically accurate detection can fail operationally if it fires too often, duplicates other analytics or consumes more analyst time than the risk justifies. Next we move from query quality to operational quality.
Continue your KQL investigation training
🔎 KQL Academy — Module 14: Advanced Detection Engineering & Proactive Threat Hunting
Backtest KQL detections against historical telemetry
Lesson 168 of the Agent Foskett KQL Academy shows how to test candidate Microsoft Defender XDR detection logic across historical endpoint telemetry before production deployment.
Measure false positives and preserve detection coverage
Learn how to measure result prevalence, cluster recurring legitimate activity, create narrow exclusions, validate known suspicious examples and document detection tuning decisions.
