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:
98
.claude/skills/remediation-tool/scripts/get-token.sh
Normal file
98
.claude/skills/remediation-tool/scripts/get-token.sh
Normal file
@@ -0,0 +1,98 @@
|
||||
#!/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).
|
||||
set -euo pipefail
|
||||
|
||||
CLIENT_ID="fabb3421-8b34-484b-bc17-e46de9703418"
|
||||
|
||||
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
|
||||
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
|
||||
SCRIPT_DIR="$(dirname "${BASH_SOURCE[0]}")"
|
||||
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 ;;
|
||||
esac
|
||||
|
||||
CACHE_DIR="/tmp/remediation-tool/$TENANT_ID"
|
||||
mkdir -p "$CACHE_DIR"
|
||||
CACHE_FILE="$CACHE_DIR/${SCOPE_NAME}.jwt"
|
||||
|
||||
# Reuse cache if less than 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.
|
||||
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; }
|
||||
|
||||
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; }
|
||||
|
||||
# Try vault.sh first; fall back to direct sops+python if vault.sh is broken (e.g. yq shim permission issues on Windows).
|
||||
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)
|
||||
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; }
|
||||
|
||||
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:'):
|
||||
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; }
|
||||
|
||||
# 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}" \
|
||||
--data-urlencode "grant_type=client_credentials")
|
||||
|
||||
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 "$RESP" >&2
|
||||
exit 5
|
||||
fi
|
||||
|
||||
echo "$TOKEN" > "$CACHE_FILE"
|
||||
chmod 600 "$CACHE_FILE" 2>/dev/null || true
|
||||
echo "$TOKEN"
|
||||
37
.claude/skills/remediation-tool/scripts/resolve-tenant.sh
Normal file
37
.claude/skills/remediation-tool/scripts/resolve-tenant.sh
Normal file
@@ -0,0 +1,37 @@
|
||||
#!/usr/bin/env bash
|
||||
# Resolve an M365 domain (or UPN) to a tenant GUID via OpenID discovery.
|
||||
# Usage: resolve-tenant.sh <domain-or-upn-or-tenantid>
|
||||
# Output (stdout): tenant GUID. Exit 0 on success, 1 on failure.
|
||||
set -euo pipefail
|
||||
|
||||
INPUT="${1:?usage: resolve-tenant.sh <domain|upn|tenant-id>}"
|
||||
|
||||
# If it looks like a GUID already, pass through.
|
||||
if [[ "$INPUT" =~ ^[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
|
||||
echo "$INPUT"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# If it's a UPN, strip to domain.
|
||||
DOMAIN="${INPUT#*@}"
|
||||
|
||||
# Lightweight cache keyed by domain.
|
||||
CACHE_DIR="/tmp/remediation-tool/_tenant-cache"
|
||||
mkdir -p "$CACHE_DIR"
|
||||
CACHE_FILE="$CACHE_DIR/${DOMAIN}.txt"
|
||||
if [[ -f "$CACHE_FILE" ]] && [[ $(find "$CACHE_FILE" -mmin -1440 2>/dev/null) ]]; then
|
||||
cat "$CACHE_FILE"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# OpenID discovery — parse issuer URL for tenant GUID.
|
||||
RESP=$(curl -s --max-time 10 "https://login.microsoftonline.com/${DOMAIN}/v2.0/.well-known/openid-configuration")
|
||||
TENANT_ID=$(echo "$RESP" | jq -r '.issuer // empty' | sed -E 's|^https://login\.microsoftonline\.com/||;s|/v2\.0/?$||' || true)
|
||||
|
||||
if [[ -z "$TENANT_ID" ]] || [[ ! "$TENANT_ID" =~ ^[0-9a-fA-F]{8}- ]]; then
|
||||
echo "ERROR: could not resolve tenant for domain: $DOMAIN" >&2
|
||||
echo "Response: $RESP" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "$TENANT_ID" | tee "$CACHE_FILE"
|
||||
82
.claude/skills/remediation-tool/scripts/tenant-sweep.sh
Normal file
82
.claude/skills/remediation-tool/scripts/tenant-sweep.sh
Normal file
@@ -0,0 +1,82 @@
|
||||
#!/usr/bin/env bash
|
||||
# Tenant-wide signals sweep: failed sign-ins, foreign successful sign-ins, directory audits,
|
||||
# risky users, B2B guest invites, per-user location profile.
|
||||
# Usage: tenant-sweep.sh <tenant-id-or-domain>
|
||||
# Writes raw JSON to /tmp/remediation-tool/{tenant-id}/sweep/
|
||||
# Prints a priority summary to stdout.
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR="$(dirname "${BASH_SOURCE[0]}")"
|
||||
TENANT_INPUT="${1:?usage: tenant-sweep.sh <tenant-id|domain>}"
|
||||
TENANT_ID=$("$SCRIPT_DIR/resolve-tenant.sh" "$TENANT_INPUT")
|
||||
GT=$("$SCRIPT_DIR/get-token.sh" "$TENANT_ID" graph)
|
||||
|
||||
OUT="/tmp/remediation-tool/$TENANT_ID/sweep"
|
||||
mkdir -p "$OUT"
|
||||
FROM=$(date -u -d '30 days ago' +%Y-%m-%dT%H:%M:%SZ 2>/dev/null || date -u -v-30d +%Y-%m-%dT%H:%M:%SZ)
|
||||
echo "[info] tenant=$TENANT_ID window=30d from=$FROM"
|
||||
|
||||
# Enabled users list
|
||||
curl -s -H "Authorization: Bearer $GT" \
|
||||
"https://graph.microsoft.com/v1.0/users?\$top=999&\$filter=accountEnabled%20eq%20true&\$select=id,displayName,userPrincipalName,accountEnabled,userType,externalUserState,lastPasswordChangeDateTime,createdDateTime" \
|
||||
> "$OUT/users.json" &
|
||||
|
||||
# Failed sign-ins tenant-wide
|
||||
curl -s -H "Authorization: Bearer $GT" \
|
||||
"https://graph.microsoft.com/v1.0/auditLogs/signIns?\$filter=(createdDateTime%20ge%20${FROM})%20and%20(status/errorCode%20ne%200)&\$top=999" \
|
||||
> "$OUT/failed_signins.json" &
|
||||
|
||||
# Successful sign-ins tenant-wide (to find non-US)
|
||||
curl -s -H "Authorization: Bearer $GT" \
|
||||
"https://graph.microsoft.com/v1.0/auditLogs/signIns?\$filter=(createdDateTime%20ge%20${FROM})%20and%20(status/errorCode%20eq%200)&\$top=999" \
|
||||
> "$OUT/success_signins.json" &
|
||||
|
||||
# Directory audits, filtered by risky activity names
|
||||
curl -s -H "Authorization: Bearer $GT" \
|
||||
"https://graph.microsoft.com/v1.0/auditLogs/directoryAudits?\$filter=activityDateTime%20ge%20${FROM}&\$top=999" \
|
||||
> "$OUT/dir_audits.json" &
|
||||
|
||||
# Risky users (may 403 if IdentityRiskyUser scope absent)
|
||||
curl -s -H "Authorization: Bearer $GT" \
|
||||
"https://graph.microsoft.com/v1.0/identityProtection/riskyUsers?\$top=100" \
|
||||
> "$OUT/risky_users.json" &
|
||||
|
||||
# B2B guest invites
|
||||
curl -s -H "Authorization: Bearer $GT" \
|
||||
"https://graph.microsoft.com/v1.0/auditLogs/directoryAudits?\$filter=activityDateTime%20ge%20${FROM}%20and%20activityDisplayName%20eq%20'Invite%20external%20user'&\$top=100" \
|
||||
> "$OUT/guest_invites.json" &
|
||||
|
||||
wait
|
||||
|
||||
echo ""
|
||||
echo "=== Priority 1: accounts with foreign failed sign-ins (credential stuffing candidates) ==="
|
||||
jq '[.value[] | select(.location.countryOrRegion != "US" and .location.countryOrRegion != null) | {user: .userPrincipalName, ip: .ipAddress, country: .location.countryOrRegion, city: .location.city, t: .createdDateTime, err: .status.errorCode, fail: .status.failureReason}] | group_by(.user) | map({user: .[0].user, attempts: length, unique_ips: ([.[]|.ip]|unique|length), countries: ([.[]|.country]|unique), first: ([.[]|.t]|min), last: ([.[]|.t]|max)}) | sort_by(-.attempts)' "$OUT/failed_signins.json"
|
||||
|
||||
echo ""
|
||||
echo "=== Priority 2: successful sign-ins from non-US (suspicious) ==="
|
||||
jq '[.value[] | select(.location.countryOrRegion != "US" and .location.countryOrRegion != null) | {user: .userPrincipalName, ip: .ipAddress, country: .location.countryOrRegion, city: .location.city, t: .createdDateTime, app: .appDisplayName, clientApp: .clientAppUsed}] | sort_by(.t) | reverse | .[:30]' "$OUT/success_signins.json"
|
||||
|
||||
echo ""
|
||||
echo "=== Priority 3: B2B guest invites (30d) ==="
|
||||
jq '[.value[] | {t: .activityDateTime, by: (.initiatedBy.user.userPrincipalName // .initiatedBy.app.displayName), target: [.targetResources[]?|{name: .displayName, upn: .userPrincipalName}], result: .result}] | sort_by(.t) | reverse' "$OUT/guest_invites.json"
|
||||
|
||||
echo ""
|
||||
echo "=== Priority 4: directory audit - consent/role/auth-method changes ==="
|
||||
jq '[.value[] | select(.activityDisplayName | test("[Cc]onsent|[Aa]uthentication [Mm]ethod|Add service principal|Add delegated permission grant|Add app role|Add member to role"; "")) | {t: .activityDateTime, act: .activityDisplayName, by: (.initiatedBy.user.userPrincipalName // .initiatedBy.app.displayName // "system"), target: [.targetResources[]?|{type: .type, name: .displayName, upn: .userPrincipalName}], result: .result}] | sort_by(.t) | reverse | .[:50]' "$OUT/dir_audits.json"
|
||||
|
||||
echo ""
|
||||
echo "=== Risky users (if Identity Protection accessible) ==="
|
||||
if jq -e '.error' "$OUT/risky_users.json" >/dev/null 2>&1; then
|
||||
echo "BLOCKED: $(jq -r '.error.code // "?"' "$OUT/risky_users.json") — $(jq -r '.error.message // ""' "$OUT/risky_users.json")"
|
||||
echo "(Check references/gotchas.md for how to unblock IdentityRiskyUser scope)"
|
||||
else
|
||||
jq '[.value[] | {upn: .userPrincipalName, level: .riskLevel, state: .riskState, detail: .riskDetail, lastUpdated: .riskLastUpdatedDateTime}]' "$OUT/risky_users.json"
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "=== User locations profile (successful sign-ins) ==="
|
||||
jq '[.value[] | {user: .userPrincipalName, country: .location.countryOrRegion, city: .location.city}] | unique | group_by(.user) | map({user: .[0].user, locations: [.[]|{country, city}]|unique})' "$OUT/success_signins.json"
|
||||
|
||||
echo ""
|
||||
echo "[info] Enabled users in tenant: $(jq '.value | length' "$OUT/users.json")"
|
||||
echo "[info] raw artifacts: $OUT"
|
||||
141
.claude/skills/remediation-tool/scripts/user-breach-check.sh
Normal file
141
.claude/skills/remediation-tool/scripts/user-breach-check.sh
Normal file
@@ -0,0 +1,141 @@
|
||||
#!/usr/bin/env bash
|
||||
# Run the 10-point breach check on a single user.
|
||||
# Usage: user-breach-check.sh <tenant-id-or-domain> <upn>
|
||||
# Writes raw JSON to /tmp/remediation-tool/{tenant-id}/user-breach/{user-slug}/
|
||||
# Prints a summary table to stdout.
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR="$(dirname "${BASH_SOURCE[0]}")"
|
||||
|
||||
TENANT_INPUT="${1:?usage: user-breach-check.sh <tenant-id|domain> <upn>}"
|
||||
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" graph)
|
||||
EXO=$("$SCRIPT_DIR/get-token.sh" "$TENANT_ID" exchange) || EXO=""
|
||||
|
||||
USER_SLUG=$(echo "$UPN" | tr '@.' '__')
|
||||
OUT="/tmp/remediation-tool/$TENANT_ID/user-breach/$USER_SLUG"
|
||||
mkdir -p "$OUT"
|
||||
|
||||
echo "[info] tenant=$TENANT_ID user=$UPN out=$OUT"
|
||||
|
||||
# --- 0. Resolve user object ID ---
|
||||
curl -s -H "Authorization: Bearer $GT" \
|
||||
"https://graph.microsoft.com/v1.0/users/${UPN}?\$select=id,displayName,userPrincipalName,mail,accountEnabled,createdDateTime,lastPasswordChangeDateTime" \
|
||||
> "$OUT/00_user.json"
|
||||
UID_=$(jq -r '.id // empty' "$OUT/00_user.json")
|
||||
if [[ -z "$UID_" ]]; then
|
||||
echo "ERROR: user not found or Graph returned error" >&2
|
||||
cat "$OUT/00_user.json" >&2
|
||||
exit 1
|
||||
fi
|
||||
echo "[info] object id: $UID_"
|
||||
|
||||
# --- 1. Inbox rules (Graph v1.0 — visible only) ---
|
||||
curl -s -H "Authorization: Bearer $GT" \
|
||||
"https://graph.microsoft.com/v1.0/users/${UPN}/mailFolders/inbox/messageRules" \
|
||||
> "$OUT/01_inbox_rules_graph.json" &
|
||||
|
||||
# --- 2. Mailbox settings (forwarding flags) ---
|
||||
curl -s -H "Authorization: Bearer $GT" \
|
||||
"https://graph.microsoft.com/v1.0/users/${UPN}/mailboxSettings" \
|
||||
> "$OUT/02_mailbox_settings.json" &
|
||||
|
||||
# --- 4. OAuth consents + app role assignments ---
|
||||
curl -s -H "Authorization: Bearer $GT" \
|
||||
"https://graph.microsoft.com/v1.0/users/${UPN}/oauth2PermissionGrants" \
|
||||
> "$OUT/04a_oauth_grants.json" &
|
||||
curl -s -H "Authorization: Bearer $GT" \
|
||||
"https://graph.microsoft.com/v1.0/users/${UPN}/appRoleAssignments" \
|
||||
> "$OUT/04b_app_role_assignments.json" &
|
||||
|
||||
# --- 5. Authentication methods ---
|
||||
curl -s -H "Authorization: Bearer $GT" \
|
||||
"https://graph.microsoft.com/v1.0/users/${UPN}/authentication/methods" \
|
||||
> "$OUT/05_auth_methods.json" &
|
||||
|
||||
wait
|
||||
|
||||
# --- 6. Sign-ins 30d (v1.0 — interactive only) ---
|
||||
FROM=$(date -u -d '30 days ago' +%Y-%m-%dT%H:%M:%SZ 2>/dev/null || date -u -v-30d +%Y-%m-%dT%H:%M:%SZ)
|
||||
curl -s -H "Authorization: Bearer $GT" \
|
||||
"https://graph.microsoft.com/v1.0/auditLogs/signIns?\$filter=userId%20eq%20'${UID_}'%20and%20createdDateTime%20ge%20${FROM}&\$top=200" \
|
||||
> "$OUT/06_signins.json" &
|
||||
|
||||
# --- 7. Directory audits (targetResources = user) 30d ---
|
||||
curl -s -H "Authorization: Bearer $GT" \
|
||||
"https://graph.microsoft.com/v1.0/auditLogs/directoryAudits?\$filter=activityDateTime%20ge%20${FROM}%20and%20targetResources/any(t:t/id%20eq%20'${UID_}')&\$top=200" \
|
||||
> "$OUT/07_dir_audits.json" &
|
||||
|
||||
# --- 8. Risky user + risk detections (403 if app lacks IdentityRiskyUser scope) ---
|
||||
curl -s -H "Authorization: Bearer $GT" \
|
||||
"https://graph.microsoft.com/v1.0/identityProtection/riskyUsers/${UID_}" \
|
||||
> "$OUT/08a_risky_user.json" &
|
||||
curl -s -H "Authorization: Bearer $GT" \
|
||||
"https://graph.microsoft.com/v1.0/identityProtection/riskDetections?\$filter=userId%20eq%20'${UID_}'&\$top=100" \
|
||||
> "$OUT/08b_risk_detections.json" &
|
||||
|
||||
# --- 9. Sent items (last 25) ---
|
||||
curl -s -H "Authorization: Bearer $GT" \
|
||||
"https://graph.microsoft.com/v1.0/users/${UPN}/mailFolders/sentitems/messages?\$top=25&\$orderby=sentDateTime%20desc&\$select=subject,toRecipients,sentDateTime,from" \
|
||||
> "$OUT/09_sent.json" &
|
||||
|
||||
# --- 10. Deleted items (last 25) ---
|
||||
curl -s -H "Authorization: Bearer $GT" \
|
||||
"https://graph.microsoft.com/v1.0/users/${UPN}/mailFolders/deleteditems/messages?\$top=25&\$orderby=receivedDateTime%20desc&\$select=subject,from,receivedDateTime" \
|
||||
> "$OUT/10_deleted.json" &
|
||||
|
||||
wait
|
||||
|
||||
# --- 3. Exchange REST (hidden rules + delegates + SendAs + Get-Mailbox) ---
|
||||
if [[ -n "$EXO" ]]; then
|
||||
EX_URL="https://outlook.office365.com/adminapi/beta/${TENANT_ID}/InvokeCommand"
|
||||
|
||||
curl -s -X POST -H "Authorization: Bearer $EXO" -H "Content-Type: application/json" "$EX_URL" \
|
||||
-d "{\"CmdletInput\":{\"CmdletName\":\"Get-InboxRule\",\"Parameters\":{\"Mailbox\":\"${UPN}\",\"IncludeHidden\":true}}}" \
|
||||
> "$OUT/03a_InboxRule_hidden.json" &
|
||||
|
||||
curl -s -X POST -H "Authorization: Bearer $EXO" -H "Content-Type: application/json" "$EX_URL" \
|
||||
-d "{\"CmdletInput\":{\"CmdletName\":\"Get-MailboxPermission\",\"Parameters\":{\"Identity\":\"${UPN}\"}}}" \
|
||||
> "$OUT/03b_MailboxPermission.json" &
|
||||
|
||||
curl -s -X POST -H "Authorization: Bearer $EXO" -H "Content-Type: application/json" "$EX_URL" \
|
||||
-d "{\"CmdletInput\":{\"CmdletName\":\"Get-RecipientPermission\",\"Parameters\":{\"Identity\":\"${UPN}\"}}}" \
|
||||
> "$OUT/03c_RecipientPermission.json" &
|
||||
|
||||
curl -s -X POST -H "Authorization: Bearer $EXO" -H "Content-Type: application/json" "$EX_URL" \
|
||||
-d "{\"CmdletInput\":{\"CmdletName\":\"Get-Mailbox\",\"Parameters\":{\"Identity\":\"${UPN}\"}}}" \
|
||||
> "$OUT/03d_Mailbox.json" &
|
||||
|
||||
wait
|
||||
else
|
||||
echo "[warn] no Exchange token; skipping check 3 (hidden rules/delegates/SendAs/mailbox forwarding flags)"
|
||||
fi
|
||||
|
||||
# --- Summary table ---
|
||||
echo ""
|
||||
echo "=== Summary: $UPN ==="
|
||||
jq -r '"account_enabled: \(.accountEnabled) lastPwChange: \(.lastPasswordChangeDateTime) created: \(.createdDateTime)"' "$OUT/00_user.json"
|
||||
echo "01 inbox_rules (Graph): $(jq '.value | length // "error"' "$OUT/01_inbox_rules_graph.json")"
|
||||
echo "02 forwarding: fwdSmtp=$(jq -r '.automaticRepliesSetting.status // "n/a"' "$OUT/02_mailbox_settings.json" 2>/dev/null) (see mailbox Get-Mailbox for forwarding fields)"
|
||||
echo "04a oauth_grants: $(jq '.value | length // "error"' "$OUT/04a_oauth_grants.json")"
|
||||
echo "04b app_role_assignments: $(jq '.value | length // "error"' "$OUT/04b_app_role_assignments.json")"
|
||||
echo "05 auth_methods: $(jq '.value | length // "error"' "$OUT/05_auth_methods.json")"
|
||||
echo "06 signins (30d, interactive): $(jq '.value | length // "error"' "$OUT/06_signins.json") non-US: $(jq '[.value[]?|select(.location.countryOrRegion != "US" and .location.countryOrRegion != null)] | length' "$OUT/06_signins.json" 2>/dev/null)"
|
||||
echo "07 dir_audits (30d): $(jq '.value | length // "error"' "$OUT/07_dir_audits.json")"
|
||||
echo "08 risky_user: $(jq -r '.riskLevel // .error.code // "none"' "$OUT/08a_risky_user.json" 2>/dev/null)"
|
||||
echo "08 risk_detections: $(jq '.value | length // (.error.code // "error")' "$OUT/08b_risk_detections.json")"
|
||||
echo "09 sent (recent 25): $(jq '.value | length // "error"' "$OUT/09_sent.json")"
|
||||
echo "10 deleted (recent 25): $(jq '.value | length // "error"' "$OUT/10_deleted.json")"
|
||||
if [[ -f "$OUT/03a_InboxRule_hidden.json" ]]; then
|
||||
HIDDEN=$(jq '.value | length // (.error.code // "?")' "$OUT/03a_InboxRule_hidden.json" 2>/dev/null || echo "?")
|
||||
echo "03a hidden_inbox_rules: $HIDDEN"
|
||||
echo "03b mailbox_permissions: $(jq '[.value[]? | select(.User != "NT AUTHORITY\\SELF")] | length // "?"' "$OUT/03b_MailboxPermission.json" 2>/dev/null) non-SELF"
|
||||
echo "03c send_as: $(jq '[.value[]? | select(.Trustee != "NT AUTHORITY\\SELF")] | length // "?"' "$OUT/03c_RecipientPermission.json" 2>/dev/null) non-SELF"
|
||||
echo "03d mailbox_forwarding: fwdAddr=$(jq -r '.value[0].ForwardingAddress // "null"' "$OUT/03d_Mailbox.json" 2>/dev/null) fwdSmtp=$(jq -r '.value[0].ForwardingSmtpAddress // "null"' "$OUT/03d_Mailbox.json" 2>/dev/null)"
|
||||
else
|
||||
echo "03 exchange_rest: SKIPPED (no exchange token — tenant likely needs Exchange Admin role assigned)"
|
||||
fi
|
||||
echo ""
|
||||
echo "[info] raw artifacts: $OUT"
|
||||
Reference in New Issue
Block a user