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

Lesson 163 — Turning an Investigation Query Into a Detection

Lessons 161 and 162 produced something valuable: a behavioural query that found and scoped suspicious PowerShell execution. But a useful hunting query is not automatically a good detection.

A hunter can tolerate extra results, manually inspect context and change the query while investigating. A detection has to run repeatedly, produce useful evidence, avoid overwhelming analysts and continue working as the environment changes. In this lesson, we engineer the hunt into operational detection logic.

A hunt asks whether interesting behaviour exists. A detection must decide which occurrences deserve an analyst's attention every time the query runs.
Agent Foskett KQL Academy detection engineering lesson
Your engineering task

The hunt works. Now turn it into repeatable logic that preserves enough device, user, process and command-line context for an analyst to investigate immediately.

✓ Preserve the security idea
✓ Add supporting signals
✓ Measure expected noise
✓ Tune without hiding the attack

Detection engineering briefing

USEFUL HUNT Browser / Office / script host → PowerShell ↓ CAN EVERY MATCH BECOME AN ALERT? No. ↓ MEASURE Frequency + devices + users + expected activity ↓ REFINE Behaviour + supporting signals + context ↓ TUNE Only validated benign patterns ↓ OPERATIONAL DETECTION CANDIDATE Repeatable + explainable + investigable

Detection objective

Take the behaviour-led PowerShell hunt from the previous lessons and refine it into repeatable detection logic by defining the core behaviour, adding meaningful supporting signals, measuring result volume, tuning known expected activity and preserving the fields analysts need for triage.

Detection engineer's rule

Do not optimise only for fewer alerts. A quiet detection that misses the behaviour is not better than a noisy one. Tune against understood benign activity while protecting the security idea the detection was built to find.

Stage 1 — preserve the original hunting logic

Start with the simplest useful version of the behaviour. This becomes the reference query against which later tuning decisions can be compared.

01-preserve-original-hunting-logic.kql
12345678910111213
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"
)
| project Timestamp, DeviceId, DeviceName,
          AccountName, InitiatingProcessFileName,
          FileName, ProcessCommandLine, SHA1

Write down the security idea

The syntax may change many times. The idea should remain clear: selected user-facing or script-host processes launching PowerShell can be worth investigation when additional execution characteristics make the relationship unusual.

Keep investigation pivots

Device ID, device name, account, parent process, command line and hash are the fields the next analyst will use to determine what happened.

Stage 2 — add supporting behavioural signals

The original hunt may be too broad for continuous detection. Add command-line characteristics that increase investigative value without replacing the underlying behaviour.

02-add-supporting-behavioural-signals.kql
123456789101112131415161718192021
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"
)
| extend EncodedCommand =
    ProcessCommandLine has_any ("-enc", "-encodedcommand")
| extend DownloadBehaviour =
    ProcessCommandLine has_any (
        "Invoke-WebRequest", "DownloadString",
        "WebClient", "Start-BitsTransfer"
    )
| extend HiddenExecution =
    ProcessCommandLine has_any ("-w hidden", "-windowstyle hidden")
| where EncodedCommand or DownloadBehaviour or HiddenExecution
| project Timestamp, DeviceId, DeviceName, AccountName,
          InitiatingProcessFileName, ProcessCommandLine, SHA1

Explain why it matched

An analyst should understand why the event was selected. “Browser launched PowerShell with download behaviour” is more useful than an unexplained opaque score.

Keywords are context, not verdicts

Encoded commands, hidden windows and download functions can be legitimate. Their value comes from the surrounding process, user, device and execution context.

Stage 3 — measure how often the candidate would fire

Before operationalising the query, run it over a longer period and measure event, device and user volume. Detection engineering needs empirical noise data rather than guesses.

03-measure-candidate-detection-volume.kql
12345678910111213141516171819161718192021
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 Events=count(),
            Devices=dcount(DeviceName),
            Users=dcount(AccountName),
            FirstSeen=min(Timestamp),
            LastSeen=max(Timestamp)
          by InitiatingProcessFileName
| order by Events desc

Frequency changes operational cost

Three meaningful events a week create a very different analyst workload from three thousand events a day.

Look beyond the total

A high count caused by one approved automation account may be easier to understand than a smaller count scattered unpredictably across hundreds of users and devices.

Stage 4 — tune validated expected activity carefully

After legitimate sources are investigated and confirmed, narrow exclusions can be introduced. Avoid broad suppression that removes the behaviour itself.

04-tune-validated-expected-activity.kql
12345678910111213141516171819202122
let ApprovedAccounts = dynamic([
    "svc-deployment",
    "svc-endpoint-management"
]);
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"
)
| extend EncodedCommand =
    ProcessCommandLine has_any ("-enc", "-encodedcommand")
