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.

derived gcp emits CanModifyPolicy

match

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

{'scope': None} HasPolicy {'deny_policy': None}

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 typeIdentity
target typeConditionalPolicy
source?principal
target?deny_policy
permissionsiam.denypolicies.update iam.denypolicies.delete
conditionsiam_permission deny_assignment
state logicACTIVE 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.'
move · open · esc close