feat(entries): add Entra directory-role and AWS IAM privesc pairs - #63
Conversation
Closes the two cloud-IdP gaps from the corpus-review cycle. Both were asymmetries rather than absences: each is the direct analogue of a pair that already existed for the neighbouring platform. Entra privileged directory-role grant (T1098.003) — the twin of the covered Google Workspace super-admin pair. Entra was well covered on the app plane (consent-grant, device-code, sp-cred-backdoor) but had nothing on the directory-role plane. Blue keys on `Add member to role`, reads the role from the Role.DisplayName modified property rather than the top-level event, and covers the PIM `Add eligible member to role` variant — without which a standing backdoor stays invisible until it is activated. AWS IAM privilege escalation (T1098.003) — GCP had gcp-iam-policy-backdoor; AWS covered console, access-key, S3 and destroy but not the escalation. Red covers both shapes: the AttachUserPolicy self-grant and the iam:PassRole path that never touches the actor's own identity. Blue matches both, plus CreatePolicyVersion --set-as-default and PowerUserAccess, which the obvious AdministratorAccess-only query misses. Closes #62 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CzKmTKtvGgsa82MCxPPy5i
There was a problem hiding this comment.
Pull request overview
Adds two new cloud red↔blue pairs to close parity gaps identified in #62: Entra directory-role persistence and AWS IAM privilege-escalation paths, aligning coverage with existing Google Workspace and GCP analogues.
Changes:
- Add Entra privileged directory-role grant red entry and its Entra AuditLogs (KQL) detection pair (
T1098.003). - Add AWS IAM privilege escalation red entry and its CloudTrail (Splunk) detection pair (
T1098.003). - Document the additions under
[Unreleased]inCHANGELOG.md.
Reviewed changes
Copilot reviewed 5 out of 5 changed files in this pull request and generated 1 comment.
Show a summary per file
| File | Description |
|---|---|
| entries/red/entra-directory-role.md | New red entry describing Entra privileged directory-role assignment as tenant persistence. |
| entries/blue/entra-role-assign-audit.md | New blue entry with KQL over Entra AuditLogs for directory-role grants (incl. PIM eligibility). |
| entries/red/aws-iam-privesc-policy.md | New red entry describing AWS IAM privesc via AttachUserPolicy and PassRole→AssumeRole paths. |
| entries/blue/aws-iam-privesc-cloudtrail.md | New blue entry with Splunk CloudTrail query for IAM privesc-related activity. |
| CHANGELOG.md | Adds [Unreleased] changelog bullets documenting the two new pairs and rationale. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| index=aws sourcetype=aws:cloudtrail | ||
| eventName IN (AttachUserPolicy, AttachRolePolicy, AttachGroupPolicy, PutUserPolicy, CreatePolicyVersion) | ||
| | eval actor=coalesce('userIdentity.userName','userIdentity.arn','userIdentity.principalId') | ||
| | eval target=coalesce('requestParameters.userName','requestParameters.roleName','requestParameters.policyArn') | ||
| | where like('requestParameters.policyArn',"%AdministratorAccess%") | ||
| OR like('requestParameters.policyArn',"%PowerUserAccess%") | ||
| OR 'requestParameters.setAsDefault'="true" | ||
| OR actor=target | ||
| | table _time, eventName, actor, target, requestParameters.policyArn, sourceIPAddress, userAgent |
There was a problem hiding this comment.
Right, and this was the worst kind of gap to ship — a detection entry whose prose tells you to watch something the query can't see. Fixed in 9d74a02 with a second block, following the asrep-probing-4771 convention (primary query, prose lead-in, secondary query) so the renderer keeps both.
Writing it surfaced something better than the missing query. There is no PassRole CloudTrail event at all — iam:PassRole is an authorization check evaluated during the launching call, not an API call of its own. So the failure mode isn't just "this query misses that path": a defender who goes looking for an event by that name finds nothing and reasonably concludes there's nothing to detect. The passed role exists only as a request parameter on the RunInstances / CreateFunction / RegisterTaskDefinition that consumed it. The prose now states that outright, and the second query keys on requestParameters.iamInstanceProfile.arn / role / roleArn / taskRoleArn with a baseline-then-outlier shape rather than a fixed threshold.
Drift gate, pairing graph, shellcheck and htpx --preview all re-run green; CHANGELOG updated to match.
Generated by Claude Code
…tion The entry's prose described two escalation shapes but shipped only the self-grant query, so the PassRole half it told defenders to watch had nothing to run. Adds it as a second block, following the asrep-probing-4771 convention. The gap was worth more than a copy-paste: iam:PassRole is an authorization check on the launching call, NOT an API call, so there is no PassRole event to search for at all — the role appears only in the requestParameters of the RunInstances/CreateFunction that consumed it. A defender hunting an event by that name finds nothing and concludes they are covered. The prose now says so explicitly. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CzKmTKtvGgsa82MCxPPy5i
Closes #62.
Two new red↔blue pairs, both
T1098.003. The issue framed these as a prioritization call rather than a defect — nothing in the corpus is wrong — so this PR is that call being made: add them. Each is the direct analogue of a pair that already exists for the neighbouring platform, which is what made the holes visible in the first place.Entra privileged directory-role grant
entries/red/entra-directory-role.md↔entries/blue/entra-role-assign-audit.mdGoogle Workspace super-admin was covered end-to-end (
gws-super-admin↔gws-admin-audit) while the Entra twin was not. Entra was well covered on the app plane (consent-grant,device-code,sp-cred-backdoor) but had nothing on the directory-role plane.Two things I went beyond the issue's sketch on, because leaving them out would ship a detection with a hole in it:
Add member to role. But under PIM an eligible assignment writesAdd eligible member to role— a standing backdoor that stays dormant until activated. A tenant watching only the direct grant sees it at use time, not at plant time, which is the wrong half of the timeline. Both are matched.Role.DisplayName, a modified property on the target, not from the top-level event. The naive query returns every role grant in the tenant and gets tuned into uselessness.Red flags Privileged Authentication Administrator alongside GA/PRA: it can reset a Global Admin's credentials, so it's a GA grant one step removed, and it's the one most likely to be waved through on review. Blue says to treat it as equal severity.
AWS IAM privilege escalation
entries/red/aws-iam-privesc-policy.md↔entries/blue/aws-iam-privesc-cloudtrail.mdGCP had
gcp-iam-policy-backdoor; AWS covered console / access-key / S3 / destroy but not the escalation itself.The attack has two shapes and both are covered, because a detection for only the first is trivially evaded by the second:
AttachUserPolicyself-grant — actor equals target, or the policy is wildcard-admin. Blue also matchesCreatePolicyVersion --set-as-default, which is the same escalation wearing an update's clothes, andPowerUserAccessnext toAdministratorAccess, since an ARN-string match on the latter alone misses it.iam:PassRolepath, which never touches the actor's own IAM identity — pass a privileged role to an EC2 instance or Lambda you control and read the credentials out of IMDS or the function environment. Blue's guidance is to alert on a PassRole by a principal that doesn't normally launch compute, then pivot on whether the passed role's credentials show up from a new source IP.Checks
Full CI run locally, all green:
shellcheck -S warning+bash -non all four scripts./gen-views.sh --check— the drift gate (standalone, no flat-view targets present){{slots}}used; both are cloud entries and follow the existing<angle-bracket>convention, matchinggws-super-admin/gcp-iam-policy-backdoor./htpx --previewon both red entries, confirming each renders with its paired detectionSections (
Microsoft 365 / Entra ID,AWS / cloud IAM) and thedetection:backends (kql-entra-audit,splunk-cloudtrail) are existing values — no new taxonomy.Note for review
The detection queries are written from the documented shape of each audit schema, not validated against a live tenant or CloudTrail estate. The invariants they key on are the stable part; the field paths are where I'd want a second pair of eyes if you have a tenant to check them against. That's the same standing caveat as the rest of the corpus, but these two are query-heavy enough to be worth flagging.
This is
[Unreleased]only — no version cut, so no fan-out todotfiles-Kaliuntil a release is tagged.Generated by Claude Code