azure-auto-module-write

Upload a malicious PowerShell module or Python package to an Automation Account; existing runbooks that Import-Module/import it execute the injected code as the account MI.

derived azure emits CanModifyCode

match (effective permission)

{ "any_of": [ { "action": "Microsoft.Automation/automationAccounts/modules/write", "scope_type": "resource|resourceGroup|subscription" }, { "action": "Microsoft.Automation/automationAccounts/python2Packages/write", "scope_type": "resource|resourceGroup|subscription" }, { "action": "Microsoft.Automation/automationAccounts/python3Packages/write", "scope_type": "resource|resourceGroup|subscription" } ] }

where

at least one existing runbook in the account imports the module by name (or imports all modules from the gallery implicitly)

emit

source typeIdentity
target typeGenericCompute
source<principal>
target<runbook (BuildWorker) that imports the module>
permissionsMicrosoft.Automation/automationAccounts/modules/write Microsoft.Automation/automationAccounts/python2Packages/write Microsoft.Automation/automationAccounts/python3Packages/write
conditionstrigger_exists service_state
state logicCONDITIONAL(trigger_exists) when a runbook is known to Import-Module the poisoned module but no current trigger/schedule fires it for the attacker; ACTIVE when an existing schedule or webhook already runs the consuming runbook without attacker action. If no runbook in the account imports the affected module, do not emit — the supply-chain injection has no consumer. BLOCKED if deny assignment covers modules/write.

Narrative

{principal.name} can upload a malicious PowerShell/Python module to Automation Account {target.name} (automationAccounts/modules/write); if an existing runbook imports this module by name, the injected code executes as the account's managed identity on the next job trigger.

Raw rule rules/derived/azure/automation.yaml

id: azure-auto-module-write
emits: CanModifyCode
description: Upload a malicious PowerShell module or Python package to an Automation Account; existing
  runbooks that Import-Module/import it execute the injected code as the account MI.
match_effective_permission:
  any_of:
  - action: Microsoft.Automation/automationAccounts/modules/write
    scope_type: resource|resourceGroup|subscription
  - action: Microsoft.Automation/automationAccounts/python2Packages/write
    scope_type: resource|resourceGroup|subscription
  - action: Microsoft.Automation/automationAccounts/python3Packages/write
    scope_type: resource|resourceGroup|subscription
where:
- at least one existing runbook in the account imports the module by name (or imports all modules from
  the gallery implicitly)
emit:
  source_type: Identity
  target_type: GenericCompute
  source: <principal>
  target: <runbook (BuildWorker) that imports the module>
  permissions:
  - Microsoft.Automation/automationAccounts/modules/write
  - Microsoft.Automation/automationAccounts/python2Packages/write
  - Microsoft.Automation/automationAccounts/python3Packages/write
  conditions:
  - trigger_exists
  - service_state
  state_logic: "CONDITIONAL(trigger_exists) when a runbook is known to Import-Module the poisoned module\
    \ but no current trigger/schedule fires it for the attacker; ACTIVE when an existing schedule or webhook\
    \ already runs the consuming runbook without attacker action. If no runbook in the account imports\
    \ the affected module, do not emit \u2014 the supply-chain injection has no consumer. BLOCKED if deny\
    \ assignment covers modules/write."
  confidence: 0.8
  derived_from:
  - 'effective permission: Microsoft.Automation/automationAccounts/modules/write (or python2/3Packages/write)'
  - 'cicd-chains.yaml: cicd-modify-buildspec-executes-as (module is the definition the runner executes)'
  false_positive_note: Only emit when a runbook in the account actually imports the module name being
    poisoned. A module upload with no consuming runbook is not a CanModifyCode edge. The attacker must
    be able to shadow or replace an existing module (matching the name the runbook imports); uploading
    a completely new, unimported module has no execution path. Confidence 0.80 (lower than direct runbook-write)
    because the supply-chain path requires an existing consumer and an established trigger.
  narrative: '{principal.name} can upload a malicious PowerShell/Python module to Automation Account {target.name}
    (automationAccounts/modules/write); if an existing runbook imports this module by name, the injected
    code executes as the account''s managed identity on the next job trigger.'
move · open · esc close