Agent Foskett Academy • KQL Academy • Module 14 • Lesson 164 • Detection Engineering & Proactive Threat Hunting

Lesson 164 — The Detection Generated Too Many False Positives

The detection from Lesson 163 works. Unfortunately, it works a little too enthusiastically. Analysts are seeing repeated matches from software deployment, support scripts and legitimate administration mixed in with the behaviour the rule was designed to find.

This is where detection engineering becomes operational. The answer is not to keep adding exclusions until the alerts disappear. We need to understand exactly what is creating the noise, isolate repeatable benign patterns, preserve unusual outliers and tune the detection without removing its original security value.

False-positive tuning is not the art of making alerts disappear. It is the process of teaching the detection what the environment legitimately does while preserving its ability to recognise suspicious deviations.
Agent Foskett KQL Academy false positive detection tuning
Your detection problem

The rule is producing too many results. Before changing it, identify which users, devices, parent processes and command patterns are responsible for the noise.

✓ Measure the alert volume
✓ Find dominant benign patterns
✓ Preserve unusual outliers
✓ Tune narrowly and retest

Detection briefing

DETECTION DEPLOYED ↓ ALERT VOLUME TOO HIGH ↓ DO NOT START WITH EXCLUSIONS ↓ MEASURE THE NOISE Which accounts? Which devices? Which parent processes? Which commands? ↓ VALIDATE BENIGN PATTERNS ↓ TUNE NARROWLY ↓ RETEST ORIGINAL ATTACK BEHAVIOUR ↓ LOWER NOISE WITHOUT LOSING COVERAGE

Detection objective

Use KQL to analyse a noisy behavioural detection, identify the recurring activity responsible for most matches, distinguish stable benign patterns from suspicious outliers, apply narrowly scoped tuning and compare the results before and after the change.

Detection engineer's rule

Investigate the false positive before suppressing it. If you cannot explain why an event is benign, you do not yet have enough evidence to exclude it safely.

Stage 1 — measure the noise before changing anything

Run the current detection logic over a useful historical period and summarise its volume. We want to know whether the noise is distributed across the environment or concentrated in a few repeatable sources.

01-measure-current-detection-noise.kql
12345678910111213141516171819202122
DeviceProcessEvents
| where Timestamp > ago(7d)
| where FileName in~ ("powershell.exe", "pwsh.exe")
| where InitiatingProcessFileName in~ (
    "winword.exe", "excel.exe", "outlook.exe",
    "chrome.exe", "msedge.exe", "firefox.exe",
    "wscript.exe", "cscript.exe", "mshta.exe"
)
| extend EncodedCommand =
    ProcessCommandLine has_any ("-enc", "-encodedcommand")
| extend DownloadBehaviour =
    ProcessCommandLine has_any (
        "Invoke-WebRequest", "DownloadString",
        "WebClient", "Start-BitsTransfer"
    )
| where EncodedCommand or DownloadBehaviour
| summarize Matches=count(),
            Devices=dcount(DeviceName),
            Users=dcount(AccountName)
          by bin(Timestamp, 1d)
| order by Timestamp asc

Volume is only the first clue

A detection producing 500 matches is not automatically unusable. If 480 come from one well-understood management process, the problem may be highly concentrated and therefore tunable.

Look for changes over time

A sudden increase in matches can indicate a new software rollout, configuration change or genuine attack activity. Do not assume every spike is merely “more false positives”.

Stage 2 — identify the patterns generating most matches

Break the results down by account, initiating process and command line. This reveals whether a small number of repeatable patterns dominate the alert queue.

02-find-dominant-noise-patterns.kql
12345678910111213141516171819202120212223
DeviceProcessEvents
| where Timestamp > ago(7d)
| where FileName in~ ("powershell.exe", "pwsh.exe")
| where InitiatingProcessFileName in~ (
    "winword.exe", "excel.exe", "outlook.exe",
    "chrome.exe", "msedge.exe", "firefox.exe",
    "wscript.exe", "cscript.exe", "mshta.exe"
)
| extend EncodedCommand =
    ProcessCommandLine has_any ("-enc", "-encodedcommand")
| extend DownloadBehaviour =
    ProcessCommandLine has_any (
        "Invoke-WebRequest", "DownloadString",
        "WebClient", "Start-BitsTransfer"
    )
