Why PowerShell Logging Matters for Security
PowerShell is the most powerful scripting environment on Windows Server 2022, and that power makes it a favourite tool for both administrators and attackers. Malware, ransomware loaders, and post-exploitation frameworks such as Empire, Cobalt Strike, and Metasploit all leverage PowerShell heavily. Without logging, PowerShell activity is largely invisible — commands run, scripts execute, and nothing is recorded unless you explicitly configure the operating system to capture it. Enabling comprehensive PowerShell logging transforms PowerShell from an unmonitored attack vector into a rich source of audit telemetry that feeds your SIEM, incident response workflow, and compliance reporting.
Windows Server 2022 supports four layers of PowerShell logging: module logging, script block logging, transcription logging, and Protected Event Logging. Each captures different granularity and has different performance implications. This guide covers all four, the relevant registry paths, Group Policy configuration, and how to view the resulting events.
Module Logging
Module logging records the pipeline execution details for all PowerShell modules — essentially every command invocation, its parameters, and the modules it belongs to. Events land in the Microsoft-Windows-PowerShell/Operational event log under Event ID 4103.
To enable module logging via the registry on the local machine:
# Enable module logging
$regPath = "HKLM:SOFTWAREPoliciesMicrosoftWindowsPowerShellModuleLogging"
New-Item -Path $regPath -Force | Out-Null
Set-ItemProperty -Path $regPath -Name "EnableModuleLogging" -Value 1 -Type DWord
# Log all modules (wildcard)
$modNamesPath = "$regPathModuleNames"
New-Item -Path $modNamesPath -Force | Out-Null
Set-ItemProperty -Path $modNamesPath -Name "*" -Value "*" -Type String
To enable module logging through Group Policy, navigate to: Computer Configuration → Administrative Templates → Windows Components → Windows PowerShell → Turn on Module Logging. Enable the policy and in the options area, enter * in the Module Names field to log all modules. You can restrict logging to specific modules by entering their names (e.g., ActiveDirectory, NetTCPIP) if the volume of all-module logging is too high for your environment.
Script Block Logging
Script block logging is the most forensically valuable logging type. It records the full contents of every PowerShell script block that is compiled and executed, including obfuscated code after it has been deobfuscated by the engine. This is critical because many attacks use Base64 encoding or string concatenation to hide malicious commands — script block logging captures what the engine actually executes, not the raw obfuscated input. Events appear under Event ID 4104 in the Microsoft-Windows-PowerShell/Operational log.
Enable script block logging via registry:
$sbPath = "HKLM:SOFTWAREPoliciesMicrosoftWindowsPowerShellScriptBlockLogging"
New-Item -Path $sbPath -Force | Out-Null
Set-ItemProperty -Path $sbPath -Name "EnableScriptBlockLogging" -Value 1 -Type DWord
# Optionally enable logging of script block invocation start/stop (verbose, high volume)
Set-ItemProperty -Path $sbPath -Name "EnableScriptBlockInvocationLogging" -Value 1 -Type DWord
Via Group Policy: Computer Configuration → Administrative Templates → Windows Components → Windows PowerShell → Turn on PowerShell Script Block Logging. Enable the policy. The optional checkbox for logging script block invocation start/stop events generates significantly more data and is usually left disabled unless you need extremely granular forensics.
PowerShell 5.1 and later (the version included with Windows Server 2022) automatically triggers enhanced script block logging for any script block that contains commands or patterns associated with known malicious techniques — even when script block logging is not explicitly enabled. This automatic logging under Event ID 4104 with Level “Warning” captures suspicious activity as a baseline protection measure.
Transcription Logging
PowerShell transcription records a human-readable transcript of every PowerShell session to a text file. Unlike event log entries, transcripts include the full input and output of commands, making them easy to read during an investigation. The transcript files include a header showing the username, machine name, start time, and PowerShell version, followed by every command executed and its resulting output.
Enable transcription via registry:
$txPath = "HKLM:SOFTWAREPoliciesMicrosoftWindowsPowerShellTranscription"
New-Item -Path $txPath -Force | Out-Null
Set-ItemProperty -Path $txPath -Name "EnableTranscripting" -Value 1 -Type DWord
Set-ItemProperty -Path $txPath -Name "EnableInvocationHeader" -Value 1 -Type DWord
# Set the output directory (must be writable by all users who run PowerShell)
Set-ItemProperty -Path $txPath -Name "OutputDirectory" -Value "C:PSTranscripts" -Type String
Create and secure the transcript directory appropriately:
New-Item -Path "C:PSTranscripts" -ItemType Directory -Force
# Grant Authenticated Users write access (Create Files only, not read)
$acl = Get-Acl "C:PSTranscripts"
$rule = New-Object System.Security.AccessControl.FileSystemAccessRule(
"Authenticated Users", "CreateFiles, AppendData",
"ContainerInherit, ObjectInherit", "None", "Allow")
$acl.AddAccessRule($rule)
Set-Acl "C:PSTranscripts" $acl
Do not grant users read access to the transcript directory — they should be able to write their own transcripts but not read others’ transcripts, which may contain sensitive information like passwords typed at prompts.
Viewing PowerShell Events in Event Viewer
PowerShell events are stored in the Microsoft-Windows-PowerShell event channel, accessible in Event Viewer under:
Applications and Services Logs → Microsoft → Windows → PowerShell → Operational
Key event IDs to monitor:
Event ID 4103 — Module logging: pipeline execution details. High volume but valuable for understanding what modules and commands are being used.
Event ID 4104 — Script block logging: full script block content. Critical for detecting obfuscated attacks.
Event ID 4105 — Script block invocation started.
Event ID 4106 — Script block invocation completed.
Query events from PowerShell directly without opening Event Viewer:
# Get all script block logging events from the last hour
$startTime = (Get-Date).AddHours(-1)
Get-WinEvent -LogName "Microsoft-Windows-PowerShell/Operational" |
Where-Object { $_.Id -eq 4104 -and $_.TimeCreated -gt $startTime } |
Select-Object TimeCreated, Message |
Format-List
For the older Windows PowerShell event log (PowerShell 2.0 compatibility), check Windows PowerShell under Windows Logs → Application and Services. Event ID 400 records engine start, 403 records engine stop, and 600 records provider start/stop. These are less detailed than the newer Operational log but are still useful for detecting PowerShell 2.0 downgrade attacks (discussed below).
Protected Event Logging
A concern with script block logging is that the log entries themselves may contain sensitive data — passwords, API keys, or decrypted secrets that an attacker who gains read access to the event log could harvest. Protected Event Logging addresses this by encrypting the log content using a public key. Only a user or system with the corresponding private key can decrypt and read the entries.
Protected Event Logging uses the Cryptographic Message Syntax (CMS). First, obtain or generate a code-signing certificate with the Document Encryption key usage extension and export its public key as a CER file. Then enable Protected Event Logging via Group Policy:
Navigate to: Computer Configuration → Administrative Templates → Windows Components → Event Logging → Enable Protected Event Logging. Enable the policy and supply the path to the certificate’s public key (CER file) or the certificate thumbprint.
# To decrypt a protected event log entry (on the machine with the private key)
Get-WinEvent -LogName "Microsoft-Windows-PowerShell/Operational" |
Where-Object Id -eq 4104 |
Select-Object -First 1 |
ForEach-Object {
$msg = $_.Message
Unprotect-CmsMessage -Content $msg
}
Constrained Language Mode
PowerShell Constrained Language Mode (CLM) restricts the language features available in a session, blocking access to .NET types, COM objects, and other capabilities commonly used by malicious scripts. CLM is automatically activated when AppLocker or Windows Defender Application Control (WDAC) policies are present. You can check the current language mode of a PowerShell session:
$ExecutionContext.SessionState.LanguageMode
The possible values are FullLanguage (no restrictions), ConstrainedLanguage (restricted access), RestrictedLanguage (only specific commands), and NoLanguage (cmdlets only via API). For user workstations and servers that do not require PowerShell scripting by end users, deploying WDAC policies that trigger CLM significantly raises the bar for PowerShell-based attacks.
AMSI Integration
The Antimalware Scan Interface (AMSI) is integrated into PowerShell 5.1 and later. Every script block submitted to the PowerShell engine is passed through AMSI to Windows Defender (or any registered AMSI-compatible AV product) for scanning before execution. AMSI operates at the script execution level, not the file system level, so it catches in-memory attacks and fileless malware that would evade traditional file scanning.
AMSI cannot be disabled by a non-administrative user. Attackers frequently attempt to bypass AMSI by patching the AMSI DLL in memory — these attempts are themselves detectable via script block logging and Windows Defender. Keep Windows Defender definitions current and ensure Tamper Protection is enabled to harden AMSI against in-process bypass attempts.
Auditing PowerShell Activity with a Summary Script
To get a quick summary of PowerShell activity on a server, this script queries the last 500 script block events and lists the unique users and commands seen:
$events = Get-WinEvent -LogName "Microsoft-Windows-PowerShell/Operational" -MaxEvents 500 |
Where-Object Id -eq 4104
$events | ForEach-Object {
[PSCustomObject]@{
TimeCreated = $_.TimeCreated
User = $_.UserId
Level = $_.LevelDisplayName
ScriptBlock = ($_.Message -split "`n")[3..5] -join " "
}
} | Sort-Object TimeCreated -Descending | Format-Table -AutoSize
For production environments, forward these events to a centralised SIEM using Windows Event Forwarding (WEF) or a log agent. Configure a WEF subscription on a collector server to pull Event IDs 4103, 4104, 400, 403, and 600 from all servers, giving your security team a unified view of PowerShell activity across the estate without logging into individual servers.