azure-auto-webhook-trigger
Holder of a webhook URI (secret token in URL) can trigger a specific runbook job via HTTP POST without ARM RBAC.
match
A conjunctive graph pattern. Variables (?x) bind node ids; every clause must hold.
where
node_type(?webhook) ==
Webhook
?webhook.isEnabled == true
principal holds the webhook URI secret (obtained at creation or from a code/config leak)
emit
| source type | Identity |
|---|---|
| target type | GenericCompute |
| source | <principal holding the webhook URI> |
| target | <runbook (BuildWorker) the webhook targets> |
| conditions | service_state uri_possession |
| state logic | ACTIVE when the principal holds the webhook URI (uri_possession confirmed via creation event, code/config leak, or attacker creation via webhooks/write) and the webhook is enabled (isEnabled=true). CONDITIONAL(service_state) if the account is suspended. BLOCKED if the webhook is disabled (isEnabled=false). Note: the URI secret is returned ONCE at webhook creation and cannot be re-read via ARM read; evidence of URI possession must come from collection of creation events, code/config leaks, or an attacker creating a new webhook (webhooks/write). |
Narrative
{principal.name} holds the webhook URI for runbook {target.name} in Automation Account {account.name}; a POST to that URI starts a job as the account's managed identity without ARM RBAC on jobs/write.
Raw rule rules/derived/azure/automation.yaml
id: azure-auto-webhook-trigger
emits: CanTrigger
description: Holder of a webhook URI (secret token in URL) can trigger a specific runbook job via HTTP
POST without ARM RBAC.
match:
- - account: null
- Contains
- webhook: null
where:
- node_type(?webhook) == Webhook
- ?webhook.isEnabled == true
- principal holds the webhook URI secret (obtained at creation or from a code/config leak)
emit:
source_type: Identity
target_type: GenericCompute
source: <principal holding the webhook URI>
target: <runbook (BuildWorker) the webhook targets>
permissions: []
conditions:
- service_state
- uri_possession
state_logic: 'ACTIVE when the principal holds the webhook URI (uri_possession confirmed via creation
event, code/config leak, or attacker creation via webhooks/write) and the webhook is enabled (isEnabled=true).
CONDITIONAL(service_state) if the account is suspended. BLOCKED if the webhook is disabled (isEnabled=false).
Note: the URI secret is returned ONCE at webhook creation and cannot be re-read via ARM read; evidence
of URI possession must come from collection of creation events, code/config leaks, or an attacker
creating a new webhook (webhooks/write).'
confidence: 0.9
derived_from:
- webhook exists (isEnabled=true) in Automation Account
- 'cicd-chains.yaml: cicd-trigger-runner-executes-as'
false_positive_note: "The webhook URI secret is NOT readable via ARM `webhooks/read`; standard ARM enumeration\
\ reveals only webhook existence (name, enabled, expiry, runbook target). Only emit ACTIVE if there\
\ is positive evidence the principal holds the URI (creation event, code leak, or the attacker holds\
\ automationAccounts/webhooks/write and created a new one). A principal who can read webhook metadata\
\ but not the URI cannot trigger it. `webhooks/write` lets an attacker create a NEW webhook (returning\
\ the URI once) for any runbook \u2014 this is a separate CanTrigger primitive gated on webhooks/write\
\ permission."
narrative: '{principal.name} holds the webhook URI for runbook {target.name} in Automation Account {account.name};
a POST to that URI starts a job as the account''s managed identity without ARM RBAC on jobs/write.'