Skip to content

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

Siemphony’s repertoire

Cloud account enumeration cmdlets in a PowerShell script block

Matches AN1089 on the only log source the brief gives it: a PowerShell script block that names a command which lists the accounts in a cloud tenant. Two groups are separated because they are not equally suspicious. The first holds the directory-wide calls — the MSOnline, AzureAD, Microsoft Graph, Microsoft Entra and Az identity cmdlets, and the `az`, `aws iam` and `gcloud` account-listing command lines, which appear in a script block when they are typed at or invoked from a PowerShell host — and it fires on its own. MSOnline and AzureAD are retired modules and are kept only because the technique text names `Get-MsolRoleMember`; the Graph SDK and Entra cmdlets beside them are added here, not taken from MITRE, and are what a current tenant would actually be enumerated with. The second holds `Get-Recipient`, `Get-Mailbox` and `Get-User`, which are the three most-used cmdlets in ordinary Exchange administration and are therefore gated behind a bulk marker (`-ResultSize Unlimited`) so that a single lookup of one mailbox does not match while a whole-tenant sweep does. That gate is also the rule's largest blind spot, and it is authored here rather than derived: those cmdlets return 1000 objects by default, so in any tenant smaller than that a bare `Get-Mailbox` enumerates every account and matches nothing below. The same three cmdlets are equally the observable for T1087.003 Email Account and this branch cannot separate the two; they are included because MITRE names them in AN1089 under this technique's strategy. Everything sits on `ScriptBlockText` under EventID 4104, the only field in this source carrying the script as the engine compiled it: an `-EncodedCommand` payload therefore arrives decoded, but decoded is not deobfuscated, and name splitting (`"Get-Msol"+"User"`), backtick insertion and wildcard command resolution (`&(gcm Get-Msol*)`) all survive compilation and defeat every literal term below. Three limits are structural. MITRE's `CmdletVolume` knob — the real discriminator here, since the abuse differs from administration mainly in how much is enumerated per hour — needs an aggregation lib/sigma does not model, so this rule sees single invocations and its level reflects that; the `UserAgent` and `SessionContext` knobs likewise have no field in this event. The rule can only see enumeration performed on a Windows host whose PowerShell is being logged, so an adversary calling Graph from their own machine, working in the Azure or Microsoft 365 admin portal, or running the same CLI commands from Linux or macOS writes nothing here at all — the tenant audit log is where that lives, and the brief supplies no log source for it. And the rule is written for 4104's `ScriptBlockText`; an estate collecting only 4103 module logging carries the invocation in `ContextInfo`/`Payload` instead and would need its own selection. Prerequisite: EventID 4104 requires *Script Block Logging* to be enabled by policy, which is off in a default install, and with it off this rule returns zero rows — which reads as quiet when it means blind. UNVERIFIED AS AUTHORED — derived from MITRE ATT&CK DET0386, 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: Cloud account enumeration cmdlets in a PowerShell script blockid: a09fd3e3-ae5a-4df7-96f1-1c0c8c951017status: experimentaldescription: |  Matches AN1089 on the only log source the brief gives it: a PowerShell  script block that names a command which lists the accounts in a cloud tenant.  Two groups are separated because they are not equally suspicious. The first  holds the directory-wide calls — the MSOnline, AzureAD, Microsoft Graph,  Microsoft Entra and Az identity cmdlets, and the `az`, `aws iam` and `gcloud`  account-listing command lines, which appear in a script block when they are  typed at or invoked from a PowerShell host — and it fires on its own. MSOnline  and AzureAD are retired modules and are kept only because the technique text  names `Get-MsolRoleMember`; the Graph SDK and Entra cmdlets beside them are  added here, not taken from MITRE, and are what a current tenant would actually  be enumerated with. The second holds  `Get-Recipient`, `Get-Mailbox` and `Get-User`, which are the three most-used  cmdlets in ordinary Exchange administration and are therefore gated behind a  bulk marker (`-ResultSize Unlimited`) so that a single lookup of one mailbox  does not match while a whole-tenant sweep does. That gate is also the rule's  largest blind spot, and it is authored here rather than derived: those cmdlets  return 1000 objects by default, so in any tenant smaller than that a bare  `Get-Mailbox` enumerates every account and matches nothing below. The same  three cmdlets are equally the observable for T1087.003 Email Account and this  branch cannot separate the two; they are included because MITRE names them in  AN1089 under this technique's strategy. Everything sits on  `ScriptBlockText` under EventID 4104, the only field in this source carrying  the script as the engine compiled it: an `-EncodedCommand` payload therefore  arrives decoded, but decoded is not deobfuscated, and name splitting  (`"Get-Msol"+"User"`), backtick insertion and wildcard command resolution  (`&(gcm Get-Msol*)`) all survive compilation and defeat every literal term  below. Three limits are structural. MITRE's `CmdletVolume` knob — the real  discriminator here, since the abuse differs from administration mainly in how  much is enumerated per hour — needs an aggregation lib/sigma does not model,  so this rule sees single invocations and its level reflects that; the  `UserAgent` and `SessionContext` knobs likewise have no field in this event.  The rule can only see enumeration performed on a Windows host whose  PowerShell is being logged, so an adversary calling Graph from their own  machine, working in the Azure or Microsoft 365 admin portal, or running the  same CLI commands from Linux or macOS writes nothing here at all — the tenant  audit log is where that lives, and the brief supplies no log source for it.  And the rule is written for 4104's `ScriptBlockText`; an estate collecting  only 4103 module logging carries the invocation in `ContextInfo`/`Payload`  instead and would need its own selection. Prerequisite: EventID 4104 requires  *Script Block Logging* to be enabled by policy, which is off in a default  install, and with it off this rule returns zero rows — which reads as quiet  when it means blind.  UNVERIFIED AS AUTHORED — derived from MITRE ATT&CK DET0386, 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/T1087/004  - https://attack.mitre.org/detectionstrategies/DET0386author: Siemphony pipeline (machine-derived from MITRE ATT&CK v19.2)date: 2026-08-16tags:  - attack.discovery  - attack.t1087.004logsource:  product: windows  service: powershelldetection:  selection_directory:    EventID: 4104    ScriptBlockText|contains:      - 'Get-MsolUser'      - 'Get-MsolRoleMember'      - 'Get-AzureADUser'      - 'Get-AzureADDirectoryRoleMember'      - 'Get-MgUser'      - 'Get-MgBetaUser'      - 'Get-MgDirectoryRoleMember'      - 'Get-EntraUser'      - 'Get-EntraDirectoryRoleMember'      - 'Get-AzADUser'      - 'az ad user list'      - 'aws iam list-users'      - 'aws iam list-roles'      - 'gcloud iam service-accounts list'      - 'gcloud projects get-iam-policy'  selection_recipient:    EventID: 4104    ScriptBlockText|contains:      - 'Get-Recipient'      - 'Get-Mailbox'      - 'Get-User'  selection_bulk:    EventID: 4104    ScriptBlockText|contains:      - 'ResultSize Unlimited'      - 'ResultSize:Unlimited'  condition: selection_directory or (selection_recipient and selection_bulk)falsepositives:  - "Identity and licence reporting automation. Scheduled scripts that reconcile licences, feed a CMDB, build a joiner-mover-leaver report or export a tenant inventory call Get-MsolUser, Get-MgUser or Get-AzureADUser -All on a fixed interval from a management server, and enumerate far more accounts than an adversary would. On any tenant with such a job this is the highest-volume match by a wide margin and it is why this rule is low rather than medium — the volume threshold that would separate the two is MITRE's CmdletVolume knob, which cannot be expressed here."  - "Everyday Exchange Online administration. Get-Mailbox -ResultSize Unlimited is the documented way to list every mailbox in a tenant and is the first line of most support, migration and reporting scripts, so the gated branch of this rule fires on ordinary service-desk and mail-team work."  - "Backup, archiving and eDiscovery products, which page through Get-Recipient or Get-Mailbox across the whole tenant on every scheduled run, usually under a dedicated service account from a fixed host."  - "Cloud infrastructure and CI automation running the AWS, Azure or Google CLIs from a PowerShell host — an `aws iam list-users` in a compliance check, a `gcloud projects get-iam-policy` in a Terraform wrapper or an access-review export — where enumerating principals is the point of the job."  - "Authorised assessment work and tenant health checks, where a consultant's script enumerates users and directory role members as its first step and matches the entire first group in one run."level: low

