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.

derived gcp emits CanModifyCode

match

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

?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 typeIdentity
target typeCompute
source<principal>
target<Cloud Build trigger node / BuildWorker>
permissionscloudbuild.triggers.update iam.serviceAccounts.actAs
conditionsiam_permission
state logicACTIVE 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}.'
move · open · esc close