aws-cb-execute-as-role-swap

UpdateProject + PassRole swaps the service role of an existing project; the next build runs as the new (attacker-chosen) role.

derived aws emits CanExecuteAs

match

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

{'principal': None} CanModifyConfiguration {'project': None} {'principal': None} CanPassIdentity {'role': None}

where

node_type(?project) == BuildWorker ?project is an AWS::CodeBuild::Project ?role trust policy allows codebuild.amazonaws.com (or unknown — POTENTIAL) (matched(optional[0]) OR trigger_exists(?project)) — a run path exists (the principal can also StartBuild or the project has a webhook)

optional

Present → upgrade the emitted state; absent → downgrade (not a hard requirement).

{'principal': None} CanTrigger {'project': None}

emit

source typeIdentity
target typeMachineIdentity
source?principal
target?role
permissionscodebuild:UpdateProject iam:PassRole codebuild:StartBuild?
conditionsiam_permission role_compatibility
state logicACTIVE when UpdateProject + PassRole is confirmed AND a run path exists (matched(optional[0]) — the principal can also StartBuild — or the project has a webhook). This where clause enforces the run-path gate, ensuring this rule only emits ACTIVE; the CONDITIONAL(trigger_exists) case (role swapped but no trigger established) is produced by the generic can-execute-as linchpin execute-as-via-config-identity-swap. POTENTIAL(role_compatibility) when the role trust for codebuild.amazonaws.com is unresolved. BLOCKED if SCP denies iam:PassRole or codebuild:UpdateProject.

Narrative

{principal.name} can call codebuild:UpdateProject to swap {project.name}'s service role to {role.name} (iam:PassRole + codebuild:UpdateProject); the next build (started by {principal.name} or fired by a webhook) executes as {role.name}.

Raw rule rules/derived/aws/codebuild.yaml

id: aws-cb-execute-as-role-swap
emits: CanExecuteAs
description: UpdateProject + PassRole swaps the service role of an existing project; the next build runs
  as the new (attacker-chosen) role.
match:
- - principal: null
  - CanModifyConfiguration
  - project: null
- - principal: null
  - CanPassIdentity
  - role: null
where:
- node_type(?project) == BuildWorker
- ?project is an AWS::CodeBuild::Project
- "?role trust policy allows codebuild.amazonaws.com (or unknown \u2014 POTENTIAL)"
- "(matched(optional[0]) OR trigger_exists(?project)) \u2014 a run path exists (the principal can also\
  \ StartBuild or the project has a webhook)"
optional:
- - principal: null
  - CanTrigger
  - project: null
emit:
  source_type: Identity
  target_type: MachineIdentity
  source: ?principal
  target: ?role
  permissions:
  - codebuild:UpdateProject
  - iam:PassRole
  - codebuild:StartBuild?
  conditions:
  - iam_permission
  - role_compatibility
  state_logic: "ACTIVE when UpdateProject + PassRole is confirmed AND a run path exists (matched(optional[0])\
    \ \u2014 the principal can also StartBuild \u2014 or the project has a webhook). This where clause\
    \ enforces the run-path gate, ensuring this rule only emits ACTIVE; the CONDITIONAL(trigger_exists)\
    \ case (role swapped but no trigger established) is produced by the generic can-execute-as linchpin\
    \ execute-as-via-config-identity-swap. POTENTIAL(role_compatibility) when the role trust for codebuild.amazonaws.com\
    \ is unresolved. BLOCKED if SCP denies iam:PassRole or codebuild:UpdateProject."
  confidence: min(contributing_confidences) * 0.9
  derived_from:
  - ?principal CanModifyConfiguration ?project (aws-cb-can-modify-configuration)
  - ?principal CanPassIdentity ?role (aws-cb-pass-role)
  - can-execute-as.execute-as-via-config-identity-swap (cite)
  false_positive_note: "This is the UpdateProject-based role-swap escalation with an established run path.\
    \ It requires both codebuild:UpdateProject AND iam:PassRole; UpdateProject alone cannot change the\
    \ serviceRole without PassRole. The new role is bound to the project permanently (until the next UpdateProject);\
    \ this is a persistent configuration change that affects all future builds. A run path is REQUIRED\
    \ \u2014 the where clause ensures this rule only fires ACTIVE when one exists (the principal can StartBuild\
    \ or a webhook is present on the project). The CONDITIONAL(trigger_exists) case \u2014 role swapped\
    \ but no established trigger \u2014 is owned by the generic can-execute-as linchpin. Honor role trust\
    \ policy for codebuild.amazonaws.com."
  narrative: '{principal.name} can call codebuild:UpdateProject to swap {project.name}''s service role
    to {role.name} (iam:PassRole + codebuild:UpdateProject); the next build (started by {principal.name}
    or fired by a webhook) executes as {role.name}.'
move · open · esc close