PowerShell logging and auditing are critical components of enterprise security and compliance strategies. Whether you’re tracking administrative actions, investigating security incidents, or meeting regulatory requirements, proper logging configuration ensures comprehensive visibility into PowerShell activity across your environment.

This comprehensive guide explores PowerShell’s logging mechanisms, transcription features, and auditing best practices to help you build a robust security monitoring framework.

Understanding PowerShell Logging Architecture

PowerShell provides multiple logging layers that capture different aspects of script execution and command activity. Understanding these layers is essential for implementing effective monitoring strategies.

PowerShell Logging, Transcription & Auditing Best Practices: Complete Security & Compliance Guide

Key Logging Components

Module Logging captures pipeline execution details and parameter values for specified modules. Script Block Logging records the actual code blocks being executed, providing visibility into dynamic and obfuscated scripts. Transcription creates session-level recordings of all input and output, while Event Logs provide centralized storage for security monitoring.

Enabling Module Logging

Module logging captures detailed execution information for specific PowerShell modules, including all parameters and their values. This is particularly valuable for auditing administrative modules.

Registry Configuration

# Enable module logging for all modules
$regPath = "HKLM:\SOFTWARE\Policies\Microsoft\Windows\PowerShell\ModuleLogging"

# Create registry path if it doesn't exist
if (!(Test-Path $regPath)) {
    New-Item -Path $regPath -Force | Out-Null
}

# Enable module logging
Set-ItemProperty -Path $regPath -Name "EnableModuleLogging" -Value 1

# Configure module names to log
$moduleNamesPath = "$regPath\ModuleNames"
if (!(Test-Path $moduleNamesPath)) {
    New-Item -Path $moduleNamesPath -Force | Out-Null
}

# Log all modules (using wildcard)
Set-ItemProperty -Path $moduleNamesPath -Name "*" -Value "*"

Output: No direct output. Configuration is stored in registry and takes effect for new PowerShell sessions.

Group Policy Configuration

# Verify module logging status
Get-ItemProperty -Path "HKLM:\SOFTWARE\Policies\Microsoft\Windows\PowerShell\ModuleLogging" -ErrorAction SilentlyContinue

# View logged events
Get-WinEvent -LogName "Microsoft-Windows-PowerShell/Operational" -MaxEvents 10 | 
    Where-Object {$_.Id -eq 4103} | 
    Select-Object TimeCreated, Message | 
    Format-List

Sample Output:

TimeCreated : 10/22/2025 4:15:23 PM
Message     : CommandInvocation(Get-Process): "Get-Process"
              ParameterBinding(Get-Process): name="Name"; value="powershell"

TimeCreated : 10/22/2025 4:14:18 PM
Message     : CommandInvocation(Get-Service): "Get-Service"
              ParameterBinding(Get-Service): name="Name"; value="WinRM"

Implementing Script Block Logging

Script block logging is PowerShell’s most comprehensive logging mechanism, capturing the actual code being executed. This is critical for detecting malicious scripts and understanding attack patterns.

Basic Script Block Logging Setup

# Enable script block logging
$regPath = "HKLM:\SOFTWARE\Policies\Microsoft\Windows\PowerShell\ScriptBlockLogging"

if (!(Test-Path $regPath)) {
    New-Item -Path $regPath -Force | Out-Null
}

Set-ItemProperty -Path $regPath -Name "EnableScriptBlockLogging" -Value 1

# Enable invocation logging for detailed call stack information
Set-ItemProperty -Path $regPath -Name "EnableScriptBlockInvocationLogging" -Value 1

Analyzing Script Block Events

# Query script block logging events (Event ID 4104)
$scriptBlockEvents = Get-WinEvent -FilterHashtable @{
    LogName = 'Microsoft-Windows-PowerShell/Operational'
    ID = 4104
} -MaxEvents 5

foreach ($event in $scriptBlockEvents) {
    [PSCustomObject]@{
        TimeCreated = $event.TimeCreated
        ScriptBlock = $event.Properties[2].Value
        Path = $event.Properties[4].Value
    } | Format-List
    Write-Host ("-" * 80)
}

Sample Output:

TimeCreated : 10/22/2025 4:20:15 PM
ScriptBlock : Get-ChildItem -Path C:\Scripts -Recurse -Filter *.ps1 | 
              Select-Object Name, Length, LastWriteTime