Sentinel · KQL

Run this as a search.

Event| where ((EventID == 4104 and (ScriptBlockText contains "Get-MsolUser" or ScriptBlockText contains "Get-MsolRoleMember" or ScriptBlockText contains "Get-AzureADUser" or ScriptBlockText contains "Get-AzureADDirectoryRoleMember" or ScriptBlockText contains "Get-MgUser" or ScriptBlockText contains "Get-MgBetaUser" or ScriptBlockText contains "Get-MgDirectoryRoleMember" or ScriptBlockText contains "Get-EntraUser" or ScriptBlockText contains "Get-EntraDirectoryRoleMember" or ScriptBlockText contains "Get-AzADUser" or ScriptBlockText contains "az ad user list" or ScriptBlockText contains "aws iam list-users" or ScriptBlockText contains "aws iam list-roles" or ScriptBlockText contains "gcloud iam service-accounts list" or ScriptBlockText contains "gcloud projects get-iam-policy")) or ((EventID == 4104 and (ScriptBlockText contains "Get-Recipient" or ScriptBlockText contains "Get-Mailbox" or ScriptBlockText contains "Get-User")) and (EventID == 4104 and (ScriptBlockText contains "ResultSize Unlimited" or ScriptBlockText contains "ResultSize:Unlimited"))))

