Using let Statements to Build Reusable Hunting Queries.
The investigation was not complicated.
The query was.
The same values appeared again and again.
Agent Foskett knew there was a cleaner way.
Lesson overview
Learn how defenders use let statements in KQL to create reusable variables, simplify complex queries and build cleaner Microsoft Defender XDR threat hunting investigations.
Why let statements matter
The reusable hunting workflow
Step 1 — Create a simple investigation variable
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
let InvestigatedUser = "alex.wilson@contoso.com";
IdentityLogonEvents
| where Timestamp > ago(7d)
| where AccountUpn == InvestigatedUser
| project Timestamp,
AccountUpn,
DeviceName,
ActionType,
LogonType,
RemoteIP
| order by Timestamp desc
Step 2 — Reuse a time range across the hunt
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
let TimeRange = 24h;
DeviceProcessEvents
| where Timestamp > ago(TimeRange)
| where FileName in~ ("powershell.exe", "cmd.exe", "rundll32.exe")
| project Timestamp,
DeviceName,
AccountUpn,
FileName,
ProcessCommandLine
| order by Timestamp desc
Step 3 — Create a reusable suspicious process list
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
let SuspiciousProcesses = dynamic([
"powershell.exe",
"cmd.exe",
"rundll32.exe",
"mshta.exe",
"regsvr32.exe",
"certutil.exe"
]);
DeviceProcessEvents
| where Timestamp > ago(24h)
| where FileName in~ (SuspiciousProcesses)
| project Timestamp,
DeviceName,
AccountUpn,
FileName,
InitiatingProcessFileName,
ProcessCommandLine
| order by Timestamp desc
Step 4 — Reuse one device across multiple tables
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
let InvestigatedDevice = "LAPTOP-042";
let TimeRange = 24h;
DeviceProcessEvents
| where Timestamp > ago(TimeRange)
| where DeviceName == InvestigatedDevice
| project Timestamp, DeviceName, EvidenceType="Process", Detail=FileName, Extra=ProcessCommandLine
| union (
DeviceNetworkEvents
| where Timestamp > ago(TimeRange)
| where DeviceName == InvestigatedDevice
| project Timestamp, DeviceName, EvidenceType="Network", Detail=RemoteUrl, Extra=RemoteIP
)
| union (
DeviceFileEvents
| where Timestamp > ago(TimeRange)
| where DeviceName == InvestigatedDevice
| project Timestamp, DeviceName, EvidenceType="File", Detail=FileName, Extra=FolderPath
)
| order by Timestamp asc
Step 5 — Combine let statements with joins
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
let TimeRange = 24h;
let SuspiciousPowerShell =
DeviceProcessEvents
| where Timestamp > ago(TimeRange)
| where FileName =~ "powershell.exe"
| where ProcessCommandLine has_any ("-enc", "EncodedCommand", "DownloadString", "Invoke-WebRequest")
| project ProcessTime=Timestamp, DeviceId, DeviceName, AccountUpn, ProcessCommandLine;
let NetworkConnections =
DeviceNetworkEvents
| where Timestamp > ago(TimeRange)
| project NetworkTime=Timestamp, DeviceId, AccountUpn, RemoteUrl, RemoteIP, InitiatingProcessFileName;
SuspiciousPowerShell
| join kind=inner (NetworkConnections) on DeviceId, AccountUpn
| where NetworkTime between (ProcessTime .. ProcessTime + 10m)
| project ProcessTime, NetworkTime, DeviceName, AccountUpn, ProcessCommandLine, RemoteUrl, RemoteIP
| order by ProcessTime asc
Step 6 — Build an IOC hunt with reusable values
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
let TimeRange = 7d;
let SuspiciousDomains = dynamic([
"login-contoso-support.example",
"cdn-update-service.example",
"secure-document-review.example"
]);
DeviceNetworkEvents
| where Timestamp > ago(TimeRange)
| where RemoteUrl has_any (SuspiciousDomains)
| project Timestamp,
DeviceName,
AccountUpn,
RemoteUrl,
RemoteIP,
InitiatingProcessFileName
| order by Timestamp desc
How to read let-based queries
Real-world investigation
Investigation checklist
Related Agent Foskett Academy lessons
Advanced KQL milestone
Coming next
Final thought
Using let Statements to Build Reusable Hunting Queries
Agent Foskett Academy Lesson 92 teaches defenders how to use let statements in KQL to create reusable Microsoft Defender XDR hunting queries.
KQL let statements for Microsoft Defender XDR hunting
This lesson explains how let statements, reusable variables, dynamic arrays, investigation parameters, DeviceProcessEvents, DeviceNetworkEvents, DeviceFileEvents, IdentityLogonEvents, joins, unions and IOC hunts help defenders write cleaner and more maintainable KQL queries.
