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:
2026-04-20 11:34:59 -07:00
parent b0db273e1e
commit 26df2c47b9
7 changed files with 728 additions and 307 deletions

View File

@@ -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 # /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. **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 ## Subcommands
| Form | What it does | | 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 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 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 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 consent-url <domain> [--app <tier>]` | Emit admin consent URL for a tenant + app |
| `/remediation-tool remediate <upn> <action>` | **GATED:** password-reset, revoke-sessions, disable-forwarding, remove-inbox-rules, disable-account | | `/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. `<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) ### 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 ### 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. - **`remediate`** — see Remediation section below.
### 4. Write the report ### 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` - `cascadestucson.com` -> `cascades-tucson`
- `foobarwidgets.com` -> `foobar-widgets` - `grabblaw.com` -> `grabblaw`
- Use existing `clients/<slug>/` directory if present; if no matching client dir exists, ask the user for the slug before creating one. - 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 ### 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 ### 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>`: 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. 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** that will run (curl command, cmdlet name, parameters). 2. **Display the exact action** (curl command, cmdlet name, parameters).
3. **Require explicit `YES` in chat** — not approval via permission prompt. If the user types anything else, abort. 3. **Require explicit `YES` in chat** — not a permission prompt. Anything else aborts.
4. Execute via Graph/Exchange REST. Capture response to a remediation log at `/tmp/remediation-tool/{tenant}/remediation/<slug>-YYYY-MM-DDTHHMMSS.json`. 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 appending what was done and the result. 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` | `user-manager` | Graph `POST /users/{upn}/revokeSignInSessions` |
| `revoke-sessions` | Graph `POST /users/{upn}/revokeSignInSessions` | Kills all active sessions | | `disable-account` | `user-manager` | Graph `PATCH /users/{upn}` with `accountEnabled: false` |
| `disable-forwarding` | Exchange REST `Set-Mailbox -ForwardingAddress $null -ForwardingSmtpAddress $null -DeliverToMailboxAndForward $false` | Clears all forwarding | | `password-reset` | `user-manager` | Graph `PATCH /users/{upn}` with new `passwordProfile` |
| `remove-inbox-rules` | Exchange REST `Remove-InboxRule` for each non-default rule | Asks which to keep first | | `disable-forwarding` | `exchange-op` | Exchange REST `Set-Mailbox -ForwardingAddress $null -ForwardingSmtpAddress $null -DeliverToMailboxAndForward $false` |
| `disable-account` | Graph `PATCH /users/{upn}` with `accountEnabled: false` | Hard disable | | `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` - `sweep cascadestucson.com`
- `signins cascadestucson.com --user megan.hiatt@cascadestucson.com --failed-only --days 30` - `signins cascadestucson.com --user megan.hiatt@cascadestucson.com --failed-only --days 30`
- `consent-url cascadestucson.com` - `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. 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` - Permission/role gotchas + consent URLs: `.claude/skills/remediation-tool/references/gotchas.md`
- Endpoint cheatsheet: `.claude/skills/remediation-tool/references/graph-endpoints.md` - Endpoint cheatsheet: `.claude/skills/remediation-tool/references/graph-endpoints.md`
- Report template: `.claude/skills/remediation-tool/templates/breach-report.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`

View File

@@ -1,7 +1,7 @@
--- ---
name: remediation-tool name: remediation-tool
description: | 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. 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). 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 ## 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). 1. Parse the user's intent into a subcommand (check/sweep/signins/consent-url/remediate).
2. Resolve tenant ID from domain. 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/`. 4. Run checks via scripts in `scripts/`.
5. Interpret findings using `references/checklist.md`. 5. Interpret findings using `references/checklist.md`.
6. Write report to `clients/{slug}/reports/YYYY-MM-DD-{action}.md` using `templates/breach-report.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). - 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. - `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 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: 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 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 ## Conventions
- **Target identifiers**: accept UPN, domain, or tenant GUID. Normalize to tenant GUID internally. - **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. - **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). - **Reports**: `clients/{slug}/reports/YYYY-MM-DD-{action}.md`. Derive slug from domain (strip TLD, hyphenate).
- **UTC dates everywhere**. - **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 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 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.

View File