Path        : C:\Scripts\Audit-Scripts.ps1

--------------------------------------------------------------------------------
TimeCreated : 10/22/2025 4:19:42 PM
ScriptBlock : $users = Get-ADUser -Filter * -Properties LastLogonDate
              $users | Where-Object {$_.LastLogonDate -lt (Get-Date).AddDays(-90)}
Path        : C:\Admin\Check-InactiveUsers.ps1

PowerShell Logging, Transcription & Auditing Best Practices: Complete Security & Compliance Guide

Detecting Suspicious Script Blocks

# Search for potentially malicious patterns
$suspiciousPatterns = @(
    'Invoke-Expression',
    'IEX',
    'DownloadString',
    'Net.WebClient',
    'Invoke-WebRequest.*-UseBasicParsing',
    'EncodedCommand',
    'FromBase64String'
)

$pattern = $suspiciousPatterns -join '|'

Get-WinEvent -FilterHashtable @{
    LogName = 'Microsoft-Windows-PowerShell/Operational'
    ID = 4104
} -MaxEvents 1000 | Where-Object {
    $_.Properties[2].Value -match $pattern
} | Select-Object TimeCreated, 
    @{N='ScriptBlock';E={$_.Properties[2].Value}},
    @{N='User';E={$_.Properties[0].Value}} |
    Format-Table -Wrap

PowerShell Transcription Configuration

Transcription provides session-level recording, capturing all commands entered and their output to text files. This creates a complete audit trail of interactive PowerShell sessions.

System-Wide Transcription Setup

# Configure transcription via registry
$transcriptPath = "HKLM:\SOFTWARE\Policies\Microsoft\Windows\PowerShell\Transcription"

if (!(Test-Path $transcriptPath)) {
    New-Item -Path $transcriptPath -Force | Out-Null
}

# Enable transcription
Set-ItemProperty -Path $transcriptPath -Name "EnableTranscripting" -Value 1

# Set output directory
Set-ItemProperty -Path $transcriptPath -Name "OutputDirectory" -Value "C:\PSTranscripts"

# Include invocation headers for better context
Set-ItemProperty -Path $transcriptPath -Name "EnableInvocationHeader" -Value 1

# Create output directory
New-Item -Path "C:\PSTranscripts" -ItemType Directory -Force | Out-Null

Session-Level Transcription

# Start transcription manually
$transcriptFile = "C:\Logs\Transcript_$(Get-Date -Format 'yyyyMMdd_HHmmss').txt"
Start-Transcript -Path $transcriptFile -Append

# Perform administrative tasks
Get-Service | Where-Object {$_.Status -eq 'Running'} | Select-Object -First 5
Get-Process | Sort-Object CPU -Descending | Select-Object -First 5

# Stop transcription
Stop-Transcript

Transcript Output Example (C:\Logs\Transcript_20251022_162345.txt):

**********************
Windows PowerShell transcript start
Start time: 20251022162345
Username: DOMAIN\Administrator
RunAs User: DOMAIN\Administrator
Configuration Name: 
Machine: SERVER01 (Microsoft Windows NT 10.0.20348.0)
Host Application: C:\Windows\System32\WindowsPowerShell\v1.0\powershell.exe
Process ID: 4532
**********************

PS C:\> Get-Service | Where-Object {$_.Status -eq 'Running'} | Select-Object -First 5

Status   Name               DisplayName
------   ----               -----------
Running  AppHostSvc         Application Host Helper Service
Running  BFE                Base Filtering Engine
Running  BrokerInfrastru... Background Tasks Infrastructure Ser...
Running  CertPropSvc        Certificate Propagation
Running  CryptSvc           Cryptographic Services

PS C:\> Get-Process | Sort-Object CPU -Descending | Select-Object -First 5

Handles  NPM(K)    PM(K)      WS(K)     CPU(s)     Id  SI ProcessName
-------  ------    -----      -----     ------     --  -- -----------
   1245      89   245632     312456      45.23   4532   1 powershell
    892      45   123456     198765      32.15   2341   0 sqlservr
    654      32    98765     145632      28.94   1892   1 chrome

**********************
Windows PowerShell transcript end
End time: 20251022162415
**********************

Automated Transcript Management

