Phishing Detection Rules for Microsoft Defender and Sentinel (T1566)
Phishing is the front door for 80%+ of targeted intrusions. The email gateway catches the obvious stuff — the interesting attacks arrive looking boring. No malware attached, no obvious typos, sent from a newly-registered but cleanly-authenticated domain, and pitched at exactly the right employee. By the time your SOC sees the alert, someone has already clicked.
Writing a good phishing detection rule means accepting two facts. First, no single signal catches everything — you need layered detection across email verdict data, URL click telemetry, and post-click endpoint behavior. Second, the highest-fidelity signals sit on the endpoint, not in the mail flow — the attacker can spoof a sender, but they can’t spoof winword.exe spawning PowerShell.
This guide walks through real KQL and SPL queries covering the three main sub-techniques of MITRE ATT&CK T1566 — Phishing: spearphishing attachment, spearphishing link, and spearphishing via service. Every query here is from the df00tech detection library and ready to paste into Sentinel or Microsoft 365 Defender Advanced Hunting.
1. Why Email Is Still the #1 Initial Access Vector
Every year, someone announces that phishing is dying. Every year, the Verizon DBIR shows it isn’t. The reason is structural: email is the only communication channel where a stranger can reach any employee, at any time, with a file or a link, bypassing every network control you have. Zero-trust architectures, EDR, and MFA all raise the cost of post-click actions — but none of them stop the email from arriving.
Modern phishing campaigns have evolved past the “Nigerian prince” era. The patterns that succeed in 2026 look like this:
- Thread hijacking — the attacker compromises a legitimate account, replies to an existing conversation, and attaches the payload. SPF, DKIM, and DMARC all pass.
- OAuth consent phishing — no malware, no credential prompt. Just a link to login.microsoftonline.com asking the user to grant an innocent-looking app Mail.Read permission.
- ISO and LNK containers — attachments wrapped in ISO, IMG, or VHD files to strip the Mark-of-the-Web zone identifier, bypassing SmartScreen entirely.
- Callback phishing and Teams vishing — Storm-1811 (Black Basta) and Luna Moth send emails with no payload at all, just a phone number. The victim calls, is social-engineered into installing Quick Assist or AnyDesk, and the ransomware lands hours later.
None of this shows up in a “block .exe attachments” filter. You need detection content tuned to each delivery variant — and that starts with knowing which sub-technique you’re hunting.
2. Detecting Spearphishing Attachments (T1566.001)
Spearphishing attachments remain the single most common initial access technique. The attacker sends a Word, Excel, RTF, or ISO file. The victim opens it, a macro or embedded exploit fires, and a child process is spawned — almost always a shell or scripting engine.
The email gateway can catch the file if it’s seen the hash before. It usually hasn’t. What you can catch reliably is the post-open behavior: Office applications should not, under normal circumstances, spawn powershell.exe, cmd.exe, or mshta.exe. That parent-child relationship is the single highest-fidelity phishing detection signal available in endpoint telemetry.
Here’s the full T1566.001 KQL detection from the df00tech library:
// Primary detection: Office applications spawning suspicious child processes
// This is the strongest post-attachment-open indicator available in endpoint telemetry
let OfficeApps = dynamic([
"winword.exe", "excel.exe", "powerpnt.exe", "outlook.exe",
"mspub.exe", "msaccess.exe", "onenote.exe", "visio.exe", "eqnedt32.exe"
]);
let SuspiciousChildren = dynamic([
"cmd.exe", "powershell.exe", "pwsh.exe", "wscript.exe", "cscript.exe",
"mshta.exe", "regsvr32.exe", "rundll32.exe", "certutil.exe", "bitsadmin.exe",
"curl.exe", "wget.exe", "msbuild.exe", "installutil.exe", "schtasks.exe",
"at.exe", "wmic.exe", "odbcconf.exe", "pcalua.exe", "cmstp.exe",
"msiexec.exe", "explorer.exe", "hh.exe"
]);
DeviceProcessEvents
| where Timestamp > ago(24h)
| where InitiatingProcessFileName in~ (OfficeApps)
| where FileName in~ (SuspiciousChildren)
| extend RiskLevel = case(
FileName in~ ("powershell.exe", "pwsh.exe", "mshta.exe", "wscript.exe", "cscript.exe"), "Critical",
FileName in~ ("certutil.exe", "bitsadmin.exe", "regsvr32.exe", "rundll32.exe", "odbcconf.exe", "cmstp.exe"), "High",
"Medium"
)
| extend SuspiciousNetwork = ProcessCommandLine has_any ("http://", "https://", "ftp://", "\\\\")
| extend EncodedPayload = ProcessCommandLine has_any ("-enc", "-EncodedCommand", "FromBase64String", "/e:jscript", "/e:vbscript")
| extend TempExecution = ProcessCommandLine has_any ("\\Temp\\", "\\AppData\\", "\\Downloads\\", "%temp%", "%appdata%")
| project Timestamp, DeviceName, AccountName, FileName, ProcessCommandLine,
InitiatingProcessFileName, InitiatingProcessCommandLine,
RiskLevel, SuspiciousNetwork, EncodedPayload, TempExecution
| sort by Timestamp desc
What this catches: Any Office application spawning a scripting interpreter or LOLBin. The RiskLevel extension tags the truly scary children (PowerShell, mshta, wscript) as Critical because they have no legitimate reason to be spawned by Word or Excel in most environments. The SuspiciousNetwork, EncodedPayload, and TempExecution enrichment fields give an analyst everything they need to triage in seconds — a critical-risk winword.exe spawning powershell.exe with -enc and an http:// in the command line is a near-certain phishing compromise.
Pay special attention to eqnedt32.exe — Microsoft Equation Editor has no legitimate reason to spawn child processes in modern environments. Any hit on that parent is CVE-2017-11882 or CVE-2018-0798 exploitation, the same chain APT28 and Cobalt Group have used for years. Treat as zero-tolerance.
Tuning note: finance and legal departments often have legitimate Excel macros that call cmd.exe for report generation. Don’t whitelist by broad rule — instead, allowlist specific parent/child hash combinations with specific command-line paths. Every exclusion is a blind spot; document them.
Full playbook, atomic tests, and hunting variants at T1566.001 — Spearphishing Attachment. The companion user-execution detection is T1204.002 — User Execution: Malicious File, which catches the general “user clicked a file” pattern independent of Office.
3. Detecting Spearphishing Links (T1566.002)
Links are the attacker’s preferred vector now because they survive attachment filters entirely. Modern variants include credential-harvesting pages, drive-by exploits, HTA delivery, and OAuth consent phishing — all triggered by a single click.
The challenge: the click happens in the browser, which is a noisy environment. The signal that actually works is the same parent-child pattern, but with email clients and browsers as the parent:
let EmailClients = dynamic(["outlook.exe", "thunderbird.exe", "teams.exe", "msoutlook.exe"]);
let BrowserApps = dynamic(["msedge.exe", "chrome.exe", "firefox.exe", "iexplore.exe", "opera.exe", "brave.exe"]);
let SuspiciousChildren = dynamic(["powershell.exe", "pwsh.exe", "cmd.exe", "wscript.exe", "cscript.exe", "mshta.exe", "rundll32.exe", "regsvr32.exe", "msiexec.exe", "certutil.exe", "bitsadmin.exe", "curl.exe", "wget.exe"]);
// Vector 1: Email client directly spawning a suspicious process (link opens registered protocol handler or triggers file download)
let EmailClientSpawn = DeviceProcessEvents
| where Timestamp > ago(24h)
| where InitiatingProcessFileName has_any (EmailClients)
| where FileName has_any (SuspiciousChildren)
| extend DetectionVector = "EmailClientDirectSpawn"
| extend RiskReason = strcat("Email client '", InitiatingProcessFileName, "' spawned '", FileName, "'");
// Vector 2: Browser spawning a suspicious process (drive-by exploit or redirect to malicious file association)
let BrowserSpawn = DeviceProcessEvents
| where Timestamp > ago(24h)
| where InitiatingProcessFileName has_any (BrowserApps)
| where FileName has_any (SuspiciousChildren)
// Exclude legitimate browser internal sub-processes
| where not(ProcessCommandLine has_any ("--type=renderer", "--type=utility", "--type=gpu-process", "--type=crashpad-handler", "--extension-process", "NativeMessagingHost"))
| where not(FileName =~ "msiexec.exe" and ProcessCommandLine has_any ("MicrosoftEdgeUpdate", "GoogleUpdate", "ChromeSetup", "EdgeUpdate"))
| extend DetectionVector = "BrowserSpawnedSuspiciousProcess"
| extend RiskReason = strcat("Browser '", InitiatingProcessFileName, "' spawned '", FileName, "'");
// Vector 3: MSHTA spawning additional processes (common in phishing link -> HTA -> payload chains)
let MshtaChain = DeviceProcessEvents
| where Timestamp > ago(24h)
| where InitiatingProcessFileName =~ "mshta.exe"
| where FileName has_any (SuspiciousChildren)
| extend DetectionVector = "MshtaSpawnedSuspiciousProcess"
| extend RiskReason = strcat("mshta.exe spawned '", FileName, "' — possible HTA payload chain");
union EmailClientSpawn, BrowserSpawn, MshtaChain
| project Timestamp, DeviceName, AccountName, FileName, ProcessCommandLine,
InitiatingProcessFileName, InitiatingProcessCommandLine,
DetectionVector, RiskReason
| sort by Timestamp desc
What this catches: Three distinct phishing-link behaviors in one query. Vector 1 catches Outlook or Teams spawning a shell directly, which happens when the clicked link opens a registered protocol handler (like search-ms:// or a custom ms-office:// handler) that ultimately executes code. Vector 2 catches the browser spawning a scripting interpreter, which is the signature of a drive-by exploit or a disguised download (invoice.pdf.js saved and auto-opened). Vector 3 catches the HTA chain — attacker delivers an .hta file, the browser hands it to mshta.exe, and mshta spawns PowerShell. This is the Squirrelwaffle and DarkGate pattern.
The not(...) filters are critical. Chrome spawning msiexec for Google Update is legitimate; Chrome spawning msiexec for anything else is not. The --type=renderer exclusion handles Chromium’s internal process model, which would otherwise generate thousands of false positives.
Pair this with email-layer telemetry. If you have Microsoft Defender for Office 365 Plan 2, the UrlClickEvents table records every SafeLinks click with the original URL, user, and whether they bypassed a warning. Correlating a ClickedThrough=true event with a process spawn in the following 60 seconds is the cleanest phishing-link signal you can build.
OAuth consent phishing and device code phishing are covered as separate hunting queries in the full T1566.002 detection — they require AuditLogs and SigninLogs rather than endpoint data. The companion detection for general link-click compromise is T1204.001 — User Execution: Malicious Link.
4. Detecting Service-Based Phishing and Teams Vishing (T1566.003)
This is the sub-technique security teams most commonly miss, because the delivery channel is outside your enterprise visibility. LinkedIn recruiter messages, Telegram file shares, Discord CDN links, WhatsApp attachments, and — increasingly — Microsoft Teams vishing calls from external tenants. None of it touches your email gateway. None of it shows up in EmailEvents.
What you can see is the endpoint execution phase. Files delivered via social media land in Downloads or Desktop. Teams vishing results in Teams.exe appearing as a parent process to powershell.exe or quickassist.exe. Here’s the detection:
// T1566.003 — Spearphishing via Service
// Social media delivery occurs outside enterprise visibility. This query detects two
// high-confidence post-delivery execution signals observable in endpoint telemetry:
// Signal 1: Messaging/collaboration apps directly spawning command interpreters or LOLBins
// Signal 2: Scripts or interpreters executing from user Download/Desktop directories
let SuspiciousChildProcs = dynamic([
"cmd.exe", "powershell.exe", "pwsh.exe", "wscript.exe", "cscript.exe",
"mshta.exe", "rundll32.exe", "regsvr32.exe", "certutil.exe",
"msiexec.exe", "wmic.exe", "bitsadmin.exe", "curl.exe", "wget.exe"
]);
let MessagingClients = dynamic([
"Teams.exe", "Slack.exe", "Discord.exe", "Telegram.exe", "WhatsApp.exe",
"update.exe" // Slack/Discord updater sometimes used as parent
]);
let MessagingPaths = dynamic([
"\\Microsoft\\Teams\\", "\\Slack\\", "\\Discord\\",
"\\Telegram Desktop\\", "\\WhatsApp\\"
]);
let DownloadPaths = dynamic(["\\Downloads\\", "\\Desktop\\"]);
// Signal 1: Messaging desktop client spawning suspicious child processes
// Covers Storm-1811 Teams vishing, ToddyCat Telegram delivery
let MessagingSpawn = DeviceProcessEvents
| where Timestamp > ago(24h)
| where InitiatingProcessFileName in~ (MessagingClients)
or InitiatingProcessFolderPath has_any (MessagingPaths)
| where FileName in~ (SuspiciousChildProcs)
| extend Signal = "MessagingClientSpawn"
| extend SignalDetail = strcat(InitiatingProcessFileName, " spawned ", FileName);
// Signal 2: Scripting engines or interpreters executing directly from Download/Desktop paths
// Covers files delivered via browser after social media link-click
let DownloadExecution = DeviceProcessEvents
| where Timestamp > ago(24h)
| where FolderPath has_any (DownloadPaths)
or (ProcessCommandLine has_any (DownloadPaths)
and FileName in~ (SuspiciousChildProcs))
| where FileName in~ (SuspiciousChildProcs)
or FolderPath has_any (DownloadPaths)
| extend Signal = "DownloadDirectoryExecution"
| extend SignalDetail = strcat(FileName, " executed from ", FolderPath);
// Signal 3 (correlated): Any executable in Downloads that spawns a child interpreter
let DownloadSpawnChain = DeviceProcessEvents
| where Timestamp > ago(24h)
| where InitiatingProcessFolderPath has_any (DownloadPaths)
| where FileName in~ (SuspiciousChildProcs)
| extend Signal = "DownloadedBinarySpawnedInterpreter"
| extend SignalDetail = strcat(InitiatingProcessFileName, " (from Downloads) spawned ", FileName);
// Combine all signals
union MessagingSpawn, DownloadExecution, DownloadSpawnChain
| project Timestamp, DeviceName, AccountName, FileName, ProcessCommandLine, FolderPath,
InitiatingProcessFileName, InitiatingProcessCommandLine,
InitiatingProcessFolderPath, Signal, SignalDetail
| sort by Timestamp desc
What this catches: The MessagingClientSpawn signal is the Storm-1811 detection — Teams.exe has no legitimate reason to spawn PowerShell or quickassist.exe. When it does, you’re looking at either a Black Basta vishing call in progress or one that just completed. DownloadDirectoryExecution catches any scripting engine running out of \Downloads\ or \Desktop\, which is the standard landing zone for LinkedIn-delivered job lures (Lazarus Contagious Interview) and Telegram ZIP drops. DownloadedBinarySpawnedInterpreter is the highest-fidelity of the three — a binary downloaded from the internet spawning PowerShell is almost always a second-stage dropper.
Forensic pivot: once this fires, immediately check the Zone.Identifier alternate data stream on the executed file. Run Get-Content <file> -Stream Zone.Identifier — the HostUrl and ReferrerUrl fields reveal whether the file came from LinkedIn CDN, Discord CDN, or a specific file-sharing service. This gives you definitive delivery attribution without needing browser history, and it survives browser cache clearing. If you have Sysmon Event ID 15 enabled, this data is captured passively at every download.
Full detection, atomic tests, and ISO/Mark-of-the-Web bypass hunting queries are in T1566.003 — Spearphishing via Service.
5. Post-Click: What to Hunt When Someone Actually Clicks
Sometimes you don’t catch the delivery. You catch the aftermath. Here’s the email-layer detection from the parent T1566 — Phishing detection, which scores every inbound message against a composite ThreatScore:
// Signal 1: Inbound email with phishing or malware verdict from Microsoft Defender for Office 365
let EmailThreatEvents =
EmailEvents
| where Timestamp > ago(24h)
| where EmailDirection == "Inbound"
| where ThreatTypes has_any ("Phish", "Malware") or (DeliveryAction == "Blocked" and ConfidenceLevel == "High")
| extend AuthJson = parse_json(AuthenticationDetails)
| extend SPFResult = tostring(AuthJson.SPF)
| extend DKIMResult = tostring(AuthJson.DKIM)
| extend DMARCResult = tostring(AuthJson.DMARC)
| extend AuthFailed = (SPFResult =~ "Fail" or DKIMResult =~ "Fail" or DMARCResult =~ "Fail")
| extend SuspiciousSubject = Subject has_any (dynamic(["invoice", "payment", "urgent", "verify", "suspended", "confirm", "unusual activity", "password reset", "credentials", "wire transfer", "action required", "shared with you", "security alert", "your account"]))
| project Timestamp, NetworkMessageId, SenderFromAddress, SenderFromDomain,
SenderIPv4, RecipientEmailAddress, Subject, ThreatTypes, ConfidenceLevel,
DeliveryAction, DeliveryLocation, SuspiciousSubject,
AuthFailed, SPFResult, DKIMResult, DMARCResult;
EmailThreatEvents
| extend ThreatScore =
toint(ThreatTypes has "Phish") * 3 +
toint(ThreatTypes has "Malware") * 3 +
toint(AuthFailed) +
toint(SuspiciousSubject)
| where ThreatScore > 0
| sort by ThreatScore desc, Timestamp desc
What this catches: Every inbound message that Microsoft Defender for Office 365 flagged as Phish or Malware, weighted up by SPF/DKIM/DMARC authentication failures and subject-line keyword matches. ThreatScore >= 3 is the sweet spot for most environments — high enough to filter out noisy newsletter bounces, low enough to catch credential-harvesting campaigns with a clean sender reputation.
The critical pivot: when a message fires with ThreatScore >= 3, immediately look for an Azure AD sign-in from the recipient within the following 2 hours from a new country or IP. The full T1566 parent detection includes a pre-built hunt that joins EmailEvents with AADSignInLogs for exactly this correlation — it’s the fastest way to identify credential-harvesting success.
If the click already happened and you’re racing to scope the blast radius, the sequence is:
- Pull the full process tree for the recipient device in the ±30 minute window around email delivery — look for Office, browser, or Teams spawning anything suspicious.
- Check
UrlClickEventsfor theNetworkMessageId— did the user click, and did they click through a SafeLinks warning? - Check
AuditLogsfor any new OAuth consent grants from the recipient in the last 48 hours — consent phishing leaves no endpoint trace. - Check
SigninLogsforAuthenticationProtocol = "deviceCode"entries from unmanaged devices — device code phishing similarly leaves no file artifacts.
This is where the work happens. The phishing detection rule fires; the hunt confirms whether the click succeeded.
Browse the Full Library
Phishing is layered, so your detection has to be too. Email-level verdicts tell you what arrived. URL click telemetry tells you who clicked. Endpoint process creation tells you what happened next. A phishing detection rule that covers only one of those layers will miss the interesting attacks — the ones without malware, with clean authentication, with a 30-second window between click and credential theft.
Browse the full df00tech detection library — 700+ KQL and SPL queries mapped to MITRE ATT&CK, free forever. Every detection includes a response playbook, atomic tests for validation, tuning guidance, and hunting queries for the signals that adjacent to the primary rule.