@@ -1,48 +1,48 @@
# Breach-Check Rubric # Breach-Check Rubric
How to interpret the outputs from `user-breach-check.sh` and `tenant-sweep.sh`. How to interpret the outputs from `user-breach-check.sh` and `tenant-sweep.sh`.
## Single-user check — the 10 points ## Single-user check — the 10 points
| # | Check | What "clean" looks like | Red flags | | # | 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) | | 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 | | 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 | | 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 | | 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 | | 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 | | 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 | | 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 | | 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 | | 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 | | 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 ### 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. 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 ## Tenant-wide sweep — priorities
| Priority | Signal | Action | | 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 | 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. | | 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 | 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. | | 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 | 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. | | 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. | | P4 | Isolated wrong-password attempt from foreign IP | Record and move on. Single attempts are noise unless repeated. |
## False positives to filter out ## False positives to filter out
- `sysadmin@<tenant>` failures during onboarding of the remediation tool itself (error 65001 against app **ComputerGuru - AI Remediation**). - `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. - `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. - 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. - `error 50140` "Keep me signed in interrupt" is a browser prompt, not a failed auth.
## When to escalate beyond this tool ## When to escalate beyond this tool
- Data exfiltration suspected -> pull Unified Audit Log via Purview (this tool does not access UAL). - 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. - 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). - 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. - Attacker still active and exfiltrating -> consider disabling the user via the `remediate` subcommand and rotating the mailbox password at the same time.

View File

@@ -1,77 +1,113 @@
# Gotchas — Permissions, Roles, Consent # Gotchas — Permissions, Roles, Consent
## App identity ## App Suite (tiered architecture)
- **App ID (client_id):** `fabb3421-8b34-484b-bc17-e46de9703418` Five multi-tenant apps replace the old single over-permissioned app. Use minimum necessary tier.
- **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`
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 ## 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 | | Operation | App tier | Required directory role on that SP |
|---|---| |---|---|---|
| Password reset, user property updates | User Administrator | | Exchange REST read (Get-InboxRule, Get-Mailbox) | `investigator-exo` | Exchange Administrator |
| Exchange REST (hidden inbox rules, mailbox permissions, SendAs, transport rules, Get-Mailbox) | Exchange Administrator | | Exchange REST write (Set-Mailbox, Remove-InboxRule) | `exchange-op` | Exchange Administrator |
| Conditional Access policy reads/writes | Conditional Access Administrator OR Security Administrator | | Password reset, user property updates | `user-manager` | User Administrator |
| Teams policies | Teams 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: 1. Sign into the customer's Entra admin center as Global Admin:
`https://entra.microsoft.com/#@{customer-domain}` `https://entra.microsoft.com/#@{customer-domain}`
2. Identity -> Roles & admins -> All roles -> select the role (e.g., Exchange Administrator). 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). 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. Each app must be individually consented in each customer tenant. Format:
**Admin consent URL (per tenant):**
``` ```
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. **Security Investigator** (consent this first — needed for all breach checks):
- 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. https://login.microsoftonline.com/{TENANT_ID}/adminconsent?client_id=bfbc12a4-f0dd-4e12-b06d-997e7271e10c&redirect_uri=https://azcomputerguru.com&prompt=consent
- Verify consent took effect by checking `/servicePrincipals/{sp-id}/appRoleAssignments` — the timestamps on new grants should be `today`. ```
**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" ## Diagnosing "required scopes are missing"
Token returned 403 with `"required scopes are missing in the token"`: Token returned 403 with `"required scopes are missing in the token"`:
1. Decode the JWT payload (2nd segment, base64url) and check the `roles` claim. 1. Decode the JWT payload (2nd segment, base64url) and check the `roles` claim.
2. If the scope you expected is not in `roles`: 2. If the expected scope is missing from `roles`:
- Confirm the scope is in the app's API permissions in the home tenant (not just selected in the picker — must be saved). - Confirm the scope is in the app manifest in the home tenant (saved, not just selected).
- Grant admin consent in the home tenant. - 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). 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 ## Diagnosing Exchange REST 403
- Invalid token scope: make sure you requested `https://outlook.office365.com/.default` (not the Graph scope). - 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 SP in that tenant. - 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 you just assigned it, wait and retry. - 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 ## Common, benign "failures" in sign-in logs
- `error 50140` "Keep me signed in interrupt" — KMSI prompt, not a real failure. - `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 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 our onboarding is typo/retry noise — check `ipAddress` matches Mike's known IPs before flagging. - `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 | | Tenant | Tenant ID | Security Investigator | Exchange Operator | User Manager | Tenant Admin | Defender | Directory roles | Notes |
|---|---|---|---| |---|---|---|---|---|---|---|---|---|
| Valleywide Plastering | 5c53ae9f... | User Administrator | | | Valleywide Plastering | 5c53ae9f... | old app only | — | — | — | — | User Admin (old app) | Needs migration to new app suite |
| Dataforth | 7dfa3ce8... | User Administrator, Exchange Administrator | | | Dataforth | 7dfa3ce8... | old app only | — | — | — | — | User Admin + Exchange Admin (old app) | Needs migration |
| Cascades Tucson | 207fa277-e9d8-4eb7-ada1-1064d2221498 | User Administrator, Exchange Administrator | IdentityRiskyUser scope still not consented as of 2026-04-16 | | 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 | none | Consent broken (2026-03-31); Reyna needs full access to Jsosa mailbox | | 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.