# Function to archive old transcripts
function Move-OldTranscripts {
    param(
        [string]$SourcePath = "C:\PSTranscripts",
        [string]$ArchivePath = "C:\PSTranscripts\Archive",
        [int]$DaysOld = 30
    )
    
    $cutoffDate = (Get-Date).AddDays(-$DaysOld)
    
    Get-ChildItem -Path $SourcePath -Filter "*.txt" -File |
        Where-Object {$_.LastWriteTime -lt $cutoffDate} |
        ForEach-Object {
            $yearMonth = $_.LastWriteTime.ToString("yyyy-MM")
            $destPath = Join-Path $ArchivePath $yearMonth
            
            if (!(Test-Path $destPath)) {
                New-Item -Path $destPath -ItemType Directory -Force | Out-Null
            }
            
            Move-Item -Path $_.FullName -Destination $destPath -Force
            Write-Output "Archived: $($_.Name) to $destPath"
        }
}

# Execute archival
Move-OldTranscripts -DaysOld 30

Windows Event Log Configuration

PowerShell events are stored in Windows Event Log channels that require proper configuration for optimal security monitoring.

Event Log Channels Overview

PowerShell Logging, Transcription & Auditing Best Practices: Complete Security & Compliance Guide

Configuring Event Log Size and Retention

# Configure PowerShell Operational log
$logName = "Microsoft-Windows-PowerShell/Operational"

# Set maximum log size to 100 MB
wevtutil sl $logName /ms:104857600

# Set retention policy (overwrite as needed)
wevtutil sl $logName /rt:false

# Enable the log
wevtutil sl $logName /e:true

# Verify configuration
$logConfig = Get-WinEvent -ListLog $logName
[PSCustomObject]@{
    LogName = $logConfig.LogName
    MaxSizeBytes = $logConfig.MaximumSizeInBytes
    IsEnabled = $logConfig.IsEnabled
    RecordCount = $logConfig.RecordCount
} | Format-List

Output:

LogName        : Microsoft-Windows-PowerShell/Operational
MaxSizeBytes   : 104857600
IsEnabled      : True
RecordCount    : 15432

Key Event IDs for Monitoring

# Define critical PowerShell event IDs
$eventMapping = @{
    4103 = "Module Logging - Pipeline Execution"
    4104 = "Script Block Logging - Code Execution"
    4105 = "Script Block Logging - Start"
    4106 = "Script Block Logging - Stop"
    40961 = "PowerShell Console Starting"
    40962 = "PowerShell Console Ready"
    53504 = "PowerShell Remote Session"
}

# Monitor critical events in real-time
$endTime = Get-Date
$startTime = $endTime.AddHours(-1)

Get-WinEvent -FilterHashtable @{
    LogName = 'Microsoft-Windows-PowerShell/Operational'
    ID = 4103, 4104, 4105, 4106
    StartTime = $startTime
    EndTime = $endTime
} | Group-Object Id | Select-Object Count, 
    @{N='EventID';E={$_.Name}},
    @{N='Description';E={$eventMapping[[int]$_.Name]}} |
    Format-Table -AutoSize

Output:

Count EventID Description
----- ------- -----------
  142    4104 Script Block Logging - Code Execution
   89    4103 Module Logging - Pipeline Execution
   12    4105 Script Block Logging - Start
   12    4106 Script Block Logging - Stop

Implementing Centralized Log Collection

For enterprise environments, centralized log collection is essential for security operations and compliance.

Windows Event Forwarding Setup

# Configure Windows Event Forwarding on collector server
# Run on the collector server

# Enable Windows Remote Management
Enable-PSRemoting -Force

# Configure WinRM for event forwarding
winrm quickconfig

# Create subscription configuration
$subscriptionXml = @"
<Subscription xmlns="http://schemas.microsoft.com/2006/03/windows/events/subscription">
    <SubscriptionId>PowerShell-Logging</SubscriptionId>
    <SubscriptionType>SourceInitiated</SubscriptionType>
    <Description>PowerShell Activity Logs</Description>
    <Enabled>true</Enabled>
    <Uri>http://schemas.microsoft.com/wbem/wsman/1/windows/EventLog</Uri>
    <ConfigurationMode>Custom</ConfigurationMode>
    <Delivery Mode="Push">
        <Batching>
            <MaxItems>5</MaxItems>
            <MaxLatencyTime>300000</MaxLatencyTime>
        </Batching>
        <PushSettings>
            <Heartbeat Interval="900000"/>
        </PushSettings>
    </Delivery>
    <Query>
        <![CDATA[
        <QueryList>
            <Query Id="0">
                <Select Path="Microsoft-Windows-PowerShell/Operational">
                    *[System[(EventID=4103 or EventID=4104)]]
                </Select>
            </Query>
        </QueryList>
        ]]>
    </Query>
    <ReadExistingEvents>false</ReadExistingEvents>
    <TransportName>http</TransportName>
    <AllowedSourceDomainComputers>O:NSG:BAD:P(A;;GA;;;DC)S:</AllowedSourceDomainComputers>
