sync: auto-sync from GURU-5070 at 2026-07-17 05:35:10

Author: Mike Swanson
Machine: GURU-5070
Timestamp: 2026-07-17 05:35:10
This commit is contained in:
2026-07-17 05:35:39 -07:00
parent 7db991b037
commit 51b09dacbb
11 changed files with 200 additions and 7 deletions

View File

@@ -0,0 +1 @@
.bb-tenant.json

View File

@@ -14,7 +14,7 @@ Five multi-tenant apps cover distinct privilege tiers. Use only what the task re
| 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 |
| `investigator-exo` | ComputerGuru Security Investigator | `bfbc12a4-f0dd-4e12-b06d-997e7271e10c` | `computerguru-security-investigator.sops.yaml` | Exchange Online read**BROKEN: app lacks `Exchange.ManageAsApp`, adminapi `InvokeCommand` 401s. Use `exchange-op` for ALL Exchange REST (read + write).** |
| `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 |
@@ -23,7 +23,7 @@ Five multi-tenant apps cover distinct privilege tiers. Use only what the task re
**The suite has broad, working access across ALL of M365 — Graph, Exchange Online, Defender, AND SharePoint Online.** Before concluding "the tool can't do X / access denied," verify against the live permission map in `references/app-permissions-and-sharepoint.md` (decode the token `roles` claim). An `accessDenied` usually means wrong tier or wrong endpoint for a scope we DO hold — not a real gap. Two recurring traps: (1) **SharePoint app-only requires a certificate** — a `client_secret` token is rejected everywhere in SharePoint with `"Unsupported app only token"` (get-token.sh forces cert for the `sharepoint*` tiers); (2) Graph `GET /admin/sharepoint/settings` needs a scope no app holds — read/write SharePoint tenant settings via the **CSOM/REST admin API** (`sharepoint-admin` tier) instead. Full map, gotchas, and CSOM examples: `references/app-permissions-and-sharepoint.md`.
**Default for breach checks:** use `investigator` (Graph) + `investigator-exo` (Exchange read). Escalate to write tiers only when remediating.
**Default for breach checks:** use `investigator` (Graph) + **`exchange-op`** for ALL Exchange REST (read AND write). NOTE: `investigator-exo` 401s on the Exchange adminapi — the Security Investigator app holds the Exchange Admin *role* but NOT the `Exchange.ManageAsApp` *app-permission* that `InvokeCommand` requires — so even read-only `Get-*` cmdlets go through `exchange-op`. Escalate nothing extra; `exchange-op` is the read path too. Full detail: `references/gotchas.md`.
## Auto-Invocation Behavior
@@ -41,7 +41,7 @@ When triggered automatically (vs. via `/remediation-tool`), follow the same work
- The SOPS vault is accessible via `.claude/identity.json` `vault_path` field. The scripts auto-resolve the vault location from identity.json — no hardcoded paths.
- `jq`, `curl`, `bash` are available.
- 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`.
- **Exchange REST (adminapi `InvokeCommand`) — read AND write — ALWAYS use the `exchange-op` tier.** The Exchange Operator app (`b43e7342-5b4b-492f-890f-bb5a4f7f40e9`) is the ONLY suite app that holds `Exchange.ManageAsApp` (required for adminapi) PLUS the Exchange Administrator directory role. `investigator-exo` returns **401** on every Exchange cmdlet — the Security Investigator app has the Exchange Admin *role* but NOT the `Exchange.ManageAsApp` *permission* (verified live on ACG 2026-07-16: Investigator SP `Exchange.ManageAsApp`=NO, Operator SP=YES). Diagnosis rule: **401 = wrong app (missing `Exchange.ManageAsApp`) → switch to `exchange-op`; 403 = the `exchange-op` SP is missing the Exchange Administrator directory role on that tenant** → emit the Entra Roles link from `references/gotchas.md`. Do NOT "assign Exchange Admin to the Security Investigator SP" — it does not fix the 401 (the app-permission, not the role, is the blocker).
- 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.

View File