View File

@@ -1,144 +1,159 @@
# Graph + Exchange REST Cheatsheet # Graph + Exchange REST Cheatsheet
All examples assume `$GT` = Graph token, `$EXO` = Exchange token, `$TID` = tenant ID, `$UPN`/`$UID` = user identifiers. All examples assume:
- `$GT` = Graph token (`investigator` tier)
## Graph API (`https://graph.microsoft.com/v1.0`) - `$EXO_R` = Exchange read token (`investigator-exo` tier) — Get-* cmdlets
- `$EXO_W` = Exchange write token (`exchange-op` tier) — Set-*/Remove-* cmdlets
### User lookup / status - `$UT` = User Manager graph token (`user-manager` tier) — user write ops
- `$TID` = tenant ID, `$UPN`/`$UID` = user identifiers
```bash
# By UPN Acquire tokens:
curl -s -H "Authorization: Bearer $GT" \ ```bash
"https://graph.microsoft.com/v1.0/users/$UPN?\$select=id,displayName,userPrincipalName,mail,accountEnabled,createdDateTime,lastPasswordChangeDateTime" 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)
# All users (filter, paged) EXO_W=$(bash .claude/skills/remediation-tool/scripts/get-token.sh $TID exchange-op) # remediation only
curl -s -H "Authorization: Bearer $GT" \ UT=$(bash .claude/skills/remediation-tool/scripts/get-token.sh $TID user-manager) # remediation only
"https://graph.microsoft.com/v1.0/users?\$top=999&\$filter=accountEnabled%20eq%20true" ```
```
## Graph API (`https://graph.microsoft.com/v1.0`)
### Mailbox
### User lookup / status
```bash
# Visible inbox rules (Graph v1.0 — does NOT return hidden rules) ```bash
/users/$UPN/mailFolders/inbox/messageRules # By UPN
curl -s -H "Authorization: Bearer $GT" \
# Mailbox settings (auto-reply, delegates meeting option, NOT forwarding flags) "https://graph.microsoft.com/v1.0/users/$UPN?\$select=id,displayName,userPrincipalName,mail,accountEnabled,createdDateTime,lastPasswordChangeDateTime"
/users/$UPN/mailboxSettings
# All users (filter, paged)
# Recent sent / deleted curl -s -H "Authorization: Bearer $GT" \
/users/$UPN/mailFolders/sentitems/messages?$top=25&$orderby=sentDateTime%20desc "https://graph.microsoft.com/v1.0/users?\$top=999&\$filter=accountEnabled%20eq%20true"
/users/$UPN/mailFolders/deleteditems/messages?$top=25&$orderby=receivedDateTime%20desc ```
```
### Mailbox
### Authentication methods
```bash
```bash # Visible inbox rules (Graph v1.0 — does NOT return hidden rules)
/users/$UPN/authentication/methods /users/$UPN/mailFolders/inbox/messageRules
# Watch for new methods added within the attack window
``` # Mailbox settings (auto-reply, delegates meeting option, NOT forwarding flags)
/users/$UPN/mailboxSettings
### OAuth + app role assignments
# Recent sent / deleted
```bash /users/$UPN/mailFolders/sentitems/messages?$top=25&$orderby=sentDateTime%20desc
/users/$UPN/oauth2PermissionGrants # user-level consents /users/$UPN/mailFolders/deleteditems/messages?$top=25&$orderby=receivedDateTime%20desc
/users/$UPN/appRoleAssignments # apps assigned to this user ```
/servicePrincipals/$SP_ID/appRoleAssignments # what scopes a SP has
``` ### Authentication methods
### Sign-ins (needs Entra ID P1 or higher) ```bash
/users/$UPN/authentication/methods
```bash # Watch for new methods added within the attack window
# Interactive sign-ins v1.0 (does NOT include non-interactive/service-principal) ```
/auditLogs/signIns?$filter=userId eq '$UID' and createdDateTime ge $FROM&$top=200
### OAuth + app role assignments
# All sign-in event types (beta endpoint)
/beta/auditLogs/signIns?$filter=userId eq '$UID' and (signInEventTypes/any(t:t eq 'nonInteractiveUser')) ```bash
/users/$UPN/oauth2PermissionGrants # user-level consents
# Foreign successful sign-ins tenant-wide /users/$UPN/appRoleAssignments # apps assigned to this user
/auditLogs/signIns?$filter=(status/errorCode eq 0) and (location/countryOrRegion ne 'US') /servicePrincipals/$SP_ID/appRoleAssignments # what scopes a SP has
``` ```
### Directory audits ### Sign-ins (needs Entra ID P1 or higher)
```bash ```bash
# Changes targeting a specific user # Interactive sign-ins v1.0 (does NOT include non-interactive/service-principal)
/auditLogs/directoryAudits?$filter=targetResources/any(t:t/id eq '$UID') /auditLogs/signIns?$filter=userId eq '$UID' and createdDateTime ge $FROM&$top=200
# Tenant-wide consent / auth-method / role events # All sign-in event types (beta endpoint)
/auditLogs/directoryAudits?$filter=activityDateTime ge $FROM /beta/auditLogs/signIns?$filter=userId eq '$UID' and (signInEventTypes/any(t:t eq 'nonInteractiveUser'))
# Then client-side filter by activityDisplayName ~ Consent|Authentication Method|Add service principal|Add member to role
``` # Foreign successful sign-ins tenant-wide
/auditLogs/signIns?$filter=(status/errorCode eq 0) and (location/countryOrRegion ne 'US')
### Identity Protection (needs IdentityRiskyUser.Read.All) ```
```bash ### Directory audits
/identityProtection/riskyUsers
/identityProtection/riskyUsers/$UID ```bash
/identityProtection/riskDetections?$filter=userId eq '$UID' # Changes targeting a specific user
``` /auditLogs/directoryAudits?$filter=targetResources/any(t:t/id eq '$UID')
### B2B guests # Tenant-wide consent / auth-method / role events
/auditLogs/directoryAudits?$filter=activityDateTime ge $FROM
```bash # Then client-side filter by activityDisplayName ~ Consent|Authentication Method|Add service principal|Add member to role
# Get guest by gmail/external address ```
/users?$filter=startswith(userPrincipalName,'dunedolly21')
### Identity Protection (needs IdentityRiskyUser.Read.All)
# Invite audits
/auditLogs/directoryAudits?$filter=activityDisplayName eq 'Invite external user' ```bash
``` /identityProtection/riskyUsers
/identityProtection/riskyUsers/$UID
## Exchange Online REST (`https://outlook.office365.com/adminapi/beta/{tenant-id}/InvokeCommand`) /identityProtection/riskDetections?$filter=userId eq '$UID'
```
POST with JSON body `{"CmdletInput":{"CmdletName":"<cmdlet>","Parameters":{...}}}`. Token scope: `https://outlook.office365.com/.default`.
### B2B guests
### Inbox rules (INCLUDING hidden)
```bash
```json # Get guest by gmail/external address
{"CmdletInput":{"CmdletName":"Get-InboxRule","Parameters":{"Mailbox":"user@domain.com","IncludeHidden":true}}} /users?$filter=startswith(userPrincipalName,'dunedolly21')
```
# Invite audits
Why this matters: attackers commonly create hidden rules that Graph v1.0 cannot see. /auditLogs/directoryAudits?$filter=activityDisplayName eq 'Invite external user'
```
### Mailbox forwarding / properties
## Exchange Online REST (`https://outlook.office365.com/adminapi/beta/{tenant-id}/InvokeCommand`)
```json
{"CmdletInput":{"CmdletName":"Get-Mailbox","Parameters":{"Identity":"user@domain.com"}}} 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)
Check: `ForwardingAddress`, `ForwardingSmtpAddress`, `DeliverToMailboxAndForward`, `GrantSendOnBehalfTo`, `HiddenFromAddressListsEnabled`.
### Inbox rules (INCLUDING hidden)
### Mailbox permissions (delegates / FullAccess)
```json
```json {"CmdletInput":{"CmdletName":"Get-InboxRule","Parameters":{"Mailbox":"user@domain.com","IncludeHidden":true}}}
{"CmdletInput":{"CmdletName":"Get-MailboxPermission","Parameters":{"Identity":"user@domain.com"}}} ```
```
Why this matters: attackers commonly create hidden rules that Graph v1.0 cannot see.
Filter out `NT AUTHORITY\\SELF` — anything else is a delegate.
### Mailbox forwarding / properties
### SendAs permissions
```json
```json {"CmdletInput":{"CmdletName":"Get-Mailbox","Parameters":{"Identity":"user@domain.com"}}}
{"CmdletInput":{"CmdletName":"Get-RecipientPermission","Parameters":{"Identity":"user@domain.com"}}} ```
```
Check: `ForwardingAddress`, `ForwardingSmtpAddress`, `DeliverToMailboxAndForward`, `GrantSendOnBehalfTo`, `HiddenFromAddressListsEnabled`.
### Transport rules (tenant-wide mail flow)
### Mailbox permissions (delegates / FullAccess)
```json
{"CmdletInput":{"CmdletName":"Get-TransportRule","Parameters":{}}} ```json
``` {"CmdletInput":{"CmdletName":"Get-MailboxPermission","Parameters":{"Identity":"user@domain.com"}}}
```
Check for rules that reroute, delete, or exfiltrate mail.
Filter out `NT AUTHORITY\\SELF` — anything else is a delegate.
### SMTP AUTH
### SendAs permissions
```json
{"CmdletInput":{"CmdletName":"Get-CASMailbox","Parameters":{"Identity":"user@domain.com"}}} ```json
``` {"CmdletInput":{"CmdletName":"Get-RecipientPermission","Parameters":{"Identity":"user@domain.com"}}}
```
Check `SmtpClientAuthenticationDisabled`. To disable SMTP AUTH on a single mailbox (remediation): `Set-CASMailbox -SmtpClientAuthenticationDisabled $true`.
### Transport rules (tenant-wide mail flow)
## Rate limits / pagination
```json
- Graph signIns endpoints cap `$top` at 999. For >999 results, follow `@odata.nextLink`. {"CmdletInput":{"CmdletName":"Get-TransportRule","Parameters":{}}}
- Exchange REST has undocumented throttling — if you hit 429, back off 3060s. ```
- Token is valid ~60 minutes. Script caches for 55 min.
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 3060s.
- Token is valid ~60 minutes. Script caches for 55 min.

View File

@@ -1,17 +1,23 @@
#!/usr/bin/env bash #!/usr/bin/env bash
# Acquire a client-credentials token for the Claude-MSP-Access (ComputerGuru - AI Remediation) app. # Acquire a client-credentials bearer token for a ComputerGuru MSP app tier.
# Usage: get-token.sh <tenant-id-or-domain> <scope> # Usage: get-token.sh <tenant-id-or-domain> <tier>
# <scope>: graph | exchange | defender | sharepoint #
# Output (stdout): token. Exit 0 on success. # Tiers and their app + resource scope:
# Cache: /tmp/remediation-tool/{tenant-id}/{scope}.jwt (55-min TTL). # 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 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>}" # Resolve domain to tenant GUID if needed
SCOPE_NAME="${2:?usage: get-token.sh <tenant-id|domain> <scope>}"
# Resolve to tenant-id
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 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" TENANT_ID="$TARGET"
else else
@@ -19,67 +25,103 @@ else
TENANT_ID=$("$SCRIPT_DIR/resolve-tenant.sh" "$TARGET") TENANT_ID=$("$SCRIPT_DIR/resolve-tenant.sh" "$TARGET")
fi fi
case "$SCOPE_NAME" in # Map tier -> client_id, vault SOPS path, resource scope
graph) SCOPE_URL="https://graph.microsoft.com/.default" ;; case "$TIER" in
exchange) SCOPE_URL="https://outlook.office365.com/.default" ;; investigator)
defender) SCOPE_URL="https://api.securitycenter.microsoft.com/.default" ;; CLIENT_ID="bfbc12a4-f0dd-4e12-b06d-997e7271e10c"
sharepoint) VAULT_PATH="msp-tools/computerguru-security-investigator.sops.yaml"
# SharePoint token scope depends on tenant hostname. Caller must set SHAREPOINT_HOST=contoso.sharepoint.com. SCOPE_URL="https://graph.microsoft.com/.default"
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 ;; 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 esac
CACHE_DIR="/tmp/remediation-tool/$TENANT_ID" CACHE_DIR="/tmp/remediation-tool/$TENANT_ID"
mkdir -p "$CACHE_DIR" 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 if [[ -f "$CACHE_FILE" ]] && [[ $(find "$CACHE_FILE" -mmin -55 2>/dev/null) ]]; then
cat "$CACHE_FILE" cat "$CACHE_FILE"
exit 0 exit 0
fi fi
# Locate the vault repo. # Locate vault repo
VAULT_ROOT="" VAULT_ROOT=""
for candidate in "D:/vault" "$HOME/vault" "/d/vault"; do for candidate in "D:/vault" "$HOME/vault" "/d/vault"; do
[[ -d "$candidate" ]] && VAULT_ROOT="$candidate" && break [[ -d "$candidate" ]] && VAULT_ROOT="$candidate" && break
done 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" SOPS_FILE="$VAULT_ROOT/$VAULT_PATH"
[[ ! -f "$SOPS_FILE" ]] && { echo "ERROR: SOPS file not found: $SOPS_FILE" >&2; exit 3; } [[ ! -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="" CLIENT_SECRET=""
if [[ -f "$VAULT_ROOT/scripts/vault.sh" ]]; then 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 fi
if [[ -z "$CLIENT_SECRET" ]]; then if [[ -z "$CLIENT_SECRET" ]]; then
# Direct fallback: sops decrypt + python YAML parse. Works without vault.sh / yq.
PYTHON_BIN="" PYTHON_BIN=""
for p in python python3 py; do command -v "$p" >/dev/null 2>&1 && PYTHON_BIN="$p" && break; done for p in python3 python 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; } [[ -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)" >&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 " CLIENT_SECRET=$(sops -d "$SOPS_FILE" 2>/dev/null | "$PYTHON_BIN" -c "
import sys, re import sys, re
t = sys.stdin.read() t = sys.stdin.read()
# minimal YAML: find 'credentials:' block then 'credential:' key
m = re.search(r'^credentials:\s*\n((?:[ \t]+.*\n)+)', t, re.MULTILINE) m = re.search(r'^credentials:\s*\n((?:[ \t]+.*\n)+)', t, re.MULTILINE)
if not m: sys.exit(1) if not m: sys.exit(1)
for line in m.group(1).splitlines(): for line in m.group(1).splitlines():
line = line.strip() line = line.strip()
if line.startswith('credential:'): if line.startswith('client_secret:') or line.startswith('credential:'):
print(line.split(':', 1)[1].strip().strip('\"').strip(\"'\")) print(line.split(':', 1)[1].strip().strip('\"').strip(\"'\"))
break break
" | tr -d '\r\n') " | tr -d '\r\n')
fi 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. # Request token
RESP=$(curl -s --max-time 15 -X POST "https://login.microsoftonline.com/${TENANT_ID}/oauth2/v2.0/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_id=${CLIENT_ID}" \
--data-urlencode "client_secret=${CLIENT_SECRET}" \ --data-urlencode "client_secret=${CLIENT_SECRET}" \
--data-urlencode "scope=${SCOPE_URL}" \ --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') TOKEN=$(echo "$RESP" | jq -r '.access_token // empty')
if [[ -z "$TOKEN" ]]; then 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 echo "$RESP" >&2
exit 5 exit 5
fi fi

View 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 |