Splunk · SPL

Run this as a search.

index=* ((EventID="4104" AND (ScriptBlockText="*Get-MsolUser*" OR ScriptBlockText="*Get-MsolRoleMember*" OR ScriptBlockText="*Get-AzureADUser*" OR ScriptBlockText="*Get-AzureADDirectoryRoleMember*" OR ScriptBlockText="*Get-MgUser*" OR ScriptBlockText="*Get-MgBetaUser*" OR ScriptBlockText="*Get-MgDirectoryRoleMember*" OR ScriptBlockText="*Get-EntraUser*" OR ScriptBlockText="*Get-EntraDirectoryRoleMember*" OR ScriptBlockText="*Get-AzADUser*" OR ScriptBlockText="*az ad user list*" OR ScriptBlockText="*aws iam list-users*" OR ScriptBlockText="*aws iam list-roles*" OR ScriptBlockText="*gcloud iam service-accounts list*" OR ScriptBlockText="*gcloud projects get-iam-policy*")) OR ((EventID="4104" AND (ScriptBlockText="*Get-Recipient*" OR ScriptBlockText="*Get-Mailbox*" OR ScriptBlockText="*Get-User*")) AND (EventID="4104" AND (ScriptBlockText="*ResultSize Unlimited*" OR ScriptBlockText="*ResultSize:Unlimited*"))))

Elastic · ES|QL

Run this as a search.

FROM logs-*| WHERE ((event.code == 4104 AND (TO_LOWER(powershell.file.script_block_text) LIKE "*get-msoluser*" OR TO_LOWER(powershell.file.script_block_text) LIKE "*get-msolrolemember*" OR TO_LOWER(powershell.file.script_block_text) LIKE "*get-azureaduser*" OR TO_LOWER(powershell.file.script_block_text) LIKE "*get-azureaddirectoryrolemember*" OR TO_LOWER(powershell.file.script_block_text) LIKE "*get-mguser*" OR TO_LOWER(powershell.file.script_block_text) LIKE "*get-mgbetauser*" OR TO_LOWER(powershell.file.script_block_text) LIKE "*get-mgdirectoryrolemember*" OR TO_LOWER(powershell.file.script_block_text) LIKE "*get-entrauser*" OR TO_LOWER(powershell.file.script_block_text) LIKE "*get-entradirectoryrolemember*" OR TO_LOWER(powershell.file.script_block_text) LIKE "*get-azaduser*" OR TO_LOWER(powershell.file.script_block_text) LIKE "*az ad user list*" OR TO_LOWER(powershell.file.script_block_text) LIKE "*aws iam list-users*" OR TO_LOWER(powershell.file.script_block_text) LIKE "*aws iam list-roles*" OR TO_LOWER(powershell.file.script_block_text) LIKE "*gcloud iam service-accounts list*" OR TO_LOWER(powershell.file.script_block_text) LIKE "*gcloud projects get-iam-policy*")) OR ((event.code == 4104 AND (TO_LOWER(powershell.file.script_block_text) LIKE "*get-recipient*" OR TO_LOWER(powershell.file.script_block_text) LIKE "*get-mailbox*" OR TO_LOWER(powershell.file.script_block_text) LIKE "*get-user*")) AND (event.code == 4104 AND (TO_LOWER(powershell.file.script_block_text) LIKE "*resultsize unlimited*" OR TO_LOWER(powershell.file.script_block_text) LIKE "*resultsize:unlimited*"))))

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. -->  <!-- 2 rules: the Sigma condition ORs across different fields,       which one rule cannot express. Any one matching is a hit. -->  <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)(Get-MsolUser|Get-MsolRoleMember|Get-AzureADUser|Get-AzureADDirectoryRoleMember|Get-MgUser|Get-MgBetaUser|Get-MgDirectoryRoleMember|Get-EntraUser|Get-EntraDirectoryRoleMember|Get-AzADUser|az ad user list|aws iam list-users|aws iam list-roles|gcloud iam service-accounts list|gcloud projects get-iam-policy)</field>    <description>Cloud account enumeration cmdlets in a PowerShell script block (1/2)</description>    <mitre>      <id>T1087.004</id>    </mitre>  </rule>   <rule id="100001" 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)(Get-Recipient|Get-Mailbox|Get-User)</field>    <field name="EventID" type="pcre2">(?i)^4104$</field>    <field name="ScriptBlockText" type="pcre2">(?i)(ResultSize Unlimited|ResultSize:Unlimited)</field>    <description>Cloud account enumeration cmdlets in a PowerShell script block (2/2)</description>    <mitre>      <id>T1087.004</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.

No comments yet. Someone who deploys this will have something to say about it.