
Advanced hunting works best when weak individual signals are correlated into one clear detection path. Image: AceQ7, CC0, via Wikimedia Commons.
AC/DC brought the Power Up Tour to Commonwealth Stadium in Edmonton on August 9, 2026. The morning after a stadium show built around volume, timing, and unmistakable signals feels like the right time to look at threat hunting the same way: start with the noise, isolate the riff that does not belong, and follow it until the entire sequence becomes visible.
Basic KQL finds events. Advanced KQL builds relationships between them. A suspicious PowerShell command by itself may be administrative activity. A public network connection by itself may be normal. The same script launching an encoded command, reaching a rare destination, and repeating outside its normal baseline is a much stronger security story.
This article builds a controlled Microsoft Defender XDR hunt that correlates process and network telemetry, creates a reusable filtered data set, assigns a transparent risk score, and identifies unusual scripting volume. The AC/DC theme stays light; the technical work does not. Bringing a couple of my own passions for metal & KQL together, this article will just go better with some music on a bit too loud…If your desk neighbour complains, it’s about the right volume!
| GOAL Build and validate an advanced KQL hunt that identifies suspicious scripting activity, correlates it with outbound network connections, prioritizes results, and prepares the logic for operational use. |
What are we solving?
Attackers rarely rely on a single event that announces the full compromise. They use a chain: a script interpreter starts, a command is obfuscated, content is downloaded, a new process is created, and a remote endpoint is contacted. Analysts must reconstruct that sequence from separate telemetry tables.
This is where advanced KQL becomes valuable. The query can reduce high-volume telemetry early, normalize command-line variations, reuse an expensive subquery, correlate activity across tables, calculate a risk score, and preserve the entity fields needed for investigation.
The result is not a perfect verdict. It is a better queue: fewer rows, stronger context, and a clearer reason for the analyst to investigate.
Security outcome
- Suspicious scripting is correlated with network activity from the same device and process.
- Command-line variations are normalized before matching to reduce brittle detections.
- Results are prioritized with a readable score rather than an unexplained black-box value.
- Reusable query blocks reduce duplication and simplify tuning.
- The hunt produces device, account, process, hash, URL, IP, and timing context for triage.
- The final logic can be adapted into a Microsoft Defender XDR custom detection after validation.
What you will Deploy & Configure
- Microsoft Defender for Endpoint telemetry available in Microsoft Defender XDR.
- Access to Investigation & response > Hunting > Advanced hunting in the Microsoft Defender portal.
- Permission to query the required device tables.
- Representative endpoint activity in DeviceProcessEvents and DeviceNetworkEvents.
- An owner for tuning, testing, false-positive review, and custom detection lifecycle management.
| MICROSOFT BASELINE Apply restrictive filters as early as possible, especially time, table, file name, and action filters. Keep the smaller data set on the left side of a join, project only required columns, and review query execution details before operationalizing a complex query. |
| SPECIAL HIGHLIGHT KQL should read like an investigation plan. Use comments, descriptive let names, explicit projections, and scoring logic that another analyst can explain during an incident review. |
Prerequisites
- Microsoft Defender for Endpoint is deployed to the devices being hunted.
- The analyst has Advanced Hunting access through Microsoft Defender XDR unified RBAC or the applicable workload permissions.
- The required tables appear in the Advanced Hunting schema.
- Device clocks and portal time settings are understood. Advanced Hunting query time is evaluated in UTC.
- The query is tested in a non-production or tightly scoped context before it is used for automated detection or response.
Before-state checklist
Capture the current state before creating or changing detection logic. This supports peer review, change approval, and rollback.
- Record the existing saved queries and custom detections that already cover PowerShell, command interpreters, downloads, or suspicious outbound connections.
- Capture current query execution time and resource classification.
- Identify approved management tools, automation accounts, software deployment systems, and administrative scripts that may match the logic.
- Document known proxy, update, package-management, and security-service destinations.
- Confirm who approves exclusions and who owns the detection after deployment.
- Run an initial count-only query to estimate volume before returning detailed rows.
Lab build
1. Sound check: establish the scripting baseline
Begin with a narrow inventory of script-interpreter execution. This query does not declare the activity malicious. It establishes where the volume is coming from and which accounts and devices deserve attention.
| let Lookback = 7d; DeviceProcessEvents | where Timestamp > ago(Lookback) | where FileName in~ ( “powershell.exe”, “pwsh.exe”, “powershell_ise.exe”, “cmd.exe”, “wscript.exe”, “cscript.exe”, “mshta.exe” ) | summarize Executions = count(), DistinctCommands = dcount(ProcessCommandLine), FirstSeen = min(Timestamp), LastSeen = max(Timestamp) by DeviceName, AccountName, FileName | order by Executions desc |
What to review
- Devices with much higher execution volume than peers.
- Service or administrative accounts running scripts on user workstations.
- Interpreters that are rare in your environment, such as mshta.exe or cscript.exe.
- Large numbers of distinct command lines from one account or device.
This is the baseline track. Do not tune the final hunt until you know what normal sounds like.

