🎁 New User? Get 20% off your first purchase with code NEWUSER20 Register Now →
Menu

Categories

PowerShell Logging and Transcription: The 2026 Reference

PowerShell Logging and Transcription: The 2026 Reference
PowerShell logging and transcription - Dargslan 2026

PowerShell logging is the single most useful detective control on a Windows fleet. It is also one of the most commonly disabled, because the defaults generate too much noise and because many teams confuse the three independent logging features. This guide pulls them apart, shows the configuration that survives an audit, and explains how to keep the SIEM bill under control.

The three logging features

PowerShell ships three independent logging features that are often confused:

  • ScriptBlock logging — every block of code that the engine compiles, including obfuscated payloads after they decode. Event ID 4104 in Microsoft-Windows-PowerShell/Operational.
  • Module logging — pipeline events from selected modules. Event ID 4103.
  • Transcription — a per-session text file capturing input and output, written by the engine to a directory you choose.

You want all three on, but for different reasons.

ScriptBlock logging (the important one)

If you only enable one feature, enable this one. It captures the actual code the engine ran, after any obfuscation, in EVT 4104. The signal-to-noise ratio is excellent because legitimate admin work generates a small number of distinct script blocks, while attacker tooling typically generates dozens of unusual ones.

$k = 'HKLM:\Software\Policies\Microsoft\Windows\PowerShell\ScriptBlockLogging'
New-Item $k -Force | Out-Null
Set-ItemProperty $k EnableScriptBlockLogging 1
Set-ItemProperty $k EnableScriptBlockInvocationLogging 0  # noisy

Leave InvocationLogging off — it logs every invocation including loop iterations and overwhelms the channel.

Module logging

Module logging emits 4103 events for the cmdlets in the modules you specify. Set the value * to log everything (high volume) or list the high-value modules — ActiveDirectory, Microsoft.PowerShell.Management, Hyper-V, NetSecurity:

$k = 'HKLM:\Software\Policies\Microsoft\Windows\PowerShell\ModuleLogging'
New-Item "$k\ModuleNames" -Force | Out-Null
Set-ItemProperty $k EnableModuleLogging 1
Set-ItemProperty "$k\ModuleNames" '*' '*'

Transcription

Transcription writes a plain-text record of every interactive session and every script invocation to disk. It is the only feature that captures multi-line output and prompts. Configure the output directory as a UNC path on a write-once share — local-only transcripts are useless after a host is compromised:

$k = 'HKLM:\Software\Policies\Microsoft\Windows\PowerShell\Transcription'
New-Item $k -Force | Out-Null
Set-ItemProperty $k EnableTranscripting 1
Set-ItemProperty $k IncludeInvocationHeader 1
Set-ItemProperty $k OutputDirectory '\\logs\ps-transcripts'

Permission the share so every host can write but cannot list or delete other hosts' transcripts. CREATE FILES + WRITE DATA only.

Apply via GPO

Single-host registry edits are fine for a lab. For a fleet, push all three features via Computer Configuration > Administrative Templates > Windows Components > Windows PowerShell. The settings are present from the standard ADMX templates that ship with Windows 11 / Server 2022 RSAT.

Forward to a SIEM

Configure Windows Event Forwarding (WEF) or your agent (Sysmon-with-Olaf-Hartong-config, Wazuh, Splunk UF) to ship the Microsoft-Windows-PowerShell/Operational channel to the SIEM. The two queries every analyst should have saved:

EventID = 4104 AND ScriptBlockText matches IEX|Invoke-Expression|FromBase64String|DownloadString|.bytes
EventID = 4103 AND ContextInfo matches HostApplication=powershell.exe -nop -w hidden

Reducing the noise

The biggest mistake is leaving InvocationLogging on. The second is forwarding 4103 from every module — limit to a list. The third is logging your own monitoring scripts; tag them and exclude by ScriptBlockId in the SIEM.

Audit checklist

  1. EnableScriptBlockLogging = 1 (1 pt)
  2. EnableScriptBlockInvocationLogging = 0 (1 pt)
  3. EnableModuleLogging = 1, ModuleNames non-empty (1 pt)
  4. EnableTranscripting = 1 with UNC OutputDirectory (1 pt)
  5. Operational channel forwarded to SIEM (1 pt)

5/5 = PASS, 3-4 = WARN, <3 = FAIL.

Beyond the basics: protected event logging and SIEM ingestion

Transcription and ScriptBlockLogging give you raw data on the host. Two more pieces turn that data into something defensible: protecting the logs from the attacker who just landed on the box, and getting the events off the host fast enough to matter.

Protected Event Logging with a public key

Protected Event Logging encrypts the body of selected events with a certificate's public key. Only a holder of the matching private key — usually a central log server, never the workstation itself — can decrypt them. An attacker who reads Microsoft-Windows-PowerShell/Operational after compromise sees ciphertext, not the script that found their backdoor.

# On the central log server, export the public key only
Get-ChildItem Cert:\LocalMachine\My\<thumbprint> |
    Export-Certificate -FilePath C:\pel\log-public.cer

# On every endpoint, deploy the .cer and point the registry at it
$key = 'HKLM:\SOFTWARE\Policies\Microsoft\Windows\EventLog\ProtectedEventLogging'
New-Item $key -Force | Out-Null
Set-ItemProperty $key 'EnableProtectedEventLogging' 1
Set-ItemProperty $key 'EncryptionCertificate' @(
  (Get-Content C:\pel\log-public.cer -Raw)
)

Test by running a known-noisy script (e.g. Get-Process | Out-Null) and inspecting the event in the log: the Message field should be a base64 blob, not plain text.

