Files
linux-scripts/azure-blob-manager.sh
chiefgeek a1a17e81a1 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.
2026-05-25 03:31:08 +02:00

598 lines
24 KiB
Bash

#!/usr/bin/env bash
#########################################################################################
#### azure-blob-manager.sh — Manage Azure Blob Storage containers, lifecycle, and ####
#### access auditing via az CLI. Upload, sync, tier, and audit blob storage ####
#### Requires: bash 4+, az CLI, jq ####
#### ####
#### Author: Phil Connor ####
#### Contact: contact@mylinux.work ####
#### License: MIT ####
#### Version 1.01 ####
#### ####
#### Usage: ####
#### ./azure-blob-manager.sh --list ####
#### ####
#### 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; }
section_header() {
echo ""
echo -e " ${BOLD}${CYAN}── $1 ──${RESET}"
echo ""
}
field() {
printf " ${BOLD}%-22s${RESET} %s\n" "$1" "$2"
}
field_color() {
printf " ${BOLD}%-22s${RESET} %b\n" "$1" "$2"
}
elapsed() {
local end_time
end_time=$(date +%s)
echo "$(( end_time - START_TIME ))s"
}
# ── Severity counters (for audit mode) ───────────────────────────────
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=""
STORAGE_ACCOUNT=""
CONTAINER_NAME=""
RESOURCE_GROUP=""
OUTPUT_FORMAT="${ABM_FORMAT:-text}"
VERBOSE="${VERBOSE:-false}"
COLOR="${COLOR:-auto}"
MAX_AGE="${ABM_MAX_AGE:-90}"
TIER_TARGET=""
SOURCE_PATH=""
SUBSCRIPTION=""
# ── State ─────────────────────────────────────────────────────────────
SCRIPT_NAME="$(basename "$0")"
readonly SCRIPT_NAME
START_TIME=""
# ── Dependency and credential checks ────────────────────────────────
check_deps() {
command -v az &>/dev/null || die "az CLI is required (install: https://aka.ms/InstallAzureCLIDeb)"
command -v jq &>/dev/null || die "jq is required"
}
check_credentials() {
local acct
acct=$(az account show --output json 2>&1) || die "Azure credentials not configured — run 'az login'"
local sub_name
sub_name=$(echo "$acct" | jq -r '.name')
log "Subscription: ${sub_name}"
if [[ -n "$SUBSCRIPTION" ]]; then
az account set --subscription "$SUBSCRIPTION" 2>/dev/null \
|| die "Cannot switch to subscription: ${SUBSCRIPTION}"
fi
}
# ── Azure CLI wrapper ────────────────────────────────────────────────
az_cmd() {
local args=("$@")
[[ -n "$SUBSCRIPTION" ]] && args+=(--subscription "$SUBSCRIPTION")
verbose "az ${args[*]}"
az "${args[@]}"
}
# ══════════════════════════════════════════════════════════════════════
# LIST
# ══════════════════════════════════════════════════════════════════════
do_list() {
if [[ -n "$STORAGE_ACCOUNT" && -n "$CONTAINER_NAME" ]]; then
list_blobs
elif [[ -n "$STORAGE_ACCOUNT" ]]; then
list_containers
else
list_accounts
fi
}
list_accounts() {
section_header "Storage Accounts"
local accounts
local args=(storage account list --output json)
[[ -n "$RESOURCE_GROUP" ]] && args+=(--resource-group "$RESOURCE_GROUP")
accounts=$(az_cmd "${args[@]}" 2>/dev/null)
local count
count=$(echo "$accounts" | jq 'length')
printf " %-28s %-16s %-12s %-12s %s\n" \
"ACCOUNT" "RESOURCE_GROUP" "KIND" "REPLICATION" "LOCATION"
printf " %s\n" "$(printf '%.0s─' {1..90})"
echo "$accounts" | jq -c '.[]' | while IFS= read -r acct; do
local name rg kind repl location
name=$(echo "$acct" | jq -r '.name')
rg=$(echo "$acct" | jq -r '.resourceGroup')
kind=$(echo "$acct" | jq -r '.kind')
repl=$(echo "$acct" | jq -r '.sku.name')
location=$(echo "$acct" | jq -r '.location')
printf " %-28s %-16s %-12s %-12s %s\n" \
"${name:0:27}" "${rg:0:15}" "${kind:0:11}" "${repl:0:11}" "$location"
done
echo ""
field "Total accounts:" "$count"
}
list_containers() {
section_header "Containers in ${STORAGE_ACCOUNT}"
local containers
containers=$(az_cmd storage container list \
--account-name "$STORAGE_ACCOUNT" --auth-mode login \
--output json 2>/dev/null) || die "Failed to list containers — check permissions"
local count
count=$(echo "$containers" | jq 'length')
printf " %-32s %-16s %-12s %s\n" \
"CONTAINER" "PUBLIC_ACCESS" "LEASE_STATE" "LAST_MODIFIED"
printf " %s\n" "$(printf '%.0s─' {1..80})"
echo "$containers" | jq -c '.[]' | while IFS= read -r ctr; do
local name public_access lease_state last_mod
name=$(echo "$ctr" | jq -r '.name')
public_access=$(echo "$ctr" | jq -r '.properties.publicAccess // "none"')
lease_state=$(echo "$ctr" | jq -r '.properties.leaseState // "available"')
last_mod=$(echo "$ctr" | jq -r '.properties.lastModified // ""' | cut -dT -f1)
printf " %-32s %-16s %-12s %s\n" \
"${name:0:31}" "$public_access" "$lease_state" "$last_mod"
done
echo ""
field "Total containers:" "$count"
}
list_blobs() {
section_header "Blobs in ${STORAGE_ACCOUNT}/${CONTAINER_NAME}"
local blobs
blobs=$(az_cmd storage blob list \
--account-name "$STORAGE_ACCOUNT" --container-name "$CONTAINER_NAME" \
--auth-mode login --output json 2>/dev/null) \
|| die "Failed to list blobs — check permissions"
local count
count=$(echo "$blobs" | jq 'length')
printf " %-40s %-12s %-8s %s\n" \
"NAME" "SIZE" "TIER" "LAST_MODIFIED"
printf " %s\n" "$(printf '%.0s─' {1..80})"
echo "$blobs" | jq -c '.[]' | while IFS= read -r blob; do
local name size tier last_mod size_str
name=$(echo "$blob" | jq -r '.name')
size=$(echo "$blob" | jq -r '.properties.contentLength // 0')
tier=$(echo "$blob" | jq -r '.properties.blobTier // "N/A"')
last_mod=$(echo "$blob" | jq -r '.properties.lastModified // ""' | cut -dT -f1)
if (( size > 1073741824 )); then
size_str="$(( size / 1073741824 )) GB"
elif (( size > 1048576 )); then
size_str="$(( size / 1048576 )) MB"
elif (( size > 1024 )); then
size_str="$(( size / 1024 )) KB"
else
size_str="${size} B"
fi
printf " %-40s %-12s %-8s %s\n" \
"${name:0:39}" "$size_str" "$tier" "$last_mod"
done
echo ""
field "Total blobs:" "$count"
}
# ══════════════════════════════════════════════════════════════════════
# AUDIT
# ══════════════════════════════════════════════════════════════════════
do_audit() {
section_header "Storage Security Audit"
local accounts
local args=(storage account list --output json)
[[ -n "$RESOURCE_GROUP" ]] && args+=(--resource-group "$RESOURCE_GROUP")
accounts=$(az_cmd "${args[@]}" 2>/dev/null)
printf " %-28s %-16s %-14s %-14s %s\n" \
"ACCOUNT" "HTTPS_ONLY" "PUBLIC_BLOB" "NETWORK_RULES" "SEVERITY"
printf " %s\n" "$(printf '%.0s─' {1..95})"
echo "$accounts" | jq -c '.[]' | while IFS= read -r acct; do
local name https_only public_access net_default
name=$(echo "$acct" | jq -r '.name')
https_only=$(echo "$acct" | jq -r '.enableHttpsTrafficOnly // true')
public_access=$(echo "$acct" | jq -r '.allowBlobPublicAccess // false')
net_default=$(echo "$acct" | jq -r '.networkRuleSet.defaultAction // "Allow"')
local severity="OK" color="$GREEN"
if [[ "$public_access" == "true" ]]; then
severity="CRITICAL"; color="$RED"; flag_crit
elif [[ "$https_only" != "true" ]]; then
severity="WARN"; color="$YELLOW"; flag_warn
elif [[ "$net_default" == "Allow" ]]; then
severity="WARN"; color="$YELLOW"; flag_warn
else
flag_ok
fi
printf " %-28s %-16s %-14s %-14s %b%s%b\n" \
"${name:0:27}" "$https_only" "$public_access" "$net_default" \
"$color" "$severity" "$RESET"
done
echo ""
# Check individual containers for public access
log "Checking container-level public access..."
echo ""
echo "$accounts" | jq -r '.[].name' | while IFS= read -r acct_name; do
local containers
containers=$(az_cmd storage container list \
--account-name "$acct_name" --auth-mode login \
--output json 2>/dev/null) || continue
echo "$containers" | jq -c '.[]' | while IFS= read -r ctr; do
local ctr_name public_access
ctr_name=$(echo "$ctr" | jq -r '.name')
public_access=$(echo "$ctr" | jq -r '.properties.publicAccess // "none"')
if [[ "$public_access" != "none" && "$public_access" != "null" ]]; then
printf " %-28s %-28s %-14s %b%s%b\n" \
"${acct_name:0:27}" "${ctr_name:0:27}" "$public_access" \
"$RED" "CRITICAL" "$RESET"
flag_crit
fi
done
done
print_summary
}
# ══════════════════════════════════════════════════════════════════════
# SYNC
# ══════════════════════════════════════════════════════════════════════
do_sync() {
[[ -z "$STORAGE_ACCOUNT" ]] && die "--sync requires --account"
[[ -z "$CONTAINER_NAME" ]] && die "--sync requires --container"
[[ -z "$SOURCE_PATH" ]] && die "--sync requires --source PATH"
[[ -d "$SOURCE_PATH" ]] || die "Source path does not exist: ${SOURCE_PATH}"
section_header "Syncing to ${STORAGE_ACCOUNT}/${CONTAINER_NAME}"
field "Source:" "$SOURCE_PATH"
echo ""
if az_cmd storage blob upload-batch \
--account-name "$STORAGE_ACCOUNT" \
--destination "$CONTAINER_NAME" \
--source "$SOURCE_PATH" \
--auth-mode login \
--overwrite 2>/dev/null; then
echo -e " ${GREEN}${RESET} Sync complete"
else
die "Sync failed"
fi
}
# ══════════════════════════════════════════════════════════════════════
# TIER
# ══════════════════════════════════════════════════════════════════════
do_tier() {
[[ -z "$STORAGE_ACCOUNT" ]] && die "--tier requires --account"
[[ -z "$CONTAINER_NAME" ]] && die "--tier requires --container"
[[ -z "$TIER_TARGET" ]] && die "--tier requires --set-tier TIER"
section_header "Changing Blob Tier"
field "Account:" "$STORAGE_ACCOUNT"
field "Container:" "$CONTAINER_NAME"
field "Target tier:" "$TIER_TARGET"
echo ""
local blobs
blobs=$(az_cmd storage blob list \
--account-name "$STORAGE_ACCOUNT" --container-name "$CONTAINER_NAME" \
--auth-mode login --output json 2>/dev/null) \
|| die "Failed to list blobs"
local changed=0 errors=0
echo "$blobs" | jq -r '.[].name' | while IFS= read -r blob_name; do
if az_cmd storage blob set-tier \
--account-name "$STORAGE_ACCOUNT" --container-name "$CONTAINER_NAME" \
--name "$blob_name" --tier "$TIER_TARGET" \
--auth-mode login 2>/dev/null; then
echo -e " ${GREEN}${RESET} ${blob_name}${TIER_TARGET}"
((changed++)) || true
else
echo -e " ${RED}${RESET} ${blob_name} — failed"
((errors++)) || true
fi
done
echo ""
field "Changed:" "$changed"
[[ "$errors" -gt 0 ]] && field_color "Errors:" "${RED}${errors}${RESET}"
}
# ══════════════════════════════════════════════════════════════════════
# LIFECYCLE
# ══════════════════════════════════════════════════════════════════════
do_lifecycle() {
[[ -z "$STORAGE_ACCOUNT" ]] && die "--lifecycle requires --account"
[[ -z "$RESOURCE_GROUP" ]] && die "--lifecycle requires --resource-group"
section_header "Lifecycle Management Policy"
field "Account:" "$STORAGE_ACCOUNT"
echo ""
local policy
policy=$(az_cmd storage account management-policy show \
--account-name "$STORAGE_ACCOUNT" --resource-group "$RESOURCE_GROUP" \
--output json 2>/dev/null)
if [[ -z "$policy" || "$policy" == "null" ]]; then
log "No lifecycle policy configured"
else
echo "$policy" | jq '.policy.rules[] | {name: .name, type: .type, definition: .definition}'
fi
}
# ══════════════════════════════════════════════════════════════════════
# STATS
# ══════════════════════════════════════════════════════════════════════
do_stats() {
section_header "Storage Statistics"
local accounts
local args=(storage account list --output json)
[[ -n "$RESOURCE_GROUP" ]] && args+=(--resource-group "$RESOURCE_GROUP")
accounts=$(az_cmd "${args[@]}" 2>/dev/null)
local total_accounts
total_accounts=$(echo "$accounts" | jq 'length')
printf " %-28s %-16s %-12s %s\n" \
"ACCOUNT" "LOCATION" "KIND" "REPLICATION"
printf " %s\n" "$(printf '%.0s─' {1..75})"
echo "$accounts" | jq -c '.[]' | while IFS= read -r acct; do
local name location kind repl
name=$(echo "$acct" | jq -r '.name')
location=$(echo "$acct" | jq -r '.location')
kind=$(echo "$acct" | jq -r '.kind')
repl=$(echo "$acct" | jq -r '.sku.name')
printf " %-28s %-16s %-12s %s\n" \
"${name:0:27}" "$location" "${kind:0:11}" "$repl"
done
echo ""
field "Total accounts:" "$total_accounts"
}
# ══════════════════════════════════════════════════════════════════════
# SUMMARY
# ══════════════════════════════════════════════════════════════════════
print_summary() {
echo ""
echo " ══════════════════════════════════════════"
echo " Storage 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 " ──────────────────────────────────────────"
echo ""
if [[ "$TOTAL_CRIT" -gt 0 ]]; then
echo -e " ${RED}${BOLD}Action required:${RESET} ${TOTAL_CRIT} critical finding(s)"
echo ""
echo " Top recommendations:"
echo " • Disable public blob access on all storage accounts"
echo " • Set container access level to private"
echo " • Enable HTTPS-only traffic"
echo " • Configure network rules to restrict access"
echo ""
elif [[ "$TOTAL_WARN" -gt 0 ]]; then
echo -e " ${YELLOW}Review recommended:${RESET} ${TOTAL_WARN} warning(s)"
echo ""
else
echo -e " ${GREEN}All checks passed${RESET}"
echo ""
fi
}
# ══════════════════════════════════════════════════════════════════════
# HELP
# ══════════════════════════════════════════════════════════════════════
show_help() {
cat <<EOF
${BOLD}${SCRIPT_NAME}${RESET} — Azure Blob Manager
Manage Azure Blob Storage containers, lifecycle policies, and
security auditing via az CLI.
${BOLD}MODES${RESET}
--list List storage accounts, containers, or blobs
--audit Audit storage security (public access, HTTPS, network rules)
--sync Upload local directory to a container
--tier Change blob access tier
--lifecycle Show lifecycle management policy
--stats Storage statistics summary
${BOLD}TARGETING${RESET}
--account NAME Target storage account
--container NAME Target container
--resource-group RG Limit to resource group
${BOLD}OPTIONS${RESET}
--set-tier TIER Target tier: Hot, Cool, Cold, Archive
--max-age DAYS Flag blobs older than N days in audit (default: 90)
--source PATH Local directory for sync
--format FMT Output format: text (default)
--subscription SUB Azure subscription name or ID
--verbose Debug output
--no-color Disable colored output
--help Show this help message
${BOLD}ENVIRONMENT VARIABLES${RESET}
ABM_FORMAT Default output format
ABM_MAX_AGE Default stale threshold for audit (days)
VERBOSE Enable verbose output (true/false)
COLOR Color mode: auto, always, never
${BOLD}EXAMPLES${RESET}
# List all storage accounts
${SCRIPT_NAME} --list
# List containers in an account
${SCRIPT_NAME} --list --account mystorageacct
# List blobs in a container
${SCRIPT_NAME} --list --account mystorageacct --container backups
# Audit storage security
${SCRIPT_NAME} --audit
# Sync local directory to container
${SCRIPT_NAME} --sync --account mystorageacct --container uploads --source /data/uploads
# Change tier for all blobs in a container
${SCRIPT_NAME} --tier --account mystorageacct --container archive --set-tier Cool
# Show lifecycle policy
${SCRIPT_NAME} --lifecycle --account mystorageacct --resource-group prod-rg
# Storage statistics
${SCRIPT_NAME} --stats
${BOLD}EXIT CODES${RESET}
0 Success
1 Runtime error
2 Critical findings (audit mode)
EOF
}
# ══════════════════════════════════════════════════════════════════════
# PARSE ARGS
# ══════════════════════════════════════════════════════════════════════
parse_args() {
while [[ $# -gt 0 ]]; do
case "$1" in
--list) RUN_MODE="list"; shift ;;
--audit) RUN_MODE="audit"; shift ;;
--sync) RUN_MODE="sync"; shift ;;
--tier) RUN_MODE="tier"; shift ;;
--lifecycle) RUN_MODE="lifecycle"; shift ;;
--stats) RUN_MODE="stats"; shift ;;
--account) STORAGE_ACCOUNT="${2:?--account requires a name}"; shift 2 ;;
--container) CONTAINER_NAME="${2:?--container requires a name}"; shift 2 ;;
--resource-group) RESOURCE_GROUP="${2:?--resource-group requires a value}"; shift 2 ;;
--set-tier) TIER_TARGET="${2:?--set-tier requires a tier}"; shift 2 ;;
--max-age) MAX_AGE="${2:?--max-age requires days}"; shift 2 ;;
--source) SOURCE_PATH="${2:?--source requires a path}"; shift 2 ;;
--format) OUTPUT_FORMAT="${2:?--format requires a value}"; shift 2 ;;
--subscription) SUBSCRIPTION="${2:?--subscription 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
}
# ══════════════════════════════════════════════════════════════════════
# MAIN
# ══════════════════════════════════════════════════════════════════════
main() {
parse_args "$@"
setup_colors
if [[ -z "$RUN_MODE" ]]; then
err "No mode specified"
echo ""
show_help
exit 1
fi
check_deps
check_credentials
START_TIME=$(date +%s)
case "$RUN_MODE" in
list) do_list ;;
audit) do_audit ;;
sync) do_sync ;;
tier) do_tier ;;
lifecycle) do_lifecycle ;;
stats) do_stats ;;
*) die "Unknown mode: ${RUN_MODE}" ;;
esac
echo ""
field "Duration:" "$(elapsed)"
if [[ "$RUN_MODE" == "audit" && "$TOTAL_CRIT" -gt 0 ]]; then
exit 2
fi
}
main "$@"