Skip to content

Siemphony is in beta and still being built. How a rule earns its badge.

Siemphony’s repertoire

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 description

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

title: PowerShell history saving disabled via Set-PSReadLineOptionid: 81636d9f-bb72-4455-b584-85c66c4b1305status: experimentaldescription: |  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.references:  - https://attack.mitre.org/techniques/T1690  - https://attack.mitre.org/detectionstrategies/DET0563author: Siemphony pipeline (machine-derived from MITRE ATT&CK v19.2)date: 2026-08-18tags:  - attack.defense-evasion  - attack.t1690logsource:  product: windows  service: powershelldetection:  selection_event:    EventID: 4104  selection_cmdlet:    ScriptBlockText|contains: 'Set-PSReadLineOption'  selection_savenothing:    ScriptBlockText|contains: 'SaveNothing'  selection_nullpath:    ScriptBlockText|contains:      - 'HistorySavePath $null'      - 'HistorySavePath NUL'      - 'HistorySavePath /dev/null'  condition: selection_event and selection_cmdlet and (selection_savenothing or selection_nullpath)falsepositives:  - "Security-hardening baselines for shared jump hosts and PAM bastions that deliberately set SaveNothing at profile load to stop credentials typed during a privileged session from persisting into a shared history file — a legitimate control that produces the identical script block."  - "Self-hosted CI/CD runner initialization scripts (Azure DevOps, GitHub Actions Windows runners) that disable PSReadLine history at session start so secrets echoed during a pipeline run are never written to disk on a reused runner."  - "Detection-engineering and atomic-test validation runs that execute this exact cmdlet and argument to demonstrate or self-test the analytic rather than to conceal anything."level: low

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.

Log in to react
Not yet reported on

Nobody has run this in a real environment and said what happened.

Verdicts from engineers who deployed it

0 cast

No 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.