Ship logs with Windows Event Forwarding (WEF)

Local logs roll over. A 20 MB Operational log on a busy server can wrap in under an hour during an incident, destroying exactly the evidence you need. WEF subscribes a central collector to the events you care about and pulls them off the host in near real time.

# On the collector, create the subscription
wecutil cs C:\wef\powershell-subscription.xml

# Sample query inside the XML — only ScriptBlock and Module logging
<Query Id="0">
  <Select Path="Microsoft-Windows-PowerShell/Operational">
    *[System[(EventID=4103 or EventID=4104)]]
  </Select>
</Query>

Push the WinRM configuration to endpoints by GPO, add the collector's machine account to the Event Log Readers group on each source, and the events flow with no third-party agent.

Detection rules worth writing on day one

Three rules cover most early-stage PowerShell abuse. Build them in your SIEM before you deploy the logging policy fleet-wide, otherwise you will have data and no alerts.

  • Encoded command. Event 4104 where ScriptBlockText contains FromBase64String or the parent process used -EncodedCommand.
  • Download cradles. ScriptBlockText matching (Net.WebClient|Invoke-WebRequest|Start-BitsTransfer).*DownloadString|DownloadFile.
  • AMSI bypass attempts. Strings like amsiInitFailed, System.Management.Automation.AmsiUtils, or repeated Reflection.Assembly + GetField patterns inside one block.

Common pitfalls

  • Transcripts on a local disk only. If the attacker can write the script, they can delete the transcript. Send transcripts to a UNC path with write-only ACLs (Modify for Authenticated Users, no Read, Delete denied).
  • Logging without size and rotation. The default Microsoft-Windows-PowerShell/Operational log is 15 MB. Raise it to 1 GB on servers (wevtutil sl) and ship to WEF, or you will lose the only copy.
  • ScriptBlockLogging on, ModuleLogging off. Both matter. ScriptBlock catches inline code; Module catches calls into already-imported modules where the script itself is short.
  • Forgetting Constrained Language Mode. Logging tells you what happened; CLM stops the dangerous calls in the first place. Pair them.
  • No baseline. Without a normal-day volume of 4104 events, you cannot recognise the spike that signals an incident. Capture a week of baseline before alerting.

Quick-reference playbook: rolling out logging in week-one

The fastest path from "no PowerShell logging" to a defensible deployment is a five-day plan that stages the controls, captures a baseline and locks the configuration. The plan below assumes one engineer and a fleet of 50–500 endpoints.

  1. Day 1 — pilot. Pick five hosts (one DC, two member servers, two workstations). Enable ScriptBlockLogging and ModuleLogging via local policy. Confirm events 4103/4104 appear within 10 minutes.
  2. Day 2 — central collection. Stand up the WEF collector, register the subscription, and verify the five pilot hosts are forwarding. Validate event lag stays under 60 seconds at p95.
  3. Day 3 — transcription. Add a write-only UNC share, push the transcription policy, and force-rotate one user session. Confirm a transcript file lands in the share with the correct ACL.
  4. Day 4 — alerting. Implement the three core detection rules (encoded command, download cradle, AMSI bypass) in your SIEM. Run synthetic test cases from a non-admin shell and verify each fires.
  5. Day 5 — fleet rollout. Promote the local policy to a domain GPO scoped to the workstation OU, then the server OU 24 hours later. Watch the WEF collector for ingestion spikes and adjust quota accordingly.

Document the rollback at every step: how to pull the GPO, how to disable the subscription, how to revoke the share. An incident is the worst time to discover that nobody wrote down which registry key turns the logging back off.

FAQ

Does this work on PowerShell 7?

Yes. PowerShell 7 honours the same Group Policy paths and writes to the PowerShellCore/Operational channel as well.

How big does the channel need to be?

Default 15 MB is too small for any production server. Set the channel size to at least 256 MB and rotate to disk, or forward immediately.

Can attackers disable logging?

An admin can change the registry. The defence is to alert on the registry change itself — Sysmon Event ID 13 against the policy keys.

Is Constrained Language Mode required?

Not for logging — but combining logging with CLM and AppLocker is the recommended posture.

Will Protected Event Logging slow PowerShell down?

Negligible cost on modern hardware — the encryption is per-event and asynchronous. The only meaningful overhead is at decrypt time on the log server, which happens out of band.

Can I exclude specific accounts from transcription?

No, and you should not want to. Transcription is most valuable for privileged accounts. If a service account generates noise, fix the service, do not disable the audit.

What about PowerShell Core (7+) on Linux?

ScriptBlockLogging works and writes to syslog via the journal. The Windows-specific bits (Protected Event Logging, WEF) do not apply, but Sysmon-for-Linux and a SIEM agent give you the equivalent shipping path.

How do I prove logging is enabled to an auditor?

Run Get-WinEvent -ListLog Microsoft-Windows-PowerShell/Operational | Format-List * and screenshot the IsEnabled, MaximumSizeInBytes and LogFilePath. Combine with a screenshot of the GPO and the SIEM dashboard showing live event flow.

Related Dargslan resources

Share this article:
Dargslan Editorial Team (Dargslan)
About the Author

Dargslan Editorial Team (Dargslan)

Collective of Software Developers, System Administrators, DevOps Engineers, and IT Authors

Dargslan is an independent technology publishing collective formed by experienced software developers, system administrators, and IT specialists.

The Dargslan editorial team works collaboratively to create practical, hands-on technology books focused on real-world use cases. Each publication is developed, reviewed, and...

Programming Languages Linux Administration Web Development Cybersecurity Networking

Stay Updated

Subscribe to our newsletter for the latest tutorials, tips, and exclusive offers.