Lesson 166 — The Baseline Changed — Was It Drift or an Attack?
In Lesson 165 we built a behavioural baseline before choosing a detection threshold. But normal behaviour does not stay frozen forever. Users change roles. New management tools are deployed. Servers take on different workloads. Automation expands. Legitimate activity can move far enough that yesterday's baseline no longer describes today's environment.
The difficult question is whether the change is harmless drift — or whether an attacker has created the new pattern. If we automatically teach the baseline to accept every change, malicious behaviour can eventually become “normal”. If we never allow the baseline to move, the detection becomes noisy and obsolete.

Your investigation problem
PowerShell activity that was historically rare has become common for several users. Is the environment changing legitimately, or has suspicious activity begun reshaping the baseline?
Detection briefing
Investigation objective
Use KQL to compare an established historical baseline with recent behaviour, quantify the amount of change, identify the users and devices responsible, inspect the process evidence behind the change and decide whether the baseline should be recalibrated or the activity escalated.
Detection engineer's rule
Do not let new behaviour automatically redefine normal. A baseline that learns without validation can slowly absorb the very attack behaviour it was supposed to expose.
Stage 1 — compare the established baseline with recent activity
Start by separating the older reference period from the recent observation period. Here we compare PowerShell execution per account across two windows and calculate the percentage change.
let Historical =
DeviceProcessEvents
| where Timestamp between (ago(30d) .. ago(7d))
| where FileName in~ ("powershell.exe", "pwsh.exe")
| summarize HistoricalExecutions=count()
by AccountName;
let Recent =
DeviceProcessEvents
| where Timestamp > ago(7d)
| where FileName in~ ("powershell.exe", "pwsh.exe")
| summarize RecentExecutions=count()
by AccountName;
Historical
| join kind=fullouter Recent on AccountName
| extend HistoricalExecutions =
coalesce(HistoricalExecutions, 0),
RecentExecutions =
coalesce(RecentExecutions, 0)
| extend Change =
RecentExecutions - HistoricalExecutions
| extend ChangePercent =
iff(HistoricalExecutions == 0,
real(null),
100.0 * Change / HistoricalExecutions)
| order by ChangePercent desc nulls firstA percentage can mislead
An increase from one execution to five is a 400% rise, but only four additional events. Look at both absolute volume and percentage change before deciding that drift is meaningful.
Zero-history entities matter
An account with no historical PowerShell activity but substantial recent execution cannot produce a useful percentage. Treat new behaviour as its own investigation category rather than hiding it inside the arithmetic.
Stage 2 — find users whose normal range has shifted
Total volume is useful, but we also want to know whether the shape of normal behaviour changed. Compare percentile values across the historical and recent periods.
let HistoricalHourly =
DeviceProcessEvents
| where Timestamp between (ago(30d) .. ago(7d))
| where FileName in~ ("powershell.exe", "pwsh.exe")
| summarize Executions=count()
by AccountName, bin(Timestamp, 1h)
| summarize HistoricalP95=percentile(Executions, 95),
HistoricalMax=max(Executions)
by AccountName;
let RecentHourly =
DeviceProcessEvents
| where Timestamp > ago(7d)
| where FileName in~ ("powershell.exe", "pwsh.exe")
| summarize Executions=count()
by AccountName, bin(Timestamp, 1h)
| summarize RecentP95=percentile(Executions, 95),
RecentMax=max(Executions)
by AccountName;
HistoricalHourly
| join kind=fullouter RecentHourly on AccountName
| extend HistoricalP95=coalesce(HistoricalP95, 0.0),
RecentP95=coalesce(RecentP95, 0.0)
| extend P95Change=RecentP95-HistoricalP95
| where P95Change > 0
| project AccountName, HistoricalP95,
RecentP95, P95Change,
HistoricalMax, RecentMax
| order by P95Change descDrift can be legitimate
A user moved into an engineering role, a new administration process was introduced or a deployment project began. Those are plausible explanations — but they should be verified rather than assumed.
Drift can also be persistence
If malicious automation runs frequently enough for long enough, a rolling baseline can begin treating it as expected behaviour. This is why baseline changes need investigation context.
Stage 3 — identify the devices behind the change
Now pivot from the changed identity into device context. Is the increased behaviour occurring on familiar endpoints, or has the account suddenly started using PowerShell on systems where it historically did not?
let HistoricalPairs =
DeviceProcessEvents
| where Timestamp between (ago(30d) .. ago(7d))
| where FileName in~ ("powershell.exe", "pwsh.exe")
| distinct AccountName, DeviceName;
let RecentPairs =
DeviceProcessEvents
| where Timestamp > ago(7d)
| where FileName in~ ("powershell.exe", "pwsh.exe")
| summarize RecentExecutions=count(),
FirstSeen=min(Timestamp),
LastSeen=max(Timestamp)
by AccountName, DeviceName;
RecentPairs
| join kind=leftanti HistoricalPairs
on AccountName, DeviceName
| project FirstSeen, LastSeen,
AccountName, DeviceName,
RecentExecutions
| order by RecentExecutions descNew combinations are useful
The user and device may both be familiar individually while the combination is new. Entity relationships often reveal change more clearly than either entity alone.
Ask why the relationship changed
A new workstation, support task or server responsibility may explain the pair. A compromised account moving to another endpoint may produce the same observation. Context decides.
Stage 4 — inspect what actually changed
Statistics tell us that behaviour moved. Process telemetry tells us what created the movement. Examine parent processes and command lines for the entities showing the largest drift.
DeviceProcessEvents
| where Timestamp > ago(7d)
| where FileName in~ ("powershell.exe", "pwsh.exe")
| summarize Executions=count(),
Devices=dcount(DeviceName),
FirstSeen=min(Timestamp),
LastSeen=max(Timestamp)
by AccountName,
InitiatingProcessFileName,
ProcessCommandLine
| order by Executions desc
| project AccountName,
InitiatingProcessFileName,
ProcessCommandLine,
Executions,
Devices,
FirstSeen,
LastSeenNumbers cannot explain intent
A higher P95 does not tell you whether the cause was a software deployment, new job responsibility, malicious script or attacker persistence. Return to the raw evidence before changing the baseline.
Look for consistency
Legitimate automation often produces repeatable commands, known paths and expected parent processes. Suspicious drift may introduce new parents, obfuscation, download behaviour, unusual destinations or inconsistent command lines.
Stage 5 — classify the change before recalibrating
Build a compact evidence view for recent PowerShell behaviour. The goal is not to make KQL pronounce the final verdict, but to give the analyst enough context to decide whether the changed baseline is explainable.
DeviceProcessEvents
| where Timestamp > ago(7d)
| where FileName in~ ("powershell.exe", "pwsh.exe")
| extend Encoded =
ProcessCommandLine has_any (
"-enc", "-encodedcommand"
)
| extend DownloadBehaviour =
ProcessCommandLine has_any (
"Invoke-WebRequest", "DownloadString",
"WebClient", "Start-BitsTransfer"
)
| summarize Executions=count(),
Devices=dcount(DeviceName),
EncodedExecutions=countif(Encoded),
DownloadExecutions=countif(DownloadBehaviour),
Parents=make_set(InitiatingProcessFileName, 10),
SampleCommands=make_set(ProcessCommandLine, 5)
by AccountName
| extend ReviewPriority =
case(EncodedExecutions > 0 or DownloadExecutions > 0,
"Higher",
Devices > 3, "Review",
"Context required")
| order by Executions descValidated drift can become the new baseline
If the changed behaviour is confirmed as legitimate and expected to continue, recalibration may be appropriate. Record why the baseline changed and who validated the new behaviour.
Unexplained drift stays suspicious
If no operational change explains the new pattern, do not simply absorb it into normal. Preserve the old reference, escalate the activity and investigate the surrounding identity, endpoint and network evidence.
Agent Foskett's baseline-drift workflow
Drift or attack?
| Observation | Possible legitimate drift | Possible suspicious interpretation |
|---|---|---|
| PowerShell volume rises | New administration or deployment workflow. | Persistent malicious automation. |
| User appears on new devices | Role change or support responsibility. | Credential misuse or lateral movement. |
| Commands become highly repetitive | Approved enterprise automation. | Automated persistence or execution. |
| New parent process appears | New approved application workflow. | Changed execution chain or initial access. |
| Baseline remains elevated | Permanent business change. | Attack behaviour being absorbed into normal. |
Write the drift decision like a detection engineer
Example: Comparison of the established 23-day reference period with the most recent seven days showed a material increase in PowerShell activity for several identities. The change was concentrated in a small number of new user-device relationships. Process review showed that two identities were associated with a newly deployed administration workflow and were validated by the system owner. A third identity showed previously unseen browser-parented PowerShell with download-related command-line behaviour and no corresponding operational change. The validated administrative activity was approved for baseline recalibration, while the unexplained behaviour was retained outside the new baseline and escalated for investigation.
Lesson 166 key takeaways
- Behavioural baselines naturally change as environments evolve.
- Compare both absolute and percentage change when measuring drift.
- Compare percentile ranges, not only total event counts.
- Identify new user-device relationships created during the changed period.
- Return to raw process evidence to understand what caused statistical drift.
- Validate legitimate environmental changes with appropriate owners.
- Do not automatically absorb unexplained new behaviour into the baseline.
- Rolling baselines can accidentally normalise persistent attack activity.
- Document why and when a baseline is recalibrated.
- Preserve suspicious drift as an investigation lead.
Module 14 — from drift to confidence
Lesson 166 showed why a baseline cannot simply learn every new behaviour. Next we take another step toward mature detection engineering by combining several weak signals into a stronger detection rather than expecting one condition to carry the entire investigation.
Continue your KQL investigation training
🔎 KQL Academy — Module 14: Advanced Detection Engineering & Proactive Threat Hunting
Investigate behavioural baseline drift with KQL
Lesson 166 of the Agent Foskett KQL Academy shows how to compare historical and recent Microsoft Defender XDR activity, identify changed users and devices, inspect the evidence behind the change and determine whether a behavioural baseline should be recalibrated.
Distinguish legitimate environmental drift from suspicious activity
Learn why adaptive baselines must not automatically trust every new pattern, how persistent attack activity can become normalised and how KQL can preserve suspicious deviations for investigation.