</Subscription>
"@

# Save and create subscription
$subscriptionXml | Out-File "C:\Temp\PSLogSubscription.xml"
wecutil cs "C:\Temp\PSLogSubscription.xml"

# Verify subscription
wecutil gs PowerShell-Logging

Exporting Logs to SIEM

# Export PowerShell events to JSON for SIEM ingestion
function Export-PowerShellEvents {
    param(
        [string]$OutputPath = "C:\Logs\PSEvents",
        [int]$Hours = 24
    )
    
    $startTime = (Get-Date).AddHours(-$Hours)
    $outputFile = Join-Path $OutputPath "PSEvents_$(Get-Date -Format 'yyyyMMdd_HHmmss').json"
    
    $events = Get-WinEvent -FilterHashtable @{
        LogName = 'Microsoft-Windows-PowerShell/Operational'
        ID = 4103, 4104
        StartTime = $startTime
    } -ErrorAction SilentlyContinue
    
    $exportData = $events | ForEach-Object {
        [PSCustomObject]@{
            TimeCreated = $_.TimeCreated.ToString("o")
            EventID = $_.Id
            Computer = $_.MachineName
            User = $_.UserId.Value
            Message = $_.Message
            ScriptBlock = if ($_.Id -eq 4104) { $_.Properties[2].Value } else { $null }
            Level = $_.LevelDisplayName
        }
    }
    
    $exportData | ConvertTo-Json -Depth 10 | Out-File $outputFile -Encoding UTF8
    Write-Output "Exported $($events.Count) events to $outputFile"
}

# Execute export
Export-PowerShellEvents -Hours 24

Output:

Exported 1247 events to C:\Logs\PSEvents\PSEvents_20251022_163045.json

Security Monitoring and Threat Detection

Effective security monitoring requires analyzing PowerShell logs for suspicious patterns and potential threats.

PowerShell Logging, Transcription & Auditing Best Practices: Complete Security & Compliance Guide

Detecting Obfuscated Commands

# Analyze script blocks for obfuscation indicators
function Test-ObfuscatedCommand {
    param([string]$ScriptBlock)
    
    $indicators = @{
        Base64 = $ScriptBlock -match 'FromBase64String|ToBase64String'
        Compression = $ScriptBlock -match 'IO\.Compression|GZipStream'
        CharSubstitution = ($ScriptBlock -match '`' -and ($ScriptBlock.Split('`').Count -gt 10))
        RandomCase = ($ScriptBlock -match '[a-z][A-Z]') -and 
                     ($ScriptBlock.ToCharArray() | Where-Object {$_ -cmatch '[A-Z]'}).Count -gt 
                     ($ScriptBlock.Length * 0.3)
        EncodedCommand = $ScriptBlock -match '-enc.*oded.*command|-ec '
        StringConcatenation = ($ScriptBlock.Split('+').Count -gt 15)
    }
    
    $score = ($indicators.Values | Where-Object {$_}).Count
    
    [PSCustomObject]@{
        ObfuscationScore = $score
        Indicators = ($indicators.GetEnumerator() | Where-Object {$_.Value} | 
                     Select-Object -ExpandProperty Name) -join ', '
        RiskLevel = switch ($score) {
            {$_ -ge 4} { 'Critical' }
            {$_ -ge 2} { 'High' }
            {$_ -eq 1} { 'Medium' }
            default { 'Low' }
        }
    }
}

