Lesson 167 — Three Weak Signals Became One Strong Detection
Security investigations are full of events that are interesting but not suspicious enough to alert on by themselves. PowerShell execution is common. A browser starting a child process can be legitimate. An outbound connection may be completely normal.
But what happens when those behaviours occur together? A browser launches PowerShell, the command line contains download behaviour, and the same process connects to an external destination minutes later. None of those observations has to prove compromise individually. Their combination can create a much stronger detection story.

Your detection problem
Each individual signal is too common to alert on confidently. Correlate them around the same process and device to identify the small number of events where several suspicious behaviours converge.
Detection briefing
Detection objective
Use KQL to define several individually weak behaviours, correlate them around common process and device evidence, calculate a simple behavioural score and retain the evidence explaining exactly why an event became a higher-priority detection candidate.
Detection engineer's rule
Do not add signals just to make the score bigger. Correlated signals should describe a coherent security hypothesis. More conditions do not automatically mean better detection.
Stage 1 — define the first weak signal
Start with browser-parented PowerShell. We investigated this pattern earlier in the Academy, but as detection logic it can still be too broad. Browsers, management tools and legitimate workflows can occasionally produce unusual child processes.
DeviceProcessEvents
| where Timestamp > ago(1d)
| where FileName in~ ("powershell.exe", "pwsh.exe")
| where InitiatingProcessFileName in~ (
"chrome.exe",
"msedge.exe",
"firefox.exe"
)
| project Timestamp,
DeviceId,
DeviceName,
AccountName,
ProcessId,
FileName,
ProcessCommandLine,
InitiatingProcessFileName,
SHA1Useful does not mean sufficient
This relationship deserves attention, but alerting on every occurrence may create unnecessary noise. Treat it as the beginning of a hypothesis rather than the conclusion.
Preserve correlation fields
Keep DeviceId, ProcessId, timestamp and process context. We will need those fields when we ask whether other suspicious behaviours belong to the same execution.
Stage 2 — add command-line behaviour
Now enrich the first signal with command-line characteristics. Download functions and encoded execution can increase concern, but they still need context because legitimate scripts can use the same capabilities.
DeviceProcessEvents
| where Timestamp > ago(1d)
| 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"
)
| where BrowserParent
| project Timestamp, DeviceId, DeviceName,
AccountName, ProcessId,
ProcessCommandLine,
BrowserParent,
DownloadBehaviour,
EncodedCommandSignals can have different weights
A browser parent may be interesting, while encoded download behaviour may be more suspicious. Later, scoring lets us represent those differences without pretending every clue carries equal evidential value.
Keep the raw evidence
Boolean columns are useful for detection logic, but never discard the original command line. Analysts need to see what actually executed, not merely that a rule labelled it suspicious.
Stage 3 — correlate network activity from the same process
Next, join the process event with DeviceNetworkEvents using DeviceId and InitiatingProcessId. Restrict the time relationship so an unrelated connection hours later does not accidentally strengthen the detection.
let SuspiciousProcesses =
DeviceProcessEvents
| where Timestamp > ago(1d)
| 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"
)
| where BrowserParent
| project ProcessTime=Timestamp,
DeviceId, DeviceName,
AccountName, ProcessId,
ProcessCommandLine,
BrowserParent,
DownloadBehaviour;
SuspiciousProcesses
| join kind=leftouter (
DeviceNetworkEvents
| where Timestamp > ago(1d)
| project NetworkTime=Timestamp,
DeviceId,
InitiatingProcessId,
RemoteIP,
RemotePort,
RemoteUrl
) on DeviceId
| where InitiatingProcessId == ProcessId
| where NetworkTime between
(ProcessTime .. ProcessTime + 10m)
| project ProcessTime, NetworkTime,
DeviceName, AccountName,
ProcessId, ProcessCommandLine,
BrowserParent, DownloadBehaviour,
RemoteIP, RemotePort, RemoteUrlCorrelation needs boundaries
Joining on a device alone is not enough. A busy endpoint can generate thousands of unrelated connections. Process identity and a reasonable time window make the relationship much stronger.
A network event is still context
An external connection does not automatically mean command-and-control. The destination, reputation, URL, port, process ancestry and surrounding behaviour all matter.
Stage 4 — turn the signals into an explainable score
A simple score can help rank candidates. Here the browser parent contributes one point, download behaviour contributes two, and correlated network activity contributes one. The exact weights should be tested against your own environment.
let Processes =
DeviceProcessEvents
| where Timestamp > ago(1d)
| 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"
)
| project Timestamp, DeviceId, DeviceName,
AccountName, ProcessId,
ProcessCommandLine,
BrowserParent, DownloadBehaviour;
Processes
| join kind=leftouter (
DeviceNetworkEvents
| where Timestamp > ago(1d)
| summarize NetworkConnections=count(),
RemoteIPs=make_set(RemoteIP, 10),
RemoteUrls=make_set(RemoteUrl, 10)
by DeviceId, InitiatingProcessId
) on DeviceId
| where isnull(InitiatingProcessId)
or InitiatingProcessId == ProcessId
| extend NetworkSignal =
iff(NetworkConnections > 0, 1, 0)
| extend DetectionScore =
toint(BrowserParent)
+ (2 * toint(DownloadBehaviour))
+ NetworkSignal
| where DetectionScore >= 3
| order by DetectionScore descA score is not truth
The score is a prioritisation mechanism. It should help analysts understand why activity was surfaced, not create a mysterious number that replaces investigation.
Weights need evidence
Do not assign a signal five points merely because it sounds scary. Backtest combinations, examine false positives and confirm that higher scores genuinely correspond with higher-value investigation candidates.
Stage 5 — produce the detection candidate with its reasons
The final query should tell the analyst more than “score = 4”. Build a human-readable reason field and retain the process and network evidence required for triage.
let Candidates =
DeviceProcessEvents
| where Timestamp > ago(1d)
| 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
| extend DetectionReasons =
strcat(
iff(BrowserParent, "Browser parent; ", ""),
iff(DownloadBehaviour, "Download behaviour; ", ""),
iff(EncodedCommand, "Encoded command; ", "")
)
| project Timestamp, DeviceId, DeviceName,
AccountName, ProcessId,
ProcessCommandLine,
SignalCount, DetectionReasons;
Candidates
| order by SignalCount desc, Timestamp descExplainability improves triage
An analyst should immediately understand which behaviours caused the candidate to surface. That shortens triage and makes tuning decisions easier to review later.
Correlation should survive scrutiny
Before production use, validate that the signals really belong to the same security story. Poor joins and overly broad time windows can create convincing-looking correlations that never actually occurred.
Agent Foskett's multi-signal detection workflow
Weak signals and what strengthens them
| Weak signal | Why it is weak alone | What strengthens it |
|---|---|---|
| PowerShell executed | Common legitimate administrative tool. | Unexpected parent, suspicious command or unusual user context. |
| Browser spawned a process | Some applications legitimately launch helpers. | Browser → PowerShell plus suspicious command behaviour. |
| Encoded command | Encoding can be used by legitimate automation. | Unexpected ancestry, download behaviour and unusual destination. |
| External connection | Endpoints constantly communicate externally. | Connection tied to the same suspicious process and time window. |
| Several correlated behaviours | Still requires validation. | Coherent sequence, uncommon context and supporting evidence. |
Write the detection logic like a detection engineer
Example: Individual occurrences of browser-parented PowerShell, download-related command-line activity and outbound network connections were too common to provide sufficient confidence independently. The detection therefore correlates these behaviours around the same endpoint, process identity and bounded time window. Candidates are ranked using an explainable behavioural score while retaining the original command line and network evidence. Historical testing is used to validate whether higher-scoring combinations produce materially better investigation candidates before the logic is promoted into production detection.
Lesson 167 key takeaways
- Many useful security signals are too weak to alert on independently.
- Combine signals only when they support the same security hypothesis.
- Preserve process, device and time fields required for reliable correlation.
- Use bounded time windows to avoid unrelated events strengthening a detection.
- Keep raw command-line and network evidence for analyst triage.
- Different signals can carry different weights.
- A behavioural score is a prioritisation mechanism, not proof of compromise.
- Backtest scoring logic against legitimate and suspicious activity.
- Make detection reasons visible and explainable.
- More signals do not automatically produce a better detection.
Module 14 — now test whether the detection really works
Lesson 167 combined weak behaviours into a stronger detection candidate. The next step is critical: testing. A query that looks excellent on paper can still fail when faced with real historical data, missing telemetry, edge cases and known attack examples.
Continue your KQL investigation training
🔎 KQL Academy — Module 14: Advanced Detection Engineering & Proactive Threat Hunting
Combine weak security signals with KQL
Lesson 167 of the Agent Foskett KQL Academy shows how to correlate process, command-line and network telemetry in Microsoft Defender XDR to create higher-confidence behavioural detection candidates.
Build explainable multi-signal detections
Learn how to correlate around process identity and time, weight behavioural signals, preserve detection reasons and avoid treating a scoring model as proof of compromise.
