← All posts

PowerShell module logging is on but logging nothing: the ModuleNames trap

EnableModuleLogging=1 makes audits read green while almost nothing is captured. Here is why CIS L1 requires ModuleNames='*' and how WinSentinel catches the gap.

Turn on PowerShell module logging, re-run your audit, watch the finding go green, move on. That is the trap. EnableModuleLogging = 1 flips the top-level flag that most tools (and most humans) check — but it is only half of the setting. The other half, the ModuleNames list, decides which modules actually get logged. Miss it and you have a policy that reads “enabled” while capturing almost nothing.

Two knobs, not one

Script Block Logging is a single switch: on or off. Module logging is different. It lives under:

HKLM\SOFTWARE\Policies\Microsoft\Windows\PowerShell\ModuleLogging
    EnableModuleLogging = 1           (DWORD)
    ModuleNames\                      (subkey)
        *  =  *                       (which modules to log)

EnableModuleLogging turns the feature on. ModuleNames scopes it. If ModuleNames is empty, or lists only a handful of modules, then pipeline execution is recorded only for those modules. Everything else — the ad-hoc iex one-liner, the attacker’s downloaded cradle, the reconnaissance script that never touches a named module — runs completely unlogged.

In the Group Policy UI the same trap is easy to fall into: you enable “Turn on Module Logging,” the checkbox goes green, and you close the dialog without clicking Show… to add * to the module-names list. Policy applied, coverage zero.

Why CIS L1 says *

The CIS Microsoft Windows Benchmark (Level 1) does not just require module logging to be enabled — it requires ModuleNames to contain the single wildcard entry *, so that every module’s pipeline activity is logged to Event ID 4103. A scoped list is not a partial win; for forensic purposes it is usually indistinguishable from off, because the modules an attacker abuses are rarely the ones an admin thought to name.

“Enabled but scoped” is worse than “disabled” in one specific way: it produces a green checkmark. A disabled control is an honest gap you will remediate. A misconfigured one is a blind spot you believe is covered.

How WinSentinel catches it

The free WinSentinel PowerShell Security module reads both halves of the setting. It grades module logging into three distinct states instead of a single on/off:

Crucially, if the audit can’t read the ModuleNames subkey at all (access denied, key absent), it treats coverage as unknown and stays quiet rather than crying wolf. A false “you’re not logging” alarm is its own kind of noise.

Fixing it in one line

The remediation is to add the wildcard to the module-names list:

New-Item -Path 'HKLM:\SOFTWARE\Policies\Microsoft\Windows\PowerShell\ModuleLogging\ModuleNames' -Force | Out-Null
New-ItemProperty -Path 'HKLM:\SOFTWARE\Policies\Microsoft\Windows\PowerShell\ModuleLogging\ModuleNames' `
    -Name '*' -Value '*' -PropertyType String -Force

Prefer Group Policy for fleet-wide consistency: Administrative Templates → Windows Components → Windows PowerShell → Turn on Module Logging, then click Show… and add a single row with value *. Pair it with Script Block Logging and Transcription for a complete PowerShell audit trail.

Run the check yourself

The PowerShell Security module ships free in the CLI — single-machine audits are fully unlimited, no license required:

dotnet tool install --global WinSentinel.Cli
winsentinel audit --module powershell

You’ll get the module-logging coverage finding alongside execution policy, AMSI status, ConstrainedLanguageMode, transcription and profile checks — each with the exact registry path and remediation command. Running dozens of machines and want the coverage rollup across the whole fleet? That’s what WinSentinel Pro adds on top of the free agent.