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,17 +1,23 @@
#!/usr/bin/env bash
# Acquire a client-credentials token for the Claude-MSP-Access (ComputerGuru - AI Remediation) app.
# Usage: get-token.sh <tenant-id-or-domain> <scope>
# <scope>: graph | exchange | defender | sharepoint
# Output (stdout): token. Exit 0 on success.
# Cache: /tmp/remediation-tool/{tenant-id}/{scope}.jwt (55-min TTL).
# Acquire a client-credentials bearer token for a ComputerGuru MSP app tier.
# Usage: get-token.sh <tenant-id-or-domain> <tier>
#
# Tiers and their app + resource scope:
# investigator ComputerGuru Security Investigator -> Graph API (read-only breach checks)
# investigator-exo ComputerGuru Security Investigator -> Exchange Online (EXO read: Get-InboxRule, Get-Mailbox)
# exchange-op ComputerGuru Exchange Operator -> Exchange Online (EXO write: Set-Mailbox, Remove-InboxRule, revoke sessions)
# user-manager ComputerGuru User Manager -> Graph API (user create/update/disable, license assign, MFA reset)
# tenant-admin ComputerGuru Tenant Admin -> Graph API (app roles, CA policy, directory write — high privilege)
# defender ComputerGuru Defender Add-on -> Defender ATP API (MDE-licensed tenants only)
#
# Output (stdout): bearer token. Exits 0 on success.
# Cache: /tmp/remediation-tool/{tenant-id}/{tier}.jwt TTL 55 minutes.
set -euo pipefail
CLIENT_ID="fabb3421-8b34-484b-bc17-e46de9703418"
TARGET="${1:?usage: get-token.sh <tenant-id|domain> <tier>}"
TIER="${2:?usage: get-token.sh <tenant-id|domain> <tier>}"
TARGET="${1:?usage: get-token.sh <tenant-id|domain> <scope>}"
SCOPE_NAME="${2:?usage: get-token.sh <tenant-id|domain> <scope>}"
# Resolve to tenant-id
# Resolve domain to tenant GUID if needed
if [[ "$TARGET" =~ ^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$ ]]; then
TENANT_ID="$TARGET"
else
@@ -19,67 +25,103 @@ else
TENANT_ID=$("$SCRIPT_DIR/resolve-tenant.sh" "$TARGET")
fi
case "$SCOPE_NAME" in
graph) SCOPE_URL="https://graph.microsoft.com/.default" ;;
exchange) SCOPE_URL="https://outlook.office365.com/.default" ;;
defender) SCOPE_URL="https://api.securitycenter.microsoft.com/.default" ;;
sharepoint)
# SharePoint token scope depends on tenant hostname. Caller must set SHAREPOINT_HOST=contoso.sharepoint.com.
SCOPE_URL="https://${SHAREPOINT_HOST:?set SHAREPOINT_HOST for sharepoint scope}/.default" ;;
*) echo "ERROR: unknown scope '$SCOPE_NAME'. Expected: graph|exchange|defender|sharepoint" >&2; exit 2 ;;
# Map tier -> client_id, vault SOPS path, resource scope
case "$TIER" in
investigator)
CLIENT_ID="bfbc12a4-f0dd-4e12-b06d-997e7271e10c"
VAULT_PATH="msp-tools/computerguru-security-investigator.sops.yaml"
SCOPE_URL="https://graph.microsoft.com/.default"
;;
investigator-exo)
CLIENT_ID="bfbc12a4-f0dd-4e12-b06d-997e7271e10c"
VAULT_PATH="msp-tools/computerguru-security-investigator.sops.yaml"
SCOPE_URL="https://outlook.office365.com/.default"
;;
exchange-op)
CLIENT_ID="b43e7342-5b4b-492f-890f-bb5a4f7f40e9"
VAULT_PATH="msp-tools/computerguru-exchange-operator.sops.yaml"
SCOPE_URL="https://outlook.office365.com/.default"
;;
user-manager)
CLIENT_ID="64fac46b-8b44-41ad-93ee-7da03927576c"
VAULT_PATH="msp-tools/computerguru-user-manager.sops.yaml"
SCOPE_URL="https://graph.microsoft.com/.default"
;;
tenant-admin)
CLIENT_ID="709e6eed-0711-4875-9c44-2d3518c47063"
VAULT_PATH="msp-tools/computerguru-tenant-admin.sops.yaml"
SCOPE_URL="https://graph.microsoft.com/.default"
;;
defender)
CLIENT_ID="dbf8ad1a-54f4-4bb8-8a9e-ea5b9634635b"
VAULT_PATH="msp-tools/computerguru-defender-addon.sops.yaml"
SCOPE_URL="https://api.securitycenter.microsoft.com/.default"
;;
*)
echo "ERROR: unknown tier '$TIER'." >&2
echo "Valid tiers: investigator | investigator-exo | exchange-op | user-manager | tenant-admin | defender" >&2
exit 2
;;
esac
CACHE_DIR="/tmp/remediation-tool/$TENANT_ID"
mkdir -p "$CACHE_DIR"
CACHE_FILE="$CACHE_DIR/${SCOPE_NAME}.jwt"
CACHE_FILE="$CACHE_DIR/${TIER}.jwt"
# Reuse cache if less than 55 minutes old.
# Return cached token if < 55 minutes old
if [[ -f "$CACHE_FILE" ]] && [[ $(find "$CACHE_FILE" -mmin -55 2>/dev/null) ]]; then
cat "$CACHE_FILE"
exit 0
fi
# Locate the vault repo.
# Locate vault repo
VAULT_ROOT=""
for candidate in "D:/vault" "$HOME/vault" "/d/vault"; do
[[ -d "$candidate" ]] && VAULT_ROOT="$candidate" && break
done
[[ -z "$VAULT_ROOT" ]] && { echo "ERROR: SOPS vault repo not found at D:/vault or ~/vault" >&2; exit 3; }
[[ -z "$VAULT_ROOT" ]] && { echo "ERROR: SOPS vault not found (tried D:/vault ~/vault /d/vault)" >&2; exit 3; }
SOPS_FILE="$VAULT_ROOT/msp-tools/claude-msp-access-graph-api.sops.yaml"
[[ ! -f "$SOPS_FILE" ]] && { echo "ERROR: SOPS file not found: $SOPS_FILE" >&2; exit 3; }
SOPS_FILE="$VAULT_ROOT/$VAULT_PATH"
[[ ! -f "$SOPS_FILE" ]] && { echo "ERROR: vault file not found: $SOPS_FILE" >&2; exit 3; }
# Try vault.sh first; fall back to direct sops+python if vault.sh is broken (e.g. yq shim permission issues on Windows).
# Read client secret via vault.sh (fast path), falling back to raw sops+python
CLIENT_SECRET=""
if [[ -f "$VAULT_ROOT/scripts/vault.sh" ]]; then
CLIENT_SECRET=$(bash "$VAULT_ROOT/scripts/vault.sh" get-field msp-tools/claude-msp-access-graph-api.sops.yaml credentials.credential 2>/dev/null | tr -d '\r\n' || true)
# Try both field names — new files use client_secret, legacy used credential
CLIENT_SECRET=$(bash "$VAULT_ROOT/scripts/vault.sh" get-field "$VAULT_PATH" credentials.client_secret 2>/dev/null | tr -d '\r\n' || true)
if [[ -z "$CLIENT_SECRET" ]]; then
CLIENT_SECRET=$(bash "$VAULT_ROOT/scripts/vault.sh" get-field "$VAULT_PATH" credentials.credential 2>/dev/null | tr -d '\r\n' || true)
fi
fi
if [[ -z "$CLIENT_SECRET" ]]; then
# Direct fallback: sops decrypt + python YAML parse. Works without vault.sh / yq.
PYTHON_BIN=""
for p in python python3 py; do command -v "$p" >/dev/null 2>&1 && PYTHON_BIN="$p" && break; done
[[ -z "$PYTHON_BIN" ]] && { echo "ERROR: neither vault.sh worked nor python is available for fallback parse" >&2; exit 3; }
command -v sops >/dev/null 2>&1 || { echo "ERROR: sops not on PATH (needed for fallback)" >&2; exit 3; }
for p in python3 python py; do command -v "$p" >/dev/null 2>&1 && PYTHON_BIN="$p" && break; done
[[ -z "$PYTHON_BIN" ]] && { echo "ERROR: vault.sh failed and python unavailable for SOPS fallback" >&2; exit 3; }
command -v sops >/dev/null 2>&1 || { echo "ERROR: sops not on PATH (needed for fallback decrypt)" >&2; exit 3; }
CLIENT_SECRET=$(sops -d "$SOPS_FILE" 2>/dev/null | "$PYTHON_BIN" -c "
import sys, re
t = sys.stdin.read()
# minimal YAML: find 'credentials:' block then 'credential:' key
m = re.search(r'^credentials:\s*\n((?:[ \t]+.*\n)+)', t, re.MULTILINE)
if not m: sys.exit(1)
for line in m.group(1).splitlines():
line = line.strip()
if line.startswith('credential:'):
if line.startswith('client_secret:') or line.startswith('credential:'):
print(line.split(':', 1)[1].strip().strip('\"').strip(\"'\"))
break
" | tr -d '\r\n')
fi
[[ -z "$CLIENT_SECRET" ]] && { echo "ERROR: could not read client secret from vault (vault.sh and sops+python fallback both failed)" >&2; exit 4; }
[[ -z "$CLIENT_SECRET" ]] && {
echo "ERROR: could not read secret from $VAULT_PATH" >&2
echo " Check field: credentials.client_secret (or credentials.credential for older entries)" >&2
exit 4
}
# Request token.
RESP=$(curl -s --max-time 15 -X POST "https://login.microsoftonline.com/${TENANT_ID}/oauth2/v2.0/token" \
# Request token
RESP=$(curl -s --max-time 15 -X POST \
"https://login.microsoftonline.com/${TENANT_ID}/oauth2/v2.0/token" \
--data-urlencode "client_id=${CLIENT_ID}" \
--data-urlencode "client_secret=${CLIENT_SECRET}" \
--data-urlencode "scope=${SCOPE_URL}" \
@@ -88,7 +130,7 @@ RESP=$(curl -s --max-time 15 -X POST "https://login.microsoftonline.com/${TENANT
TOKEN=$(echo "$RESP" | jq -r '.access_token // empty')
if [[ -z "$TOKEN" ]]; then
echo "ERROR: token request failed for tenant=$TENANT_ID scope=$SCOPE_NAME" >&2
echo "ERROR: token request failed (tenant=$TENANT_ID tier=$TIER)" >&2
echo "$RESP" >&2
exit 5
fi