| extend DownloadBehaviour =
    ProcessCommandLine has_any (
        "Invoke-WebRequest", "DownloadString",
        "WebClient", "Start-BitsTransfer"
    )
| where EncodedCommand or DownloadBehaviour
| where AccountName !in~ (ApprovedAccounts)
| project Timestamp, DeviceId, DeviceName, AccountName,
          InitiatingProcessFileName, ProcessCommandLine, SHA1

Every exclusion creates a blind spot

If an approved service account is later compromised, a broad exclusion may hide attacker activity. Where possible, constrain exclusions with additional expected context.

Document the reason

A future analyst should know whether an exclusion was added after validation or simply because someone wanted the alert count to fall.

Stage 5 — build the operational detection candidate

Use a short recurring evaluation window and require multiple meaningful signals. Preserve the entities and evidence needed for immediate triage.

05-build-operational-detection-candidate.kql
12345678910111213141516171819202122232425
DeviceProcessEvents
| where Timestamp > ago(1h)
| 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"
    )
| extend HiddenExecution =
    ProcessCommandLine has_any ("-w hidden", "-windowstyle hidden")
| extend SignalCount =
    toint(EncodedCommand) +
    toint(DownloadBehaviour) +
    toint(HiddenExecution)
| where SignalCount >= 2
| project Timestamp, DeviceId, DeviceName, AccountName,
          InitiatingProcessFileName, ProcessCommandLine,
          SHA1, SignalCount

Running is not finished

Operationalisation begins a feedback cycle. Analysts should record false positives, missed scenarios and useful contextual fields so the detection can improve.

Detection is a product

A mature detection has a purpose, owner, data requirements, testing method, tuning history and response guidance. The KQL is one part of the engineering work.

Agent Foskett's detection engineering workflow

USEFUL HUNT ↓ PRESERVE THE SECURITY IDEA ↓ ADD SUPPORTING SIGNALS ↓ MEASURE HISTORICAL VOLUME ↓ VALIDATE EXPECTED ACTIVITY ↓ TUNE NARROWLY ↓ PRESERVE TRIAGE CONTEXT Device + user + parent + command + hash ↓ OPERATIONAL CANDIDATE ↓ TEST → REVIEW → IMPROVE
The best detection is not the cleverest query. It is the one that repeatedly produces evidence an analyst can understand and act on.

Hunting query versus detection logic

CharacteristicThreat huntOperational detection
PurposeExplore a hypothesis and discover interesting activity.Repeatedly identify activity that deserves review.
Noise toleranceCan be relatively high during exploration.Must fit sustainable analyst workload.
Query changesFrequently adjusted during investigation.Changes should be tested and documented.
ContextHunter can manually pivot for more information.Should return useful triage entities immediately.
TuningOften temporary and investigation-specific.Should be validated, narrow and maintainable.

Write the detection rationale like an engineer

Example: This detection candidate identifies PowerShell launched by selected user-facing or script-host processes when the command line also contains encoded execution, download-related functionality or hidden execution characteristics. The logic originated from a proactive hunting query and was evaluated across historical endpoint telemetry to understand expected frequency, affected users and devices. Known legitimate activity should be excluded only after validation and with the narrowest practical conditions. Detection output preserves device, account, parent process, command line and hash context to support immediate triage. A match represents suspicious behaviour requiring investigation rather than confirmed malicious execution.

Lesson 163 key takeaways

  • A useful hunting query is not automatically ready to become a detection.
  • Preserve the security idea behind the query before tuning it.
  • Keep entity and evidence fields that support analyst triage.
  • Add supporting behavioural signals when they improve investigative value.
  • Measure historical result volume before operational deployment.
  • Understand which users and devices generate expected matches.
  • Tune validated benign activity narrowly rather than suppressing broad behaviour.
  • Every exclusion can create a future blind spot.
  • Detection matches are investigation candidates, not proof of compromise.
  • Detection engineering continues through testing, analyst feedback and revision.

Module 14 — now the detection meets reality

Lesson 163 turned a useful hunt into an operational detection candidate. The next problem is one every detection engineer eventually meets: the logic works, but it produces far too many alerts.

Next: Lesson 164 — The Detection Generated Too Many False Positives.

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.

Turn a KQL threat hunting query into a detection

Lesson 163 of the Agent Foskett KQL Academy introduces practical detection engineering by taking a behaviour-led Microsoft Defender hunting query and preparing it for repeatable operational use.

Build and tune behavioural detections with KQL

Learn how to preserve a detection's security idea, add supporting signals, measure historical result volume, tune validated expected activity and return the device, user and process context analysts need for triage.