PowerShell history saving disabled via Set-PSReadLineOption
AN1557 names `Set-PSReadLineOption` with `SaveNothing` or an altered `HistorySavePath` as the observable, and PowerShell script block logging is the one feed in the brief that can carry the cmdlet's arguments verbatim. Script block logging is off by default and requires the "Turn on PowerShell Script Block Logging" Group Policy (or the equivalent `HKLM\SOFTWARE\Policies\Microsoft\Windows\PowerShell\ScriptBlockLogging` registry key) enabled before EventID 4104 exists at all; without it this rule is blind, not quiet. This rule matches the cmdlet name together with either the `SaveNothing` save-style value or a `HistorySavePath` argument pointed at a null destination (`$null`, `NUL`, or `/dev/null`, the forms available under Windows PowerShell and PowerShell 7). It is deliberately narrower than matching `HistorySavePath` alone, since that argument also appears in benign scripts that read or display the current path without redirecting it anywhere. AN1555 (Linux) and AN1556 (macOS) are not authored under this technique. AN1555's own log source is `auditd:SYSCALL::execve` matching `unset`/`export` of `HISTFILE`/`HISTCONTROL` — but `unset` and `export` are bash builtins with no external binary of that name, so they never reach `execve()` and this selector could never fire, the same class of dead selector AUTHORING.md documents for Java/Python wrappers on this same log source. AN1556's macOS unifiedlog leg gives no field or message pattern at all, only the prose "Set or unset HIST* variables in shell environment." T1070.003's published rule for this corpus explicitly declines `Set-PSReadLineOption -HistorySaveStyle SaveNothing` as out of scope for clearing console history and reserves it for this technique — this rule is that reservation being claimed. It does not see history suppressed through `$env:PSReadLineHistorySaveStyle` set before the module loads, through a compiled or C#-hosted PowerShell runspace with no script block to log, or through direct deletion/truncation of the underlying history file, which is T1070.003's ground, not this one's. UNVERIFIED AS AUTHORED — derived from MITRE ATT&CK DET0563, not hand-written and not tested by its author. Any lab result is on this rule's own page.Full descriptionShow less
The detection
The 4 SIEM queries below are previews produced by Siemphony’s own translator, not pySigma, and none has been executed against a real backend — review before you deploy.
detection.yml
Sentinel · KQL
Run this as a search.
Event| where ((EventID == 4104 and ScriptBlockText contains "Set-PSReadLineOption") and (ScriptBlockText contains "SaveNothing" or (ScriptBlockText contains "HistorySavePath $null" or ScriptBlockText contains "HistorySavePath NUL" or ScriptBlockText contains "HistorySavePath /dev/null")))
Splunk · SPL
Run this as a search.
index=* ((EventID="4104" AND ScriptBlockText="*Set-PSReadLineOption*") AND (ScriptBlockText="*SaveNothing*" OR (ScriptBlockText="*HistorySavePath $null*" OR ScriptBlockText="*HistorySavePath NUL*" OR ScriptBlockText="*HistorySavePath /dev/null*")))Elastic · ES|QL
Run this as a search.
FROM logs-*| WHERE ((event.code == 4104 AND TO_LOWER(powershell.file.script_block_text) LIKE "*set-psreadlineoption*") AND (TO_LOWER(powershell.file.script_block_text) LIKE "*savenothing*" OR (TO_LOWER(powershell.file.script_block_text) LIKE "*historysavepath $null*" OR TO_LOWER(powershell.file.script_block_text) LIKE "*historysavepath nul*" OR TO_LOWER(powershell.file.script_block_text) LIKE "*historysavepath /dev/null*")))
Wazuh · XML rule
Deploy to your manager — this is a rule, not a search.
<group name="sigma,windows,"> <!-- Rule ids must be unique on your manager; 100000+ is the user range. --> <rule id="100000" level="5"> <!-- Set <if_sid> to the decoder/base rule for windows so this only evaluates relevant events. --> <field name="EventID" type="pcre2">(?i)^4104$</field> <field name="ScriptBlockText" type="pcre2">(?i)Set-PSReadLineOption</field> <field name="ScriptBlockText" type="pcre2">(?i)(SaveNothing|(HistorySavePath \$null|HistorySavePath NUL|HistorySavePath /dev/null))</field> <description>PowerShell history saving disabled via Set-PSReadLineOption</description> <mitre> <id>T1690</id> </mitre> </rule></group>
Verdicts · reactions · comments
Community
Verdicts from engineers who actually deployed it, and the conversation around it.
Nobody has run this in a real environment and said what happened.
Verdicts from engineers who deployed it
0 castNo verdicts reported yet. The first one is worth more than the tenth — say what you ran it against.
Log in to report a verdict.
Log in to join the discussion.
1 thread
@tomas-eriksen
"Blind, not quiet" is the whole story on this one. We shipped it, saw zero hits for two months, felt good about ourselves, and then found script block logging was enabled on about 300 of 4,000 hosts. I would go further than the description does: a rule with a hard telemetry prerequisite should not be allowed to look healthy until someone has proved the channel exists on the hosts it claims to cover.
@nadia-brandt
We solved that with a canary: one host per OU runs the cmdlet on a schedule, and the rule is expected to fire there. If the canary goes quiet, we have lost the telemetry rather than the adversary having gone away. Cheap to build and it has caught two GPO regressions that nothing else noticed.