Advanced KQL: Building Reusable KQL Functions.
The hunt worked perfectly.
So the analyst copied it into another query. Then another. Then another.
Six months later, the same logic existed in twenty different places.
Agent Foskett realised that professional KQL is not just about writing a query that works. It is about building trusted logic once, then reusing it everywhere.
Lesson overview
Learn how to build reusable KQL functions that standardise threat hunting logic, simplify complex investigations and support scalable Microsoft Defender XDR workflows.
Why reusable KQL functions matter
The reusable function mindset
Step 1 — Create your first reusable function
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
let RecentLogons = () {
IdentityLogonEvents
| where Timestamp > ago(7d)
| project Timestamp,
AccountUpn,
ActionType,
IPAddress,
Location,
DeviceName
};
RecentLogons()
| order by Timestamp descStep 2 — Add a user parameter
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
let UserLogons = (User:string) {
IdentityLogonEvents
| where Timestamp > ago(14d)
| where AccountUpn =~ User
| project Timestamp,
AccountUpn,
ActionType,
IPAddress,
Location,
DeviceName
};
UserLogons("user@contoso.com")
| order by Timestamp ascStep 3 — Pass a timeframe into the function
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
let UserLogons = (User:string, Lookback:timespan) {
IdentityLogonEvents
| where Timestamp > ago(Lookback)
| where AccountUpn =~ User
| project Timestamp,
AccountUpn,
ActionType,
IPAddress,
Location,
DeviceName
};
UserLogons("user@contoso.com", 30d)
| order by Timestamp ascStep 4 — Build a reusable high-risk logon filter
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
let HighRiskLogons = (Lookback:timespan) {
IdentityLogonEvents
| where Timestamp > ago(Lookback)
| where ActionType has_any ("LogonFailed", "LogonSuccess")
| where FailureReason has_any ("MFA", "Conditional Access", "Risk")
or Location !in ("Australia", "New Zealand")
| project Timestamp,
AccountUpn,
ActionType,
IPAddress,
Location,
FailureReason
};
HighRiskLogons(14d)
| summarize Events = count(), FirstSeen = min(Timestamp), LastSeen = max(Timestamp) by AccountUpn, Location
| order by Events descStep 5 — Build a reusable suspicious PowerShell function
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
let SuspiciousPowerShell = (Lookback:timespan) {
DeviceProcessEvents
| where Timestamp > ago(Lookback)
| where FileName in~ ("powershell.exe", "pwsh.exe")
| where ProcessCommandLine has_any ("-enc", "-encodedcommand", "downloadstring", "iex", "bypass", "hidden")
| project Timestamp,
DeviceName,
InitiatingProcessAccountName,
FileName,
ProcessCommandLine,
InitiatingProcessFileName
};
SuspiciousPowerShell(7d)
| order by Timestamp descStep 6 — Build a reusable network connection function
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
let DeviceOutboundConnections = (Device:string, Lookback:timespan) {
DeviceNetworkEvents
| where Timestamp > ago(Lookback)
| where DeviceName =~ Device
| where isnotempty(RemoteUrl) or isnotempty(RemoteIP)
| project Timestamp,
DeviceName,
InitiatingProcessFileName,
RemoteUrl,
RemoteIP,
RemotePort,
Protocol
};
DeviceOutboundConnections("device01.contoso.com", 24h)
| order by Timestamp descStep 7 — Build a reusable email investigation function
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
let RecentEmailFromSender = (Sender:string, Lookback:timespan) {
EmailEvents
| where Timestamp > ago(Lookback)
| where SenderFromAddress =~ Sender or SenderMailFromAddress =~ Sender
| project Timestamp,
SenderFromAddress,
SenderMailFromAddress,
RecipientEmailAddress,
Subject,
DeliveryAction,
ThreatTypes,
NetworkMessageId
};
RecentEmailFromSender("sender@example.com", 7d)
| order by Timestamp descStep 8 — Build a reusable user timeline function
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
let UserActivityTimeline = (User:string, Lookback:timespan) {
let IdentityActivity =
IdentityLogonEvents
| where Timestamp > ago(Lookback)
| where AccountUpn =~ User
| project Timestamp,
Source = "IdentityLogonEvents",
Account = AccountUpn,
Activity = ActionType,
Detail = strcat(IPAddress, " ", Location);
let CloudActivity =
CloudAppEvents
| where Timestamp > ago(Lookback)
| where AccountDisplayName =~ User
| project Timestamp,
Source = "CloudAppEvents",
Account = AccountDisplayName,
Activity = ActionType,
Detail = strcat(Application, " ", ObjectName);
union IdentityActivity, CloudActivity
};
UserActivityTimeline("user@contoso.com", 14d)
| order by Timestamp ascStep 9 — Call one function from another
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
let SuspiciousPowerShell = (Lookback:timespan) {
DeviceProcessEvents
| where Timestamp > ago(Lookback)
| where FileName in~ ("powershell.exe", "pwsh.exe")
| where ProcessCommandLine has_any ("-enc", "downloadstring", "bypass", "hidden")
| project Timestamp, DeviceName, Account = InitiatingProcessAccountName, ProcessCommandLine
};
let PowerShellByUser = (User:string, Lookback:timespan) {
SuspiciousPowerShell(Lookback)
| where Account =~ User
};
PowerShellByUser("alice", 7d)
| order by Timestamp descStep 10 — Build a reusable investigation summary
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
let SuspiciousPowerShellSummary = (Lookback:timespan) {
DeviceProcessEvents
| where Timestamp > ago(Lookback)
| where FileName in~ ("powershell.exe", "pwsh.exe")
| where ProcessCommandLine has_any ("-enc", "downloadstring", "bypass", "hidden")
| summarize Events = count(),
Devices = dcount(DeviceName),
Users = dcount(InitiatingProcessAccountName),
FirstSeen = min(Timestamp),
LastSeen = max(Timestamp)
by FileName
};
SuspiciousPowerShellSummary(30d)
| order by Events descFunction design checklist
Common reusable KQL function libraries
Related Agent Foskett Academy lessons
Coming next
Final thought
Advanced KQL Building Reusable KQL Functions in Microsoft Defender XDR
Agent Foskett Academy Lesson 125 teaches defenders how to build reusable KQL functions for Microsoft Defender XDR, parameterise hunting logic and create scalable investigation libraries.
Learn reusable KQL functions for Microsoft security investigations
Reusable KQL functions help security analysts reduce duplication, standardise investigations, simplify complex hunting queries and improve maintainability across Defender XDR and Microsoft Sentinel environments.
Microsoft Defender XDR KQL function tutorial
This lesson explains how to create KQL functions, pass parameters, build reusable filters, create timeline functions and design enterprise hunting libraries for identity, endpoint, email, cloud and network investigations.
