Sync all scripts from website downloads — 352 scripts total
Includes updated JS challenge scripts with Claude-User whitelist, same-site referer bypass, Blackbox-Exporter allowed bot, and all new exporters, cheat sheets, and automation scripts.
This commit is contained in:
Executable
+649
@@ -0,0 +1,649 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
#########################################################################################
|
||||
#### azure-ad-audit.sh — Audit Azure Entra ID for stale users, MFA gaps, risky ####
|
||||
#### sign-ins, excessive permissions, and service principal hygiene via az CLI ####
|
||||
#### Requires: bash 4+, az CLI, jq ####
|
||||
#### ####
|
||||
#### Author: Phil Connor ####
|
||||
#### Contact: contact@mylinux.work ####
|
||||
#### License: MIT ####
|
||||
#### Version 1.01 ####
|
||||
#### ####
|
||||
#### Usage: ####
|
||||
#### ./azure-ad-audit.sh --full ####
|
||||
#### ####
|
||||
#### See --help for all options. ####
|
||||
#########################################################################################
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
# ── Colors (pre-initialized) ─────────────────────────────────────────
|
||||
RED="" GREEN="" YELLOW="" BLUE="" CYAN="" BOLD="" DIM="" RESET=""
|
||||
|
||||
setup_colors() {
|
||||
if [[ "${COLOR:-auto}" == "never" ]]; then
|
||||
return
|
||||
fi
|
||||
if [[ "${COLOR:-auto}" == "always" ]] || [[ -t 1 ]]; then
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[0;33m'
|
||||
BLUE='\033[0;34m'
|
||||
CYAN='\033[0;36m'
|
||||
BOLD='\033[1m'
|
||||
DIM='\033[2m'
|
||||
RESET='\033[0m'
|
||||
fi
|
||||
}
|
||||
|
||||
# ── Logging ───────────────────────────────────────────────────────────
|
||||
log() { echo -e "${BLUE}[INFO]${RESET} $*"; }
|
||||
warn() { echo -e "${YELLOW}[WARN]${RESET} $*" >&2; }
|
||||
err() { echo -e "${RED}[ERROR]${RESET} $*" >&2; }
|
||||
verbose() { if [[ "$VERBOSE" == "true" ]]; then echo -e "${DIM}[DEBUG]${RESET} $*"; fi; }
|
||||
die() { err "$*"; exit 1; }
|
||||
|
||||
# ── Severity counters ────────────────────────────────────────────────
|
||||
TOTAL_CRIT=0
|
||||
TOTAL_WARN=0
|
||||
TOTAL_INFO=0
|
||||
TOTAL_OK=0
|
||||
|
||||
flag_crit() { ((TOTAL_CRIT++)) || true; }
|
||||
flag_warn() { ((TOTAL_WARN++)) || true; }
|
||||
flag_info() { ((TOTAL_INFO++)) || true; }
|
||||
flag_ok() { ((TOTAL_OK++)) || true; }
|
||||
|
||||
# ── Defaults ──────────────────────────────────────────────────────────
|
||||
RUN_MODE=""
|
||||
STALE_DAYS="${STALE_DAYS:-90}"
|
||||
VERBOSE="${VERBOSE:-false}"
|
||||
COLOR="${COLOR:-auto}"
|
||||
|
||||
# ── State ─────────────────────────────────────────────────────────────
|
||||
SCRIPT_NAME="$(basename "$0")"
|
||||
readonly SCRIPT_NAME
|
||||
START_TIME=""
|
||||
|
||||
# ── Dependency checks ────────────────────────────────────────────────
|
||||
check_deps() {
|
||||
command -v az &>/dev/null || die "az CLI is required (https://aka.ms/install-azure-cli)"
|
||||
command -v jq &>/dev/null || die "jq is required"
|
||||
}
|
||||
|
||||
check_credentials() {
|
||||
if ! az account show &>/dev/null 2>&1; then
|
||||
die "Not logged in to Azure CLI — run 'az login' first"
|
||||
fi
|
||||
verbose "Azure CLI authenticated"
|
||||
}
|
||||
|
||||
# ── Date helpers ─────────────────────────────────────────────────────
|
||||
days_since() {
|
||||
local date_str="$1"
|
||||
if [[ -z "$date_str" || "$date_str" == "null" || "$date_str" == "None" ]]; then
|
||||
echo "never"
|
||||
return
|
||||
fi
|
||||
local then_epoch now_epoch
|
||||
then_epoch=$(date -d "${date_str}" +%s 2>/dev/null || echo 0)
|
||||
now_epoch=$(date +%s)
|
||||
if [[ "$then_epoch" -eq 0 ]]; then
|
||||
echo "unknown"
|
||||
return
|
||||
fi
|
||||
echo $(( (now_epoch - then_epoch) / 86400 ))
|
||||
}
|
||||
|
||||
days_until() {
|
||||
local date_str="$1"
|
||||
if [[ -z "$date_str" || "$date_str" == "null" || "$date_str" == "None" ]]; then
|
||||
echo "unknown"
|
||||
return
|
||||
fi
|
||||
local target_epoch now_epoch
|
||||
target_epoch=$(date -d "${date_str}" +%s 2>/dev/null || echo 0)
|
||||
now_epoch=$(date +%s)
|
||||
if [[ "$target_epoch" -eq 0 ]]; then
|
||||
echo "unknown"
|
||||
return
|
||||
fi
|
||||
echo $(( (target_epoch - now_epoch) / 86400 ))
|
||||
}
|
||||
|
||||
# ══════════════════════════════════════════════════════════════════════
|
||||
# STALE USERS AUDIT
|
||||
# ══════════════════════════════════════════════════════════════════════
|
||||
audit_stale_users() {
|
||||
log "Auditing stale user accounts (threshold: ${STALE_DAYS} days)..."
|
||||
echo ""
|
||||
|
||||
printf " %-36s %-24s %-20s %-10s %s\n" \
|
||||
"UPN" "DISPLAY_NAME" "LAST_SIGN_IN" "DAYS_IDLE" "SEVERITY"
|
||||
printf " %s\n" "$(printf '%.0s─' {1..110})"
|
||||
|
||||
local users_json
|
||||
# shellcheck disable=SC2016
|
||||
users_json=$(az rest --method GET \
|
||||
--url 'https://graph.microsoft.com/v1.0/users?$select=userPrincipalName,displayName,signInActivity,accountEnabled&$top=999' \
|
||||
2>/dev/null || echo '{"value":[]}')
|
||||
|
||||
echo "$users_json" | jq -c '.value[]? // empty' 2>/dev/null | while IFS= read -r user; do
|
||||
local upn display_name enabled last_sign_in
|
||||
upn=$(echo "$user" | jq -r '.userPrincipalName // "unknown"' 2>/dev/null)
|
||||
display_name=$(echo "$user" | jq -r '.displayName // "unknown"' 2>/dev/null)
|
||||
enabled=$(echo "$user" | jq -r '.accountEnabled // true' 2>/dev/null)
|
||||
last_sign_in=$(echo "$user" | jq -r '.signInActivity.lastSignInDateTime // "null"' 2>/dev/null)
|
||||
|
||||
[[ "$enabled" == "false" ]] && continue
|
||||
|
||||
local idle_days last_sign_display
|
||||
idle_days=$(days_since "$last_sign_in")
|
||||
|
||||
if [[ "$last_sign_in" == "null" || -z "$last_sign_in" ]]; then
|
||||
last_sign_display="Never"
|
||||
else
|
||||
last_sign_display="${last_sign_in:0:10}"
|
||||
fi
|
||||
|
||||
if [[ "$idle_days" == "never" ]]; then
|
||||
printf " %-36s %-24s %-20s %-10s %b%s%b\n" \
|
||||
"${upn:0:34}" "${display_name:0:22}" "$last_sign_display" \
|
||||
"N/A" "$YELLOW" "WARN" "$RESET"
|
||||
flag_warn
|
||||
elif [[ "$idle_days" == "unknown" ]]; then
|
||||
verbose "Skipping ${upn}: unable to parse sign-in date"
|
||||
elif [[ "$idle_days" -gt "$STALE_DAYS" ]]; then
|
||||
printf " %-36s %-24s %-20s %-10s %b%s%b\n" \
|
||||
"${upn:0:34}" "${display_name:0:22}" "$last_sign_display" \
|
||||
"$idle_days" "$YELLOW" "WARN" "$RESET"
|
||||
flag_warn
|
||||
else
|
||||
verbose "User ${upn}: active (${idle_days}d idle)"
|
||||
flag_ok
|
||||
fi
|
||||
done
|
||||
|
||||
echo ""
|
||||
}
|
||||
|
||||
# ══════════════════════════════════════════════════════════════════════
|
||||
# MFA AUDIT
|
||||
# ══════════════════════════════════════════════════════════════════════
|
||||
audit_mfa() {
|
||||
log "Auditing MFA registration status..."
|
||||
echo ""
|
||||
|
||||
printf " %-36s %-14s %-10s %s\n" \
|
||||
"UPN" "MFA_STATUS" "IS_ADMIN" "SEVERITY"
|
||||
printf " %s\n" "$(printf '%.0s─' {1..80})"
|
||||
|
||||
local reg_json
|
||||
# shellcheck disable=SC2016
|
||||
reg_json=$(az rest --method GET \
|
||||
--url 'https://graph.microsoft.com/v1.0/reports/credentialUserRegistrationDetails?$top=999' \
|
||||
2>/dev/null || echo '{"value":[]}')
|
||||
|
||||
local admin_upns
|
||||
admin_upns=$(az rest --method GET \
|
||||
--url 'https://graph.microsoft.com/v1.0/directoryRoles' \
|
||||
2>/dev/null | jq -r '.value[]? | select(.displayName | test("Admin|Administrator"; "i")) | .id' 2>/dev/null || true)
|
||||
|
||||
local admin_members=""
|
||||
while IFS= read -r role_id; do
|
||||
[[ -z "$role_id" ]] && continue
|
||||
local members
|
||||
members=$(az rest --method GET \
|
||||
--url "https://graph.microsoft.com/v1.0/directoryRoles/${role_id}/members" \
|
||||
2>/dev/null | jq -r '.value[]?.userPrincipalName // empty' 2>/dev/null || true)
|
||||
admin_members="${admin_members}${members}"$'\n'
|
||||
done <<< "$admin_upns"
|
||||
|
||||
echo "$reg_json" | jq -c '.value[]? // empty' 2>/dev/null | while IFS= read -r entry; do
|
||||
local upn mfa_registered
|
||||
upn=$(echo "$entry" | jq -r '.userPrincipalName // "unknown"' 2>/dev/null)
|
||||
mfa_registered=$(echo "$entry" | jq -r '.isMfaRegistered // false' 2>/dev/null)
|
||||
|
||||
local is_admin="No"
|
||||
if echo "$admin_members" | grep -qi "^${upn}$" 2>/dev/null; then
|
||||
is_admin="Yes"
|
||||
fi
|
||||
|
||||
local mfa_display severity
|
||||
if [[ "$mfa_registered" == "true" ]]; then
|
||||
mfa_display="Registered"
|
||||
severity="OK"
|
||||
printf " %-36s %-14s %-10s %b%s%b\n" \
|
||||
"${upn:0:34}" "$mfa_display" "$is_admin" "$GREEN" "$severity" "$RESET"
|
||||
flag_ok
|
||||
else
|
||||
mfa_display="Not registered"
|
||||
if [[ "$is_admin" == "Yes" ]]; then
|
||||
severity="CRITICAL"
|
||||
printf " %-36s %-14s %-10s %b%s%b\n" \
|
||||
"${upn:0:34}" "$mfa_display" "$is_admin" "$RED" "$severity" "$RESET"
|
||||
flag_crit
|
||||
else
|
||||
severity="WARN"
|
||||
printf " %-36s %-14s %-10s %b%s%b\n" \
|
||||
"${upn:0:34}" "$mfa_display" "$is_admin" "$YELLOW" "$severity" "$RESET"
|
||||
flag_warn
|
||||
fi
|
||||
fi
|
||||
done
|
||||
|
||||
echo ""
|
||||
}
|
||||
|
||||
# ══════════════════════════════════════════════════════════════════════
|
||||
# ADMIN ROLES AUDIT
|
||||
# ══════════════════════════════════════════════════════════════════════
|
||||
audit_admins() {
|
||||
log "Auditing privileged role assignments..."
|
||||
echo ""
|
||||
|
||||
printf " %-36s %-28s %-20s %s\n" \
|
||||
"UPN" "ROLE" "SCOPE" "SEVERITY"
|
||||
printf " %s\n" "$(printf '%.0s─' {1..95})"
|
||||
|
||||
local owner_json
|
||||
owner_json=$(az role assignment list --role "Owner" --all 2>/dev/null || echo '[]')
|
||||
|
||||
echo "$owner_json" | jq -c '.[]? // empty' 2>/dev/null | while IFS= read -r assignment; do
|
||||
local upn role scope
|
||||
upn=$(echo "$assignment" | jq -r '.principalName // "unknown"' 2>/dev/null)
|
||||
role=$(echo "$assignment" | jq -r '.roleDefinitionName // "unknown"' 2>/dev/null)
|
||||
scope=$(echo "$assignment" | jq -r '.scope // "/"' 2>/dev/null)
|
||||
|
||||
[[ -z "$upn" || "$upn" == "unknown" ]] && continue
|
||||
|
||||
local scope_display
|
||||
scope_display="${scope##*/}"
|
||||
[[ -z "$scope_display" ]] && scope_display="$scope"
|
||||
|
||||
printf " %-36s %-28s %-20s %b%s%b\n" \
|
||||
"${upn:0:34}" "${role:0:26}" "${scope_display:0:18}" \
|
||||
"$CYAN" "INFO" "$RESET"
|
||||
flag_info
|
||||
done
|
||||
|
||||
local ga_json
|
||||
ga_json=$(az rest --method GET \
|
||||
--url 'https://graph.microsoft.com/v1.0/directoryRoles' \
|
||||
2>/dev/null | jq -c '.value[]? | select(.displayName == "Global Administrator")' 2>/dev/null || echo '')
|
||||
|
||||
if [[ -n "$ga_json" ]]; then
|
||||
local ga_role_id
|
||||
ga_role_id=$(echo "$ga_json" | jq -r '.id' 2>/dev/null)
|
||||
|
||||
local ga_members
|
||||
ga_members=$(az rest --method GET \
|
||||
--url "https://graph.microsoft.com/v1.0/directoryRoles/${ga_role_id}/members" \
|
||||
2>/dev/null || echo '{"value":[]}')
|
||||
|
||||
local ga_count
|
||||
ga_count=$(echo "$ga_members" | jq '[.value[]?] | length' 2>/dev/null || echo 0)
|
||||
|
||||
echo "$ga_members" | jq -c '.value[]? // empty' 2>/dev/null | while IFS= read -r member; do
|
||||
local m_upn
|
||||
m_upn=$(echo "$member" | jq -r '.userPrincipalName // "unknown"' 2>/dev/null)
|
||||
|
||||
printf " %-36s %-28s %-20s %b%s%b\n" \
|
||||
"${m_upn:0:34}" "Global Administrator" "Tenant" \
|
||||
"$CYAN" "INFO" "$RESET"
|
||||
flag_info
|
||||
done
|
||||
|
||||
if [[ "$ga_count" -gt 5 ]]; then
|
||||
echo ""
|
||||
warn "Excessive Global Administrators: ${ga_count} found (recommended: ≤5)"
|
||||
printf " %-36s %-28s %-20s %b%s%b\n" \
|
||||
"— policy —" "Global Admin count: ${ga_count}" ">5 threshold" \
|
||||
"$YELLOW" "WARN" "$RESET"
|
||||
flag_warn
|
||||
elif [[ "$ga_count" -gt 0 ]]; then
|
||||
verbose "Global Administrator count: ${ga_count} (within threshold)"
|
||||
flag_ok
|
||||
fi
|
||||
fi
|
||||
|
||||
echo ""
|
||||
}
|
||||
|
||||
# ══════════════════════════════════════════════════════════════════════
|
||||
# SERVICE PRINCIPALS AUDIT
|
||||
# ══════════════════════════════════════════════════════════════════════
|
||||
audit_service_principals() {
|
||||
log "Auditing service principal credentials..."
|
||||
echo ""
|
||||
|
||||
printf " %-30s %-14s %-20s %-14s %s\n" \
|
||||
"APP_NAME" "CRED_TYPE" "EXPIRY" "STATUS" "SEVERITY"
|
||||
printf " %s\n" "$(printf '%.0s─' {1..100})"
|
||||
|
||||
local sp_json
|
||||
sp_json=$(az ad sp list --all --query "[].{appDisplayName:appDisplayName,appId:appId,keyCredentials:keyCredentials,passwordCredentials:passwordCredentials}" 2>/dev/null || echo '[]')
|
||||
|
||||
echo "$sp_json" | jq -c '.[]? // empty' 2>/dev/null | while IFS= read -r sp; do
|
||||
local app_name app_id
|
||||
app_name=$(echo "$sp" | jq -r '.appDisplayName // "unnamed"' 2>/dev/null)
|
||||
app_id=$(echo "$sp" | jq -r '.appId // "unknown"' 2>/dev/null)
|
||||
|
||||
[[ "$app_name" == "unnamed" || -z "$app_name" ]] && app_name="$app_id"
|
||||
|
||||
echo "$sp" | jq -c '.passwordCredentials[]? // empty' 2>/dev/null | while IFS= read -r cred; do
|
||||
local end_date
|
||||
end_date=$(echo "$cred" | jq -r '.endDateTime // "null"' 2>/dev/null)
|
||||
|
||||
local remaining status severity
|
||||
remaining=$(days_until "$end_date")
|
||||
|
||||
if [[ "$remaining" == "unknown" ]]; then
|
||||
status="Unknown"
|
||||
severity="INFO"
|
||||
printf " %-30s %-14s %-20s %-14s %b%s%b\n" \
|
||||
"${app_name:0:28}" "Password" "Unknown" "$status" \
|
||||
"$CYAN" "$severity" "$RESET"
|
||||
flag_info
|
||||
elif [[ "$remaining" -lt 0 ]]; then
|
||||
status="Expired"
|
||||
severity="WARN"
|
||||
printf " %-30s %-14s %-20s %-14s %b%s%b\n" \
|
||||
"${app_name:0:28}" "Password" "${end_date:0:10}" "$status" \
|
||||
"$YELLOW" "$severity" "$RESET"
|
||||
flag_warn
|
||||
elif [[ "$remaining" -lt 30 ]]; then
|
||||
status="Expiring (${remaining}d)"
|
||||
severity="WARN"
|
||||
printf " %-30s %-14s %-20s %-14s %b%s%b\n" \
|
||||
"${app_name:0:28}" "Password" "${end_date:0:10}" "$status" \
|
||||
"$YELLOW" "$severity" "$RESET"
|
||||
flag_warn
|
||||
else
|
||||
verbose "SP ${app_name}: password credential valid (${remaining}d remaining)"
|
||||
flag_ok
|
||||
fi
|
||||
done
|
||||
|
||||
echo "$sp" | jq -c '.keyCredentials[]? // empty' 2>/dev/null | while IFS= read -r cred; do
|
||||
local end_date
|
||||
end_date=$(echo "$cred" | jq -r '.endDateTime // "null"' 2>/dev/null)
|
||||
|
||||
local remaining status severity
|
||||
remaining=$(days_until "$end_date")
|
||||
|
||||
if [[ "$remaining" == "unknown" ]]; then
|
||||
status="Unknown"
|
||||
severity="INFO"
|
||||
printf " %-30s %-14s %-20s %-14s %b%s%b\n" \
|
||||
"${app_name:0:28}" "Certificate" "Unknown" "$status" \
|
||||
"$CYAN" "$severity" "$RESET"
|
||||
flag_info
|
||||
elif [[ "$remaining" -lt 0 ]]; then
|
||||
status="Expired"
|
||||
severity="WARN"
|
||||
printf " %-30s %-14s %-20s %-14s %b%s%b\n" \
|
||||
"${app_name:0:28}" "Certificate" "${end_date:0:10}" "$status" \
|
||||
"$YELLOW" "$severity" "$RESET"
|
||||
flag_warn
|
||||
elif [[ "$remaining" -lt 30 ]]; then
|
||||
status="Expiring (${remaining}d)"
|
||||
severity="WARN"
|
||||
printf " %-30s %-14s %-20s %-14s %b%s%b\n" \
|
||||
"${app_name:0:28}" "Certificate" "${end_date:0:10}" "$status" \
|
||||
"$YELLOW" "$severity" "$RESET"
|
||||
flag_warn
|
||||
else
|
||||
verbose "SP ${app_name}: key credential valid (${remaining}d remaining)"
|
||||
flag_ok
|
||||
fi
|
||||
done
|
||||
done
|
||||
|
||||
echo ""
|
||||
}
|
||||
|
||||
# ══════════════════════════════════════════════════════════════════════
|
||||
# GUEST USERS AUDIT
|
||||
# ══════════════════════════════════════════════════════════════════════
|
||||
audit_guests() {
|
||||
log "Auditing guest user accounts (threshold: ${STALE_DAYS} days)..."
|
||||
echo ""
|
||||
|
||||
printf " %-36s %-20s %-20s %s\n" \
|
||||
"UPN" "CREATED" "LAST_ACTIVITY" "SEVERITY"
|
||||
printf " %s\n" "$(printf '%.0s─' {1..90})"
|
||||
|
||||
local guests_json
|
||||
# shellcheck disable=SC2016
|
||||
guests_json=$(az rest --method GET \
|
||||
--url 'https://graph.microsoft.com/v1.0/users?$filter=userType%20eq%20%27Guest%27&$select=userPrincipalName,displayName,createdDateTime,signInActivity&$top=999' \
|
||||
2>/dev/null || echo '{"value":[]}')
|
||||
|
||||
echo "$guests_json" | jq -c '.value[]? // empty' 2>/dev/null | while IFS= read -r guest; do
|
||||
local upn created last_activity
|
||||
upn=$(echo "$guest" | jq -r '.userPrincipalName // "unknown"' 2>/dev/null)
|
||||
created=$(echo "$guest" | jq -r '.createdDateTime // "null"' 2>/dev/null)
|
||||
last_activity=$(echo "$guest" | jq -r '.signInActivity.lastSignInDateTime // "null"' 2>/dev/null)
|
||||
|
||||
local created_display last_display
|
||||
if [[ "$created" == "null" || -z "$created" ]]; then
|
||||
created_display="Unknown"
|
||||
else
|
||||
created_display="${created:0:10}"
|
||||
fi
|
||||
|
||||
if [[ "$last_activity" == "null" || -z "$last_activity" ]]; then
|
||||
last_display="Never"
|
||||
else
|
||||
last_display="${last_activity:0:10}"
|
||||
fi
|
||||
|
||||
local idle_days
|
||||
idle_days=$(days_since "$last_activity")
|
||||
|
||||
if [[ "$idle_days" == "never" ]]; then
|
||||
printf " %-36s %-20s %-20s %b%s%b\n" \
|
||||
"${upn:0:34}" "$created_display" "$last_display" \
|
||||
"$YELLOW" "WARN" "$RESET"
|
||||
flag_warn
|
||||
elif [[ "$idle_days" == "unknown" ]]; then
|
||||
verbose "Guest ${upn}: unable to determine activity"
|
||||
flag_info
|
||||
elif [[ "$idle_days" -gt "$STALE_DAYS" ]]; then
|
||||
printf " %-36s %-20s %-20s %b%s%b\n" \
|
||||
"${upn:0:34}" "$created_display" "$last_display" \
|
||||
"$YELLOW" "WARN" "$RESET"
|
||||
flag_warn
|
||||
else
|
||||
verbose "Guest ${upn}: active (${idle_days}d idle)"
|
||||
flag_ok
|
||||
fi
|
||||
done
|
||||
|
||||
echo ""
|
||||
}
|
||||
|
||||
# ══════════════════════════════════════════════════════════════════════
|
||||
# SUMMARY
|
||||
# ══════════════════════════════════════════════════════════════════════
|
||||
print_summary() {
|
||||
local elapsed
|
||||
elapsed=$(( $(date +%s) - START_TIME ))
|
||||
|
||||
echo ""
|
||||
echo " ══════════════════════════════════════════"
|
||||
echo " Azure Entra ID Audit Summary"
|
||||
echo " ══════════════════════════════════════════"
|
||||
printf " %-20s %b%d%b\n" "CRITICAL:" "$RED" "$TOTAL_CRIT" "$RESET"
|
||||
printf " %-20s %b%d%b\n" "WARN:" "$YELLOW" "$TOTAL_WARN" "$RESET"
|
||||
printf " %-20s %b%d%b\n" "INFO:" "$CYAN" "$TOTAL_INFO" "$RESET"
|
||||
printf " %-20s %b%d%b\n" "OK:" "$GREEN" "$TOTAL_OK" "$RESET"
|
||||
echo " ──────────────────────────────────────────"
|
||||
printf " Completed in %ds\n" "$elapsed"
|
||||
echo ""
|
||||
|
||||
if [[ "$TOTAL_CRIT" -gt 0 ]]; then
|
||||
echo -e " ${RED}${BOLD}Action required:${RESET} ${TOTAL_CRIT} critical finding(s)"
|
||||
echo ""
|
||||
echo " Top recommendations:"
|
||||
echo " • Enable MFA for all admin accounts immediately"
|
||||
echo " • Review and remove stale user accounts"
|
||||
echo " • Rotate expired service principal credentials"
|
||||
echo " • Reduce the number of Global Administrators to ≤5"
|
||||
echo " • Remove inactive guest accounts"
|
||||
echo ""
|
||||
elif [[ "$TOTAL_WARN" -gt 0 ]]; then
|
||||
echo -e " ${YELLOW}Review recommended:${RESET} ${TOTAL_WARN} warning(s)"
|
||||
echo ""
|
||||
echo " Suggestions:"
|
||||
echo " • Disable or remove stale user accounts"
|
||||
echo " • Enforce MFA registration for all users"
|
||||
echo " • Renew expiring service principal credentials"
|
||||
echo " • Clean up inactive guest accounts"
|
||||
echo ""
|
||||
else
|
||||
echo -e " ${GREEN}All checks passed${RESET}"
|
||||
echo ""
|
||||
fi
|
||||
}
|
||||
|
||||
# ══════════════════════════════════════════════════════════════════════
|
||||
# USAGE
|
||||
# ══════════════════════════════════════════════════════════════════════
|
||||
show_help() {
|
||||
cat <<EOF
|
||||
${BOLD}${SCRIPT_NAME}${RESET} — Azure Entra ID Auditor
|
||||
|
||||
Audit Azure Entra ID (Azure AD) for stale users, MFA gaps, excessive
|
||||
permissions, service principal hygiene, and guest accounts via az CLI.
|
||||
|
||||
${BOLD}MODES${RESET}
|
||||
--full Run all audits
|
||||
--stale-users Find users with no sign-in activity > N days
|
||||
--mfa Check MFA registration status
|
||||
--admins Audit privileged role assignments
|
||||
--service-principals Audit service principal credential expiry
|
||||
--guests Find stale guest accounts
|
||||
|
||||
${BOLD}OPTIONS${RESET}
|
||||
--stale-days N Override stale threshold in days (default: 90)
|
||||
--verbose Debug output
|
||||
--no-color Disable colored output
|
||||
--help Show this help message
|
||||
|
||||
${BOLD}ENVIRONMENT VARIABLES${RESET}
|
||||
STALE_DAYS Days before a user is considered stale (default: 90)
|
||||
VERBOSE Enable verbose output (true/false)
|
||||
COLOR Color mode: auto, always, never
|
||||
|
||||
${BOLD}PREREQUISITES${RESET}
|
||||
• Azure CLI authenticated: az login
|
||||
• Microsoft Graph API permissions for sign-in activity and reports
|
||||
• Reader role or higher for role assignment queries
|
||||
|
||||
${BOLD}EXAMPLES${RESET}
|
||||
# Full audit
|
||||
${SCRIPT_NAME} --full
|
||||
|
||||
# Check stale users only
|
||||
${SCRIPT_NAME} --stale-users
|
||||
|
||||
# MFA audit with custom stale threshold
|
||||
${SCRIPT_NAME} --mfa --stale-days 60
|
||||
|
||||
# Service principal credential check
|
||||
${SCRIPT_NAME} --service-principals
|
||||
|
||||
# Guest user audit
|
||||
${SCRIPT_NAME} --guests
|
||||
|
||||
${BOLD}EXIT CODES${RESET}
|
||||
0 All checks passed
|
||||
1 Warnings found (review recommended)
|
||||
2 Critical findings (action required)
|
||||
EOF
|
||||
}
|
||||
|
||||
# ══════════════════════════════════════════════════════════════════════
|
||||
# PARSE ARGS
|
||||
# ══════════════════════════════════════════════════════════════════════
|
||||
parse_args() {
|
||||
local modes=()
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
--full)
|
||||
modes=(stale-users mfa admins service-principals guests)
|
||||
shift ;;
|
||||
--stale-users)
|
||||
modes+=(stale-users); shift ;;
|
||||
--mfa)
|
||||
modes+=(mfa); shift ;;
|
||||
--admins)
|
||||
modes+=(admins); shift ;;
|
||||
--service-principals)
|
||||
modes+=(service-principals); shift ;;
|
||||
--guests)
|
||||
modes+=(guests); shift ;;
|
||||
--stale-days)
|
||||
STALE_DAYS="${2:?--stale-days requires a value}"; shift 2 ;;
|
||||
--verbose)
|
||||
VERBOSE="true"; shift ;;
|
||||
--no-color)
|
||||
COLOR="never"; shift ;;
|
||||
--help|-h)
|
||||
setup_colors; show_help; exit 0 ;;
|
||||
*)
|
||||
die "Unknown option: $1 (see --help)" ;;
|
||||
esac
|
||||
done
|
||||
|
||||
if [[ ${#modes[@]} -eq 0 ]]; then
|
||||
err "No audit mode specified"
|
||||
echo "Run ${SCRIPT_NAME} --help for usage" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
RUN_MODE="${modes[*]}"
|
||||
}
|
||||
|
||||
# ══════════════════════════════════════════════════════════════════════
|
||||
# MAIN
|
||||
# ══════════════════════════════════════════════════════════════════════
|
||||
main() {
|
||||
parse_args "$@"
|
||||
setup_colors
|
||||
check_deps
|
||||
check_credentials
|
||||
|
||||
START_TIME=$(date +%s)
|
||||
|
||||
echo ""
|
||||
echo -e "${BOLD}Azure Entra ID Auditor${RESET}"
|
||||
echo -e "Mode: ${RUN_MODE}"
|
||||
echo -e "Time: $(date -u +%Y-%m-%dT%H:%M:%SZ)"
|
||||
echo ""
|
||||
|
||||
for mode in $RUN_MODE; do
|
||||
case "$mode" in
|
||||
stale-users) audit_stale_users ;;
|
||||
mfa) audit_mfa ;;
|
||||
admins) audit_admins ;;
|
||||
service-principals) audit_service_principals ;;
|
||||
guests) audit_guests ;;
|
||||
esac
|
||||
done
|
||||
|
||||
print_summary
|
||||
|
||||
if [[ "$TOTAL_CRIT" -gt 0 ]]; then
|
||||
exit 2
|
||||
elif [[ "$TOTAL_WARN" -gt 0 ]]; then
|
||||
exit 1
|
||||
fi
|
||||
exit 0
|
||||
}
|
||||
|
||||
main "$@"
|
||||
Reference in New Issue
Block a user