azure-devops-push-trigger-branch
Write access to the CI-trigger branch of the pipeline's source repo lets a principal inject code that the pipeline will execute on the next triggered run.
match
A conjunctive graph pattern. Variables (?x) bind node ids; every clause must hold.
{'principal': None}
CanWrite {'repo_branch': None}
{'repo_branch': None} CanTrigger {'pipeline': None}
where
node_type(?pipeline) ==
BuildWorker
?repo_branch is the branch / ref filter the pipeline's CI trigger watches
node_type(?repo_branch) in [ObjectStorage, GenericStorage] # Azure Repos branch modeled as concrete ObjectStorage or GenericStorage node
emit
| source type | Identity |
|---|---|
| target type | BuildWorker |
| source | ?principal |
| target | ?pipeline |
| permissions | Contribute (Azure Repos source namespace, vso.code_write) git push to trigger ref |
| conditions | branch_protection |
| state logic | ACTIVE when the principal can push directly to the trigger branch AND no branch policy requires pull-request review before merge (or requires minimum approver count that the attacker cannot satisfy alone). CONDITIONAL(branch_protection) when a branch policy (required-reviewer, minimum approvers, work-item linking, etc.) gates direct pushes — the attacker must craft a PR that passes review, which is not guaranteed. Inherit weakest from the CanWrite contributor: a CONDITIONAL CanWrite (branch policy) yields a CONDITIONAL CanModifyCode. |
Narrative
{principal.name} has write access to the CI-trigger branch of the repository that pipeline {pipeline.name} builds; a push to that branch triggers a run executing the committed pipeline YAML as the service-connection identity.
Raw rule rules/derived/azure/devops.yaml
id: azure-devops-push-trigger-branch
emits: CanModifyCode
description: Write access to the CI-trigger branch of the pipeline's source repo lets a principal inject
code that the pipeline will execute on the next triggered run.
applies_to:
- azure
match:
- - principal: null
- CanWrite
- repo_branch: null
- - repo_branch: null
- CanTrigger
- pipeline: null
where:
- node_type(?pipeline) == BuildWorker
- ?repo_branch is the branch / ref filter the pipeline's CI trigger watches
- 'node_type(?repo_branch) in [ObjectStorage, GenericStorage] # Azure Repos branch modeled as concrete
ObjectStorage or GenericStorage node'
emit:
source_type: Identity
target_type: BuildWorker
source: ?principal
target: ?pipeline
permissions:
- Contribute (Azure Repos source namespace, vso.code_write)
- git push to trigger ref
conditions:
- branch_protection
state_logic: "ACTIVE when the principal can push directly to the trigger branch AND no branch policy\
\ requires pull-request review before merge (or requires minimum approver count that the attacker\
\ cannot satisfy alone). CONDITIONAL(branch_protection) when a branch policy (required-reviewer, minimum\
\ approvers, work-item linking, etc.) gates direct pushes \u2014 the attacker must craft a PR that\
\ passes review, which is not guaranteed. Inherit weakest from the CanWrite contributor: a CONDITIONAL\
\ CanWrite (branch policy) yields a CONDITIONAL CanModifyCode."
confidence: 0.9
derived_from:
- ?principal CanWrite ?repo_branch
- ?repo_branch CanTrigger ?pipeline
false_positive_note: "Check branch policies on the trigger branch (GET git/repositories/{id}/refs/policies).\
\ A policy requiring at least one reviewer or pull-request review before merge prevents a direct push\
\ from yielding ACTIVE code control \u2014 downgrade to CONDITIONAL(branch_protection). GitHub-hosted\
\ repos also require checking the GitHub branch protection rules separately. For pull-request triggers\
\ (pr: config in YAML), also check fork isolation settings (azure-devops-pr-trigger-executes-as) \u2014\
\ a PR from a fork may not have service-connection access if fork isolation is enabled."
narrative: '{principal.name} has write access to the CI-trigger branch of the repository that pipeline
{pipeline.name} builds; a push to that branch triggers a run executing the committed pipeline YAML
as the service-connection identity.'