I have seen this pattern hundreds of times in a real SOC. A phishing email lands. A user opens the attachment. Forty seconds later, OUTLOOK.EXE spawns powershell.exe with a command line that starts with -EncodedCommand followed by a wall of Base64. By the time the analyst opens the alert, the payload has already reached out to an external IP on port 443.
Most blog posts about detecting this technique show you a one-liner where CommandLine has "-enc" and call it a day. That query will generate so much noise in a real enterprise environment that analysts start ignoring it within a week. This post covers how to actually build this detection — one that fires on the right events, scores them by risk, and gives you the context you need to triage in under two minutes.
Why Attackers Use Encoded Commands
PowerShell's -EncodedCommand flag accepts a Base64-encoded string and executes it directly. Attackers use this for three specific reasons.
Evading simple signature-based detection. A rule looking for Invoke-WebRequest or DownloadString in plain text won't match a Base64-encoded version of the same command. Older EDR tools and SIEM rules that relied on keyword matching were effectively blind to this.
Bypassing script block logging restrictions. Depending on PowerShell version and audit configuration, encoded commands can sometimes slip past script block logging. Attackers know which environments have version 2.0 still enabled — it has no script block logging at all.
Reducing forensic artifacts. An encoded command executed from memory leaves fewer artifacts than a script written to disk. Combined with -NoProfile -NonInteractive -WindowStyle Hidden, the execution is designed to be invisible to a user sitting at the machine.
The encoding itself is not malicious. Legitimate software deployment tools, configuration management systems, and even Microsoft's own tooling use -EncodedCommand. The detection challenge is not the encoding — it is the combination of encoding with a suspicious parent process, network activity, or a specific decoded payload content.
What You're Actually Looking For in the Logs
Before writing a single line of KQL, you need to understand what you're hunting for in SecurityEvent. When PowerShell executes with -EncodedCommand, Event ID 4688 (Process Creation) fires with:
NewProcessNamecontainingpowershell.exeorpwsh.exeCommandLinecontaining one of the encoding flags:-enc,-EncodedCommand,-ec, or obfuscated variants like-e^ncParentProcessNametelling you what launched it — this is the most important field for triage
Event ID 4688 with command line logging must be enabled via GPO before any of these queries will work. Go to Computer Configuration → Windows Settings → Security Settings → Advanced Audit Policy → Process Creation and enable command line auditing. Without this, CommandLine will be blank on every 4688 event.
The Basic Detection — and Why It's Not Enough
Here is the query most tutorials will give you. It works. It will also fire hundreds of times a day in most enterprise environments.
// Basic encoded PowerShell detection — expect high false positive volume
SecurityEvent
| where TimeGenerated >= ago(1d)
| where EventID == 4688
| where CommandLine has_any ("-enc", "-EncodedCommand", "-ec ")
| project TimeGenerated, Computer, Account, CommandLine, NewProcessName
The problem with this query in production is signal-to-noise ratio. Software deployment tools, PowerShell DSC configurations, Microsoft Endpoint Manager, Tanium, and SCCM all routinely fire this pattern. On a 2,000-endpoint environment you can expect 50-200 hits per day, most of them benign. Analysts learn to ignore noisy rules. That is how real detections get missed.
What we need is context. The encoding flag alone is not suspicious. The encoding flag combined with a phishing-prone parent process, a download cradle in the decoded content, and an immediate outbound network connection — that is suspicious.
The Production Rule: Weighted Scoring with Context
This is the detection I would actually deploy in a Sentinel Analytic Rule. It uses a weighted suspicion scoring model: each additional risk indicator adds points, and only results above a threshold generate an alert. This dramatically reduces noise while preserving detection of the combinations that matter.
// ── ENCODED POWERSHELL DETECTION WITH SCORING ─────────────────────────────
// Deploy as: Sentinel Analytic Rule → Scheduled → 1h frequency / 1d lookback
// Fires on: Score >= 30 (tune threshold up in noisy environments)
// ──────────────────────────────────────────────────────────────────────────
let LookbackWindow = 1d;
// Known management tool parents — tune this to your environment
let BenignParents = dynamic([
"taniumclient.exe", "ccmexec.exe", "devenv.exe",
"msbuild.exe", "onexagentui.exe", "msiexec.exe"
]);
// High-risk parent processes — Office apps and browsers
let HighRiskParents = dynamic([
"winword.exe", "excel.exe", "powerpnt.exe", "outlook.exe",
"onenote.exe", "msaccess.exe", "mspub.exe",
"chrome.exe", "msedge.exe", "firefox.exe", "iexplore.exe"
]);
// Download cradle keywords — strong indicator of payload staging
let DownloadCradles = dynamic([
"downloadstring", "downloadfile", "webclient",
"invoke-webrequest", "iwr", "wget", "curl",
"net.webclient", "bitstransfer"
]);
SecurityEvent
| where TimeGenerated >= ago(LookbackWindow)
| where EventID == 4688
| where NewProcessName endswith "\\powershell.exe"
or NewProcessName endswith "\\pwsh.exe"
| where CommandLine matches regex @'(?i)-e[nN]?[cC]([oO][dD][eE][dD][cC][oO][mM][mM][aA][nN][dD])?[\s:]'
| where not(ParentProcessName has_any (BenignParents))
| extend
ProcName = tostring(split(NewProcessName, "\\")[-1]),
ParentName = tostring(split(ParentProcessName, "\\")[-1]),
CmdLower = tolower(CommandLine)
| extend Score = 0
| extend Score = Score + iff(ParentName in~ (HighRiskParents), 30, 0)
| extend Score = Score + iff(CmdLower has_any (DownloadCradles), 25, 0)
| extend Score = Score + iff(CmdLower has_any ("\\temp\\", "\\appdata\\", "\\downloads\\", "\\public\\"), 20, 0)
| extend Score = Score + iff(CmdLower has_any ("-windowstyle hidden", "-w hidden", "-wi h"), 15, 0)
| extend Score = Score + iff(CmdLower has_any ("-executionpolicy bypass", "-ep bypass", "-exec bypass"), 15, 0)
| extend Score = Score + iff(CmdLower has_any ("-noninteractive", "-noprofile", "-nop"), 10, 0)
| extend AlertSeverity = case(
Score >= 60, "Critical",
Score >= 40, "High",
Score >= 20, "Medium",
"Low")
| where Score >= 20
| project TimeGenerated, AlertSeverity, Score, Computer, Account, ParentName, ProcName, CommandLine
| sort by Score desc, TimeGenerated desc
How the Scoring Works
Rather than treating every encoded PowerShell execution as equally suspicious, the scoring model stacks risk indicators. Here is what each modifier targets and why it matters:
| Indicator | Points | Why it matters |
|---|---|---|
| Office or browser parent process | +30 | Legitimate PowerShell is almost never spawned by Word or Chrome. This single indicator is the most reliable signal of phishing-delivered execution. |
| Download cradle keywords | +25 | DownloadString, WebClient, and Invoke-WebRequest in an encoded payload indicate the command is staging a second-stage payload from a remote source. |
| User-writable path execution | +20 | Execution from \Temp\, \AppData\, or \Downloads\ is a common staging pattern. Legitimate tools rarely execute from user-writable paths. |
| Hidden window flag | +15 | Legitimate PowerShell that IT teams run does not need to be hidden from users. This flag is used specifically to avoid user awareness of the execution. |
| Execution policy bypass | +15 | While some legitimate automation uses this, combined with encoding and a suspicious parent it indicates deliberate evasion of policy controls. |
| Non-interactive / no-profile | +10 | Suppresses user profile and interaction prompts. Common in automated payloads that need to execute silently without user interaction. |
A typical malicious phishing-delivered payload scores 55-70 points (Office parent + download cradle + hidden window + execution policy bypass). A legitimate SCCM script that happens to use encoding scores 0-10 points and never fires. That gap is the entire value of the scoring approach.
MITRE ATT&CK Mapping
The primary technique is T1059.001 (Command and Scripting Interpreter: PowerShell) combined with T1027 (Obfuscated Files or Information). When the scoring model fires on a download cradle, that maps to T1105 (Ingress Tool Transfer). The high-risk parent pattern maps to the phishing delivery chain starting at T1566.001.
When you confirm a true positive on this detection, the MITRE chain you are likely looking at is: T1566.001 → T1204.002 → T1059.001 / T1027 → T1105. That chain represents initial access through phishing, user execution of a malicious file, obfuscated PowerShell execution, and payload download. If you see this, assume lateral movement is the next step and check for 4648 logon events from the affected host within the next 4 hours.
Tuning Notes from the Field
This is the part that separates a detection rule that gets used from one that gets disabled after two weeks.
Build your BenignParents allowlist first
Before deploying, spend 30 minutes running the basic detection query against your last 7 days of logs. Export the ParentName field and count unique values. Any parent appearing more than 20 times is almost certainly legitimate management tooling in your environment. Add those to BenignParents before deploying. Every environment is different — what I have in the query above reflects common enterprise tools but may not match yours.
Start the threshold at 30, not 20
The query fires at score >= 20 but in a mature environment I would start at 30. A score of 20 catches execution policy bypass with no other indicators, which may still be too noisy. Tune down as you build confidence in your allowlist.
Add an alert grouping rule in Sentinel
When deploying this as a Scheduled Analytic Rule, set alert grouping to group by Computer within a 24-hour window. A single compromised endpoint often generates 10-20 of these events in quick succession. Without grouping you get 10-20 identical incidents instead of one actionable one.
After you have the scoring model stable, add a second stage that joins against CommonSecurityLog or your firewall logs on Computer within a 10-minute window of the PowerShell execution. If the same host shows an outbound connection to a non-RFC1918 IP on a non-standard port within 10 minutes of the encoded execution, you have a near-certain confirmation of command-and-control. That enriched finding deserves a P1 ticket regardless of the score.
PowerShell version 2.0 is a blind spot
If PowerShell 2.0 is still present on endpoints in your environment, attackers can use powershell -version 2 -EncodedCommand to bypass script block logging entirely. Event ID 4688 will still fire, but you will not get the decoded script content. Check for version 2 usage with CommandLine has "-version 2" combined with encoding flags — that combination should always be a High severity alert regardless of your scoring threshold.
What to Do When the Alert Fires
The detection is only useful if your triage process is ready. When this alert fires at High or Critical severity, the first three minutes matter.
- Confirm the parent process. Open the full 4688 event and verify the parent. If it is Word or Outlook, that is your initial access vector. Pull the email from your mail gateway logs.
- Decode the payload. Copy the Base64 string from the command line and decode it — PowerShell, CyberChef, or
echo [string] | base64 --decode. You need to know what the payload does before you isolate. Isolating before understanding the payload can destroy forensic artifacts if the payload is fileless. - Check for outbound connections. In Sentinel, run a quick query against
CommonSecurityLogorAzureNetworkAnalytics_CLfor outbound traffic from that host in the 15 minutes following the encoded execution timestamp. - Isolate if C2 is confirmed. If you see outbound to an unknown external IP following the execution, isolate the host in CrowdStrike or Defender before the attacker can move laterally. Revoke the user's Entra ID sessions simultaneously.
- Open an IR ticket and document everything. Even if the alert turns out to be a false positive, document your triage steps. The IR ticket is your evidence trail if the same endpoint shows up in a bigger incident next week.
Full IR Runbooks for When the Alert Is Real
This detection is one piece. When it fires on something real, you need a step-by-step runbook covering containment, evidence preservation, communication scripts for leadership, and post-incident review. That is what the IR Runbook Bundle covers — built from real incidents, not theory.