| where EncodedCommand or DownloadBehaviour
| summarize Matches=count(),
            Devices=dcount(DeviceName),
            FirstSeen=min(Timestamp),
            LastSeen=max(Timestamp)
          by AccountName,
             InitiatingProcessFileName,
             ProcessCommandLine
| order by Matches desc

Repetition can expose benign automation

A service account running an identical approved command on hundreds of endpoints may explain much of the noise. Validate that activity with the relevant system owner before tuning it.

Do not ignore the bottom of the list

The most frequent pattern may be benign while a single rare command near the bottom represents the attack behaviour the detection was intended to find.

Stage 3 — separate the common pattern from the outliers

Instead of immediately excluding the dominant command, compare it with other commands generated by the same account and parent process. Outliers deserve special attention.

03-find-outliers-inside-noisy-activity.kql
123456789101112131415161718192021222324
let DetectionEvents =
    DeviceProcessEvents
    | where Timestamp > ago(14d)
    | where FileName in~ ("powershell.exe", "pwsh.exe")
    | where InitiatingProcessFileName in~ (
        "winword.exe", "excel.exe", "outlook.exe",
        "chrome.exe", "msedge.exe", "firefox.exe",
        "wscript.exe", "cscript.exe", "mshta.exe"
    )
    | where ProcessCommandLine has_any (
        "-enc", "-encodedcommand",
        "Invoke-WebRequest", "DownloadString",
        "WebClient", "Start-BitsTransfer"
    );
DetectionEvents
| summarize CommandCount=count(),
            Devices=dcount(DeviceName)
          by AccountName, ProcessCommandLine
| where CommandCount <= 2
| order by CommandCount asc

Noise can hide the signal

A compromised administrative or service account can perform one malicious command among thousands of legitimate executions. Excluding the account entirely could remove exactly the event you need to see.

Tune the pattern, not the identity

Where possible, define the validated combination of account, parent, path, signer or command characteristics rather than broadly trusting an account forever.

Stage 4 — apply a narrow, explainable exclusion

Suppose investigation confirms that one deployment account runs a specific approved script through a known parent process. Exclude that combination rather than suppressing every event from the account.

04-apply-narrow-validated-exclusion.kql
1234567891011121314151617181920212220202122
DeviceProcessEvents
| where Timestamp > ago(1d)
| where FileName in~ ("powershell.exe", "pwsh.exe")
| where InitiatingProcessFileName in~ (
    "winword.exe", "excel.exe", "outlook.exe",
    "chrome.exe", "msedge.exe", "firefox.exe",
    "wscript.exe", "cscript.exe", "mshta.exe"
)
| where ProcessCommandLine has_any (
    "-enc", "-encodedcommand",
    "Invoke-WebRequest", "DownloadString",
    "WebClient", "Start-BitsTransfer"
)
| where not(
    AccountName =~ "svc-deployment"
    and InitiatingProcessFileName =~ "wscript.exe"
    and ProcessCommandLine has
        @"C:\Company\Scripts\ApprovedDeployment.ps1"
)
| project Timestamp, DeviceId, DeviceName,
          AccountName, InitiatingProcessFileName,
          ProcessCommandLine, SHA1

The exclusion should tell a story

A reviewer should be able to read the exclusion and understand exactly which validated activity it represents. Broad exclusions are easier to write but much harder to defend.

Exceptions need ownership

Document who validated the benign behaviour, when it was reviewed and what would cause the exception to be reconsidered.

Stage 5 — compare before and after tuning

Measure the original and tuned result sets side by side. The goal is to quantify the noise reduction while confirming that suspicious variants still survive the tuning.

05-measure-tuning-impact.kql
12345678910111213141516171819202122232425262728
let Base =
    DeviceProcessEvents
    | where Timestamp > ago(7d)
    | where FileName in~ ("powershell.exe", "pwsh.exe")
    | where InitiatingProcessFileName in~ (
        "winword.exe", "excel.exe", "outlook.exe",
        "chrome.exe", "msedge.exe", "firefox.exe",
        "wscript.exe", "cscript.exe", "mshta.exe"
    )
    | where ProcessCommandLine has_any (
        "-enc", "-encodedcommand",
        "Invoke-WebRequest", "DownloadString",
        "WebClient", "Start-BitsTransfer"
    );
