Session log: remediation skill rewrite (5-app tiered arch) + Cascades breach check John Trozzi
- Rewrote get-token.sh: tiered app system (investigator/exchange-op/user-manager/tenant-admin/defender) - Updated SKILL.md, command, gotchas, checklist, graph-endpoints for new app suite - Cascades breach check: mailbox clean, inbound phishing received by John, DMARC gap noted Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,15 +1,33 @@
|
||||
---
|
||||
description: M365 tenant investigation + remediation via the Claude-MSP-Access Graph API app. Breach checks, tenant sweeps, consent URLs, and gated remediation actions.
|
||||
description: M365 tenant investigation + remediation via the ComputerGuru tiered MSP app suite. Breach checks, tenant sweeps, consent URLs, and gated remediation actions.
|
||||
---
|
||||
|
||||
# /remediation-tool
|
||||
|
||||
M365 investigation and remediation using the **Claude-MSP-Access Graph API** multi-tenant app (App ID `fabb3421-8b34-484b-bc17-e46de9703418`, display name in customer tenants: **"ComputerGuru - AI Remediation"**).
|
||||
M365 investigation and remediation using the **ComputerGuru tiered MSP app suite** — five multi-tenant apps covering read-only investigation, Exchange write operations, user lifecycle management, high-privilege tenant admin, and optional Defender ATP.
|
||||
|
||||
**Default posture: READ-ONLY.** Remediation actions require explicit `YES` confirmation in chat.
|
||||
|
||||
---
|
||||
|
||||
## App Tiers (quick reference)
|
||||
|
||||
| Tier flag | App | App ID | Use for |
|
||||
|---|---|---|---|
|
||||
| `investigator` | ComputerGuru Security Investigator | `bfbc12a4` | All read-only breach checks via Graph |
|
||||
| `investigator-exo` | ComputerGuru Security Investigator | `bfbc12a4` | Exchange read: Get-InboxRule (hidden), Get-Mailbox, permissions |
|
||||
| `exchange-op` | ComputerGuru Exchange Operator | `b43e7342` | Exchange write: Set-Mailbox, Remove-InboxRule, session revoke |
|
||||
| `user-manager` | ComputerGuru User Manager | `64fac46b` | User create/disable, license assign, MFA reset, password reset |
|
||||
| `tenant-admin` | ComputerGuru Tenant Admin | `709e6eed` | App role assignments, CA policy, high-privilege directory |
|
||||
| `defender` | ComputerGuru Defender Add-on | `dbf8ad1a` | Alerts, machine risk, vuln data — MDE-licensed tenants only |
|
||||
|
||||
Pass the tier flag to `get-token.sh`:
|
||||
```bash
|
||||
bash .claude/skills/remediation-tool/scripts/get-token.sh <tenant-id> <tier>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Subcommands
|
||||
|
||||
| Form | What it does |
|
||||
@@ -17,8 +35,8 @@ M365 investigation and remediation using the **Claude-MSP-Access Graph API** mul
|
||||
| `/remediation-tool check <upn>` | 10-point breach check on a single user |
|
||||
| `/remediation-tool sweep <domain>` | Tenant-wide signals (sign-ins, audits, risky users, guests) |
|
||||
| `/remediation-tool signins <domain> [--user upn] [--failed-only] [--days N]` | Ad-hoc sign-in query |
|
||||
| `/remediation-tool consent-url <domain>` | Emit admin consent URL for a tenant |
|
||||
| `/remediation-tool remediate <upn> <action>` | **GATED:** password-reset, revoke-sessions, disable-forwarding, remove-inbox-rules, disable-account |
|
||||
| `/remediation-tool consent-url <domain> [--app <tier>]` | Emit admin consent URL for a tenant + app |
|
||||
| `/remediation-tool remediate <upn> <action>` | **GATED:** revoke-sessions, disable-forwarding, remove-inbox-rules, disable-account, password-reset |
|
||||
|
||||
`<domain>` accepts a tenant domain (`cascadestucson.com`), a UPN (`user@domain.com`), or a tenant GUID.
|
||||
|
||||
@@ -38,38 +56,93 @@ Run `bash .claude/skills/remediation-tool/scripts/resolve-tenant.sh <domain>`
|
||||
|
||||
### 2. Acquire tokens (cached)
|
||||
|
||||
Run `bash .claude/skills/remediation-tool/scripts/get-token.sh <tenant-id> graph` and `... exchange` as needed. Tokens cache at `/tmp/remediation-tool/{tenant}/{scope}.jwt` with 55-minute TTL. The script pulls the app secret from the SOPS vault (`msp-tools/claude-msp-access-graph-api.sops.yaml`, field `credentials.credential`).
|
||||
Use the minimum-privilege tier for the task. Most breach checks only need:
|
||||
```bash
|
||||
GT=$(bash .claude/skills/remediation-tool/scripts/get-token.sh <tenant-id> investigator)
|
||||
ET=$(bash .claude/skills/remediation-tool/scripts/get-token.sh <tenant-id> investigator-exo)
|
||||
```
|
||||
|
||||
If either token returns 403/401 on first use, check `.claude/skills/remediation-tool/references/gotchas.md` for the per-tenant prerequisites (directory roles, admin consent) and emit the remediation link to the user.
|
||||
Escalate to write tiers only for remediation:
|
||||
```bash
|
||||
# Exchange write (disable-forwarding, remove-inbox-rules)
|
||||
EXO_WRITE=$(bash .claude/skills/remediation-tool/scripts/get-token.sh <tenant-id> exchange-op)
|
||||
|
||||
# User write (revoke-sessions, disable-account, password-reset, MFA reset)
|
||||
UT=$(bash .claude/skills/remediation-tool/scripts/get-token.sh <tenant-id> user-manager)
|
||||
|
||||
# Defender (MDE tenants only)
|
||||
DT=$(bash .claude/skills/remediation-tool/scripts/get-token.sh <tenant-id> defender)
|
||||
```
|
||||
|
||||
Tokens cache at `/tmp/remediation-tool/{tenant}/{tier}.jwt` with 55-minute TTL.
|
||||
|
||||
If a token returns 403/401 on first use, check `.claude/skills/remediation-tool/references/gotchas.md` for per-tenant prerequisites and emit the appropriate consent or role-assignment link.
|
||||
|
||||
### 3. Run the requested checks
|
||||
|
||||
- **`check <upn>`** -> `bash scripts/user-breach-check.sh <tenant> <upn>`. Script runs all 10 checks in parallel where possible and dumps raw JSON to `/tmp/remediation-tool/{tenant}/user-breach/<slug>/`. Claude then interprets findings against the rubric in `references/checklist.md` and writes a report.
|
||||
- **`check <upn>`** -> `bash scripts/user-breach-check.sh <tenant> <upn>`. Runs all 10 checks and dumps raw JSON to `/tmp/remediation-tool/{tenant}/user-breach/<slug>/`. Interpret against `references/checklist.md` and write report.
|
||||
|
||||
- **`sweep <domain>`** -> `bash scripts/tenant-sweep.sh <tenant>`. Script pulls tenant-wide failed sign-ins (30d), successful non-US sign-ins, directory audits filtered for consent/auth-method/service-principal changes, risky users (if permission granted), B2B guest invites, user location profile. Claude summarizes priority findings.
|
||||
- **`sweep <domain>`** -> `bash scripts/tenant-sweep.sh <tenant>`. Pulls tenant-wide failed sign-ins (30d), successful non-US sign-ins, directory audits filtered for consent/auth-method/service-principal changes, risky users, B2B guest invites. Claude summarizes priority findings.
|
||||
|
||||
- **`signins`** — build ad-hoc `curl` against Graph `/auditLogs/signIns` with the requested filter.
|
||||
- **`signins`** — build ad-hoc `curl` against Graph `/auditLogs/signIns` with the requested filter. Use `investigator` tier.
|
||||
|
||||
- **`consent-url <domain>`** — emit `https://login.microsoftonline.com/{tenant-id}/adminconsent?client_id=fabb3421-8b34-484b-bc17-e46de9703418&redirect_uri=https://login.microsoftonline.com/common/oauth2/nativeclient` plus the note that the nativeclient landing page "looks like an error, that's normal."
|
||||
- **`consent-url <domain> [--app <tier>]`** — emit the appropriate admin consent URL (see below). Default to Security Investigator (`investigator`) unless `--app` specifies another tier.
|
||||
|
||||
- **`remediate`** — see Remediation section below.
|
||||
|
||||
### 4. Write the report
|
||||
|
||||
Location: `clients/{client-slug}/reports/YYYY-MM-DD-{action}.md` (UTC date). Derive the client slug from the domain:
|
||||
Location: `clients/{client-slug}/reports/YYYY-MM-DD-{action}.md` (UTC date). Derive client slug from domain:
|
||||
- `cascadestucson.com` -> `cascades-tucson`
|
||||
- `foobarwidgets.com` -> `foobar-widgets`
|
||||
- Use existing `clients/<slug>/` directory if present; if no matching client dir exists, ask the user for the slug before creating one.
|
||||
- `grabblaw.com` -> `grabblaw`
|
||||
- Use existing `clients/<slug>/` directory if present; if no match, ask the user for the slug.
|
||||
|
||||
Use `templates/breach-report.md` as the skeleton. For single-user checks, fill in per-check findings using raw JSON in `/tmp/remediation-tool/{tenant}/user-breach/<slug>/`.
|
||||
Use `templates/breach-report.md` as skeleton. For single-user checks, fill per-check findings from raw JSON.
|
||||
|
||||
### 5. Summarize to the user
|
||||
|
||||
Short chat summary: top findings, blocked checks (with remediation links), next actions. Save raw JSON artifacts paths in the report for later re-analysis.
|
||||
Short chat summary: top findings, blocked checks (with remediation links), next actions. Save raw JSON artifact paths in the report.
|
||||
|
||||
### 6. Auto-commit
|
||||
|
||||
After writing the report, delegate to the **Gitea Agent** to commit with a message like `Remediation report: <action> for <target>`. Do not push unless the user asks.
|
||||
After writing the report, delegate to the **Gitea Agent** to commit with `Remediation report: <action> for <target>`. Do not push unless the user asks.
|
||||
|
||||
---
|
||||
|
||||
## Admin Consent URLs
|
||||
|
||||
Each app must be individually consented in each customer tenant. Consent URL format:
|
||||
|
||||
```
|
||||
https://login.microsoftonline.com/{tenant-id}/adminconsent?client_id={app-id}&redirect_uri=https://azcomputerguru.com&prompt=consent
|
||||
```
|
||||
|
||||
**Security Investigator** (read-only — consent this first):
|
||||
```
|
||||
https://login.microsoftonline.com/{tenant-id}/adminconsent?client_id=bfbc12a4-f0dd-4e12-b06d-997e7271e10c&redirect_uri=https://azcomputerguru.com&prompt=consent
|
||||
```
|
||||
|
||||
**Exchange Operator** (EXO write — consent when remediation needed):
|
||||
```
|
||||
https://login.microsoftonline.com/{tenant-id}/adminconsent?client_id=b43e7342-5b4b-492f-890f-bb5a4f7f40e9&redirect_uri=https://azcomputerguru.com&prompt=consent
|
||||
```
|
||||
|
||||
**User Manager** (user/license write):
|
||||
```
|
||||
https://login.microsoftonline.com/{tenant-id}/adminconsent?client_id=64fac46b-8b44-41ad-93ee-7da03927576c&redirect_uri=https://azcomputerguru.com&prompt=consent
|
||||
```
|
||||
|
||||
**Tenant Admin** (high-privilege — use sparingly):
|
||||
```
|
||||
https://login.microsoftonline.com/{tenant-id}/adminconsent?client_id=709e6eed-0711-4875-9c44-2d3518c47063&redirect_uri=https://azcomputerguru.com&prompt=consent
|
||||
```
|
||||
|
||||
**Defender Add-on** (MDE-licensed tenants only):
|
||||
```
|
||||
https://login.microsoftonline.com/{tenant-id}/adminconsent?client_id=dbf8ad1a-54f4-4bb8-8a9e-ea5b9634635b&redirect_uri=https://azcomputerguru.com&prompt=consent
|
||||
```
|
||||
|
||||
The customer admin must sign in as Global Admin of that tenant and click Accept. Redirect lands on azcomputerguru.com — that is expected. Verify consent via `/servicePrincipals/{sp-id}/appRoleAssignments` (new grants should be timestamped today).
|
||||
|
||||
---
|
||||
|
||||
@@ -77,21 +150,22 @@ After writing the report, delegate to the **Gitea Agent** to commit with a messa
|
||||
|
||||
When the user runs `/remediation-tool remediate <upn> <action>`:
|
||||
|
||||
1. **Confirm read-only context first**: the skill must have recently run a `check <upn>` in this session (check `/tmp/remediation-tool/{tenant}/user-breach/<slug>/` exists). If not, tell the user to run the check first.
|
||||
2. **Display the exact action** that will run (curl command, cmdlet name, parameters).
|
||||
3. **Require explicit `YES` in chat** — not approval via permission prompt. If the user types anything else, abort.
|
||||
4. Execute via Graph/Exchange REST. Capture response to a remediation log at `/tmp/remediation-tool/{tenant}/remediation/<slug>-YYYY-MM-DDTHHMMSS.json`.
|
||||
5. Update the user's report with a `## Remediation Actions` section appending what was done and the result.
|
||||
1. **Confirm read-only context first**: skill must have recently run `check <upn>` in this session (check `/tmp/remediation-tool/{tenant}/user-breach/<slug>/` exists). If not, tell the user to run the check first.
|
||||
2. **Display the exact action** (curl command, cmdlet name, parameters).
|
||||
3. **Require explicit `YES` in chat** — not a permission prompt. Anything else aborts.
|
||||
4. Execute via the appropriate app tier. Capture response to `/tmp/remediation-tool/{tenant}/remediation/<slug>-YYYY-MM-DDTHHMMSS.json`.
|
||||
5. Update the user's report with a `## Remediation Actions` section.
|
||||
|
||||
Allowed `<action>` values:
|
||||
Allowed actions and which tier handles them:
|
||||
|
||||
| Action | API | Result |
|
||||
| Action | App tier | API |
|
||||
|---|---|---|
|
||||
| `password-reset` | Graph `PATCH /users/{upn}` with new `passwordProfile` | Forces sign-in; revokes refresh tokens |
|
||||
| `revoke-sessions` | Graph `POST /users/{upn}/revokeSignInSessions` | Kills all active sessions |
|
||||
| `disable-forwarding` | Exchange REST `Set-Mailbox -ForwardingAddress $null -ForwardingSmtpAddress $null -DeliverToMailboxAndForward $false` | Clears all forwarding |
|
||||
| `remove-inbox-rules` | Exchange REST `Remove-InboxRule` for each non-default rule | Asks which to keep first |
|
||||
| `disable-account` | Graph `PATCH /users/{upn}` with `accountEnabled: false` | Hard disable |
|
||||
| `revoke-sessions` | `user-manager` | Graph `POST /users/{upn}/revokeSignInSessions` |
|
||||
| `disable-account` | `user-manager` | Graph `PATCH /users/{upn}` with `accountEnabled: false` |
|
||||
| `password-reset` | `user-manager` | Graph `PATCH /users/{upn}` with new `passwordProfile` |
|
||||
| `disable-forwarding` | `exchange-op` | Exchange REST `Set-Mailbox -ForwardingAddress $null -ForwardingSmtpAddress $null -DeliverToMailboxAndForward $false` |
|
||||
| `remove-inbox-rules` | `exchange-op` | Exchange REST `Remove-InboxRule` per non-default rule (ask which to keep first) |
|
||||
| `disable-smtp-auth` | `exchange-op` | Exchange REST `Set-CASMailbox -SmtpClientAuthenticationDisabled $true` |
|
||||
|
||||
---
|
||||
|
||||
@@ -103,7 +177,8 @@ Allowed `<action>` values:
|
||||
- `sweep cascadestucson.com`
|
||||
- `signins cascadestucson.com --user megan.hiatt@cascadestucson.com --failed-only --days 30`
|
||||
- `consent-url cascadestucson.com`
|
||||
- `remediate megan.hiatt@cascadestucson.com password-reset`
|
||||
- `consent-url grabblaw.com --app exchange-op`
|
||||
- `remediate megan.hiatt@cascadestucson.com revoke-sessions`
|
||||
|
||||
If the user's phrasing is loose ("check john's box at cascades", "who's being attacked"), infer intent from CONTEXT.md and session logs. Prefer asking one clarifying question to guessing.
|
||||
|
||||
@@ -115,4 +190,3 @@ If the user's phrasing is loose ("check john's box at cascades", "who's being at
|
||||
- Permission/role gotchas + consent URLs: `.claude/skills/remediation-tool/references/gotchas.md`
|
||||
- Endpoint cheatsheet: `.claude/skills/remediation-tool/references/graph-endpoints.md`
|
||||
- Report template: `.claude/skills/remediation-tool/templates/breach-report.md`
|
||||
- Memory note on what the tool IS: `.claude/memory/feedback_365_remediation_tool.md`
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
---
|
||||
name: remediation-tool
|
||||
description: |
|
||||
M365 tenant investigation and remediation using the Claude-MSP-Access Graph API app (App ID fabb3421-8b34-484b-bc17-e46de9703418, known as "ComputerGuru - AI Remediation" in customer tenants). Auto-invoke when the user says "remediation tool", "365 remediation", "check <user>'s mailbox/box", "credential stuffing" against an M365 user, "breach check" on an M365 tenant, or needs M365 admin API work that client-credentials Graph + Exchange REST can perform. NOT for CIPP — this is the direct Graph API app.
|
||||
M365 tenant investigation and remediation using the ComputerGuru tiered MSP app suite (5 apps: Security Investigator, Exchange Operator, User Manager, Tenant Admin, Defender Add-on). Auto-invoke when the user says "remediation tool", "365 remediation", "check <user>'s mailbox/box", "credential stuffing" against an M365 user, "breach check" on an M365 tenant, or needs M365 admin API work that client-credentials Graph + Exchange REST can perform. NOT for CIPP — this is the direct Graph API app suite.
|
||||
|
||||
Also invoke when the user needs any of: inbox rule enumeration, mailbox forwarding check, delegate/SendAs audit, OAuth consent audit, sign-in log queries, risky user lookup, directory audit queries, B2B guest invite audit against M365.
|
||||
|
||||
@@ -12,13 +12,28 @@ description: |
|
||||
|
||||
Read-only by default. All remediation actions require explicit `YES` confirmation in chat (not a permission prompt).
|
||||
|
||||
## App Architecture (Tiered)
|
||||
|
||||
Five multi-tenant apps cover distinct privilege tiers. Use only what the task requires.
|
||||
|
||||
| Tier | App display name | App ID | Vault file | Scope |
|
||||
|---|---|---|---|---|
|
||||
| `investigator` | ComputerGuru Security Investigator | `bfbc12a4-f0dd-4e12-b06d-997e7271e10c` | `computerguru-security-investigator.sops.yaml` | Graph read-only |
|
||||
| `investigator-exo` | ComputerGuru Security Investigator | `bfbc12a4-f0dd-4e12-b06d-997e7271e10c` | `computerguru-security-investigator.sops.yaml` | Exchange Online read |
|
||||
| `exchange-op` | ComputerGuru Exchange Operator | `b43e7342-5b4b-492f-890f-bb5a4f7f40e9` | `computerguru-exchange-operator.sops.yaml` | Exchange Online write |
|
||||
| `user-manager` | ComputerGuru User Manager | `64fac46b-8b44-41ad-93ee-7da03927576c` | `computerguru-user-manager.sops.yaml` | Graph user/group write |
|
||||
| `tenant-admin` | ComputerGuru Tenant Admin | `709e6eed-0711-4875-9c44-2d3518c47063` | `computerguru-tenant-admin.sops.yaml` | Graph high-privilege |
|
||||
| `defender` | ComputerGuru Defender Add-on | `dbf8ad1a-54f4-4bb8-8a9e-ea5b9634635b` | `computerguru-defender-addon.sops.yaml` | Defender ATP (MDE only) |
|
||||
|
||||
**Default for breach checks:** use `investigator` (Graph) + `investigator-exo` (Exchange read). Escalate to write tiers only when remediating.
|
||||
|
||||
## Auto-Invocation Behavior
|
||||
|
||||
When triggered automatically (vs. via `/remediation-tool`), follow the same workflow described in `.claude/commands/remediation-tool.md`:
|
||||
When triggered automatically (vs. via `/remediation-tool`), follow the same workflow in `.claude/commands/remediation-tool.md`:
|
||||
|
||||
1. Parse the user's intent into a subcommand (check/sweep/signins/consent-url/remediate).
|
||||
2. Resolve tenant ID from domain.
|
||||
3. Acquire tokens (cached).
|
||||
3. Acquire tokens via `get-token.sh <tenant> <tier>` — use lowest-privilege tier needed.
|
||||
4. Run checks via scripts in `scripts/`.
|
||||
5. Interpret findings using `references/checklist.md`.
|
||||
6. Write report to `clients/{slug}/reports/YYYY-MM-DD-{action}.md` using `templates/breach-report.md`.
|
||||
@@ -28,13 +43,15 @@ When triggered automatically (vs. via `/remediation-tool`), follow the same work
|
||||
|
||||
- The SOPS vault is accessible: `test -f D:/vault/scripts/vault.sh` (Windows) or `test -f ~/vault/scripts/vault.sh` (other).
|
||||
- `jq`, `curl`, `bash` are available.
|
||||
- For Exchange REST checks: confirm the target tenant has **Exchange Administrator** role assigned to the app's service principal (display name "ComputerGuru - AI Remediation"). If any Exchange REST call returns 403, emit the tenant-scoped Entra Roles link from `references/gotchas.md`.
|
||||
- For Identity Protection checks: app manifest must include `IdentityRiskyUser.Read.All` or `.ReadWrite.All`, AND the tenant must have admin-consented after that permission was added. If 403, emit the consent URL.
|
||||
- For Exchange REST checks: confirm the target tenant has **Exchange Administrator** role assigned to the **Security Investigator** SP (for reads) or **Exchange Operator** SP (for writes). If any Exchange REST call returns 403, emit the tenant-scoped Entra Roles link from `references/gotchas.md`.
|
||||
- For Identity Protection checks: `IdentityRiskyUser.Read.All` is in the Security Investigator manifest AND the tenant has consented to that app. If 403, emit the per-app consent URL from `references/gotchas.md`.
|
||||
- For Defender checks: confirm tenant has Microsoft Defender for Endpoint (MDE) license before using `defender` tier — it returns AADSTS650052 otherwise.
|
||||
|
||||
## Conventions
|
||||
|
||||
- **Target identifiers**: accept UPN, domain, or tenant GUID. Normalize to tenant GUID internally.
|
||||
- **Token cache**: `/tmp/remediation-tool/{tenant-id}/{scope}.jwt`. TTL 55 minutes. Check `-mmin -55` before reuse.
|
||||
- **Token tiers**: minimum necessary privilege. Never use `tenant-admin` for a read-only check.
|
||||
- **Token cache**: `/tmp/remediation-tool/{tenant-id}/{tier}.jwt`. TTL 55 minutes. Check `-mmin -55` before reuse.
|
||||
- **Raw JSON artifacts**: `/tmp/remediation-tool/{tenant-id}/{check}/` — keep so the user can re-analyze.
|
||||
- **Reports**: `clients/{slug}/reports/YYYY-MM-DD-{action}.md`. Derive slug from domain (strip TLD, hyphenate).
|
||||
- **UTC dates everywhere**.
|
||||
@@ -43,4 +60,5 @@ When triggered automatically (vs. via `/remediation-tool`), follow the same work
|
||||
|
||||
- **Not a replacement for CIPP.** Use CIPP for bulk baseline configuration, templates, standards alerting. Use this tool for focused investigation and point-in-time remediation.
|
||||
- **Not for creating/modifying Entra apps or Conditional Access policies.** Those are sensitive enough to stay manual in the portal.
|
||||
- **Not for Graph permissions the app doesn't have.** If a call 403s and the scope isn't in the app manifest, stop and tell the user — don't try to work around it.
|
||||
- **Not for Graph permissions the apps don't have.** If a call 403s and the scope isn't in the relevant app's manifest, stop and tell the user — don't try to work around it.
|
||||
- **Defender tier requires MDE license.** If the tenant doesn't have MDE, the token request succeeds but API calls return AADSTS650052. Check before using.
|
||||
|
||||
@@ -1,48 +1,48 @@
|
||||
# Breach-Check Rubric
|
||||
|
||||
How to interpret the outputs from `user-breach-check.sh` and `tenant-sweep.sh`.
|
||||
|
||||
## Single-user check — the 10 points
|
||||
|
||||
| # | Check | What "clean" looks like | Red flags |
|
||||
|---|---|---|---|
|
||||
| 1 | Inbox rules (Graph) | Empty, or only benign filters | ForwardTo / RedirectTo / ForwardAsAttachmentTo set; DeleteMessage+MarkAsRead combos; rules filtered on "password", "bank", "invoice", "CEO name", "security"; rules with name like "." or " " (attacker hiding) |
|
||||
| 2 | Mailbox settings / auto-reply | Auto-reply disabled or legitimate | Auto-reply active with external audience + unfamiliar message body |
|
||||
| 3 | Exchange REST (hidden rules, delegates, SendAs, Get-Mailbox forwarding fields) | Only SELF in permissions; no forwarding | **Hidden** inbox rule moving to RSS/Notes/Conversation History; non-SELF FullAccess/SendAs; ForwardingAddress or ForwardingSmtpAddress set to external |
|
||||
| 4 | OAuth consents + app role assignments | Legitimate apps only (Teams, Outlook mobile, BlueMail, etc.); dates match user history | New consent in attack window; unknown app with `Mail.ReadWrite`, `Files.ReadWrite`, `offline_access`; publisher not verified |
|
||||
| 5 | Auth methods | All methods predate the attack window | New phone/Authenticator registered within hours of first suspicious sign-in; duplicate entries with the same device name but different createdDateTime |
|
||||
| 6 | Sign-ins 30d | Consistent US IPs, user's known geography | Any successful sign-in from a country the user never visits; IMAP/POP/Authenticated SMTP client apps (legacy auth); sign-ins from TOR exit nodes or known residential-proxy ranges |
|
||||
| 7 | Directory audits | Only legit admin/system actions | `Update user` by non-admin principal; password reset the user didn't initiate; auth method change from `Microsoft Substrate Management` is normal but repeated changes are not |
|
||||
| 8 | Risky users / risk detections | `riskLevel: none` | Any `medium` or `high`; `riskDetail: userPerformedSecuredPasswordChange` just means resolved — check the original detection |
|
||||
| 9 | Sent items (recent 25) | Normal business correspondence | Blast emails to random external recipients; forwards of internal financial/HR info externally; anything after-hours from an unusual client app |
|
||||
| 10 | Deleted items (recent 25) | Marketing/spam, routine notifications | Deleted security alerts, password-reset emails, MFA notifications, bounce notices the user wouldn't delete — all signs of attacker cleanup |
|
||||
|
||||
### Cross-check rule
|
||||
|
||||
If inbox rules and forwarding are clean **but** sign-ins show successful foreign access — attacker may have used OAuth-based access (check OAuth grants) or already extracted data and cleaned up. Pull sent items + deleted items aggressively and check `/auditLogs/signIns/beta` for non-interactive sign-ins.
|
||||
|
||||
## Tenant-wide sweep — priorities
|
||||
|
||||
| Priority | Signal | Action |
|
||||
|---|---|---|
|
||||
| P1 | User with ≥20 failed sign-ins from ≥2 foreign countries | Likely active credential-stuffing target. Reset password, disable SMTP AUTH, monitor. |
|
||||
| P1 | Successful sign-in from non-US | Verify with user immediately. If not them: force password reset + revoke sessions + full breach check. |
|
||||
| P2 | New OAuth consent to unfamiliar app in attack window | Review app publisher, scopes, and requesting user. Revoke if unknown. |
|
||||
| P2 | B2B guest invite to personal email domain (gmail.com, outlook.com, yahoo.com) | Confirm with inviter it's intentional. Guest invites are a known persistence mechanism. |
|
||||
| P3 | Transport rule created/modified by a non-admin | Transport rules can redirect mail tenant-wide. Review body/actions carefully. |
|
||||
| P3 | Service principal added by non-admin or by "PowerApps Service" unexpectedly | Usually benign, but worth noting. |
|
||||
| P4 | Isolated wrong-password attempt from foreign IP | Record and move on. Single attempts are noise unless repeated. |
|
||||
|
||||
## False positives to filter out
|
||||
|
||||
- `sysadmin@<tenant>` failures during onboarding of the remediation tool itself (error 65001 against app **ComputerGuru - AI Remediation**).
|
||||
- `Microsoft Substrate Management` and `Azure MFA StrongAuthenticationService` routinely update user records — those are not attacker activity.
|
||||
- Our own consent attempts show up as `Consent to application` in directory audits. Filter `sysadmin` + target = ComputerGuru-AI-Remediation during the onboarding window.
|
||||
- `error 50140` "Keep me signed in interrupt" is a browser prompt, not a failed auth.
|
||||
|
||||
## When to escalate beyond this tool
|
||||
|
||||
- Data exfiltration suspected -> pull Unified Audit Log via Purview (this tool does not access UAL).
|
||||
- Tenant-wide phishing campaign -> enable Purview Content Search, quarantine messages.
|
||||
- Domain-joined workstation compromise -> GuruRMM + Bitdefender workflow (see `clients/ace-portables/reports/` for past example).
|
||||
- Attacker still active and exfiltrating -> consider disabling the user via the `remediate` subcommand and rotating the mailbox password at the same time.
|
||||
# Breach-Check Rubric
|
||||
|
||||
How to interpret the outputs from `user-breach-check.sh` and `tenant-sweep.sh`.
|
||||
|
||||
## Single-user check — the 10 points
|
||||
|
||||
| # | Check | What "clean" looks like | Red flags |
|
||||
|---|---|---|---|
|
||||
| 1 | Inbox rules (Graph) | Empty, or only benign filters | ForwardTo / RedirectTo / ForwardAsAttachmentTo set; DeleteMessage+MarkAsRead combos; rules filtered on "password", "bank", "invoice", "CEO name", "security"; rules with name like "." or " " (attacker hiding) |
|
||||
| 2 | Mailbox settings / auto-reply | Auto-reply disabled or legitimate | Auto-reply active with external audience + unfamiliar message body |
|
||||
| 3 | Exchange REST (hidden rules, delegates, SendAs, Get-Mailbox forwarding fields) | Only SELF in permissions; no forwarding | **Hidden** inbox rule moving to RSS/Notes/Conversation History; non-SELF FullAccess/SendAs; ForwardingAddress or ForwardingSmtpAddress set to external |
|
||||
| 4 | OAuth consents + app role assignments | Legitimate apps only (Teams, Outlook mobile, BlueMail, etc.); dates match user history | New consent in attack window; unknown app with `Mail.ReadWrite`, `Files.ReadWrite`, `offline_access`; publisher not verified |
|
||||
| 5 | Auth methods | All methods predate the attack window | New phone/Authenticator registered within hours of first suspicious sign-in; duplicate entries with the same device name but different createdDateTime |
|
||||
| 6 | Sign-ins 30d | Consistent US IPs, user's known geography | Any successful sign-in from a country the user never visits; IMAP/POP/Authenticated SMTP client apps (legacy auth); sign-ins from TOR exit nodes or known residential-proxy ranges |
|
||||
| 7 | Directory audits | Only legit admin/system actions | `Update user` by non-admin principal; password reset the user didn't initiate; auth method change from `Microsoft Substrate Management` is normal but repeated changes are not |
|
||||
| 8 | Risky users / risk detections | `riskLevel: none` | Any `medium` or `high`; `riskDetail: userPerformedSecuredPasswordChange` just means resolved — check the original detection |
|
||||
| 9 | Sent items (recent 25) | Normal business correspondence | Blast emails to random external recipients; forwards of internal financial/HR info externally; anything after-hours from an unusual client app |
|
||||
| 10 | Deleted items (recent 25) | Marketing/spam, routine notifications | Deleted security alerts, password-reset emails, MFA notifications, bounce notices the user wouldn't delete — all signs of attacker cleanup |
|
||||
|
||||
### Cross-check rule
|
||||
|
||||
If inbox rules and forwarding are clean **but** sign-ins show successful foreign access — attacker may have used OAuth-based access (check OAuth grants) or already extracted data and cleaned up. Pull sent items + deleted items aggressively and check `/auditLogs/signIns/beta` for non-interactive sign-ins.
|
||||
|
||||
## Tenant-wide sweep — priorities
|
||||
|
||||
| Priority | Signal | Action |
|
||||
|---|---|---|
|
||||
| P1 | User with ≥20 failed sign-ins from ≥2 foreign countries | Likely active credential-stuffing target. Reset password, disable SMTP AUTH, monitor. |
|
||||
| P1 | Successful sign-in from non-US | Verify with user immediately. If not them: force password reset + revoke sessions + full breach check. |
|
||||
| P2 | New OAuth consent to unfamiliar app in attack window | Review app publisher, scopes, and requesting user. Revoke if unknown. |
|
||||
| P2 | B2B guest invite to personal email domain (gmail.com, outlook.com, yahoo.com) | Confirm with inviter it's intentional. Guest invites are a known persistence mechanism. |
|
||||
| P3 | Transport rule created/modified by a non-admin | Transport rules can redirect mail tenant-wide. Review body/actions carefully. |
|
||||
| P3 | Service principal added by non-admin or by "PowerApps Service" unexpectedly | Usually benign, but worth noting. |
|
||||
| P4 | Isolated wrong-password attempt from foreign IP | Record and move on. Single attempts are noise unless repeated. |
|
||||
|
||||
## False positives to filter out
|
||||
|
||||
- `sysadmin@<tenant>` failures during onboarding (error 65001 against any **ComputerGuru** app — Security Investigator, Exchange Operator, User Manager, Tenant Admin, or Defender Add-on).
|
||||
- `Microsoft Substrate Management` and `Azure MFA StrongAuthenticationService` routinely update user records — those are not attacker activity.
|
||||
- Our own consent attempts show up as `Consent to application` in directory audits. Filter `sysadmin` + target matching any "ComputerGuru" app display name during the onboarding window.
|
||||
- `error 50140` "Keep me signed in interrupt" is a browser prompt, not a failed auth.
|
||||
|
||||
## When to escalate beyond this tool
|
||||
|
||||
- Data exfiltration suspected -> pull Unified Audit Log via Purview (this tool does not access UAL).
|
||||
- Tenant-wide phishing campaign -> enable Purview Content Search, quarantine messages.
|
||||
- Domain-joined workstation compromise -> GuruRMM + Bitdefender workflow (see `clients/ace-portables/reports/` for past example).
|
||||
- Attacker still active and exfiltrating -> consider disabling the user via the `remediate` subcommand and rotating the mailbox password at the same time.
|
||||
|
||||
@@ -1,77 +1,113 @@
|
||||
# Gotchas — Permissions, Roles, Consent
|
||||
|
||||
## App identity
|
||||
## App Suite (tiered architecture)
|
||||
|
||||
- **App ID (client_id):** `fabb3421-8b34-484b-bc17-e46de9703418`
|
||||
- **Internal name (home tenant / registration):** Claude-MSP-Access
|
||||
- **Display name in customer tenants:** **ComputerGuru - AI Remediation**
|
||||
- **Client secret:** SOPS vault `msp-tools/claude-msp-access-graph-api.sops.yaml` -> field `credentials.credential`
|
||||
Five multi-tenant apps replace the old single over-permissioned app. Use minimum necessary tier.
|
||||
|
||||
When searching customer admin portals for the service principal (role assignments, app role assignments, conditional access exclusions), **search for "ComputerGuru - AI Remediation"** — not "Claude-MSP-Access".
|
||||
| Tier | Display name in customer tenant | App ID | Vault file |
|
||||
|---|---|---|---|
|
||||
| `investigator` / `investigator-exo` | ComputerGuru Security Investigator | `bfbc12a4-f0dd-4e12-b06d-997e7271e10c` | `computerguru-security-investigator.sops.yaml` |
|
||||
| `exchange-op` | ComputerGuru Exchange Operator | `b43e7342-5b4b-492f-890f-bb5a4f7f40e9` | `computerguru-exchange-operator.sops.yaml` |
|
||||
| `user-manager` | ComputerGuru User Manager | `64fac46b-8b44-41ad-93ee-7da03927576c` | `computerguru-user-manager.sops.yaml` |
|
||||
| `tenant-admin` | ComputerGuru Tenant Admin | `709e6eed-0711-4875-9c44-2d3518c47063` | `computerguru-tenant-admin.sops.yaml` |
|
||||
| `defender` | ComputerGuru Defender Add-on | `dbf8ad1a-54f4-4bb8-8a9e-ea5b9634635b` | `computerguru-defender-addon.sops.yaml` |
|
||||
|
||||
**Deprecated (do not use):** ~~ComputerGuru - AI Remediation~~ (`fabb3421`) — old single-app with 159 permissions including Defender ATP. Broke consent on tenants without MDE license. Retire/delete from portal when confirmed no active tenants depend on it.
|
||||
|
||||
When searching customer admin portals for a service principal (role assignments, app role assignments, CA exclusions), search by the display name for that tier (e.g., "ComputerGuru Security Investigator").
|
||||
|
||||
## Per-tenant prerequisites
|
||||
|
||||
Graph API permissions alone are not enough. Most privileged operations require directory roles on the service principal *in that tenant*:
|
||||
Graph API permissions alone are not enough. Most privileged operations require directory roles on the specific service principal *in that tenant*:
|
||||
|
||||
| Operation | Required directory role |
|
||||
|---|---|
|
||||
| Password reset, user property updates | User Administrator |
|
||||
| Exchange REST (hidden inbox rules, mailbox permissions, SendAs, transport rules, Get-Mailbox) | Exchange Administrator |
|
||||
| Conditional Access policy reads/writes | Conditional Access Administrator OR Security Administrator |
|
||||
| Teams policies | Teams Administrator |
|
||||
| Operation | App tier | Required directory role on that SP |
|
||||
|---|---|---|
|
||||
| Exchange REST read (Get-InboxRule, Get-Mailbox) | `investigator-exo` | Exchange Administrator |
|
||||
| Exchange REST write (Set-Mailbox, Remove-InboxRule) | `exchange-op` | Exchange Administrator |
|
||||
| Password reset, user property updates | `user-manager` | User Administrator |
|
||||
| MFA method reset | `user-manager` | Authentication Administrator |
|
||||
| Conditional Access reads/writes | `tenant-admin` | Conditional Access Administrator OR Security Administrator |
|
||||
| Teams policies | `tenant-admin` | Teams Administrator |
|
||||
|
||||
### How to assign a role to the SP in a customer tenant
|
||||
### How to assign a role to an SP in a customer tenant
|
||||
|
||||
1. Sign into the customer's Entra admin center as Global Admin:
|
||||
`https://entra.microsoft.com/#@{customer-domain}`
|
||||
2. Identity -> Roles & admins -> All roles -> select the role (e.g., Exchange Administrator).
|
||||
3. Add assignments -> search **"ComputerGuru - AI Remediation"** -> Assign (Active, permanent — service principals cannot activate eligible assignments).
|
||||
`https://entra.microsoft.com/#@{customer-domain}`
|
||||
2. Identity -> Roles & admins -> All roles -> select the role (e.g., Exchange Administrator)
|
||||
3. Add assignments -> search by the app display name (e.g., "ComputerGuru Security Investigator") -> Assign
|
||||
(Active, permanent — service principals cannot activate eligible assignments)
|
||||
|
||||
## Admin consent
|
||||
## Admin consent URLs
|
||||
|
||||
When you add new Graph scopes to the app manifest in the home tenant, each customer tenant must re-consent for those scopes to flow into tokens.
|
||||
|
||||
**Admin consent URL (per tenant):**
|
||||
Each app must be individually consented in each customer tenant. Format:
|
||||
|
||||
```
|
||||
https://login.microsoftonline.com/{tenant-id}/adminconsent?client_id=fabb3421-8b34-484b-bc17-e46de9703418&redirect_uri=https://login.microsoftonline.com/common/oauth2/nativeclient
|
||||
https://login.microsoftonline.com/{tenant-id}/adminconsent?client_id={app-id}&redirect_uri=https://azcomputerguru.com&prompt=consent
|
||||
```
|
||||
|
||||
- Customer admin must sign in as Global Admin of that tenant.
|
||||
- The consent page lists all permissions in the current manifest; admin clicks Accept.
|
||||
- Redirect lands on a blank Microsoft "native client" page that looks like an error — **that is normal**. Consent is recorded on Accept, not on redirect success.
|
||||
- Verify consent took effect by checking `/servicePrincipals/{sp-id}/appRoleAssignments` — the timestamps on new grants should be `today`.
|
||||
**Security Investigator** (consent this first — needed for all breach checks):
|
||||
```
|
||||
https://login.microsoftonline.com/{TENANT_ID}/adminconsent?client_id=bfbc12a4-f0dd-4e12-b06d-997e7271e10c&redirect_uri=https://azcomputerguru.com&prompt=consent
|
||||
```
|
||||
|
||||
**Exchange Operator** (consent when remediation scope is needed):
|
||||
```
|
||||
https://login.microsoftonline.com/{TENANT_ID}/adminconsent?client_id=b43e7342-5b4b-492f-890f-bb5a4f7f40e9&redirect_uri=https://azcomputerguru.com&prompt=consent
|
||||
```
|
||||
|
||||
**User Manager**:
|
||||
```
|
||||
https://login.microsoftonline.com/{TENANT_ID}/adminconsent?client_id=64fac46b-8b44-41ad-93ee-7da03927576c&redirect_uri=https://azcomputerguru.com&prompt=consent
|
||||
```
|
||||
|
||||
**Tenant Admin**:
|
||||
```
|
||||
https://login.microsoftonline.com/{TENANT_ID}/adminconsent?client_id=709e6eed-0711-4875-9c44-2d3518c47063&redirect_uri=https://azcomputerguru.com&prompt=consent
|
||||
```
|
||||
|
||||
**Defender Add-on** (MDE-licensed tenants only — AADSTS650052 if no MDE license):
|
||||
```
|
||||
https://login.microsoftonline.com/{TENANT_ID}/adminconsent?client_id=dbf8ad1a-54f4-4bb8-8a9e-ea5b9634635b&redirect_uri=https://azcomputerguru.com&prompt=consent
|
||||
```
|
||||
|
||||
The customer admin signs in as Global Admin, clicks Accept. Redirect lands on azcomputerguru.com — expected. Verify via `/servicePrincipals/{sp-id}/appRoleAssignments` (grants timestamped today confirm success).
|
||||
|
||||
## Diagnosing "required scopes are missing"
|
||||
|
||||
Token returned 403 with `"required scopes are missing in the token"`:
|
||||
|
||||
1. Decode the JWT payload (2nd segment, base64url) and check the `roles` claim.
|
||||
2. If the scope you expected is not in `roles`:
|
||||
- Confirm the scope is in the app's API permissions in the home tenant (not just selected in the picker — must be saved).
|
||||
2. If the expected scope is missing from `roles`:
|
||||
- Confirm the scope is in the app manifest in the home tenant (saved, not just selected).
|
||||
- Grant admin consent in the home tenant.
|
||||
- Re-run the customer admin consent URL above.
|
||||
- Re-run the customer admin consent URL above for that specific app.
|
||||
3. If the scope IS in `roles` but you still get 403: check for a missing directory role (see table above).
|
||||
|
||||
## Diagnosing Exchange REST 403
|
||||
|
||||
- Invalid token scope: make sure you requested `https://outlook.office365.com/.default` (not the Graph scope).
|
||||
- Missing Exchange Administrator role on the SP in that tenant.
|
||||
- Propagation delay: newly assigned role can take up to 15 minutes to reach Exchange Online. If you just assigned it, wait and retry.
|
||||
- Wrong token scope: must request `https://outlook.office365.com/.default` (use `investigator-exo` or `exchange-op` tier, NOT `investigator`).
|
||||
- Missing Exchange Administrator role on the specific SP in that tenant.
|
||||
- Propagation delay: newly assigned role can take up to 15 minutes to reach Exchange Online. If just assigned, wait and retry.
|
||||
|
||||
## AADSTS650052 — service not licensed
|
||||
|
||||
If token request or API call returns AADSTS650052 referencing `WindowsDefenderATP` (`fc780465`): the tenant does not have an MDE license. Do not use the `defender` tier for this tenant. Security investigation proceeds with `investigator` + `investigator-exo` only.
|
||||
|
||||
## Common, benign "failures" in sign-in logs
|
||||
|
||||
- `error 50140` "Keep me signed in interrupt" — KMSI prompt, not a real failure.
|
||||
- `error 65001` "has not consented to use the application" — this fires during onboarding consent and when a user (or admin) signs in before granting consent. If the `appDisplayName` is **ComputerGuru - AI Remediation**, those are our own consent attempts, not attacker activity.
|
||||
- `error 50126` from the sysadmin account during our onboarding is typo/retry noise — check `ipAddress` matches Mike's known IPs before flagging.
|
||||
- `error 65001` "has not consented to use the application" — fires during onboarding and before consent granted. If `appDisplayName` matches any ComputerGuru app, those are our own consent attempts, not attacker activity.
|
||||
- `error 50126` from the sysadmin account during onboarding is typo/retry noise — check `ipAddress` against Mike's known IPs before flagging.
|
||||
|
||||
## Tenants where the app is already set up (as of 2026-04-16)
|
||||
## Tenants where apps are consented (as of 2026-04-20)
|
||||
|
||||
| Tenant | Tenant ID | Directory roles assigned | Notes |
|
||||
|---|---|---|---|
|
||||
| Valleywide Plastering | 5c53ae9f... | User Administrator | |
|
||||
| Dataforth | 7dfa3ce8... | User Administrator, Exchange Administrator | |
|
||||
| Cascades Tucson | 207fa277-e9d8-4eb7-ada1-1064d2221498 | User Administrator, Exchange Administrator | IdentityRiskyUser scope still not consented as of 2026-04-16 |
|
||||
| Grabblaw | 032b383e-96e4-491b-880d-3fd3295672c3 | none | Consent broken (2026-03-31); Reyna needs full access to Jsosa mailbox |
|
||||
| Tenant | Tenant ID | Security Investigator | Exchange Operator | User Manager | Tenant Admin | Defender | Directory roles | Notes |
|
||||
|---|---|---|---|---|---|---|---|---|
|
||||
| Valleywide Plastering | 5c53ae9f... | old app only | — | — | — | — | User Admin (old app) | Needs migration to new app suite |
|
||||
| Dataforth | 7dfa3ce8... | old app only | — | — | — | — | User Admin + Exchange Admin (old app) | Needs migration |
|
||||
| Cascades Tucson | 207fa277-e9d8-4eb7-ada1-1064d2221498 | old app only | — | — | — | — | User Admin + Exchange Admin (old app) | IdentityRiskyUser scope still not consented as of 2026-04-16 |
|
||||
| Grabblaw | 032b383e-96e4-491b-880d-3fd3295672c3 | YES (2026-04-20) | — | YES (2026-04-20) | — | — | none | No directory roles assigned yet |
|
||||
|
||||
Keep this table updated when you roll out to a new tenant.
|
||||
**Migration note:** Valleywide, Dataforth, and Cascades still use the old deprecated app. Next visit: consent Security Investigator + assign Exchange Administrator role to new SP, then retire old app consent.
|
||||
|
||||
Keep this table updated when rolling out to new tenants or migrating existing ones.
|
||||
|
||||
@@ -1,144 +1,159 @@
|
||||
# Graph + Exchange REST Cheatsheet
|
||||
|
||||
All examples assume `$GT` = Graph token, `$EXO` = Exchange token, `$TID` = tenant ID, `$UPN`/`$UID` = user identifiers.
|
||||
|
||||
## Graph API (`https://graph.microsoft.com/v1.0`)
|
||||
|
||||
### User lookup / status
|
||||
|
||||
```bash
|
||||
# By UPN
|
||||
curl -s -H "Authorization: Bearer $GT" \
|
||||
"https://graph.microsoft.com/v1.0/users/$UPN?\$select=id,displayName,userPrincipalName,mail,accountEnabled,createdDateTime,lastPasswordChangeDateTime"
|
||||
|
||||
# All users (filter, paged)
|
||||
curl -s -H "Authorization: Bearer $GT" \
|
||||
"https://graph.microsoft.com/v1.0/users?\$top=999&\$filter=accountEnabled%20eq%20true"
|
||||
```
|
||||
|
||||
### Mailbox
|
||||
|
||||
```bash
|
||||
# Visible inbox rules (Graph v1.0 — does NOT return hidden rules)
|
||||
/users/$UPN/mailFolders/inbox/messageRules
|
||||
|
||||
# Mailbox settings (auto-reply, delegates meeting option, NOT forwarding flags)
|
||||
/users/$UPN/mailboxSettings
|
||||
|
||||
# Recent sent / deleted
|
||||
/users/$UPN/mailFolders/sentitems/messages?$top=25&$orderby=sentDateTime%20desc
|
||||
/users/$UPN/mailFolders/deleteditems/messages?$top=25&$orderby=receivedDateTime%20desc
|
||||
```
|
||||
|
||||
### Authentication methods
|
||||
|
||||
```bash
|
||||
/users/$UPN/authentication/methods
|
||||
# Watch for new methods added within the attack window
|
||||
```
|
||||
|
||||
### OAuth + app role assignments
|
||||
|
||||
```bash
|
||||
/users/$UPN/oauth2PermissionGrants # user-level consents
|
||||
/users/$UPN/appRoleAssignments # apps assigned to this user
|
||||
/servicePrincipals/$SP_ID/appRoleAssignments # what scopes a SP has
|
||||
```
|
||||
|
||||
### Sign-ins (needs Entra ID P1 or higher)
|
||||
|
||||
```bash
|
||||
# Interactive sign-ins v1.0 (does NOT include non-interactive/service-principal)
|
||||
/auditLogs/signIns?$filter=userId eq '$UID' and createdDateTime ge $FROM&$top=200
|
||||
|
||||
# All sign-in event types (beta endpoint)
|
||||
/beta/auditLogs/signIns?$filter=userId eq '$UID' and (signInEventTypes/any(t:t eq 'nonInteractiveUser'))
|
||||
|
||||
# Foreign successful sign-ins tenant-wide
|
||||
/auditLogs/signIns?$filter=(status/errorCode eq 0) and (location/countryOrRegion ne 'US')
|
||||
```
|
||||
|
||||
### Directory audits
|
||||
|
||||
```bash
|
||||
# Changes targeting a specific user
|
||||
/auditLogs/directoryAudits?$filter=targetResources/any(t:t/id eq '$UID')
|
||||
|
||||
# Tenant-wide consent / auth-method / role events
|
||||
/auditLogs/directoryAudits?$filter=activityDateTime ge $FROM
|
||||
# Then client-side filter by activityDisplayName ~ Consent|Authentication Method|Add service principal|Add member to role
|
||||
```
|
||||
|
||||
### Identity Protection (needs IdentityRiskyUser.Read.All)
|
||||
|
||||
```bash
|
||||
/identityProtection/riskyUsers
|
||||
/identityProtection/riskyUsers/$UID
|
||||
/identityProtection/riskDetections?$filter=userId eq '$UID'
|
||||
```
|
||||
|
||||
### B2B guests
|
||||
|
||||
```bash
|
||||
# Get guest by gmail/external address
|
||||
/users?$filter=startswith(userPrincipalName,'dunedolly21')
|
||||
|
||||
# Invite audits
|
||||
/auditLogs/directoryAudits?$filter=activityDisplayName eq 'Invite external user'
|
||||
```
|
||||
|
||||
## Exchange Online REST (`https://outlook.office365.com/adminapi/beta/{tenant-id}/InvokeCommand`)
|
||||
|
||||
POST with JSON body `{"CmdletInput":{"CmdletName":"<cmdlet>","Parameters":{...}}}`. Token scope: `https://outlook.office365.com/.default`.
|
||||
|
||||
### Inbox rules (INCLUDING hidden)
|
||||
|
||||
```json
|
||||
{"CmdletInput":{"CmdletName":"Get-InboxRule","Parameters":{"Mailbox":"user@domain.com","IncludeHidden":true}}}
|
||||
```
|
||||
|
||||
Why this matters: attackers commonly create hidden rules that Graph v1.0 cannot see.
|
||||
|
||||
### Mailbox forwarding / properties
|
||||
|
||||
```json
|
||||
{"CmdletInput":{"CmdletName":"Get-Mailbox","Parameters":{"Identity":"user@domain.com"}}}
|
||||
```
|
||||
|
||||
Check: `ForwardingAddress`, `ForwardingSmtpAddress`, `DeliverToMailboxAndForward`, `GrantSendOnBehalfTo`, `HiddenFromAddressListsEnabled`.
|
||||
|
||||
### Mailbox permissions (delegates / FullAccess)
|
||||
|
||||
```json
|
||||
{"CmdletInput":{"CmdletName":"Get-MailboxPermission","Parameters":{"Identity":"user@domain.com"}}}
|
||||
```
|
||||
|
||||
Filter out `NT AUTHORITY\\SELF` — anything else is a delegate.
|
||||
|
||||
### SendAs permissions
|
||||
|
||||
```json
|
||||
{"CmdletInput":{"CmdletName":"Get-RecipientPermission","Parameters":{"Identity":"user@domain.com"}}}
|
||||
```
|
||||
|
||||
### Transport rules (tenant-wide mail flow)
|
||||
|
||||
```json
|
||||
{"CmdletInput":{"CmdletName":"Get-TransportRule","Parameters":{}}}
|
||||
```
|
||||
|
||||
Check for rules that reroute, delete, or exfiltrate mail.
|
||||
|
||||
### SMTP AUTH
|
||||
|
||||
```json
|
||||
{"CmdletInput":{"CmdletName":"Get-CASMailbox","Parameters":{"Identity":"user@domain.com"}}}
|
||||
```
|
||||
|
||||
Check `SmtpClientAuthenticationDisabled`. To disable SMTP AUTH on a single mailbox (remediation): `Set-CASMailbox -SmtpClientAuthenticationDisabled $true`.
|
||||
|
||||
## Rate limits / pagination
|
||||
|
||||
- Graph signIns endpoints cap `$top` at 999. For >999 results, follow `@odata.nextLink`.
|
||||
- Exchange REST has undocumented throttling — if you hit 429, back off 30–60s.
|
||||
- Token is valid ~60 minutes. Script caches for 55 min.
|
||||
# Graph + Exchange REST Cheatsheet
|
||||
|
||||
All examples assume:
|
||||
- `$GT` = Graph token (`investigator` tier)
|
||||
- `$EXO_R` = Exchange read token (`investigator-exo` tier) — Get-* cmdlets
|
||||
- `$EXO_W` = Exchange write token (`exchange-op` tier) — Set-*/Remove-* cmdlets
|
||||
- `$UT` = User Manager graph token (`user-manager` tier) — user write ops
|
||||
- `$TID` = tenant ID, `$UPN`/`$UID` = user identifiers
|
||||
|
||||
Acquire tokens:
|
||||
```bash
|
||||
GT=$(bash .claude/skills/remediation-tool/scripts/get-token.sh $TID investigator)
|
||||
EXO_R=$(bash .claude/skills/remediation-tool/scripts/get-token.sh $TID investigator-exo)
|
||||
EXO_W=$(bash .claude/skills/remediation-tool/scripts/get-token.sh $TID exchange-op) # remediation only
|
||||
UT=$(bash .claude/skills/remediation-tool/scripts/get-token.sh $TID user-manager) # remediation only
|
||||
```
|
||||
|
||||
## Graph API (`https://graph.microsoft.com/v1.0`)
|
||||
|
||||
### User lookup / status
|
||||
|
||||
```bash
|
||||
# By UPN
|
||||
curl -s -H "Authorization: Bearer $GT" \
|
||||
"https://graph.microsoft.com/v1.0/users/$UPN?\$select=id,displayName,userPrincipalName,mail,accountEnabled,createdDateTime,lastPasswordChangeDateTime"
|
||||
|
||||
# All users (filter, paged)
|
||||
curl -s -H "Authorization: Bearer $GT" \
|
||||
"https://graph.microsoft.com/v1.0/users?\$top=999&\$filter=accountEnabled%20eq%20true"
|
||||
```
|
||||
|
||||
### Mailbox
|
||||
|
||||
```bash
|
||||
# Visible inbox rules (Graph v1.0 — does NOT return hidden rules)
|
||||
/users/$UPN/mailFolders/inbox/messageRules
|
||||
|
||||
# Mailbox settings (auto-reply, delegates meeting option, NOT forwarding flags)
|
||||
/users/$UPN/mailboxSettings
|
||||
|
||||
# Recent sent / deleted
|
||||
/users/$UPN/mailFolders/sentitems/messages?$top=25&$orderby=sentDateTime%20desc
|
||||
/users/$UPN/mailFolders/deleteditems/messages?$top=25&$orderby=receivedDateTime%20desc
|
||||
```
|
||||
|
||||
### Authentication methods
|
||||
|
||||
```bash
|
||||
/users/$UPN/authentication/methods
|
||||
# Watch for new methods added within the attack window
|
||||
```
|
||||
|
||||
### OAuth + app role assignments
|
||||
|
||||
```bash
|
||||
/users/$UPN/oauth2PermissionGrants # user-level consents
|
||||
/users/$UPN/appRoleAssignments # apps assigned to this user
|
||||
/servicePrincipals/$SP_ID/appRoleAssignments # what scopes a SP has
|
||||
```
|
||||
|
||||
### Sign-ins (needs Entra ID P1 or higher)
|
||||
|
||||
```bash
|
||||
# Interactive sign-ins v1.0 (does NOT include non-interactive/service-principal)
|
||||
/auditLogs/signIns?$filter=userId eq '$UID' and createdDateTime ge $FROM&$top=200
|
||||
|
||||
# All sign-in event types (beta endpoint)
|
||||
/beta/auditLogs/signIns?$filter=userId eq '$UID' and (signInEventTypes/any(t:t eq 'nonInteractiveUser'))
|
||||
|
||||
# Foreign successful sign-ins tenant-wide
|
||||
/auditLogs/signIns?$filter=(status/errorCode eq 0) and (location/countryOrRegion ne 'US')
|
||||
```
|
||||
|
||||
### Directory audits
|
||||
|
||||
```bash
|
||||
# Changes targeting a specific user
|
||||
/auditLogs/directoryAudits?$filter=targetResources/any(t:t/id eq '$UID')
|
||||
|
||||
# Tenant-wide consent / auth-method / role events
|
||||
/auditLogs/directoryAudits?$filter=activityDateTime ge $FROM
|
||||
# Then client-side filter by activityDisplayName ~ Consent|Authentication Method|Add service principal|Add member to role
|
||||
```
|
||||
|
||||
### Identity Protection (needs IdentityRiskyUser.Read.All)
|
||||
|
||||
```bash
|
||||
/identityProtection/riskyUsers
|
||||
/identityProtection/riskyUsers/$UID
|
||||
/identityProtection/riskDetections?$filter=userId eq '$UID'
|
||||
```
|
||||
|
||||
### B2B guests
|
||||
|
||||
```bash
|
||||
# Get guest by gmail/external address
|
||||
/users?$filter=startswith(userPrincipalName,'dunedolly21')
|
||||
|
||||
# Invite audits
|
||||
/auditLogs/directoryAudits?$filter=activityDisplayName eq 'Invite external user'
|
||||
```
|
||||
|
||||
## Exchange Online REST (`https://outlook.office365.com/adminapi/beta/{tenant-id}/InvokeCommand`)
|
||||
|
||||
POST with JSON body `{"CmdletInput":{"CmdletName":"<cmdlet>","Parameters":{...}}}`.
|
||||
- **Read ops** (Get-*): use `$EXO_R` — Security Investigator token (`investigator-exo` tier)
|
||||
- **Write ops** (Set-*, Remove-*): use `$EXO_W` — Exchange Operator token (`exchange-op` tier)
|
||||
|
||||
### Inbox rules (INCLUDING hidden)
|
||||
|
||||
```json
|
||||
{"CmdletInput":{"CmdletName":"Get-InboxRule","Parameters":{"Mailbox":"user@domain.com","IncludeHidden":true}}}
|
||||
```
|
||||
|
||||
Why this matters: attackers commonly create hidden rules that Graph v1.0 cannot see.
|
||||
|
||||
### Mailbox forwarding / properties
|
||||
|
||||
```json
|
||||
{"CmdletInput":{"CmdletName":"Get-Mailbox","Parameters":{"Identity":"user@domain.com"}}}
|
||||
```
|
||||
|
||||
Check: `ForwardingAddress`, `ForwardingSmtpAddress`, `DeliverToMailboxAndForward`, `GrantSendOnBehalfTo`, `HiddenFromAddressListsEnabled`.
|
||||
|
||||
### Mailbox permissions (delegates / FullAccess)
|
||||
|
||||
```json
|
||||
{"CmdletInput":{"CmdletName":"Get-MailboxPermission","Parameters":{"Identity":"user@domain.com"}}}
|
||||
```
|
||||
|
||||
Filter out `NT AUTHORITY\\SELF` — anything else is a delegate.
|
||||
|
||||
### SendAs permissions
|
||||
|
||||
```json
|
||||
{"CmdletInput":{"CmdletName":"Get-RecipientPermission","Parameters":{"Identity":"user@domain.com"}}}
|
||||
```
|
||||
|
||||
### Transport rules (tenant-wide mail flow)
|
||||
|
||||
```json
|
||||
{"CmdletInput":{"CmdletName":"Get-TransportRule","Parameters":{}}}
|
||||
```
|
||||
|
||||
Check for rules that reroute, delete, or exfiltrate mail.
|
||||
|
||||
### SMTP AUTH
|
||||
|
||||
```json
|
||||
{"CmdletInput":{"CmdletName":"Get-CASMailbox","Parameters":{"Identity":"user@domain.com"}}}
|
||||
```
|
||||
|
||||
Check `SmtpClientAuthenticationDisabled`. To disable SMTP AUTH on a single mailbox (remediation): `Set-CASMailbox -SmtpClientAuthenticationDisabled $true`.
|
||||
|
||||
## Rate limits / pagination
|
||||
|
||||
- Graph signIns endpoints cap `$top` at 999. For >999 results, follow `@odata.nextLink`.
|
||||
- Exchange REST has undocumented throttling — if you hit 429, back off 30–60s.
|
||||
- Token is valid ~60 minutes. Script caches for 55 min.
|
||||
|
||||
@@ -1,17 +1,23 @@
|
||||
#!/usr/bin/env bash
|
||||
# Acquire a client-credentials token for the Claude-MSP-Access (ComputerGuru - AI Remediation) app.
|
||||
# Usage: get-token.sh <tenant-id-or-domain> <scope>
|
||||
# <scope>: graph | exchange | defender | sharepoint
|
||||
# Output (stdout): token. Exit 0 on success.
|
||||
# Cache: /tmp/remediation-tool/{tenant-id}/{scope}.jwt (55-min TTL).
|
||||
# Acquire a client-credentials bearer token for a ComputerGuru MSP app tier.
|
||||
# Usage: get-token.sh <tenant-id-or-domain> <tier>
|
||||
#
|
||||
# Tiers and their app + resource scope:
|
||||
# investigator ComputerGuru Security Investigator -> Graph API (read-only breach checks)
|
||||
# investigator-exo ComputerGuru Security Investigator -> Exchange Online (EXO read: Get-InboxRule, Get-Mailbox)
|
||||
# exchange-op ComputerGuru Exchange Operator -> Exchange Online (EXO write: Set-Mailbox, Remove-InboxRule, revoke sessions)
|
||||
# user-manager ComputerGuru User Manager -> Graph API (user create/update/disable, license assign, MFA reset)
|
||||
# tenant-admin ComputerGuru Tenant Admin -> Graph API (app roles, CA policy, directory write — high privilege)
|
||||
# defender ComputerGuru Defender Add-on -> Defender ATP API (MDE-licensed tenants only)
|
||||
#
|
||||
# Output (stdout): bearer token. Exits 0 on success.
|
||||
# Cache: /tmp/remediation-tool/{tenant-id}/{tier}.jwt TTL 55 minutes.
|
||||
set -euo pipefail
|
||||
|
||||
CLIENT_ID="fabb3421-8b34-484b-bc17-e46de9703418"
|
||||
TARGET="${1:?usage: get-token.sh <tenant-id|domain> <tier>}"
|
||||
TIER="${2:?usage: get-token.sh <tenant-id|domain> <tier>}"
|
||||
|
||||
TARGET="${1:?usage: get-token.sh <tenant-id|domain> <scope>}"
|
||||
SCOPE_NAME="${2:?usage: get-token.sh <tenant-id|domain> <scope>}"
|
||||
|
||||
# Resolve to tenant-id
|
||||
# Resolve domain to tenant GUID if needed
|
||||
if [[ "$TARGET" =~ ^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$ ]]; then
|
||||
TENANT_ID="$TARGET"
|
||||
else
|
||||
@@ -19,67 +25,103 @@ else
|
||||
TENANT_ID=$("$SCRIPT_DIR/resolve-tenant.sh" "$TARGET")
|
||||
fi
|
||||
|
||||
case "$SCOPE_NAME" in
|
||||
graph) SCOPE_URL="https://graph.microsoft.com/.default" ;;
|
||||
exchange) SCOPE_URL="https://outlook.office365.com/.default" ;;
|
||||
defender) SCOPE_URL="https://api.securitycenter.microsoft.com/.default" ;;
|
||||
sharepoint)
|
||||
# SharePoint token scope depends on tenant hostname. Caller must set SHAREPOINT_HOST=contoso.sharepoint.com.
|
||||
SCOPE_URL="https://${SHAREPOINT_HOST:?set SHAREPOINT_HOST for sharepoint scope}/.default" ;;
|
||||
*) echo "ERROR: unknown scope '$SCOPE_NAME'. Expected: graph|exchange|defender|sharepoint" >&2; exit 2 ;;
|
||||
# Map tier -> client_id, vault SOPS path, resource scope
|
||||
case "$TIER" in
|
||||
investigator)
|
||||
CLIENT_ID="bfbc12a4-f0dd-4e12-b06d-997e7271e10c"
|
||||
VAULT_PATH="msp-tools/computerguru-security-investigator.sops.yaml"
|
||||
SCOPE_URL="https://graph.microsoft.com/.default"
|
||||
;;
|
||||
investigator-exo)
|
||||
CLIENT_ID="bfbc12a4-f0dd-4e12-b06d-997e7271e10c"
|
||||
VAULT_PATH="msp-tools/computerguru-security-investigator.sops.yaml"
|
||||
SCOPE_URL="https://outlook.office365.com/.default"
|
||||
;;
|
||||
exchange-op)
|
||||
CLIENT_ID="b43e7342-5b4b-492f-890f-bb5a4f7f40e9"
|
||||
VAULT_PATH="msp-tools/computerguru-exchange-operator.sops.yaml"
|
||||
SCOPE_URL="https://outlook.office365.com/.default"
|
||||
;;
|
||||
user-manager)
|
||||
CLIENT_ID="64fac46b-8b44-41ad-93ee-7da03927576c"
|
||||
VAULT_PATH="msp-tools/computerguru-user-manager.sops.yaml"
|
||||
SCOPE_URL="https://graph.microsoft.com/.default"
|
||||
;;
|
||||
tenant-admin)
|
||||
CLIENT_ID="709e6eed-0711-4875-9c44-2d3518c47063"
|
||||
VAULT_PATH="msp-tools/computerguru-tenant-admin.sops.yaml"
|
||||
SCOPE_URL="https://graph.microsoft.com/.default"
|
||||
;;
|
||||
defender)
|
||||
CLIENT_ID="dbf8ad1a-54f4-4bb8-8a9e-ea5b9634635b"
|
||||
VAULT_PATH="msp-tools/computerguru-defender-addon.sops.yaml"
|
||||
SCOPE_URL="https://api.securitycenter.microsoft.com/.default"
|
||||
;;
|
||||
*)
|
||||
echo "ERROR: unknown tier '$TIER'." >&2
|
||||
echo "Valid tiers: investigator | investigator-exo | exchange-op | user-manager | tenant-admin | defender" >&2
|
||||
exit 2
|
||||
;;
|
||||
esac
|
||||
|
||||
CACHE_DIR="/tmp/remediation-tool/$TENANT_ID"
|
||||
mkdir -p "$CACHE_DIR"
|
||||
CACHE_FILE="$CACHE_DIR/${SCOPE_NAME}.jwt"
|
||||
CACHE_FILE="$CACHE_DIR/${TIER}.jwt"
|
||||
|
||||
# Reuse cache if less than 55 minutes old.
|
||||
# Return cached token if < 55 minutes old
|
||||
if [[ -f "$CACHE_FILE" ]] && [[ $(find "$CACHE_FILE" -mmin -55 2>/dev/null) ]]; then
|
||||
cat "$CACHE_FILE"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Locate the vault repo.
|
||||
# Locate vault repo
|
||||
VAULT_ROOT=""
|
||||
for candidate in "D:/vault" "$HOME/vault" "/d/vault"; do
|
||||
[[ -d "$candidate" ]] && VAULT_ROOT="$candidate" && break
|
||||
done
|
||||
[[ -z "$VAULT_ROOT" ]] && { echo "ERROR: SOPS vault repo not found at D:/vault or ~/vault" >&2; exit 3; }
|
||||
[[ -z "$VAULT_ROOT" ]] && { echo "ERROR: SOPS vault not found (tried D:/vault ~/vault /d/vault)" >&2; exit 3; }
|
||||
|
||||
SOPS_FILE="$VAULT_ROOT/msp-tools/claude-msp-access-graph-api.sops.yaml"
|
||||
[[ ! -f "$SOPS_FILE" ]] && { echo "ERROR: SOPS file not found: $SOPS_FILE" >&2; exit 3; }
|
||||
SOPS_FILE="$VAULT_ROOT/$VAULT_PATH"
|
||||
[[ ! -f "$SOPS_FILE" ]] && { echo "ERROR: vault file not found: $SOPS_FILE" >&2; exit 3; }
|
||||
|
||||
# Try vault.sh first; fall back to direct sops+python if vault.sh is broken (e.g. yq shim permission issues on Windows).
|
||||
# Read client secret via vault.sh (fast path), falling back to raw sops+python
|
||||
CLIENT_SECRET=""
|
||||
if [[ -f "$VAULT_ROOT/scripts/vault.sh" ]]; then
|
||||
CLIENT_SECRET=$(bash "$VAULT_ROOT/scripts/vault.sh" get-field msp-tools/claude-msp-access-graph-api.sops.yaml credentials.credential 2>/dev/null | tr -d '\r\n' || true)
|
||||
# Try both field names — new files use client_secret, legacy used credential
|
||||
CLIENT_SECRET=$(bash "$VAULT_ROOT/scripts/vault.sh" get-field "$VAULT_PATH" credentials.client_secret 2>/dev/null | tr -d '\r\n' || true)
|
||||
if [[ -z "$CLIENT_SECRET" ]]; then
|
||||
CLIENT_SECRET=$(bash "$VAULT_ROOT/scripts/vault.sh" get-field "$VAULT_PATH" credentials.credential 2>/dev/null | tr -d '\r\n' || true)
|
||||
fi
|
||||
fi
|
||||
|
||||
if [[ -z "$CLIENT_SECRET" ]]; then
|
||||
# Direct fallback: sops decrypt + python YAML parse. Works without vault.sh / yq.
|
||||
PYTHON_BIN=""
|
||||
for p in python python3 py; do command -v "$p" >/dev/null 2>&1 && PYTHON_BIN="$p" && break; done
|
||||
[[ -z "$PYTHON_BIN" ]] && { echo "ERROR: neither vault.sh worked nor python is available for fallback parse" >&2; exit 3; }
|
||||
command -v sops >/dev/null 2>&1 || { echo "ERROR: sops not on PATH (needed for fallback)" >&2; exit 3; }
|
||||
for p in python3 python py; do command -v "$p" >/dev/null 2>&1 && PYTHON_BIN="$p" && break; done
|
||||
[[ -z "$PYTHON_BIN" ]] && { echo "ERROR: vault.sh failed and python unavailable for SOPS fallback" >&2; exit 3; }
|
||||
command -v sops >/dev/null 2>&1 || { echo "ERROR: sops not on PATH (needed for fallback decrypt)" >&2; exit 3; }
|
||||
|
||||
CLIENT_SECRET=$(sops -d "$SOPS_FILE" 2>/dev/null | "$PYTHON_BIN" -c "
|
||||
import sys, re
|
||||
t = sys.stdin.read()
|
||||
# minimal YAML: find 'credentials:' block then 'credential:' key
|
||||
m = re.search(r'^credentials:\s*\n((?:[ \t]+.*\n)+)', t, re.MULTILINE)
|
||||
if not m: sys.exit(1)
|
||||
for line in m.group(1).splitlines():
|
||||
line = line.strip()
|
||||
if line.startswith('credential:'):
|
||||
if line.startswith('client_secret:') or line.startswith('credential:'):
|
||||
print(line.split(':', 1)[1].strip().strip('\"').strip(\"'\"))
|
||||
break
|
||||
" | tr -d '\r\n')
|
||||
fi
|
||||
|
||||
[[ -z "$CLIENT_SECRET" ]] && { echo "ERROR: could not read client secret from vault (vault.sh and sops+python fallback both failed)" >&2; exit 4; }
|
||||
[[ -z "$CLIENT_SECRET" ]] && {
|
||||
echo "ERROR: could not read secret from $VAULT_PATH" >&2
|
||||
echo " Check field: credentials.client_secret (or credentials.credential for older entries)" >&2
|
||||
exit 4
|
||||
}
|
||||
|
||||
# Request token.
|
||||
RESP=$(curl -s --max-time 15 -X POST "https://login.microsoftonline.com/${TENANT_ID}/oauth2/v2.0/token" \
|
||||
# Request token
|
||||
RESP=$(curl -s --max-time 15 -X POST \
|
||||
"https://login.microsoftonline.com/${TENANT_ID}/oauth2/v2.0/token" \
|
||||
--data-urlencode "client_id=${CLIENT_ID}" \
|
||||
--data-urlencode "client_secret=${CLIENT_SECRET}" \
|
||||
--data-urlencode "scope=${SCOPE_URL}" \
|
||||
@@ -88,7 +130,7 @@ RESP=$(curl -s --max-time 15 -X POST "https://login.microsoftonline.com/${TENANT
|
||||
TOKEN=$(echo "$RESP" | jq -r '.access_token // empty')
|
||||
|
||||
if [[ -z "$TOKEN" ]]; then
|
||||
echo "ERROR: token request failed for tenant=$TENANT_ID scope=$SCOPE_NAME" >&2
|
||||
echo "ERROR: token request failed (tenant=$TENANT_ID tier=$TIER)" >&2
|
||||
echo "$RESP" >&2
|
||||
exit 5
|
||||
fi
|
||||
|
||||
236
session-logs/2026-04-20-session.md
Normal file
236
session-logs/2026-04-20-session.md
Normal file
@@ -0,0 +1,236 @@
|
||||
# Session Log — 2026-04-20
|
||||
|
||||
## User
|
||||
- **User:** Mike Swanson (mike)
|
||||
- **Machine:** DESKTOP-0O8A1RL
|
||||
- **Role:** admin
|
||||
|
||||
## Session Summary
|
||||
|
||||
This session continued from a previous context that ran out of window. It covered two bodies of work:
|
||||
|
||||
1. **Remediation skill complete rewrite** — Updated all skill files to reflect the new 5-app tiered Entra architecture (Security Investigator, Exchange Operator, User Manager, Tenant Admin, Defender Add-on) replacing the old single over-permissioned app.
|
||||
|
||||
2. **Cascades Tucson breach check on John Trozzi** — Ran a full 10-point breach check after John reported spoofed email in his inbox. Mailbox confirmed clean; incident is inbound phishing, not account compromise.
|
||||
|
||||
---
|
||||
|
||||
## Work 1: Remediation Skill Rewrite
|
||||
|
||||
### Context (from prior session — summarized)
|
||||
|
||||
Previous session built out the full tiered MSP Entra app architecture:
|
||||
- Created all 5 apps + management app via ComputerGuru-Management SP
|
||||
- Granted admin consent in Grabblaw for Security Investigator + User Manager
|
||||
- Old app `fabb3421` (ComputerGuru - AI Remediation) had 159 permissions including Defender ATP, which broke consent on tenants without MDE license (AADSTS650052)
|
||||
|
||||
### Files Rewritten
|
||||
|
||||
**`D:/claudetools/.claude/skills/remediation-tool/scripts/get-token.sh`**
|
||||
- Completely rewritten: accepts `<tenant-id|domain> <tier>` instead of `<tenant-id> <scope>`
|
||||
- Tiers: `investigator` | `investigator-exo` | `exchange-op` | `user-manager` | `tenant-admin` | `defender`
|
||||
- Each tier maps to correct CLIENT_ID, vault SOPS path, and resource scope URL
|
||||
- Cache key is now `{tenant}/{tier}.jwt` (not `{tenant}/{scope}.jwt`)
|
||||
- Falls back through both `credentials.client_secret` and `credentials.credential` field names
|
||||
- Falls back to raw sops+python if vault.sh fails
|
||||
|
||||
**`D:/claudetools/.claude/skills/remediation-tool/SKILL.md`**
|
||||
- Added full app tier table with App IDs and vault files
|
||||
- Updated Exchange REST prerequisites to name correct SP per operation
|
||||
- Added minimum-privilege guidance ("never use tenant-admin for read-only check")
|
||||
|
||||
**`D:/claudetools/.claude/commands/remediation-tool.md`**
|
||||
- Rewrote token acquisition to use tier flags
|
||||
- Added per-app consent URLs in correct format (`redirect_uri=https://azcomputerguru.com`)
|
||||
- Added `disable-smtp-auth` action
|
||||
- Mapped each remediation action to its correct app tier (Exchange Operator for EXO write, User Manager for user ops)
|
||||
|
||||
**`D:/claudetools/.claude/skills/remediation-tool/references/gotchas.md`**
|
||||
- Full rewrite: 5-app suite table, per-app consent URLs, directory role map updated
|
||||
- Tenant table updated: Grabblaw now shows Security Investigator + User Manager consented (2026-04-20)
|
||||
- Migration note for Valleywide/Dataforth/Cascades (still on old app)
|
||||
- AADSTS650052 note for Defender tier on non-MDE tenants
|
||||
|
||||
**`D:/claudetools/.claude/skills/remediation-tool/references/checklist.md`**
|
||||
- False-positive filter updated to match any "ComputerGuru" app display name
|
||||
|
||||
**`D:/claudetools/.claude/skills/remediation-tool/references/graph-endpoints.md`**
|
||||
- Added token acquisition block at top with tier variable mapping
|
||||
- Exchange REST section now distinguishes `$EXO_R` (Security Investigator) from `$EXO_W` (Exchange Operator)
|
||||
|
||||
### Vault Field Name Note
|
||||
|
||||
New SOPS files for the 5 apps were created in previous session. The field name for client secrets may be `credentials.client_secret` or `credentials.credential`. The updated `get-token.sh` tries both. If neither works on first use, check field with:
|
||||
```bash
|
||||
bash D:/vault/scripts/vault.sh get msp-tools/computerguru-security-investigator.sops.yaml
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Work 2: Cascades Tucson — John Trozzi Breach Check
|
||||
|
||||
### Trigger
|
||||
John reported "spoofed email in his inbox." He forwarded the phishing email to howard@azcomputerguru.com and emailed Mike at 12:26 UTC with subject "Spoof emails."
|
||||
|
||||
### Approach Note
|
||||
Cascades Tucson still uses the OLD app (`fabb3421`) — the new Security Investigator hasn't been consented there yet. Used old app credentials from vault (`msp-tools/claude-msp-access-graph-api.sops.yaml`) for this investigation.
|
||||
|
||||
### Tenant
|
||||
- **Domain:** cascadestucson.com
|
||||
- **Tenant ID:** `207fa277-e9d8-4eb7-ada1-1064d2221498`
|
||||
- **App used:** fabb3421 (old app, still active at Cascades)
|
||||
|
||||
### User
|
||||
- **UPN:** john.trozzi@cascadestucson.com
|
||||
- **User ID:** a638f4b9-6936-4401-a9b7-015b9900e49e
|
||||
- **Last password change:** 2026-04-16T16:05:11Z (self-service, after April 16 remediation)
|
||||
|
||||
### 10-Point Results — All Clean
|
||||
|
||||
| Check | Result |
|
||||
|---|---|
|
||||
| Graph inbox rules | CLEAN — no custom rules |
|
||||
| Exchange REST rules (incl. hidden) | CLEAN — Junk E-mail Rule only |
|
||||
| Mailbox forwarding | CLEAN — null/false on all fields |
|
||||
| Delegates / FullAccess | CLEAN — no non-SELF |
|
||||
| SendAs grants | CLEAN — no non-SELF |
|
||||
| OAuth consents | CLEAN — BlueMail (2022) + EAS, both legitimate |
|
||||
| Auth methods | NOTE — duplicate Authenticator (SM-F731U null date), low risk |
|
||||
| Sign-ins 30d | CLEAN — all US/Phoenix, IP 184.191.143.62, no foreign access |
|
||||
| Risky user | CLEAN — riskLevel: none |
|
||||
| Directory audits | EXPECTED — April 16 sysadmin reset cycle + John self-service pw change |
|
||||
|
||||
### Incident Finding
|
||||
|
||||
John received phishing email:
|
||||
- **Subject:** "ATTN!! — Pending 5 (Pages) Documents expires in 2 days REF, ID:f1bb60a2a1d6ae023a3c3e0c0f959a8d"
|
||||
- Classic credential-harvesting lure
|
||||
- John correctly identified it, forwarded to Howard, reported to Mike
|
||||
- No evidence of link click or credential entry
|
||||
- Original email no longer in inbox/deleted — John deleted it
|
||||
|
||||
### Google Account Alert
|
||||
John received a security alert (16:01 UTC today) from no-reply@accounts.google.com for `201cascades@gmail.com`. Possibly a shared facility account. Confirm it has 2FA.
|
||||
|
||||
### DMARC Finding
|
||||
```
|
||||
_dmarc.cascadestucson.com: v=DMARC1;p=none;pct=100;rua=mailto:info@cascadestucson.com
|
||||
cascadestucson.com SPF: v=spf1 ip4:72.194.62.5 include:spf.protection.outlook.com -all
|
||||
```
|
||||
- SPF is tight (`-all`) — good
|
||||
- DMARC is `p=none` — monitoring only, no enforcement. Phishing emails can land in inboxes
|
||||
- **Action needed:** Upgrade to `p=quarantine` after confirming DKIM. Coordinate with Meredith.
|
||||
|
||||
### Recommendations
|
||||
1. Confirm John didn't click anything or enter credentials (if he did: revoke-sessions + password-reset)
|
||||
2. Howard should delete the forwarded phishing email without clicking
|
||||
3. Upgrade DMARC to p=quarantine (coordinate with Meredith)
|
||||
4. Confirm DKIM is configured for cascadestucson.com in Exchange Online
|
||||
5. Verify 201cascades@gmail.com Google alert
|
||||
6. Clean up duplicate Authenticator entry (SM-F731U, null date) — low priority
|
||||
|
||||
### Report Location
|
||||
`D:/claudetools/clients/cascades-tucson/reports/2026-04-20-breach-check-john-trozzi.md`
|
||||
Committed: db157e3
|
||||
|
||||
---
|
||||
|
||||
## Credentials Reference (from prior session — carried forward)
|
||||
|
||||
### ComputerGuru-Management App (ACG home tenant)
|
||||
- **App ID:** `0df4e185-4cf2-478c-a490-cc4ef36c6118`
|
||||
- **Tenant ID:** `ce61461e-81a0-4c84-bb4a-7b354a9a356d`
|
||||
- **Secret:** `C8t8Q~.bN-q5kJ~ARhkK3oMgg~w6pQhRq3-IKc15`
|
||||
- **Vault:** `D:/vault/msp-tools/computerguru-management.sops.yaml`
|
||||
- **Permissions:** Application.ReadWrite.All, AppRoleAssignment.ReadWrite.All, User.Read.All
|
||||
|
||||
### ComputerGuru Security Investigator (multi-tenant, read-only breach checks)
|
||||
- **App ID:** `bfbc12a4-f0dd-4e12-b06d-997e7271e10c`
|
||||
- **Secret:** `LS28Q~wHInqBB1y1TOWfwamKHBz~D2IFeSyUCcb0`
|
||||
- **Vault:** `D:/vault/msp-tools/computerguru-security-investigator.sops.yaml`
|
||||
- **Tenants consented:** Grabblaw (032b383e) — Security Investigator + Exchange Admin role needed
|
||||
- **Note:** NOT YET CONSENTED at Cascades, Dataforth, Valleywide — still using old app there
|
||||
|
||||
### ComputerGuru Exchange Operator (multi-tenant, EXO write)
|
||||
- **App ID:** `b43e7342-5b4b-492f-890f-bb5a4f7f40e9`
|
||||
- **Secret:** `Ct28Q~fKYUu.RvkMaGNAV1YeK6h-HBewCTPnwa.Y`
|
||||
- **Vault:** `D:/vault/msp-tools/computerguru-exchange-operator.sops.yaml`
|
||||
|
||||
### ComputerGuru User Manager (multi-tenant, user/group write)
|
||||
- **App ID:** `64fac46b-8b44-41ad-93ee-7da03927576c`
|
||||
- **Secret:** `GEQ8Q~Xl0_Lrbq85QjEyUsvK9rMe1m-C.ze.0ahN`
|
||||
- **Vault:** `D:/vault/msp-tools/computerguru-user-manager.sops.yaml`
|
||||
- **Tenants consented:** Grabblaw (032b383e)
|
||||
|
||||
### ComputerGuru Tenant Admin (multi-tenant, high-privilege)
|
||||
- **App ID:** `709e6eed-0711-4875-9c44-2d3518c47063`
|
||||
- **Secret:** `GYe8Q~I-hzqK1hUsh2uUg6RVFwM1TQt8C7h8XaUm`
|
||||
- **Vault:** `D:/vault/msp-tools/computerguru-tenant-admin.sops.yaml`
|
||||
|
||||
### ComputerGuru Defender Add-on (multi-tenant, MDE only)
|
||||
- **App ID:** `dbf8ad1a-54f4-4bb8-8a9e-ea5b9634635b`
|
||||
- **Secret:** `r1h8Q~L4kPb3STTjazbxNDsYw3JuHm1yIhTy5bqM`
|
||||
- **Vault:** `D:/vault/msp-tools/computerguru-defender-addon.sops.yaml`
|
||||
|
||||
### Old App (DEPRECATED — still active at Cascades/Dataforth/Valleywide)
|
||||
- **App ID:** `fabb3421-8b34-484b-bc17-e46de9703418`
|
||||
- **Display name:** ComputerGuru - AI Remediation
|
||||
- **Vault:** `D:/vault/msp-tools/claude-msp-access-graph-api.sops.yaml` → field: `credentials.credential`
|
||||
- **Status:** Retire after migrating remaining tenants to new app suite
|
||||
|
||||
### Mike's Entra User ID (for app ownership)
|
||||
- **Object ID:** `f34ebe40-9565-4135-af4c-2e808df57a25`
|
||||
- **ACG Tenant:** `ce61461e-81a0-4c84-bb4a-7b354a9a356d`
|
||||
|
||||
### Grabblaw
|
||||
- **Tenant ID:** `032b383e-96e4-491b-880d-3fd3295672c3`
|
||||
- **Svetlana Larionova:** slarionova@grabblaw.com (created 2026-04-20, temp pw: TempGrabb2026!, User ID: affab40c-5535-4c1a-9a78-a2eda1a4a3b7)
|
||||
- **License:** M365 Business Premium (f245ecc8-75af-4f8e-b61f-27d8114de5f3)
|
||||
- **Apps consented:** Security Investigator, User Manager
|
||||
- **Directory roles:** none assigned yet (no Exchange Admin on either SP)
|
||||
|
||||
### Cascades Tucson
|
||||
- **Tenant ID:** `207fa277-e9d8-4eb7-ada1-1064d2221498`
|
||||
- **Apps:** Still using old app fabb3421. New app not yet consented.
|
||||
- **Directory roles (old app):** Exchange Administrator, User Administrator
|
||||
|
||||
---
|
||||
|
||||
## Pending Items
|
||||
|
||||
### MSP App Suite Migration
|
||||
- [ ] Consent Security Investigator at Cascades, Dataforth, Valleywide
|
||||
- [ ] Assign Exchange Administrator role to Security Investigator SP in Cascades + Dataforth
|
||||
- [ ] Publisher verification on Apps 3-5 and Defender Add-on (MPN 6149186 — Arizona Computer Guru LLC)
|
||||
- [ ] Retire/delete old ComputerGuru - AI Remediation app (`fabb3421`) after migration
|
||||
|
||||
### Cascades Tucson
|
||||
- [ ] Confirm John Trozzi didn't click phishing link / enter credentials
|
||||
- [ ] Howard: delete the forwarded phishing email
|
||||
- [ ] DMARC p=none → p=quarantine (after DKIM confirmed) — coordinate with Meredith
|
||||
- [ ] Confirm DKIM configured for cascadestucson.com in Exchange Online
|
||||
- [ ] Verify 201cascades@gmail.com Google security alert
|
||||
- [ ] Clean up duplicate John Authenticator entry (low priority)
|
||||
|
||||
### azcomputerguru.com (from prior session)
|
||||
- [ ] Add ToS + Privacy URLs to new app registrations in portal (Branding & properties)
|
||||
- [ ] Vault cPanel credentials at `clients/azcomputerguru/cpanel.sops.yaml`
|
||||
- [ ] Add Windows SSH key to authorized_keys on 172.16.3.10
|
||||
- [ ] Reset mike WP password to permanent value and vault it
|
||||
|
||||
### ClaudeTools
|
||||
- [ ] Add Windows SSH key to authorized_keys on 172.16.3.30
|
||||
|
||||
---
|
||||
|
||||
## Key Infrastructure Reference
|
||||
|
||||
| Resource | Details |
|
||||
|---|---|
|
||||
| IX Web Hosting | 172.16.3.10 (ext: 72.194.62.5) WHM port 2087 |
|
||||
| cPanel user | azcomputerguru |
|
||||
| WP DB | azcomputerguru_acg2025, table prefix Lvkai5BQ_, mike/TempWP2026! |
|
||||
| ClaudeTools API | http://172.16.3.30:8001 |
|
||||
| GuruRMM server | 172.16.3.30:3001 |
|
||||
| Gitea | http://172.16.3.20:3000 (internal) |
|
||||
| ACG Tenant | ce61461e-81a0-4c84-bb4a-7b354a9a356d |
|
||||
Reference in New Issue
Block a user