# Scan recent script blocks
Get-WinEvent -FilterHashtable @{
    LogName = 'Microsoft-Windows-PowerShell/Operational'
    ID = 4104
} -MaxEvents 100 | ForEach-Object {
    $scriptBlock = $_.Properties[2].Value
    $analysis = Test-ObfuscatedCommand -ScriptBlock $scriptBlock
    
    if ($analysis.RiskLevel -in 'High','Critical') {
        [PSCustomObject]@{
            TimeCreated = $_.TimeCreated
            Computer = $_.MachineName
            RiskLevel = $analysis.RiskLevel
            ObfuscationScore = $analysis.ObfuscationScore
            Indicators = $analysis.Indicators
            Preview = $scriptBlock.Substring(0, [Math]::Min(100, $scriptBlock.Length))
        }
    }
} | Format-Table -Wrap

Monitoring Privileged Operations

# Track privileged PowerShell commands
$privilegedPatterns = @(
    'Add-ADGroupMember',
    'Set-ADAccountPassword',
    'New-ADUser',
    'Enable-PSRemoting',
    'Set-ExecutionPolicy',
    'New-ItemProperty.*HKLM:',
    'Invoke-WmiMethod.*Win32_Process',
    'Start-Process.*-Verb RunAs',
    'Enter-PSSession',
    'Invoke-Command.*-ComputerName'
)

$pattern = $privilegedPatterns -join '|'

# Query last 7 days
$events = Get-WinEvent -FilterHashtable @{
    LogName = 'Microsoft-Windows-PowerShell/Operational'
    ID = 4104
    StartTime = (Get-Date).AddDays(-7)
} -MaxEvents 5000

$privilegedOps = $events | Where-Object {
    $_.Properties[2].Value -match $pattern
} | ForEach-Object {
    [PSCustomObject]@{
        Time = $_.TimeCreated
        User = $_.UserId.Translate([System.Security.Principal.NTAccount]).Value
        Computer = $_.MachineName
        Command = ($_.Properties[2].Value -split "`n")[0].Trim()
    }
}

# Summarize by user
$privilegedOps | Group-Object User | Select-Object Count, Name | 
    Sort-Object Count -Descending | Format-Table

Output:

Count Name
----- ----
   42 DOMAIN\admin.user1
   28 DOMAIN\admin.user2
   15 DOMAIN\serviceaccount
    8 DOMAIN\admin.user3
    3 DOMAIN\backup.operator

Compliance and Retention Policies

Meeting regulatory requirements often mandates specific log retention periods and audit capabilities. Implement automated compliance checks to ensure continuous adherence.

Automated Compliance Verification

# Compliance verification script
function Test-PowerShellLoggingCompliance {
    $results = @{
        ModuleLogging = $false
        ScriptBlockLogging = $false
        Transcription = $false
        LogRetention = $false
        EventForwarding = $false
    }
    
    # Check Module Logging
    $modulePath = "HKLM:\SOFTWARE\Policies\Microsoft\Windows\PowerShell\ModuleLogging"
    if (Test-Path $modulePath) {
        $enabled = Get-ItemProperty -Path $modulePath -Name "EnableModuleLogging" -ErrorAction SilentlyContinue
        $results.ModuleLogging = ($enabled.EnableModuleLogging -eq 1)
    }
    
    # Check Script Block Logging
    $sblPath = "HKLM:\SOFTWARE\Policies\Microsoft\Windows\PowerShell\ScriptBlockLogging"
    if (Test-Path $sblPath) {
        $enabled = Get-ItemProperty -Path $sblPath -Name "EnableScriptBlockLogging" -ErrorAction SilentlyContinue
        $results.ScriptBlockLogging = ($enabled.EnableScriptBlockLogging -eq 1)
    }
    
    # Check Transcription
    $transPath = "HKLM:\SOFTWARE\Policies\Microsoft\Windows\PowerShell\Transcription"
    if (Test-Path $transPath) {
        $enabled = Get-ItemProperty -Path $transPath -Name "EnableTranscripting" -ErrorAction SilentlyContinue
        $results.Transcription = ($enabled.EnableTranscripting -eq 1)
    }
    
    # Check Log Retention (minimum 90 days)
    $log = Get-WinEvent -ListLog "Microsoft-Windows-PowerShell/Operational"
    $minSize = 90 * 1024 * 1024  # 90 MB minimum
    $results.LogRetention = ($log.MaximumSizeInBytes -ge $minSize)
    
    # Check Event Forwarding
    $subscriptions = wecutil es 2>$null
    $results.EventForwarding = ($subscriptions.Count -gt 0)
    
    # Generate report
    $complianceStatus = [PSCustomObject]@{
        ModuleLogging = if ($results.ModuleLogging) { "✓ Compliant" } else { "✗ Non-Compliant" }
        ScriptBlockLogging = if ($results.ScriptBlockLogging) { "✓ Compliant" } else { "✗ Non-Compliant" }
        Transcription = if ($results.Transcription) { "✓ Compliant" } else { "✗ Non-Compliant" }
        LogRetention = if ($results.LogRetention) { "✓ Compliant" } else { "✗ Non-Compliant" }
        EventForwarding = if ($results.EventForwarding) { "✓ Compliant" } else { "✗ Non-Compliant" }
        OverallCompliance = if (($results.Values | Where-Object {$_ -eq $false}).Count -eq 0) { 
            "✓ COMPLIANT" 
        } else { 
            "✗ NON-COMPLIANT" 
        }
    }
    
    return $complianceStatus
}