@@ -38,13 +38,32 @@ Graph API permissions alone are not enough. Most privileged operations require d
| Operation | App tier | Required directory role on that SP |
|---|---|---|
| Exchange REST read (Get-InboxRule, Get-Mailbox) | `investigator-exo` | Exchange Administrator |
| Exchange REST read (Get-InboxRule, Get-Mailbox, Get-MailboxPermission, Get-RecipientPermission) | **`exchange-op`** (NOT `investigator-exo` — see callout) | Exchange Administrator |
| Exchange REST write (Set-Mailbox, Remove-InboxRule) | `exchange-op` | Exchange Administrator |
| Password reset, user property updates | `user-manager` | User Administrator |
| MFA method reset | `user-manager` | Authentication Administrator |
| Conditional Access reads/writes | `tenant-admin` | Conditional Access Administrator OR Security Administrator |
| Teams policies | `tenant-admin` | Teams Administrator |
> **[AUTHORITATIVE — stop re-deriving this] Exchange REST goes through `exchange-op`, NEVER `investigator-exo`.**
> The Exchange adminapi (`https://outlook.office365.com/adminapi/beta/{tenant}/InvokeCommand`) requires the
> calling app to hold the **`Exchange.ManageAsApp`** application permission (Office 365 Exchange Online app role
> `dc50a0fb-09a3-484d-be87-e023b12c6440`) **in addition to** the Exchange Administrator directory role. Only the
> **Exchange Operator** app (`exchange-op`, `b43e7342-5b4b-492f-890f-bb5a4f7f40e9`) has BOTH. The **Security
> Investigator** app (`investigator`/`investigator-exo`, `bfbc12a4-...`) has the Exchange Admin *role* but NOT
> `Exchange.ManageAsApp`, so **every** `investigator-exo` adminapi call returns **401** — including read-only
> `Get-*` cmdlets. Verified live on ACG (`azcomputerguru.com`) 2026-07-16: Investigator SP `Exchange.ManageAsApp`=**NO**,
> Operator SP=**YES**. **So use `exchange-op` for ALL Exchange cmdlets, read and write.**
>
> **401 vs 403:** `401` on adminapi = wrong app (missing `Exchange.ManageAsApp`) → you are on `investigator-exo`,
> switch to `exchange-op`. `403` = the `exchange-op` SP is missing the **Exchange Administrator** directory role on
> that specific tenant → assign it (steps below). Assigning Exchange Admin to the *Security Investigator* SP does
> **not** help — the missing piece there is the app-permission, which is baked into the app registration, not a
> per-tenant role. Authoritative ACG SP→role map (verified 2026-07-16): `exchange-op` SP `83c225f1-b38d-4063-9fdd-642b6b09ae8b`
> = Exchange Administrator (+ `Exchange.ManageAsApp`); `investigator` SP `9d242c15-6cd3-46ec-96d3-bcafaaaca333` = Exchange
> Administrator only (no ManageAsApp); `user-manager` SP `04020a4e-...` = Authentication Administrator + User Administrator;
> `tenant-admin` SP `046a7f70-...` = Conditional Access Administrator.
### How to assign a role to an SP in a customer tenant
1. Sign into the customer's Entra admin center as Global Admin:

View File

@@ -0,0 +1,2 @@
.breakglass-lastcheck
.breakglass-alert.log

View File

