PowerShell Script Block Logging (4104): A Forensics Guide
What event ID 4104 records, how Windows splits long script blocks across events, why warning-level blocks appear without full logging, and how to read it in a case.
TL;DR. Script Block Logging records the code PowerShell actually ran in event ID 4104, inside Microsoft-Windows-PowerShell/Operational. It captures the deobfuscated text, so an -EncodedCommand or a concatenated one-liner is stored in expanded form. Long blocks are split across several 4104 events that share a ScriptBlockId; reassemble them by MessageNumber/MessageTotal. Even with the policy off, Windows logs suspicious blocks at Warning level. Open the parser to reassemble and decode them in your browser.
What 4104 records
Script Block Logging was added in PowerShell 5.0 (Windows 10 / WMF 5.0). When enabled, the engine writes the source of every script block it compiles to event ID 4104. Microsoft documents that it "records blocks of code as they are executed" and that this happens after de-obfuscation, so it defeats most encoding and string tricks (Microsoft: about_Logging_Windows, PowerShell ❤ the Blue Team).
Each 4104 event carries these EventData fields:
| Field | Meaning |
|---|---|
ScriptBlockText | The code of this part of the block |
ScriptBlockId | A GUID shared by every part of one block |
MessageNumber / MessageTotal | This part n of N |
Path | The script file, when the block came from a .ps1 |
Reassembling a split block
The Windows event log caps a single event's size, so PowerShell breaks a long block into several 4104 events. They all carry the same ScriptBlockId and are numbered MessageNumber of MessageTotal. To recover the script you group by ScriptBlockId, order by MessageNumber, and concatenate the ScriptBlockText. If a part is missing — rolled out of a wrapped log, or never collected — the reconstruction has a gap, and any interpretation of the code has to account for it. The PowerShell Parser does this grouping for you and flags blocks with a missing part. See event ID 4104 and Script Block Logging in the glossary.
Warning-level blocks without the policy
Even when the Script Block Logging policy is not set, PowerShell 5 still logs script blocks whose content matches an internal list of suspicious terms — at the Warning level (Level 3) rather than Verbose. This means an environment with "no logging" often still has a partial record of the most interesting activity (Lee Holmes). The parser marks these warning-level blocks so you do not mistake them for benign.
What to do with it
Read 4104 together with the classic Windows PowerShell log and the other event IDs, decode any encoded command, and corroborate with PSReadLine history and transcripts. To turn a folder of .evtx files into a searchable, decoded timeline without installing anything, analyze them in your browser.
FAQ
What is event ID 4104?
It is the Script Block Logging event in the Microsoft-Windows-PowerShell/Operational log. Each 4104 event records a block of PowerShell code as the engine compiled it, after any -EncodedCommand or string obfuscation has been resolved.
Why do I see 4104 events at Warning level when script block logging is disabled?
PowerShell 5 automatically logs script blocks that contain terms it considers suspicious at the Warning level, even when the Script Block Logging policy is off. Full, Verbose-level logging of every block requires the policy.
How are long scripts stored?
A long script block is split across several 4104 events that share one ScriptBlockId; each carries MessageNumber of MessageTotal. Reassemble them in order to recover the full text.