gcp-cloudbuild-trigger-update-code
cloudbuild.triggers.update lets the principal replace a trigger's build definition (inline steps, cloudbuild.yaml path, or substitutions); the next trigger fire executes the attacker-controlled definition as the trigger's SA.
match
A conjunctive graph pattern. Variables (?x) bind node ids; every clause must hold.
where
?principal has effective cloudbuild.triggers.update on the project
node_type(?sa) in [
ServiceAccount, MachineIdentity] # the build SA
?sa is the trigger's current serviceAccount
emit
| source type | Identity |
|---|---|
| target type | Compute |
| source | <principal> |
| target | <Cloud Build trigger node / BuildWorker> |
| permissions | cloudbuild.triggers.update iam.serviceAccounts.actAs |
| conditions | iam_permission |
| state logic | ACTIVE if the principal has effective cloudbuild.triggers.update on the project AND iam.serviceAccounts.actAs on the trigger's current SA is confirmed (via the CanPassIdentity edge). The CanModifyCode edge is ACTIVE for the code-injection fact; the downstream CanExecuteAs (from cicd-chains cicd-modify-buildspec-executes-as) is ACTIVE when a run path exists (the trigger is armed by an SCM event or the principal can also run it) or CONDITIONAL(run_path) if no run path is established. BLOCKED if a deny policy applies to either permission. |
Narrative
{principal.name} can update Cloud Build trigger {trigger.name} (cloudbuild.triggers.update) to replace its build definition (inline steps or cloudbuild.yaml path), with iam.serviceAccounts.actAs on trigger's SA {sa.name} confirmed; the next trigger fire executes the poisoned steps as {sa.name}.
Raw rule rules/derived/gcp/cloudbuild.yaml
id: gcp-cloudbuild-trigger-update-code
emits: CanModifyCode
description: cloudbuild.triggers.update lets the principal replace a trigger's build definition (inline
steps, cloudbuild.yaml path, or substitutions); the next trigger fire executes the attacker-controlled
definition as the trigger's SA.
match:
- - ?principal
- CanPassIdentity
- ?sa
where:
- ?principal has effective cloudbuild.triggers.update on the project
- 'node_type(?sa) in [ServiceAccount, MachineIdentity] # the build SA'
- ?sa is the trigger's current serviceAccount
emit:
source_type: Identity
target_type: Compute
source: <principal>
target: <Cloud Build trigger node / BuildWorker>
permissions:
- cloudbuild.triggers.update
- iam.serviceAccounts.actAs
conditions:
- iam_permission
state_logic: ACTIVE if the principal has effective cloudbuild.triggers.update on the project AND iam.serviceAccounts.actAs
on the trigger's current SA is confirmed (via the CanPassIdentity edge). The CanModifyCode edge is
ACTIVE for the code-injection fact; the downstream CanExecuteAs (from cicd-chains cicd-modify-buildspec-executes-as)
is ACTIVE when a run path exists (the trigger is armed by an SCM event or the principal can also run
it) or CONDITIONAL(run_path) if no run path is established. BLOCKED if a deny policy applies to either
permission.
confidence: 0.93
derived_from:
- <CanPassIdentity edge_id for ?principal -> ?sa>
- <cloudbuild.triggers.update effective permission>
false_positive_note: "actAs on the trigger's CURRENT SA is required to run any update, even if only\
\ the build spec (not the SA) is being changed \u2014 this is confirmed via CanPassIdentity edge match.\
\ Changing the SA to a different SA requires actAs on BOTH the old and new SA; this rule covers the\
\ case where the current SA is confirmed. The trigger fires only when its source event occurs (SCM\
\ push/PR, Pub/Sub, schedule) or when manually run \u2014 scope run-path state accordingly."
narrative: '{principal.name} can update Cloud Build trigger {trigger.name} (cloudbuild.triggers.update)
to replace its build definition (inline steps or cloudbuild.yaml path), with iam.serviceAccounts.actAs
on trigger''s SA {sa.name} confirmed; the next trigger fire executes the poisoned steps as {sa.name}.'