let Original = Base | summarize Matches=count()
    | extend Version="Original";
let Tuned = Base
    | where not(
        AccountName =~ "svc-deployment"
        and InitiatingProcessFileName =~ "wscript.exe"
        and ProcessCommandLine has
            @"C:\Company\Scripts\ApprovedDeployment.ps1"
    )
    | summarize Matches=count()
    | extend Version="Tuned";
union Original, Tuned
| project Version, Matches

Lower volume is not enough

A 95% reduction sounds impressive, but it is only successful if the remaining logic still detects the behaviours the rule was created to identify.

Retest known suspicious cases

After every material tuning change, replay or re-query representative malicious or suspicious examples where possible. Detection tuning should have regression testing just like other engineering work.

Agent Foskett's false-positive tuning workflow

TOO MANY ALERTS ↓ MEASURE How much noise? ↓ GROUP Account + device + parent + command ↓ VALIDATE Which patterns are genuinely benign? ↓ SEARCH FOR OUTLIERS What does not match the benign pattern? ↓ TUNE NARROWLY Exclude the validated combination NOT the entire identity or behaviour ↓ COMPARE BEFORE / AFTER ↓ RETEST SUSPICIOUS CASES ↓ DEPLOY + REVIEW
If tuning removes the attack along with the noise, you have not improved the detection. You have silenced it.

Good tuning versus dangerous tuning

Tuning decisionRiskBetter approach
Exclude an entire service accountCompromise of that account becomes invisible.Exclude only its validated command and execution context.
Exclude all PowerShell from a device groupRemoves a broad attack surface from visibility.Identify the specific legitimate process or script.
Remove encoded-command detectionDestroys part of the original behavioural coverage.Combine encoded execution with context and other signals.
Suppress the most common exact benign patternLower risk when independently validated.Document, monitor and periodically review the exception.
Retest after tuningHelps reveal accidental loss of coverage.Make regression testing part of every material change.

Write the tuning decision like a detection engineer

Example: Historical analysis showed that the majority of detection matches were generated by the svc-deployment account executing a consistent approved deployment script through wscript.exe. The activity was validated as legitimate enterprise automation. Rather than excluding the service account globally, the detection was tuned only for the validated combination of account, initiating process and approved script path. Low-frequency command variants associated with the same account remain visible for investigation. Pre- and post-tuning volumes were compared, and representative suspicious PowerShell patterns were retested to confirm that the original behavioural coverage remained effective.

Lesson 164 key takeaways

  • Measure false-positive volume before changing detection logic.
  • Identify which users, devices, parent processes and commands generate most matches.
  • Validate recurring benign activity before excluding it.
  • Frequent patterns may be benign while rare outliers remain suspicious.
  • A trusted or approved account can still be compromised.
  • Prefer narrow contextual exclusions over broad account or device exclusions.
  • Document why every important exclusion exists.
  • Compare detection volume before and after tuning.
  • Retest representative suspicious behaviour after material changes.
  • Successful tuning reduces unnecessary alerts without destroying detection coverage.

Module 14 — the next problem is the threshold

Lesson 164 reduced false positives by understanding the environment instead of blindly suppressing alerts. Next, we deal with another common detection-engineering mistake: choosing a threshold before establishing what normal activity actually looks like.

Next: Lesson 165 — Building a Baseline Before Setting the Threshold.

Continue your KQL investigation training

Module 14 turns investigation knowledge into proactive hunts, reusable analytics and higher-confidence detections.

🔎 KQL Academy — Module 14: Advanced Detection Engineering & Proactive Threat Hunting

Turn investigation knowledge into proactive hunts, reusable analytics and higher-confidence detections.

Reduce false positives in KQL detections

Lesson 164 of the Agent Foskett KQL Academy shows how to investigate noisy Microsoft Defender detection logic, identify recurring benign patterns and tune false positives without removing the suspicious behaviour the rule was designed to detect.

Tune behavioural detections without creating blind spots

Learn how to measure detection volume, identify dominant patterns, preserve suspicious outliers, create narrow exclusions and compare pre- and post-tuning results with KQL.