2. Back in black: normalize and filter suspicious command lines
Exact command-line matching is fragile. Quoting, case, spacing, environment variables, and argument order can change without changing the behavior. Normalize the text first, then apply several durable indicators.
| let Lookback = 7d; let SuspiciousTerms = dynamic([ “-enc”, “-encodedcommand”, “frombase64string”, “downloadstring”, “downloadfile”, “invoke-webrequest”, “invoke-expression”, “iex “, “webclient”, “bitsadmin”, “certutil”, “rundll32”, “regsvr32”, “mshta” ]); DeviceProcessEvents | where Timestamp > ago(Lookback) | where FileName in~ ( “powershell.exe”, “pwsh.exe”, “cmd.exe”, “wscript.exe”, “cscript.exe”, “mshta.exe” ) | extend CanonicalCommandLine = tolower(ProcessCommandLine) | extend CanonicalCommandLine = replace_string(CanonicalCommandLine, “\””, “”) | where CanonicalCommandLine has_any (SuspiciousTerms) | project Timestamp, DeviceId, DeviceName, AccountName, FileName, ProcessId, ProcessCommandLine, CanonicalCommandLine, InitiatingProcessFileName, InitiatingProcessCommandLine, SHA1, ReportId | order by Timestamp desc |
The terms are intentionally broad enough for hunting and too broad for automatic containment. Treat them as investigation pivots, not proof of compromise.
3. Thunderstruck: correlate the process with outbound network activity
Now build two tightly filtered data sets and join them. materialize() evaluates the suspicious process block once during the query and makes it reusable. The join matches the device and process identifier, then applies a five-minute time window to reduce unrelated process-ID reuse.
| let Lookback = 7d; let CorrelationWindow = 5m; let SuspiciousTerms = dynamic([ “-enc”, “-encodedcommand”, “frombase64string”, “downloadstring”, “downloadfile”, “invoke-webrequest”, “invoke-expression”, “iex “, “webclient”, “bitsadmin”, “certutil”, “rundll32”, “regsvr32”, “mshta” ]); let SuspiciousProcesses = materialize( DeviceProcessEvents | where Timestamp > ago(Lookback) | where FileName in~ ( “powershell.exe”, “pwsh.exe”, “cmd.exe”, “wscript.exe”, “cscript.exe”, “mshta.exe” ) | extend CanonicalCommandLine = tolower(ProcessCommandLine) | extend CanonicalCommandLine = replace_string(CanonicalCommandLine, “\””, “”) | where CanonicalCommandLine has_any (SuspiciousTerms) | project ProcessTimestamp = Timestamp, DeviceId, DeviceName, AccountName, FileName, ProcessId, ProcessCommandLine, CanonicalCommandLine, InitiatingProcessFileName, InitiatingProcessCommandLine, SHA1, ProcessReportId = ReportId ); let OutboundConnections = DeviceNetworkEvents | where Timestamp > ago(Lookback) | where ActionType == “ConnectionSuccess” | where isnotempty(RemoteIP) or isnotempty(RemoteUrl) | project NetworkTimestamp = Timestamp, DeviceId, ProcessId = InitiatingProcessId, RemoteIP, RemoteIPType, RemoteUrl, RemotePort, Protocol, NetworkReportId = ReportId; SuspiciousProcesses | join kind=innerunique OutboundConnections on DeviceId, ProcessId | where NetworkTimestamp between (ProcessTimestamp .. ProcessTimestamp + CorrelationWindow) | project ProcessTimestamp, NetworkTimestamp, DeviceId, DeviceName, AccountName, FileName, ProcessCommandLine, InitiatingProcessFileName, InitiatingProcessCommandLine, SHA1, RemoteIP, RemoteIPType, RemoteUrl, RemotePort, Protocol, ProcessReportId, NetworkReportId | order by NetworkTimestamp desc |
Why this query is stronger
- The time filter is applied before parsing and joining.
- The suspicious-process table should be much smaller than the network table, so it is placed on the left side of the join.
- Only the required columns are projected into each side.
- The process-to-network timing check reduces accidental correlations.
- The query preserves both process and network report identifiers for investigation.
4. Shoot to thrill: add a transparent risk score
Scoring helps order the queue, but the calculation must remain understandable. The following version adds points for public destinations, non-standard ports, encoded or download-oriented commands, suspicious parent processes, and direct-IP connections without a URL.
| let Lookback = 7d; let CorrelationWindow = 5m; let SuspiciousTerms = dynamic([ “-enc”, “-encodedcommand”, “frombase64string”, “downloadstring”, “downloadfile”, “invoke-webrequest”, “invoke-expression”, “iex “, “webclient”, “bitsadmin”, “certutil”, “rundll32”, “regsvr32”, “mshta” ]); let HighRiskTerms = dynamic([ “-enc”, “-encodedcommand”, “frombase64string”, “downloadstring”, “invoke-expression”, “iex “ ]); let SuspiciousParents = dynamic([ “winword.exe”, “excel.exe”, “powerpnt.exe”, “outlook.exe”, “acrord32.exe”, “mshta.exe”, “wscript.exe”, “cscript.exe” ]); let SuspiciousProcesses = materialize( DeviceProcessEvents | where Timestamp > ago(Lookback) | where FileName in~ ( “powershell.exe”, “pwsh.exe”, “cmd.exe”, “wscript.exe”, “cscript.exe”, “mshta.exe” ) | extend CanonicalCommandLine = tolower(ProcessCommandLine) | extend CanonicalCommandLine = replace_string(CanonicalCommandLine, “\””, “”) | where CanonicalCommandLine has_any (SuspiciousTerms) | project ProcessTimestamp = Timestamp, DeviceId, DeviceName, AccountName, FileName, ProcessId, ProcessCommandLine, CanonicalCommandLine, InitiatingProcessFileName, InitiatingProcessCommandLine, SHA1 ); let OutboundConnections = DeviceNetworkEvents | where Timestamp > ago(Lookback) | where ActionType == “ConnectionSuccess” | where isnotempty(RemoteIP) or isnotempty(RemoteUrl) | project NetworkTimestamp = Timestamp, DeviceId, ProcessId = InitiatingProcessId, RemoteIP, RemoteIPType, RemoteUrl, RemotePort, Protocol, NetworkReportId = ReportId; SuspiciousProcesses | join kind=innerunique OutboundConnections on DeviceId, ProcessId | where NetworkTimestamp between (ProcessTimestamp .. ProcessTimestamp + CorrelationWindow) | extend RiskScore = 20 + iff(RemoteIPType =~ “Public”, 20, 0) + iff(RemotePort !in (80, 443), 10, 0) + iff(CanonicalCommandLine has_any (HighRiskTerms), 25, 0) + iff(InitiatingProcessFileName in~ (SuspiciousParents), 15, 0) + iff(isempty(RemoteUrl) and isnotempty(RemoteIP), 10, 0) | extend RiskBand = case( RiskScore >= 70, “High”, RiskScore >= 45, “Medium”, “Low” ) | summarize FirstSeen = min(ProcessTimestamp), LastSeen = max(NetworkTimestamp), Connections = count(), RemoteIPs = make_set(RemoteIP, 20), RemoteUrls = make_set(RemoteUrl, 20), RemotePorts = make_set(RemotePort, 20), MaximumRiskScore = max(RiskScore), ExampleCommandLine = any(ProcessCommandLine), ExampleParent = any(InitiatingProcessFileName), ExampleSHA1 = any(SHA1) by DeviceId, DeviceName, AccountName, FileName, RiskBand | order by MaximumRiskScore desc, Connections desc |
The score is a prioritization aid. Validate each contributing condition against your environment and avoid turning the first version into an automatic response rule.
5. High voltage: find abnormal scripting spikes
Known indicators are useful, but abnormal volume can reveal new or modified tradecraft. The following query uses a time series to identify hourly script-execution spikes relative to the device’s recent baseline.
| let StartTime = startofday(ago(14d)); let EndTime = now(); DeviceProcessEvents | where Timestamp between (StartTime .. EndTime) | where FileName in~ ( “powershell.exe”, “pwsh.exe”, “cmd.exe”, “wscript.exe”, “cscript.exe”, “mshta.exe” ) | make-series Executions = count() default=0 on Timestamp from StartTime to EndTime step 1h by DeviceName | extend (Anomalies, AnomalyScore, Baseline) = series_decompose_anomalies(Executions, 3.0, -1, “linefit”) | mv-expand Timestamp to typeof(datetime), Executions to typeof(long), Anomalies to typeof(double), AnomalyScore to typeof(double), Baseline to typeof(double) | where Anomalies > 0 | project Timestamp, DeviceName, Executions, ExpectedExecutions = round(Baseline, 2), AnomalyScore = round(AnomalyScore, 2) | order by AnomalyScore desc |
A spike is not automatically malicious. Patch windows, software deployment, logon scripts, incident-response activity, and administrative maintenance can all create legitimate anomalies. The output is a pivot into the correlated hunt, not a standalone verdict.
6. For those about to hunt: prepare a detection-ready result
After the hunting query is stable, reduce the lookback to the intended detection frequency and return a clear event timestamp and entity identifiers. Microsoft currently recommends returning Timestamp or TimeGenerated for custom detections so generated alerts have an accurate event time.
The following example returns the network event as the alerting record. Test the result set first, then use Create detection rule from Advanced Hunting.
| let Lookback = 1h; let CorrelationWindow = 5m; let SuspiciousTerms = dynamic([ “-enc”, “-encodedcommand”, “frombase64string”, “downloadstring”, “invoke-webrequest”, “invoke-expression”, “iex “ ]); let SuspiciousProcesses = DeviceProcessEvents | where Timestamp > ago(Lookback) | where FileName in~ (“powershell.exe”, “pwsh.exe”, “cmd.exe”, “mshta.exe”) | extend CanonicalCommandLine = replace_string(tolower(ProcessCommandLine), “\””, “”) | where CanonicalCommandLine has_any (SuspiciousTerms) | project ProcessTimestamp = Timestamp, DeviceId, DeviceName, AccountName, ProcessId, ProcessCommandLine, CanonicalCommandLine, InitiatingProcessFileName, SHA1; let OutboundConnections = DeviceNetworkEvents | where Timestamp > ago(Lookback) | where ActionType == “ConnectionSuccess” | where isnotempty(RemoteIP) or isnotempty(RemoteUrl) | project Timestamp, DeviceId, ProcessId = InitiatingProcessId, RemoteIP, RemoteIPType, RemoteUrl, RemotePort, ReportId; SuspiciousProcesses | join kind=innerunique OutboundConnections on DeviceId, ProcessId | where Timestamp between (ProcessTimestamp .. ProcessTimestamp + CorrelationWindow) | project Timestamp, DeviceId, ReportId, DeviceName, AccountName, ProcessCommandLine, InitiatingProcessFileName, SHA1, RemoteIP, RemoteIPType, RemoteUrl, RemotePort |
Before enabling the rule, configure the alert title, severity, MITRE ATT&CK techniques, impacted entities, schedule, lookback, and response actions. Keep response actions disabled until the rule has passed a controlled observation period.
Guided validation walkthrough
- Open the Microsoft Defender portal.
- Go to Investigation & response > Hunting > Advanced hunting.
- Confirm that DeviceProcessEvents and DeviceNetworkEvents appear in the schema.
- Run the sound-check query with Lookback = 1d.
- Record the highest-volume devices and known administrative accounts.
- Run the normalized command-line query and inspect at least ten results manually.
- Add one approved administrator, management tool, or script pattern at a time as a tightly scoped exclusion only when the activity is verified.
- Run the correlation query and select a result to inspect the device, process, hash, IP, and URL entities.
- Open Query details and review execution time and resource usage.
- Run the scored query and confirm that the highest-ranked rows are materially more suspicious than the low-ranked rows.
- Run the anomaly query and compare spikes with change windows and endpoint-management activity.
- Save the query with an owner, purpose, version, and review date in the description.
- Observe the query manually before creating a custom detection.
- Create the detection only after exclusions, severity, and response ownership have been approved.
Change-window notes
- Begin with a short lookback and a limited device group when possible.
- Do not add a global exclusion for a binary such as powershell.exe; exclude verified behavior with the narrowest stable attributes available.
- Separate the query-development change from the automated-response change.
- Capture before-and-after result counts for every material tuning change.
- Keep the original validated query version available for rollback.
- Schedule the first production run when a SOC analyst and endpoint owner are available to validate unexpected matches.
Production hardening
- Store common exclusions in a controlled watchlist or reusable function when the platform and rule type support it.
- Require peer review for changes to indicator lists, risk weights, joins, and automated actions.
- Add version comments at the top of the saved query.
- Track query execution time and resource classification after every major change.
- Keep the time range aligned with the detection schedule to avoid duplicate or missed coverage.
- Tune against verified business behavior, not simply against alert volume.
- Map the rule to the most relevant MITRE ATT&CK techniques only after the observed behavior supports the mapping.
- Include device and account criticality in triage, but do not let asset importance replace behavioral evidence.
- Use automated isolation, account containment, or file quarantine only after the detection has demonstrated sufficient precision and the response path has rollback ownership.
Operational Handoff
- Assign a named rule owner and backup owner.
- Define the expected daily or weekly match volume.
- Document first-response actions for High, Medium, and Low risk bands.
- Store validated exclusions with business owner, justification, approval date, and expiry date.
- Add the query to the SOC content review cadence.
- Review false positives, false negatives, query performance, and schema changes after Defender platform updates.
- Retain one example true positive or controlled test case so future changes can be regression-tested.
Finished-state Validation
| Control | Expected result | Evidence to capture |
| Table access | Required Defender tables return current data | Query result screenshot |
| Baseline | Normal high-volume devices and accounts are documented | Baseline export |
| Normalization | Command-line variants match without exact-string dependence | Sample result set |
| Correlation | Process and network events align by device, process, and time | Correlated event record |
| Scoring | High-ranked rows contain stronger combined indicators | Scored result export |
| Anomaly detection | Known maintenance windows are distinguishable from unexplained spikes | Time-series result |
| Performance | Query completes within an acceptable time and resource classification | Query details screenshot |
| Ownership | Rule owner, triage path, exclusions, and review date are documented | Runbook or change record |
| Detection readiness | Detection query returns a valid timestamp and entity context | Detection preview |
| Rollback | Previous query version and rule-disable procedure are available | Repository or change ticket |
Cost and Licensing Notes
Advanced Hunting availability depends on the Microsoft security products and licensing that populate the queried tables. Microsoft Defender for Endpoint Plan 2 and applicable Microsoft Defender XDR services are common sources for the device telemetry used here. Microsoft Sentinel data can also be queried from the unified Defender portal when the workspace is connected and the analyst has the required access.
KQL itself is not billed per query in Microsoft Defender XDR, but operational cost still matters. Complex detections consume analyst time, query capacity, alert-processing effort, and response capacity. In Microsoft Sentinel, ingestion, retention, analytics, automation, and related services may introduce separate Azure costs.
Known Limitations and Edge Cases
- Process IDs can be reused. The device match and tight time window reduce the risk but do not eliminate it.
- Public IP classification and URL population vary by event and network path.
- Proxy, VPN, secure web gateway, and network inspection services can obscure the original destination.
- Script interpreters are heavily used by legitimate administration and endpoint-management tooling.
- series_decompose_anomalies() needs sufficient historical data to build a meaningful baseline.
- Advanced Hunting retention is limited by the product and configuration. Longer historical analysis may require Microsoft Sentinel, streaming, or another retained data store.
- Some advanced functions or cross-service operators cannot be used in every custom detection or analytics-rule context.
- Schema fields and supported event types can change; verify the in-portal schema before production deployment.
Troubleshooting
- No results: Confirm Defender for Endpoint onboarding, table availability, permissions, and the selected time range.
- Too many results: Shorten the lookback, require stronger combinations of indicators, and add verified exclusions one at a time.
- Join is slow: Reduce both sides before the join, project fewer columns, keep the smaller data set on the left, and shorten the correlation window.
- Query reaches high resource usage: Replace broad text searches, remove unused columns, test with count, and avoid parsing before filtering.
- Expected network event is missing: Check whether the activity used a proxy, another process, a child process, cached content, or a connection outside the selected time window.
- Anomaly output is noisy: Increase the threshold, change the bin size, separate servers from workstations, or baseline device groups independently.
- Custom detection option is unavailable: Re-check role permissions, query compatibility, required output fields, and whether unsupported operators are present.
- Legitimate administration dominates: Build narrowly scoped allow conditions around signed scripts, known management paths, approved accounts, device groups, or verified destinations. Avoid broad suppression.
Rollback or cleanup
- Disable the custom detection rule before deleting it so the rule configuration and alert history can be reviewed.
- Restore the last validated query version from the repository or saved-query history.
- Remove newly introduced exclusions if they reduced visibility beyond the approved scope.
- Preserve exported results, query details, tuning decisions, and change records.
- Confirm that no automated response action remains active after the rule is disabled.
- Review any incidents created during testing and close them with an accurate classification.
The point of advanced hunting is not to make the query louder. It is to make the signal clearer. Filter early, correlate carefully, score transparently, and validate the result before automation turns the volume up.
About AzureTracks
AzureTracks publishes practical Azure and Microsoft 365 security guidance with deployment, validation, operational, and rollback considerations.
References
Kusto join operator: https://learn.microsoft.com/en-us/kusto/query/join-operator
AC/DC official Edmonton tour listing: https://www.acdc.com/tour/09-08-2026-commonwealth-stadium-edmonton-ab/
Microsoft Defender XDR Advanced Hunting query language: https://learn.microsoft.com/en-us/defender-xdr/advanced-hunting-query-language
Microsoft Defender XDR Advanced Hunting best practices: https://learn.microsoft.com/en-us/defender-xdr/advanced-hunting-best-practices
Microsoft Defender XDR Advanced Hunting overview: https://learn.microsoft.com/en-us/defender-xdr/advanced-hunting-overview
Microsoft Defender XDR custom detection rules: https://learn.microsoft.com/en-us/defender-xdr/custom-detection-rules
Kusto materialize() function: https://learn.microsoft.com/en-us/kusto/query/materialize-function
