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

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.

A changing baseline is not something to automatically accept or automatically alert on. It is something to investigate.
Agent Foskett KQL Academy baseline drift investigation
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?

✓ Compare old and recent baselines
✓ Find the entities that changed
✓ Inspect the behaviour behind the numbers
✓ Decide whether the baseline should move

Detection briefing

ESTABLISHED BASELINE ↓ RECENT BEHAVIOUR CHANGES ↓ MEASURE THE DIFFERENCE ↓ WHICH ENTITIES CHANGED? ↓ WHAT BEHAVIOUR CAUSED IT? ↓ LEGITIMATE CHANGE? OR SUSPICIOUS NEW PATTERN? ↓ VALIDATE THE EVIDENCE ↓ UPDATE BASELINE OR ESCALATE INVESTIGATION

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.

01-compare-historical-and-recent-baselines.kql
12345678910111213141516171819202122232425
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 first

A 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.

02-measure-user-baseline-drift.kql
12345678910111213141516171819202122232425262728
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 desc

Drift 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?

03-find-new-device-context.kql
123456789101112131415161718192021
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 desc

New 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.

04-inspect-the-behaviour-behind-the-drift.kql
123456789101112131415161718
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,
          LastSeen

Numbers 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.

05-build-the-drift-review-view.kql
12345678910111213141516171819202122232425
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 desc

Validated 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

BASELINE CHANGED ↓ QUANTIFY THE CHANGE Absolute + Percentage + Percentiles ↓ IDENTIFY CHANGED ENTITIES User + Device + Relationship ↓ INSPECT RAW BEHAVIOUR Parent + Command + Timing ↓ CHECK OPERATIONAL CONTEXT Role change? New tooling? Deployment? Maintenance? ↓ CORRELATE SUSPICIOUS SIGNALS Encoding? Downloads? New devices? Unusual ancestry? ↓ VALIDATED LEGITIMATE DRIFT → DOCUMENT + RECALIBRATE UNEXPLAINED / SUSPICIOUS DRIFT → PRESERVE BASELINE + ESCALATE
Never allow an attacker to earn trust simply by remaining in the environment long enough to become part of the average.

Drift or attack?

ObservationPossible legitimate driftPossible suspicious interpretation
PowerShell volume risesNew administration or deployment workflow.Persistent malicious automation.
User appears on new devicesRole change or support responsibility.Credential misuse or lateral movement.
Commands become highly repetitiveApproved enterprise automation.Automated persistence or execution.
New parent process appearsNew approved application workflow.Changed execution chain or initial access.
Baseline remains elevatedPermanent 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.

Next: Lesson 167 — Three Weak Signals Became One Strong Detection.

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.

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.