# Run compliance check
Test-PowerShellLoggingCompliance | Format-List

Output:

ModuleLogging      : ✓ Compliant
ScriptBlockLogging : ✓ Compliant
Transcription      : ✓ Compliant
LogRetention       : ✓ Compliant
EventForwarding    : ✗ Non-Compliant
OverallCompliance  : ✗ NON-COMPLIANT

Performance Optimization and Best Practices

While comprehensive logging is crucial for security, it’s important to balance visibility with system performance. Follow these optimization strategies to maintain efficient operations.

Selective Logging Strategy

# Configure selective module logging for high-value modules only
$criticalModules = @(
    'Microsoft.PowerShell.Security',
    'Microsoft.PowerShell.Management',
    'ActiveDirectory',
    'GroupPolicy',
    'NetSecurity',
    'DnsServer',
    'DHCPServer'
)

$moduleNamesPath = "HKLM:\SOFTWARE\Policies\Microsoft\Windows\PowerShell\ModuleLogging\ModuleNames"

# Remove wildcard logging
Remove-ItemProperty -Path $moduleNamesPath -Name "*" -ErrorAction SilentlyContinue

# Add specific modules
foreach ($module in $criticalModules) {
    Set-ItemProperty -Path $moduleNamesPath -Name $module -Value $module -Force
}

# Verify configuration
Get-Item -Path $moduleNamesPath | Select-Object -ExpandProperty Property

Log Rotation and Archival

# Implement automated log rotation
function Invoke-LogRotation {
    param(
        [string]$LogName = "Microsoft-Windows-PowerShell/Operational",
        [int]$RetentionDays = 90,
        [string]$ArchivePath = "\\FileServer\Logs\PowerShell"
    )
    
    # Calculate archive filename
    $archiveDate = Get-Date -Format "yyyyMMdd"
    $archiveFile = Join-Path $ArchivePath "PSLog_$archiveDate.evtx"
    
    # Export current log
    wevtutil epl $LogName $archiveFile
    
    # Clear the log
    wevtutil cl $LogName
    
    # Clean old archives
    Get-ChildItem -Path $ArchivePath -Filter "PSLog_*.evtx" |
        Where-Object {$_.LastWriteTime -lt (Get-Date).AddDays(-$RetentionDays)} |
        Remove-Item -Force
    
    Write-Output "Log rotation completed. Archive: $archiveFile"
}

