azure-msgraph-app-readwrite-cred
A service principal holding Application.ReadWrite.All (or Directory.ReadWrite.All) can add a password or certificate credential to any app registration or service principal in the tenant and then authenticate as that identity, inheriting all its permissions - the identity-plane equivalent of sts:AssumeRole.
match
A conjunctive graph pattern. Variables (?x) bind node ids; every clause must hold.
where
MachineIdentity, ApplicationIdentity]
?approle is one of: Application.ReadWrite.All (id: 1bfefb4e-e0b5-418b-a88f-73c46d2cc8e9) OR Directory.ReadWrite.All (id: 19dbc75e-c2e2-444c-a770-ec69d8559fc7) on microsoft.graph SP
?target is any ApplicationIdentity or MachineIdentity in the same tenant (or another tenant if the SP has multi-tenant consent)
node_type(?target) in [ApplicationIdentity, MachineIdentity]
?sp != ?target # self-grant via addPassword is an existing credential operation, not an escalation
emit
| source type | Identity |
|---|---|
| target type | Identity |
| source | ?sp |
| target | ?target |
| permissions | Application.ReadWrite.All Directory.ReadWrite.All |
| conditions | iam_permission service_state |
| state logic | ACTIVE when the appRoleAssignment for Application.ReadWrite.All or Directory.ReadWrite.All is granted and the SP holds a valid credential. CONDITIONAL(service_state) if the SP credential is expired/revoked. BLOCKED if the target app/SP is protected by an Azure Entra Privileged Identity Management policy that prevents credential modification (rare; not currently modeled in Graph API responses — flag UNKNOWN when PIM-for-Applications is in scope). Inherit weakest from the HasRole input edge. Do NOT emit for Directory.ReadWrite.All without confirming that addPassword/addKey is within its effective scope for the target (Directory.ReadWrite.All is broad but some per-object operations still require Application.ReadWrite.All specifically — flag CONDITIONAL for the Directory.ReadWrite.All variant). |
Narrative
{sp.name} holds Application.ReadWrite.All and can add a client secret or certificate to {target.name} via POST /applications/{id}/addPassword (for app registrations) or POST /servicePrincipals/{id}/addPassword (for service principals), then authenticate as {target.name} and inherit all of its delegated permissions and role assignments - the identity-plane equivalent of assuming a role.
Raw rule rules/derived/azure/msgraph.yaml
id: azure-msgraph-app-readwrite-cred
emits: CanCreateCredentialFor
description: "A service principal holding Application.ReadWrite.All (or Directory.ReadWrite.All) can add\
\ a password or certificate credential to any app registration or service principal in the tenant and\
\ then authenticate as that identity, inheriting all its permissions \u2014 the identity-plane equivalent\
\ of sts:AssumeRole."
match:
- - sp: null
- HasRole
- approle: null
- - target: null
- HasRole
- target_role: null
where:
- node_type(?sp) in [MachineIdentity, ApplicationIdentity]
- '?approle is one of: Application.ReadWrite.All (id: 1bfefb4e-e0b5-418b-a88f-73c46d2cc8e9) OR Directory.ReadWrite.All
(id: 19dbc75e-c2e2-444c-a770-ec69d8559fc7) on microsoft.graph SP'
- ?target is any ApplicationIdentity or MachineIdentity in the same tenant (or another tenant if the SP
has multi-tenant consent)
- node_type(?target) in [ApplicationIdentity, MachineIdentity]
- '?sp != ?target # self-grant via addPassword is an existing credential operation, not an escalation'
emit:
source_type: Identity
target_type: Identity
source: ?sp
target: ?target
permissions:
- Application.ReadWrite.All
- Directory.ReadWrite.All
conditions:
- iam_permission
- service_state
state_logic: "ACTIVE when the appRoleAssignment for Application.ReadWrite.All or Directory.ReadWrite.All\
\ is granted and the SP holds a valid credential. CONDITIONAL(service_state) if the SP credential\
\ is expired/revoked. BLOCKED if the target app/SP is protected by an Azure Entra Privileged Identity\
\ Management policy that prevents credential modification (rare; not currently modeled in Graph API\
\ responses \u2014 flag UNKNOWN when PIM-for-Applications is in scope). Inherit weakest from the HasRole\
\ input edge. Do NOT emit for Directory.ReadWrite.All without confirming that addPassword/addKey is\
\ within its effective scope for the target (Directory.ReadWrite.All is broad but some per-object\
\ operations still require Application.ReadWrite.All specifically \u2014 flag CONDITIONAL for the\
\ Directory.ReadWrite.All variant)."
confidence: '0.95'
derived_from:
- <HasRole edge_id (appRoleAssignment Application.ReadWrite.All or Directory.ReadWrite.All on ?sp)>
- "azure/identity-escalation.yaml#azure-sp-add-credential (SP OWNER path \u2014 parallel; this rule\
\ covers the app-permission path)"
- "azure/identity-escalation.yaml#azure-app-add-credential (app OWNER path \u2014 parallel)"
false_positive_note: "Application.ReadWrite.All is a very broad permission; a holder can add credentials\
\ to EVERY app and SP in the tenant, not just low-privilege ones. Model each (sp, target) pair as\
\ a separate edge instance, noting that an attacker will choose the highest-privilege target. Do NOT\
\ emit for targets that are protected by Entra's role-separation principle (Global Admin / Privileged\
\ Role Admin apps may have additional protection in future Entra hardening \u2014 flag CONDITIONAL\
\ in hardened tenants). Requires a valid SP credential to call Graph. Dedupe on (CanCreateCredentialFor,\
\ ?sp, ?target) when both Application.ReadWrite.All AND Directory.ReadWrite.All are held \u2014 one\
\ edge suffices. Directory.ReadWrite.All may not cover addPassword on all object types \u2014 prefer\
\ the Application.ReadWrite.All appRoleId match for the highest-confidence emit. The rule targets\
\ both ApplicationIdentity (via /applications/{id}/addPassword) and MachineIdentity (via /servicePrincipals/{id}/addPassword);\
\ use Identity as target_type to express that both subtypes are valid."
narrative: "{sp.name} holds Application.ReadWrite.All and can add a client secret or certificate to\
\ {target.name} via POST /applications/{id}/addPassword (for app registrations) or POST /servicePrincipals/{id}/addPassword\
\ (for service principals), then authenticate as {target.name} and inherit all of its delegated permissions\
\ and role assignments \u2014 the identity-plane equivalent of assuming a role."