azure-devops-template-repo-write
Write to a shared YAML template repo/branch that a pipeline imports via 'extends: template', injecting code into all downstream pipelines that use it.
match
A conjunctive graph pattern. Variables (?x) bind node ids; every clause must hold.
{'principal': None}
CanWrite {'template_repo_branch': None}
{'template_repo_branch': None} ContainsResourceReference {'pipeline': None}
where
node_type(?pipeline) ==
BuildWorker
?pipeline imports ?template_repo_branch via 'extends: template' in its YAML definition
?template_repo_branch is NOT pinned to a specific commit SHA or tag in the extends: block (or its parent YAML)
emit
| source type | Identity |
|---|---|
| target type | BuildWorker |
| source | ?principal |
| target | ?pipeline |
| permissions | Contribute (Azure Repos source namespace, vso.code_write) git push |
| conditions | branch_protection |
| state logic | ACTIVE when the principal can push directly to the template branch AND the pipeline enforces no required-template-from-trusted-repo control (or the attacker's push is to the very branch the control trusts) AND the template is NOT pinned to a specific commit. CONDITIONAL(branch_protection) when a branch policy requires-reviewer or when the template import is pinned to a commit SHA/tag the attacker cannot modify. Inherit weakest from CanWrite on the template repo. |
Narrative
{principal.name} can modify the shared YAML template in {template_repo_branch.name} that pipeline {pipeline.name} imports; any code injected into the template executes as the pipeline's service-connection identity on the next run.
Raw rule rules/derived/azure/devops.yaml
id: azure-devops-template-repo-write
emits: CanModifyCode
description: 'Write to a shared YAML template repo/branch that a pipeline imports via ''extends: template'',
injecting code into all downstream pipelines that use it.'
applies_to:
- azure
match:
- - principal: null
- CanWrite
- template_repo_branch: null
- - template_repo_branch: null
- ContainsResourceReference
- pipeline: null
where:
- node_type(?pipeline) == BuildWorker
- '?pipeline imports ?template_repo_branch via ''extends: template'' in its YAML definition'
- '?template_repo_branch is NOT pinned to a specific commit SHA or tag in the extends: block (or its parent
YAML)'
emit:
source_type: Identity
target_type: BuildWorker
source: ?principal
target: ?pipeline
permissions:
- Contribute (Azure Repos source namespace, vso.code_write)
- git push
conditions:
- branch_protection
state_logic: ACTIVE when the principal can push directly to the template branch AND the pipeline enforces
no required-template-from-trusted-repo control (or the attacker's push is to the very branch the control
trusts) AND the template is NOT pinned to a specific commit. CONDITIONAL(branch_protection) when a
branch policy requires-reviewer or when the template import is pinned to a commit SHA/tag the attacker
cannot modify. Inherit weakest from CanWrite on the template repo.
confidence: 0.85
derived_from:
- ?principal CanWrite ?template_repo_branch
- ?template_repo_branch ContainsResourceReference ?pipeline
false_positive_note: "Fan-out impact can be large (one template repo write reaches many pipelines).\
\ Verify the template import path is actually reachable (the pipeline's extends: block references\
\ the exact branch the principal can write). A 'Require template from trusted repo' enforcement control\
\ on the PIPELINE (not the template repo) can block using an untrusted template \u2014 but cannot\
\ block modifications to the TRUSTED template itself. Honor branch policies on the template repo.\
\ If the template import pins to a commit SHA or tag (rather than a branch), the CanModifyCode is\
\ CONDITIONAL(branch_protection) because updates to that commit require the attacker to modify the\
\ extends: block itself (which requires Edit pipeline definition, not just template-repo write)."
narrative: '{principal.name} can modify the shared YAML template in {template_repo_branch.name} that
pipeline {pipeline.name} imports; any code injected into the template executes as the pipeline''s
service-connection identity on the next run.'