Lesson 169 — The Detection Worked — But It Fired 600 Times a Day
The detection was technically correct. It found the behaviour we designed it to find. Backtesting had removed the obvious false positives, and known suspicious examples still matched.
Then it went operational — and produced roughly 600 candidate events a day. The problem was no longer whether the query detected suspicious behaviour. The problem was whether a SOC could realistically investigate what it produced.

Your detection problem
The rule works, but repeated events and duplicated behaviour create far more output than analysts can handle. Measure what is driving the volume, group related activity and turn hundreds of raw matches into a smaller number of investigation-ready candidates.
Detection briefing
Detection objective
Use KQL to measure how often a working detection fires, identify the behaviours creating excessive volume, aggregate related events into investigation groups, prioritise stronger combinations and produce an output that gives analysts useful context without creating an alert for every raw event.
Detection engineer's rule
Event count is not investigation count. Ten related process events on the same endpoint within a short period may describe one security story. Treating every telemetry row as a separate alert is often an engineering failure.
Stage 1 — measure the operational volume
Start with evidence. Before changing thresholds or adding exclusions, establish how much activity the candidate detection produces each day and how broadly it is distributed across devices and users.
let DetectionCandidates =
DeviceProcessEvents
| where Timestamp > ago(14d)
| 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;
DetectionCandidates
| summarize CandidateEvents=count(),
Devices=dcount(DeviceId),
Users=dcount(AccountName)
by bin(Timestamp, 1d)
| order by Timestamp ascLook for spikes and consistency
Six hundred matches on one unusual day is different from six hundred every day. Daily bins show whether the problem is persistent, periodic or driven by a specific operational event.
Ask what the SOC would receive
If every matching row became a separate alert, translate that into analyst workload. Detection quality includes the operational cost of triage.
Stage 2 — find what is driving the volume
Now identify repeated combinations of account, parent process and command line. High-volume detections often contain a small number of behaviours repeated many times.
let DetectionCandidates =
DeviceProcessEvents
| where Timestamp > ago(14d)
| where FileName in~ ("powershell.exe", "pwsh.exe")
| extend DownloadBehaviour =
ProcessCommandLine has_any (
"Invoke-WebRequest", "DownloadString",
"WebClient", "Start-BitsTransfer"
)
| extend EncodedCommand =
ProcessCommandLine has_any ("-enc", "-encodedcommand")
| where DownloadBehaviour or EncodedCommand;
DetectionCandidates
| summarize Events=count(),
FirstSeen=min(Timestamp),
LastSeen=max(Timestamp),
Devices=dcount(DeviceId)
by AccountName,
InitiatingProcessFileName,
ProcessCommandLine
| order by Events descFrequency needs interpretation
A repeated command may be benign automation, a noisy application or persistent malicious activity. Frequency tells you where to investigate first; it does not tell you the verdict.
Do not immediately create exclusions
The objective here is to understand the shape of the detection output. Exclusions should follow validation, not simply frustration with a large result count.
Stage 3 — group events that describe the same story
Instead of generating a candidate for every event, group activity around the same device and account within a 15-minute window. This begins turning telemetry into investigation units.
let DetectionCandidates =
DeviceProcessEvents
| where Timestamp > ago(1d)
| where FileName in~ ("powershell.exe", "pwsh.exe")
| extend DownloadBehaviour =
ProcessCommandLine has_any (
"Invoke-WebRequest", "DownloadString",
"WebClient", "Start-BitsTransfer"
)
| extend EncodedCommand =
ProcessCommandLine has_any ("-enc", "-encodedcommand")
| where DownloadBehaviour or EncodedCommand;
DetectionCandidates
| summarize EventCount=count(),
FirstSeen=min(Timestamp),
LastSeen=max(Timestamp),
Commands=make_set(ProcessCommandLine, 5)
by DeviceId, DeviceName,
AccountName,
bin(Timestamp, 15m)
| order by EventCount descAggregation changes the question
We are no longer asking “how many events matched?” We are asking “how many distinct bursts of suspicious behaviour would an analyst need to investigate?”
Choose grouping keys carefully
Device, user and time are useful starting points, but different detections may need process identity, remote destination, file hash or another entity. Poor grouping can accidentally merge unrelated activity.
Stage 4 — measure raw events versus investigation groups
Now quantify the effect. Compare the number of raw matching events with the number of grouped investigation candidates. This gives us an operational metric instead of relying on intuition.
let DetectionCandidates =
DeviceProcessEvents
| where Timestamp > ago(7d)
| where FileName in~ ("powershell.exe", "pwsh.exe")
| extend SuspiciousCommand =
ProcessCommandLine has_any (
"Invoke-WebRequest", "DownloadString",
"-enc", "-encodedcommand"
)
| where SuspiciousCommand;
DetectionCandidates
| summarize RawEvents=count(),
FirstSeen=min(Timestamp),
LastSeen=max(Timestamp)
by DeviceId, DeviceName,
AccountName,
bin(Timestamp, 30m)
| summarize InvestigationGroups=count(),
TotalRawEvents=sum(RawEvents),
MaxEventsInGroup=max(RawEvents)
by bin(FirstSeen, 1d)
| extend ReductionPercent =
round(
100.0 * (1.0 -
todouble(InvestigationGroups) /
todouble(TotalRawEvents)), 1
)
| order by FirstSeen ascReduction is not automatically improvement
A 90 percent reduction sounds impressive, but only if the grouped output still separates genuinely different incidents. Always inspect examples around the boundaries of your grouping logic.
Think in analyst workload
If 600 events become 35 coherent investigation groups, that is a meaningful operational change. The security evidence has not disappeared; it has been packaged more intelligently.
Stage 5 — prioritise the groups with stronger evidence
Not every group deserves equal priority. Add an explainable score based on the behaviours present inside each group. Here download activity and encoded execution carry more weight, while repeated activity adds supporting context.
let GroupedCandidates =
DeviceProcessEvents
| where Timestamp > ago(1d)
| where FileName in~ ("powershell.exe", "pwsh.exe")
| extend DownloadBehaviour =
ProcessCommandLine has_any (
"Invoke-WebRequest", "DownloadString",
"WebClient", "Start-BitsTransfer"
)
| extend EncodedCommand =
ProcessCommandLine has_any ("-enc", "-encodedcommand")
| summarize EventCount=count(),
DownloadEvents=countif(DownloadBehaviour),
EncodedEvents=countif(EncodedCommand),
Commands=make_set(ProcessCommandLine, 5)
by DeviceId, DeviceName,
AccountName, bin(Timestamp, 30m);
GroupedCandidates
| extend PriorityScore =
iff(DownloadEvents > 0, 2, 0)
+ iff(EncodedEvents > 0, 2, 0)
+ iff(EventCount >= 5, 1, 0)
| where PriorityScore >= 3
| order by PriorityScore desc, EventCount descThresholds should reflect capacity and risk
A threshold is not just mathematics. Test what sits immediately above and below it, and consider whether important low-frequency behaviour would be lost.
Keep the reasons visible
Analysts should know why one group was prioritised over another. Scoring should make triage easier, not hide logic behind an unexplained number.
Stage 6 — produce an investigation-ready detection output
The final output should represent the security story: when the activity started and ended, which device and account were involved, how many events contributed and what suspicious behaviours caused the group to surface.
let DetectionCandidates =
DeviceProcessEvents
| where Timestamp > ago(1h)
| where FileName in~ ("powershell.exe", "pwsh.exe")
| extend DownloadBehaviour =
ProcessCommandLine has_any (
"Invoke-WebRequest", "DownloadString",
"WebClient", "Start-BitsTransfer"
)
| extend EncodedCommand =
ProcessCommandLine has_any ("-enc", "-encodedcommand")
| where DownloadBehaviour or EncodedCommand;
DetectionCandidates
| summarize EventCount=count(),
FirstSeen=min(Timestamp),
LastSeen=max(Timestamp),
DownloadEvents=countif(DownloadBehaviour),
EncodedEvents=countif(EncodedCommand),
Commands=make_set(ProcessCommandLine, 5)
by DeviceId, DeviceName,
AccountName, bin(Timestamp, 30m)
| extend DetectionReasons =
strcat(
iff(DownloadEvents > 0, "Download behaviour; ", ""),
iff(EncodedEvents > 0, "Encoded execution; ", ""),
iff(EventCount >= 5, "Repeated activity; ", "")
)
| project FirstSeen, LastSeen,
DeviceId, DeviceName, AccountName,
EventCount, DetectionReasons, Commands
| order by FirstSeen descOne candidate, supporting evidence
The analyst receives one coherent detection candidate while retaining the command lines and event count that support it. Aggregation reduces duplicate work without throwing away the evidence.
Monitor after deployment
Operational tuning is not permanent. Track daily candidate counts and investigate significant changes. A sudden volume increase may represent environmental drift, broken tuning or a real attack campaign.
Agent Foskett's operational detection workflow
When a working detection becomes operationally noisy
| Observation | Possible problem | Engineering response |
|---|---|---|
| Hundreds of matching rows | Each telemetry event is being treated as a separate candidate. | Group related events around meaningful entities and time windows. |
| Same command repeats continuously | One workflow or behaviour dominates output. | Validate it, then decide whether grouping or a narrow exclusion is appropriate. |
| Many events on one device | A single execution chain may generate multiple telemetry records. | Correlate around device, account, process and time. |
| Alert volume exceeds SOC capacity | Technically valid logic is operationally unsustainable. | Prioritise stronger combinations while testing coverage. |
| Volume suddenly increases after deployment | Environment drift, tuning failure or real attack activity may have occurred. | Investigate the change before suppressing it. |
Write the operational tuning like a detection engineer
Example: Production evaluation showed that the detection generated approximately 600 raw matching events per day. Analysis confirmed that many events represented repeated telemetry belonging to the same device, account and short-lived execution sequence rather than independent investigations. The detection was therefore redesigned to aggregate related events into bounded investigation groups while preserving command-line evidence and behavioural reasons. Higher-value combinations were prioritised using explainable scoring, and the resulting candidate volume was measured against analyst capacity and known suspicious test cases before operational deployment.
Lesson 169 key takeaways
- A technically correct detection can still fail operationally.
- Measure daily detection volume before changing the logic.
- Raw matching events do not necessarily represent separate investigations.
- Find the users, devices, commands and processes driving excessive volume.
- Group related activity using meaningful entities and bounded time windows.
- Measure the reduction from raw events to investigation-ready candidates.
- Do not optimise purely for the lowest possible alert count.
- Use explainable prioritisation to surface stronger behavioural combinations.
- Preserve raw supporting evidence inside grouped detection output.
- Continue monitoring detection volume after production deployment.
Module 14 — next, turn the hunt into something reusable
Lesson 169 turned a noisy but valid detection into a manageable stream of investigation-ready candidates. The next challenge is repeatability: taking useful hunting logic and packaging it so another analyst can run it, understand its assumptions and obtain the same investigative value.
Continue your KQL investigation training
🔎 KQL Academy — Module 14: Advanced Detection Engineering & Proactive Threat Hunting
Reduce noisy KQL detection output
Lesson 169 of the Agent Foskett KQL Academy shows how to measure operational detection volume, identify repeated Microsoft Defender XDR events and aggregate related activity into investigation-ready groups.
Build operationally sustainable detections
Learn how to compare raw event volume with investigation count, prioritise stronger behavioural groups, preserve supporting evidence and monitor detection quality after deployment.
