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.
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 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]) — 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}.'