← 02 — Articles

PowerShell logging: what 4104 shows and what it misses

Endpoint6 min read

PowerShell is in most Windows intrusions, because it is on every machine and administrators use it all day. Process creation tells you that powershell.exe started and with which arguments. It does not tell you what the script did after it decoded itself, downloaded a second stage or ran a block typed interactively. Script block logging does, and it is one Group Policy setting away.

This article covers the three PowerShell logging features, what each one records, the policy that PowerShell 7 needs separately, the blind spots that remain, and a read-only script that reports how a machine is configured: powershell-logging-check.ps1. It builds on the audit policy baseline and on Sysmon, which cover the process side.

What you need before you start

  • Group Policy rights for the machines you want to cover.
  • A collector for the PowerShell event logs: Microsoft-Windows-PowerShell/Operational for Windows PowerShell 5.1 and PowerShellCore/Operational for PowerShell 7. Many SIEM agents collect neither by default.
  • A decision about who can read these logs. Script block logs contain whatever scripts contain, including credentials that someone hardcoded. Treat them as sensitive from the first day.

Three features, three different records

Script block logging: event 4104

When enabled, PowerShell records the content of every script block it compiles as event 4104 — scripts, functions, commands typed at the prompt, and code passed to Invoke-Expression at runtime. That last part matters: a script that decodes a Base64 payload and executes it produces a second 4104 with the decoded code.

Long scripts are split across several 4104 events. The MessageNumber and MessageTotal fields, together with ScriptBlockId, let you reassemble them. Detections that look for a string anywhere in a script need to handle that, or they will miss strings that land on a split.

Even without the policy, Windows PowerShell 5.1 logs script blocks that contain certain suspicious keywords, at Warning level. That is a safety net, not a configuration: it depends on a keyword list you do not control.

Group Policy: Administrative Templates > Windows Components > Windows PowerShell > Turn on PowerShell Script Block Logging. Leave Log script block invocation start / stop events off; it adds 4105 and 4106 for every invocation and very little detection value.

Module logging: event 4103

Module logging records pipeline execution details — commands and their parameters — as event 4103, for the modules you list. Setting the module list to * covers everything and produces a lot of events. It is useful when you need to see the parameter values that a script block only references as variables, and it is the second thing to enable, not the first.

Group Policy: Turn on Module Logging in the same folder, with module names * or a specific list.

Transcription: text files, not events

Transcription writes everything a session prints, input and output, to text files in a directory you choose. It is the closest thing to a screen recording of a PowerShell session and is very helpful in an investigation.

It is also files on a disk. If the output directory is local, any administrator — or attacker with administrator rights — can delete them. If it is a network share, the share needs permissions that let machines write but not read or delete each other’s transcripts. Without that, transcription is a collection of sensitive files that anyone can browse.

Group Policy: Turn on PowerShell Transcription, with an output directory and Include invocation headers enabled.

PowerShell 7 is a separate engine with separate policy

PowerShell 7 (pwsh.exe) does not read the Windows PowerShell policy keys. It has its own Group Policy templates, which ship with PowerShell 7 and are installed with InstallPSCorePolicyDefinitions.ps1 from its installation folder, and its own registry keys under HKLM:\SOFTWARE\Policies\Microsoft\PowerShellCore. Each setting also offers Use Windows PowerShell Policy setting, which makes PowerShell 7 follow the 5.1 configuration.

Its events go to a different log: PowerShellCore/Operational, with the same event ID 4104. Microsoft describes this in about_Logging_Windows. If PowerShell 7 is installed anywhere in your fleet, collect that log too, or every script run in pwsh.exe is invisible to your PowerShell detections.

The blind spots

  • The PowerShell 2.0 engine predates script block logging and records none of it. Running powershell.exe -Version 2 used to be a simple way around logging. Microsoft removed PowerShell 2.0 from Windows 11 version 24H2 and Windows Server 2025 during 2025; on older releases it is an optional feature that you should remove where nothing depends on it. The script reports whether it is enabled.
  • Administrators can turn logging off. The policy is a set of registry values. Collect changes to them (Sysmon registry events, if you extend the include list to \Policies\Microsoft\Windows\PowerShell\) and watch for machines whose 4104 volume suddenly drops to zero.
  • Logs can be cleared or overwritten. The PowerShell Operational log has its own maximum size. Busy automation servers can roll it over quickly; forward it.
  • Not everything that runs .NET is PowerShell. Script block logging covers the PowerShell engine. Code that does the same work in C#, or loads assemblies without PowerShell, is outside it.
  • Logs contain secrets. Microsoft recommends Protected Event Logging, which encrypts event content with a certificate, when script block logging is used for anything beyond diagnostics. It protects the logs on the endpoint; your SIEM must decrypt them to use them, which makes key handling part of the design.

Checking a machine

powershell-logging-check.ps1 is read-only. It reads the policy registry values for both engines, the state of the PowerShell 2.0 feature, the size of both PowerShell event logs and the time of the newest 4104 in each.

# Elevated PowerShell on the machine you want to check
.\powershell-logging-check.ps1 | Format-Table -AutoSize

MISSING means script block logging is not configured for that engine or the 2.0 engine is enabled. INFO rows show the rest of the configuration for you to judge. The script reads Group Policy values; settings made only in PowerShell 7’s powershell.config.json are not covered.

Testing that events arrive

  1. Script block logging: in a new PowerShell session, run Write-Output 'ew-psl-test'. Expect 4104 in Microsoft-Windows-PowerShell/Operational (or PowerShellCore/Operational in pwsh.exe) with that text in ScriptBlockText.
  2. Runtime code: run Invoke-Expression ([Text.Encoding]::UTF8.GetString([Convert]::FromBase64String('V3JpdGUtT3V0cHV0ICdldy1wc2wtZGVjb2RlZCc='))). The Base64 decodes to Write-Output 'ew-psl-decoded'; expect a second 4104 containing the decoded text. That is the event that makes script block logging worth having.
  3. Find both in the SIEM by the test string, and check that the full text arrived, not a truncated message field.

Limitations

  • We describe what Microsoft documents and what the configuration records. Volume depends on your automation; measure it on a busy management server before rolling out to all servers.
  • Script block logging shows code, not intent. Administrators run code that looks exactly like attacker tooling — remote execution, credential handling, encoded commands. Detections built on 4104 need allowlists for known administrative scripts, kept in version control.
  • The check script cannot tell you whether the logs reach your SIEM. The test above does.

Adapting this to your environment

  • ADAPT module logging: start with script block logging everywhere, then add module logging for the machines where administrators work — jump hosts, management servers.
  • ADAPT transcription to where you can protect the output: a write-only share, or not at all until you have one.
  • ADAPT access in the SIEM: restrict who can read PowerShell events, and decide whether Protected Event Logging is worth the key management.

The check script is powershell-logging-check.ps1 (MIT). Record the result per machine role in your log source inventory. If something here is wrong, write to hello@entropywatch.dev and we will correct it in the open.