Using has_any to Find Suspicious Text
Not every investigation starts with an exact match.
Sometimes you are looking for words.
Suspicious commands.
Phishing phrases.
Strange URL fragments.
That is where has_any becomes useful.
In this Agent Foskett Academy lesson, you will learn how defenders use the KQL has_any operator to search for multiple suspicious text values across Microsoft Defender XDR and Microsoft Sentinel telemetry.
Lesson overview
Learn how to search text fields for multiple suspicious words, commands and fragments without writing long chains of contains statements.
Why has_any matters
A command line may contain PowerShell flags. A URL may contain login words. An email subject may include urgent language. A file path may contain a suspicious folder name.
The has_any operator lets you search a text field for any value from a list of terms.
Investigation scenario
Several command lines include strange PowerShell behaviour, encoded commands and download activity. The analyst wants to search for multiple suspicious terms without creating a messy query.
Step 1 — The messy way with contains
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
DeviceProcessEvents | where Timestamp > ago(7d) | where ProcessCommandLine contains "encodedcommand" or ProcessCommandLine contains "downloadstring" or ProcessCommandLine contains "invoke-webrequest" | project Timestamp, DeviceName, FileName, ProcessCommandLine, InitiatingProcessAccountName
Step 2 — The cleaner way with has_any
- 1
- 2
- 3
- 4
- 5
- 6
DeviceProcessEvents | where Timestamp > ago(7d) | where ProcessCommandLine has_any ("encodedcommand", "downloadstring", "invoke-webrequest") | project Timestamp, DeviceName, FileName, ProcessCommandLine, InitiatingProcessAccountName
Step 3 — Use has_any with a let statement
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
let suspiciousTerms = dynamic(["encodedcommand", "downloadstring", "invoke-webrequest", "bypass"]); DeviceProcessEvents | where Timestamp > ago(7d) | where ProcessCommandLine has_any (suspiciousTerms) | project Timestamp, DeviceName, InitiatingProcessAccountName, FileName, ProcessCommandLine | sort by Timestamp desc
What has_any does
Think of it as asking: does this field contain any of these suspicious words?
Step 4 — Search suspicious email subjects
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
let subjectTerms = dynamic(["urgent", "password", "invoice", "verify", "payment"]); EmailEvents | where Timestamp > ago(7d) | where Subject has_any (subjectTerms) | project Timestamp, SenderFromAddress, RecipientEmailAddress, Subject, DeliveryAction | sort by Timestamp desc
Step 5 — Search suspicious URL text
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
let urlTerms = dynamic(["login", "verify", "account", "secure", "password"]); UrlClickEvents | where Timestamp > ago(7d) | where Url has_any (urlTerms) | project Timestamp, AccountUpn, Url, ActionType, ThreatTypes | sort by Timestamp desc
Step 6 — Combine has_any with useful columns
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
let suspiciousTerms = dynamic(["encodedcommand", "downloadstring", "invoke-webrequest", "bypass"]); DeviceProcessEvents | where Timestamp > ago(7d) | where ProcessCommandLine has_any (suspiciousTerms) | project Timestamp, DeviceName, Account = InitiatingProcessAccountName, FileName, ProcessCommandLine | sort by Timestamp desc
Investigator notes
If you need exact matching against known values such as IP addresses, domains, users or hashes, in may be the better operator.
What you learned
Continue your investigation
Continue learning with Creating Investigation Parameters, Using let Statements to Reuse Evidence, KQL Threat Hunting Guide, UrlClickEvents and Microsoft Security.
Develop IT. Protect IT. GEMXIT PTY LTD | GEMXIT UK LTD