@@ -0,0 +1,38 @@
#!/usr/bin/env bash
# breakglass-signin-alert.sh — alert #dev-alerts on ANY sign-in by the ACG M365
# break-glass account (sysadmin@azcomputerguru.onmicrosoft.com). A break-glass login
# should be rare and always investigated. Run on a schedule (hourly/daily).
#
# State: tracks the last-checked UTC timestamp in a sidecar so it only alerts on NEW
# sign-ins. First run seeds the timestamp (no alert for history) unless --backfill <ISO>.
# DRY_RUN=1 prints what it would post instead of posting.
#
# Usage: breakglass-signin-alert.sh [--since <ISO8601>]
set -u
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
ROOT="$(cd "$SCRIPT_DIR/../../../.." && pwd)" # skill scripts -> repo root
TEN="ce61461e-81a0-4c84-bb4a-7b354a9a356d" # azcomputerguru.com
BG="8c363d17-02ed-45e1-97d0-49e5819e6ff6" # sysadmin@azcomputerguru.onmicrosoft.com
STATE="$SCRIPT_DIR/.breakglass-lastcheck"
NOW="$(date -u +%Y-%m-%dT%H:%M:%SZ)"
if [ "${1:-}" = "--since" ] && [ -n "${2:-}" ]; then SINCE="$2";
elif [ -f "$STATE" ]; then SINCE="$(cat "$STATE")";
else SINCE="$NOW"; echo "$NOW" > "$STATE"; echo "[seed] first run — seeded $NOW, no history alerted"; exit 0; fi
G=$("$SCRIPT_DIR/get-token.sh" "$TEN" investigator 2>/dev/null) || { echo "[err] token"; exit 1; }
RESP=$(curl -s "https://graph.microsoft.com/v1.0/auditLogs/signIns?\$filter=userId+eq+'$BG'+and+createdDateTime+ge+$SINCE&\$top=50" -H "Authorization: Bearer $G" | tr -d '\000-\037')
N=$(echo "$RESP" | jq -r '.value | length' 2>/dev/null)
[ -z "$N" ] && { echo "[err] query"; exit 1; }
if [ "$N" -gt 0 ]; then
while IFS= read -r line; do
[ -z "$line" ] && continue
MSG="[SECURITY] BREAK-GLASS SIGN-IN - sysadmin@azcomputerguru.onmicrosoft.com used: $line -- investigate if unplanned."
if [ "${DRY_RUN:-0}" = "1" ]; then echo "WOULD POST: $MSG";
else bash "$ROOT/.claude/scripts/post-bot-alert.sh" "$MSG" >/dev/null 2>&1; echo "[alert] posted: $line"; fi
done < <(echo "$RESP" | jq -r '.value[] | "\(.createdDateTime) result=\(.status.errorCode) app=\(.appDisplayName) ip=\(.ipAddress) loc=\(.location.countryOrRegion)/\(.location.city // "?")"')
else
echo "[ok] no break-glass sign-ins since $SINCE"
fi
echo "$NOW" > "$STATE"

View File

@@ -0,0 +1,5 @@
@echo off
REM Wrapper for the ACG M365 break-glass sign-in alert. Invoked by the
REM "ACG-BreakGlass-SignIn-Alert" scheduled task. Uses a Git login shell so the
REM SOPS/age vault env loads, then runs the check from the repo root and logs output.
"C:\Program Files\Git\bin\bash.exe" -lc "cd /d/ClaudeTools && bash .claude/skills/remediation-tool/scripts/breakglass-signin-alert.sh >> .claude/skills/remediation-tool/scripts/.breakglass-alert.log 2>&1"

View File

@@ -13,7 +13,10 @@ UPN="${2:?usage: user-breach-check.sh <tenant-id|domain> <upn>}"
TENANT_ID=$("$SCRIPT_DIR/resolve-tenant.sh" "$TENANT_INPUT")
GT=$("$SCRIPT_DIR/get-token.sh" "$TENANT_ID" investigator)
EXO=$("$SCRIPT_DIR/get-token.sh" "$TENANT_ID" investigator-exo) || EXO=""
# Exchange REST (adminapi InvokeCommand) MUST use exchange-op: it holds Exchange.ManageAsApp
# + Exchange Administrator. investigator-exo 401s (Security Investigator app lacks ManageAsApp)
# — see references/gotchas.md "Exchange REST goes through exchange-op". (fixed 2026-07-16)
EXO=$("$SCRIPT_DIR/get-token.sh" "$TENANT_ID" exchange-op) || EXO=""
USER_SLUG=$(echo "$UPN" | tr '@.' '__')
OUT="/tmp/remediation-tool/$TENANT_ID/user-breach/$USER_SLUG"