azure-devops-sc-exposes-client-secret

An ARM service connection (secret-based) injects the backing SP's client secret / certificate into the job environment; pipeline code can read and exfiltrate it.

derived azure emits ExposesCredential

match

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

{'pipeline': None} ExecutesAs {'sc_identity': None}

where

node_type(?pipeline) == BuildWorker node_type(?sc_identity) == MachineIdentity ?sc_identity is backed by a secret-based ARM service connection (not WIF)

emit

source typeBuildWorker
target typeCredential
source?pipeline
targetclient secret / certificate for {sc_identity.name}
state logicACTIVE for every secret-based ARM service connection used by the pipeline; the secret is injected by the agent before each task that references the SC. The credential is LONG-LIVED (client secret) — not a short-lived token. Inherits CONDITIONAL(service_state) if the SC itself is CONDITIONAL.

Narrative

{pipeline.name} runs as service connection {sc_identity.name} using a stored client secret; that secret is injected into the job environment and is readable by any code running in the pipeline, including attacker-injected steps. The credential is long-lived and usable outside the pipeline context.

Raw rule rules/derived/azure/devops.yaml

id: azure-devops-sc-exposes-client-secret
emits: ExposesCredential
description: An ARM service connection (secret-based) injects the backing SP's client secret / certificate
  into the job environment; pipeline code can read and exfiltrate it.
applies_to:
- azure
match:
- - pipeline: null
  - ExecutesAs
  - sc_identity: null
where:
- node_type(?pipeline) == BuildWorker
- node_type(?sc_identity) == MachineIdentity
- ?sc_identity is backed by a secret-based ARM service connection (not WIF)
emit:
  source_type: BuildWorker
  target_type: Credential
  source: ?pipeline
  target: client secret / certificate for {sc_identity.name}
  permissions: []
  conditions: []
  state_logic: "ACTIVE for every secret-based ARM service connection used by the pipeline; the secret\
    \ is injected by the agent before each task that references the SC. The credential is LONG-LIVED (client\
    \ secret) \u2014 not a short-lived token. Inherits CONDITIONAL(service_state) if the SC itself is\
    \ CONDITIONAL."
  confidence: 0.9
  derived_from:
  - azure-devops-pipeline-executes-as-sc
  false_positive_note: "Do NOT emit ExposesCredential for WIF (federated credential) service connections\
    \ \u2014 there is no stored secret to expose. Only emit for service connections where clientSecret\
    \ or certificate is present in the SC configuration (GET serviceendpoint/endpoints/{id} shows type\
    \ and auth scheme). Secret rotation or expiry: if the SP's secret is expired, downgrade to CONDITIONAL(service_state).\
    \ The exposed secret is reusable outside the pipeline (long-lived) \u2014 this is a significant difference\
    \ from a short-lived OIDC token."
  narrative: '{pipeline.name} runs as service connection {sc_identity.name} using a stored client secret;
    that secret is injected into the job environment and is readable by any code running in the pipeline,
    including attacker-injected steps. The credential is long-lived and usable outside the pipeline context.'
move · open · esc close