Session log: multi-user setup, audit + gap fixes, Howard onboarding package
Two session logs: - session-logs/2026-04-16-session.md: cross-cutting (multi-user, audit, infrastructure) - guru-rmm session log appended: MSI installer, Len's Auto Brokerage, Uranus, migration drift Gap fixes: GrepAI initialized + MCP server added, Ollama models pulling, settings.json created (bypassPermissions), MCP_SERVERS.md written. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
48
.claude/skills/remediation-tool/references/checklist.md
Normal file
48
.claude/skills/remediation-tool/references/checklist.md
Normal file
@@ -0,0 +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.
|
||||
77
.claude/skills/remediation-tool/references/gotchas.md
Normal file
77
.claude/skills/remediation-tool/references/gotchas.md
Normal file
@@ -0,0 +1,77 @@
|
||||
# Gotchas — Permissions, Roles, Consent
|
||||
|
||||
## App identity
|
||||
|
||||
- **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`
|
||||
|
||||
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".
|
||||
|
||||
## Per-tenant prerequisites
|
||||
|
||||
Graph API permissions alone are not enough. Most privileged operations require directory roles on the 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 |
|
||||
|
||||
### How to assign a role to the 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).
|
||||
|
||||
## Admin consent
|
||||
|
||||
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):**
|
||||
|
||||
```
|
||||
https://login.microsoftonline.com/{tenant-id}/adminconsent?client_id=fabb3421-8b34-484b-bc17-e46de9703418&redirect_uri=https://login.microsoftonline.com/common/oauth2/nativeclient
|
||||
```
|
||||
|
||||
- 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`.
|
||||
|
||||
## 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).
|
||||
- Grant admin consent in the home tenant.
|
||||
- Re-run the customer admin consent URL 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
|
||||
|
||||
- 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.
|
||||
|
||||
## 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.
|
||||
|
||||
## Tenants where the app is already set up (as of 2026-04-16)
|
||||
|
||||
| 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 |
|
||||
|
||||
Keep this table updated when you roll out to a new tenant.
|
||||
144
.claude/skills/remediation-tool/references/graph-endpoints.md
Normal file
144
.claude/skills/remediation-tool/references/graph-endpoints.md
Normal file
@@ -0,0 +1,144 @@
|
||||
# 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.
|
||||
Reference in New Issue
Block a user