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.

derived azure emits CanTrigger

match

A conjunctive graph pattern. Variables (?x) bind node ids; every clause must hold.

{'account': None} Contains {'webhook': None}

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 typeIdentity
target typeGenericCompute
source<principal holding the webhook URI>
target<runbook (BuildWorker) the webhook targets>
conditionsservice_state uri_possession
state logicACTIVE 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.'
move · open · esc close