# Schedule via Task Scheduler
$action = New-ScheduledTaskAction -Execute "PowerShell.exe" -Argument `
    "-NoProfile -ExecutionPolicy Bypass -File C:\Scripts\Rotate-Logs.ps1"

$trigger = New-ScheduledTaskTrigger -Weekly -DaysOfWeek Sunday -At 2am

Register-ScheduledTask -TaskName "PowerShell Log Rotation" `
    -Action $action -Trigger $trigger -RunLevel Highest `
    -Description "Weekly PowerShell log rotation and archival"

Advanced Threat Hunting Queries

Use these advanced queries to proactively hunt for security threats in your PowerShell logs.

Detecting Lateral Movement

# Identify potential lateral movement via PowerShell remoting
$suspiciousRemoting = Get-WinEvent -FilterHashtable @{
    LogName = 'Microsoft-Windows-PowerShell/Operational'
    ID = 4104
    StartTime = (Get-Date).AddHours(-24)
} | Where-Object {
    $_.Properties[2].Value -match '(Enter-PSSession|Invoke-Command|New-PSSession).*-ComputerName'
} | ForEach-Object {
    $scriptBlock = $_.Properties[2].Value
    
    # Extract target computers
    if ($scriptBlock -match '-ComputerName\s+([^\s]+)') {
        $targetComputer = $matches[1].Trim('"').Trim("'")
        
        [PSCustomObject]@{
            TimeCreated = $_.TimeCreated
            SourceComputer = $_.MachineName
            TargetComputer = $targetComputer
            User = $_.UserId.Translate([System.Security.Principal.NTAccount]).Value
            CommandType = if ($scriptBlock -match 'Enter-PSSession') { 'Interactive' } 
                         elseif ($scriptBlock -match 'Invoke-Command') { 'Remote Execution' }
                         else { 'Session Creation' }
        }
    }
}

# Group by user to identify unusual patterns
$suspiciousRemoting | Group-Object User | 
    Where-Object {$_.Count -gt 10} |
    Select-Object Count, Name, @{N='UniqueTargets';E={
        ($_.Group | Select-Object -Unique TargetComputer).Count
    }} | Format-Table

Credential Theft Detection

# Detect potential credential dumping attempts
$credentialPatterns = @(
    'Invoke-Mimikatz',
    'Get-GPPPassword',
    'Get-CachedGPPPassword',
    'Get-UnattendedInstallFile',
    'Get-Keystrokes',
    'Get-TimedScreenshot',
    'Get-VaultCredential',
    'HKLM:\\SAM',
    'HKLM:\\SECURITY',
    'lsass\.exe',
    'sekurlsa',
    'Export-.*Credential'
)

$pattern = $credentialPatterns -join '|'

Get-WinEvent -FilterHashtable @{
    LogName = 'Microsoft-Windows-PowerShell/Operational'
    ID = 4104
    StartTime = (Get-Date).AddDays(-7)
} -MaxEvents 10000 | Where-Object {
    $_.Properties[2].Value -match $pattern
} | Select-Object TimeCreated, MachineName,
    @{N='User';E={$_.UserId.Translate([System.Security.Principal.NTAccount]).Value}},
    @{N='DetectedPattern';E={
        $sb = $_.Properties[2].Value
        $credentialPatterns | Where-Object {$sb -match $_} | Select-Object -First 1
    }},
    @{N='ScriptPreview';E={
        $_.Properties[2].Value.Substring(0, [Math]::Min(150, $_.Properties[2].Value.Length))
    }} | Format-Table -Wrap

Integration with Security Tools

Integrate PowerShell logging with existing security infrastructure for comprehensive threat detection and response.

Splunk Integration Example

# Configure Splunk Universal Forwarder for PowerShell logs
# inputs.conf configuration

$splunkInputs = @"
[WinEventLog://Microsoft-Windows-PowerShell/Operational]
disabled = 0
start_from = oldest
current_only = 0
checkpointInterval = 5
renderXml = true
index = powershell_logs

[WinEventLog://Windows PowerShell]
disabled = 0
start_from = oldest
current_only = 0
checkpointInterval = 5
renderXml = true
index = powershell_logs

[monitor://C:\PSTranscripts]
disabled = 0
sourcetype = powershell:transcript
index = powershell_logs
recursive = true
"@

# Export configuration
$splunkInputs | Out-File "C:\Program Files\SplunkUniversalForwarder\etc\system\local\inputs.conf" -Encoding ASCII

# Restart Splunk forwarder
Restart-Service SplunkForwarder

Azure Sentinel Integration

# Configure Azure Monitor Agent for PowerShell log collection
# Data Collection Rule (DCR) configuration

$dcrConfig = @{
    dataSources = @{
        windowsEventLogs = @(
            @{
                name = "PowerShell-Operational"
                streams = @("Microsoft-WindowsEvent")
                xPathQueries = @(
                    "Microsoft-Windows-PowerShell/Operational!*[System[(EventID=4103 or EventID=4104)]]"
                )
            }
        )
    }
    destinations = @{
        logAnalytics = @(
            @{
                workspaceResourceId = "/subscriptions/{subscription}/resourceGroups/{rg}/providers/Microsoft.OperationalInsights/workspaces/{workspace}"
                name = "SecurityWorkspace"
            }
        )
    }
    dataFlows = @(
        @{
            streams = @("Microsoft-WindowsEvent")
            destinations = @("SecurityWorkspace")
            transformKql = "source | where EventID in (4103, 4104)"
        }
    )
}

# Convert to JSON and deploy
$dcrConfig | ConvertTo-Json -Depth 10 | Out-File "C:\Temp\PSLogging-DCR.json"

# Deploy using Azure CLI or PowerShell Az module
# az monitor data-collection rule create --name "PSLogging-DCR" --resource-group "Security-RG" --rule-file "C:\Temp\PSLogging-DCR.json"

Troubleshooting Common Issues

Address common PowerShell logging issues with these diagnostic and remediation techniques.

Verifying Logging Status

# Comprehensive logging status check
function Get-PowerShellLoggingStatus {
    $status = @{}
    
    # Check Module Logging
    $modulePath = "HKLM:\SOFTWARE\Policies\Microsoft\Windows\PowerShell\ModuleLogging"
    $status.ModuleLogging = if (Test-Path $modulePath) {
        $val = Get-ItemProperty -Path $modulePath -Name "EnableModuleLogging" -ErrorAction SilentlyContinue
        if ($val.EnableModuleLogging -eq 1) {
            $modules = Get-Item "$modulePath\ModuleNames" -ErrorAction SilentlyContinue
            "Enabled ($($modules.Property.Count) modules configured)"
        } else { "Disabled" }
    } else { "Not Configured" }
    
    # Check Script Block Logging
    $sblPath = "HKLM:\SOFTWARE\Policies\Microsoft\Windows\PowerShell\ScriptBlockLogging"
    $status.ScriptBlockLogging = if (Test-Path $sblPath) {
        $val = Get-ItemProperty -Path $sblPath -Name "EnableScriptBlockLogging" -ErrorAction SilentlyContinue
        if ($val.EnableScriptBlockLogging -eq 1) { "Enabled" } else { "Disabled" }
    } else { "Not Configured" }
    
    # Check Transcription
    $transPath = "HKLM:\SOFTWARE\Policies\Microsoft\Windows\PowerShell\Transcription"
    $status.Transcription = if (Test-Path $transPath) {
        $val = Get-ItemProperty -Path $transPath -Name "EnableTranscripting" -ErrorAction SilentlyContinue
        if ($val.EnableTranscripting -eq 1) {
            $dir = Get-ItemProperty -Path $transPath -Name "OutputDirectory" -ErrorAction SilentlyContinue
            "Enabled (Output: $($dir.OutputDirectory))"
        } else { "Disabled" }
    } else { "Not Configured" }
    
    # Check Event Log Configuration
    $log = Get-WinEvent -ListLog "Microsoft-Windows-PowerShell/Operational" -ErrorAction SilentlyContinue
    $status.EventLog = if ($log) {
        "Enabled (Size: $([Math]::Round($log.MaximumSizeInBytes/1MB, 2)) MB, Records: $($log.RecordCount))"
    } else { "Not Available" }
    
    # Check Recent Activity
    $recentEvents = Get-WinEvent -FilterHashtable @{
        LogName = 'Microsoft-Windows-PowerShell/Operational'
        ID = 4104
        StartTime = (Get-Date).AddHours(-1)
    } -MaxEvents 1 -ErrorAction SilentlyContinue
    
    $status.RecentActivity = if ($recentEvents) {
        "Active (Last event: $($recentEvents.TimeCreated))"
    } else { "No Recent Events" }
    
    return [PSCustomObject]$status
}

# Execute diagnostic
Get-PowerShellLoggingStatus | Format-List

Output:

ModuleLogging     : Enabled (7 modules configured)
ScriptBlockLogging: Enabled
Transcription     : Enabled (Output: C:\PSTranscripts)
EventLog          : Enabled (Size: 100 MB, Records: 15432)
RecentActivity    : Active (Last event: 10/22/2025 4:15:23 PM)

Conclusion

Implementing comprehensive PowerShell logging, transcription, and auditing is fundamental to maintaining a secure Windows environment. By enabling module logging, script block logging, and transcription, combined with proper event log configuration and centralized collection, organizations can achieve full visibility into PowerShell activity across their infrastructure.

Regular monitoring of PowerShell events, combined with automated threat detection and compliance verification, creates a robust defense against malicious activity while supporting forensic investigations and regulatory compliance requirements. The strategies and examples in this guide provide a solid foundation for building an enterprise-grade PowerShell auditing solution that balances security visibility with operational performance.