gcp-iam-deny-modify-deny-policy
Principal with iam.denypolicies.update or .delete can remove/weaken a deny policy, upgrading the edges it was blocking.
match
A conjunctive graph pattern. Variables (?x) bind node ids; every clause must hold.
where
Organization, Folder, Project] # attachment scope
node_type(?deny_policy) == ConditionalPolicy
?deny_policy.provider_type == 'google.iam.DenyPolicy'
?principal has EFFECTIVE iam.denypolicies.update OR iam.denypolicies.delete at ?scope (via IAM allow policy at ?scope or an ancestor scope)
?deny_policy.rules[] contains at least one denyRule that blocks >=1 modeled edge in the graph (state_of(e)==BLOCKED and blocked_by(e)==?deny_policy)
emit
| source type | Identity |
|---|---|
| target type | ConditionalPolicy |
| source | ?principal |
| target | ?deny_policy |
| permissions | iam.denypolicies.update iam.denypolicies.delete |
| conditions | iam_permission deny_assignment |
| state logic | ACTIVE when iam.denypolicies.update or .delete is unconditionally effective at ?scope (the IAM allow binding grants it without a CEL condition, or the condition evaluates TRUE at collection time) AND the deny policy is present and blocks >=1 modeled edge. CONDITIONAL(iam_permission) if the allow binding granting the modify permission is itself gated by a CEL condition that may not always hold (e.g. time-bound, resource-tag gated). BLOCKED when ?principal is denied iam.denypolicies.update AND iam.denypolicies.delete by a SEPARATE deny policy (blocked_by != ?deny_policy) at ?scope — the attacker cannot remove the guardrail they are themselves blocked by. Circular deny chains must be resolved first (see false_positive_note). The downstream BLOCKED->CONDITIONAL(guardrail_removed) state upgrade of gated edges is NOT emitted here; it is performed by hierarchy-chains.yaml guardrail-removal-upgrades-blocked, keyed on blocked_by(?deny_policy). |
Narrative
{principal.name} holds iam.denypolicies.update or iam.denypolicies.delete at {scope.name} (via roles/iam.denyAdmin or equivalent) and can remove or weaken {deny_policy.name}, lifting the blocks it imposes on {gated_edge_summary}. Once removed, those edges are upgraded from BLOCKED to CONDITIONAL(guardrail_removed) by the generic guardrail-removal rule (hierarchy-chains.yaml). When the deny policy is attached at an Organization or Folder scope, its removal unblocks edges across the ENTIRE descendant subtree; the hierarchy-chains.yaml admin-inherits-down-contains rule (rule 1) performs the generic transitive descent to all descendants.
Raw rule rules/derived/gcp/iam-deny.yaml
id: gcp-iam-deny-modify-deny-policy
emits: CanModifyPolicy
description: Principal with iam.denypolicies.update or .delete can remove/weaken a deny policy, upgrading
the edges it was blocking.
match:
- - scope: null
- HasPolicy
- deny_policy: null
where:
- node_class(?principal) == Identity
- 'node_type(?scope) in [Organization, Folder, Project] # attachment scope'
- node_type(?deny_policy) == ConditionalPolicy
- ?deny_policy.provider_type == 'google.iam.DenyPolicy'
- ?principal has EFFECTIVE iam.denypolicies.update OR iam.denypolicies.delete at ?scope (via IAM allow
policy at ?scope or an ancestor scope)
- ?deny_policy.rules[] contains at least one denyRule that blocks >=1 modeled edge in the graph (state_of(e)==BLOCKED
and blocked_by(e)==?deny_policy)
emit:
source_type: Identity
target_type: ConditionalPolicy
source: ?principal
target: ?deny_policy
permissions:
- iam.denypolicies.update
- iam.denypolicies.delete
conditions:
- iam_permission
- deny_assignment
state_logic: "ACTIVE when iam.denypolicies.update or .delete is unconditionally effective at ?scope\
\ (the IAM allow binding grants it without a CEL condition, or the condition evaluates TRUE at collection\
\ time) AND the deny policy is present and blocks >=1 modeled edge. CONDITIONAL(iam_permission) if\
\ the allow binding granting the modify permission is itself gated by a CEL condition that may not\
\ always hold (e.g. time-bound, resource-tag gated). BLOCKED when ?principal is denied iam.denypolicies.update\
\ AND iam.denypolicies.delete by a SEPARATE deny policy (blocked_by != ?deny_policy) at ?scope \u2014\
\ the attacker cannot remove the guardrail they are themselves blocked by. Circular deny chains must\
\ be resolved first (see false_positive_note). The downstream BLOCKED->CONDITIONAL(guardrail_removed)\
\ state upgrade of gated edges is NOT emitted here; it is performed by hierarchy-chains.yaml guardrail-removal-upgrades-blocked,\
\ keyed on blocked_by(?deny_policy)."
confidence: min(contributing_confidences)
evaluator_hook_dependency: Principal ?principal is not explicitly bound by a graph edge in the match
clause; instead, it is introduced by the effective-permission evaluator (pseudo-triple '?principal
has EFFECTIVE iam.denypolicies.update OR iam.denypolicies.delete at ?scope') in the where clause.
This is architecturally consistent with the design contract (evaluator acts as a match-clause primitive),
but makes the rule dependent on an evaluator hook rather than a pure graph-pattern match. Implementors
should not attempt to treat this rule as a pure graph-pattern rule; the evaluator must resolve the
EFFECTIVE permission at ?scope before unification.
derived_from:
- ?scope HasPolicy ?deny_policy (ConditionalPolicy, google.iam.DenyPolicy)
- ?principal has EFFECTIVE iam.denypolicies.update or .delete at ?scope (via evaluator hook)
- ?deny_policy blocks >=1 modeled edge (state_of(e)==BLOCKED, blocked_by(e)==?deny_policy)
- guardrail-removal-upgrades-blocked (hierarchy-chains.yaml rule 3) performs the BLOCKED->CONDITIONAL(guardrail_removed)
state upgrade for each gated edge
false_positive_note: "Emit ONLY when the deny policy actually blocks >=1 modeled edge (blocked_by(e)==?deny_policy\
\ for some edge e in the graph). A deny policy whose deniedPermissions do not correspond to any modeled\
\ edge type suppresses nothing in the graph \u2014 do not emit. Honor exception principals: if ?principal\
\ is listed in exceptionPrincipals of the deny rule that would block it, that specific deny rule does\
\ NOT block ?principal's own capability edges (though ?principal can still modify the policy to unblock\
\ OTHER principals). Check effective scope: a project-scoped deny policy unblocks only that project's\
\ subtree; an org-scoped policy unblocks the entire org hierarchy. iam.denypolicies.update allows\
\ weakening (add exception, remove rules); .delete removes entirely \u2014 both grant the guardrail-removal\
\ capability, so use OR logic. Note that iam.denypolicies.create allows a principal to create a NEW\
\ deny policy that denies iam.denypolicies.update/.delete to all other admins except itself (a persistence/defense-evasion\
\ primitive); this is deliberately excluded from the graph model, so analysts should be aware of this\
\ gap. Do NOT fire when the only block is by a DIFFERENT deny policy (blocked_by must match THIS policy\
\ node). CRITICALLY: Do NOT emit an ACTIVE CanModifyPolicy when the principal's own modify permission\
\ (iam.denypolicies.update AND iam.denypolicies.delete) is BLOCKED by a SEPARATE deny policy \u2014\
\ emit BLOCKED instead. Note that iam.denypolicies.update and .delete are themselves deny-eligible\
\ permissions \u2014 a deny policy can recursively block manage of other deny policies. Circular deny\
\ chains (A denies B, B denies A) must be resolved to determine which is the root cause. Check for\
\ BLOCKED denial by a DIFFERENT deny policy (blocked_by != ?deny_policy) at ?scope; if found, downgrade\
\ this CanModifyPolicy to BLOCKED(deny_assignment), and let the recursive deny chain be resolved first."
narrative: '{principal.name} holds iam.denypolicies.update or iam.denypolicies.delete at {scope.name}
(via roles/iam.denyAdmin or equivalent) and can remove or weaken {deny_policy.name}, lifting the blocks
it imposes on {gated_edge_summary}. Once removed, those edges are upgraded from BLOCKED to CONDITIONAL(guardrail_removed)
by the generic guardrail-removal rule (hierarchy-chains.yaml). When the deny policy is attached at
an Organization or Folder scope, its removal unblocks edges across the ENTIRE descendant subtree;
the hierarchy-chains.yaml admin-inherits-down-contains rule (rule 1) performs the generic transitive
descent to all descendants.'