#!/usr/bin/env bash # Run the 10-point breach check on a single user. # Usage: user-breach-check.sh # 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 }" UPN="${2:?usage: user-breach-check.sh }" 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"