vault-ops/infra/versions/vault-audit-mini-scan-v2.sh
2026-04-14 11:45:15 +07:00

175 lines
6 KiB
Bash
Executable file

#!/usr/bin/env bash
set -Eeuo pipefail
# vault-audit-mini-scan-v2.sh
# Ziel:
# - Nutzung aus Vault-Audit-Log (sudo-read) zeigen
# - CERT-Mappings erkennen wie in deinen mTLS-Skripten (Priorität):
# 1) request.data.name == mapping
# 2) Marker-Policy (z.B. marker-cert-auth) im Event
# 3) Superset aller Mapping-Policies im Event
# 4) CN-Match gegen allowed_common_names
# 5) Display-Name-Match
# - PKI-Rollen: issue/sign-Events zählen
# - Policies: bei Logins vergebene Policies zählen
#
# Aufruf:
# ./vault-audit-mini-scan.sh \
# --audit /home/vault/file/audit.log --mount pki-test \
# [--since 2025-09-30T00:00:00Z] [--strict]
#./vault-audit-mini-scan.sh --since "$(date -u -d '7 days ago' +%Y-%m-%dT00:00:00Z)"
# --strict: Nur Marker oder Policy-Superset werten (ohne Name/CN/Display)
#
# Exit: 0 OK, 2 fehlende Tools/Args
# ---------- Defaults ----------
AUDIT="/home/vault/file/audit.log"
MOUNT="pki-test"
SINCE=""
STRICT=false
# ---------- Args ----------
while [[ $# -gt 0 ]]; do
case "$1" in
--audit) AUDIT="$2"; shift 2;;
--mount) MOUNT="$2"; shift 2;;
--since) SINCE="$2"; shift 2;;
--strict) STRICT=true; shift 1;;
-h|--help) sed -n '1,120p' "$0"; exit 0;;
*) echo "unknown arg: $1" >&2; exit 2;;
esac
done
# ---------- Deps ----------
need(){ command -v "$1" >/dev/null || { echo "missing: $1" >&2; exit 2; }; }
need jq; need awk; need sort; need wc; need sed
VAULT_OK=true; command -v vault >/dev/null || VAULT_OK=false
[[ -r "$AUDIT" ]] || sudo test -r "$AUDIT" || { echo "audit not readable: $AUDIT" >&2; exit 2; }
# ---------- Helpers ----------
SUDO_JQ(){ sudo jq -r "$@" "$AUDIT"; }
# usage: JQ_TIMES '<jq-filter>' [jq-extra-args...]
# gibt NUR .time zurück; respektiert --since
JQ_TIMES(){
local filter="$1"; shift || true
if [[ -n "$SINCE" ]]; then
SUDO_JQ "$@" --arg since "$SINCE" "$filter | .time | select(. >= \$since)"
else
SUDO_JQ "$@" "$filter | .time"
fi
}
summarize_times(){
local lines cnt last
lines="$(cat)"
cnt=$(printf "%s\n" "$lines" | sed '/^$/d' | wc -l | awk '{print $1}')
last=$(printf "%s\n" "$lines" | sed '/^$/d' | tail -n1)
printf "use=%-6s last=%s\n" "${cnt:-0}" "${last:--}"
}
# ---------- Inventar ----------
readarray -t CERT_MAPS < <(
if $VAULT_OK && vault list -format=json auth/cert/certs >/dev/null 2>&1; then
vault list -format=json auth/cert/certs | jq -r '.[]'
else
SUDO_JQ 'select(.request.path=="auth/cert/login") | .request.data.name' 2>/dev/null | sort -u
fi
)
readarray -t PKI_ROLES < <(
if $VAULT_OK && vault list -format=json "$MOUNT/roles" >/dev/null 2>&1; then
vault list -format=json "$MOUNT/roles" | jq -r '.[]'
else
SUDO_JQ --arg m "$MOUNT" '
select((.request.path|startswith($m+"/issue/")) or (.request.path|startswith($m+"/sign/")))
| .request.path | capture("(?<op>issue|sign)/(?<role>[^/]+)$").role' | sort -u
fi
)
readarray -t POLICIES < <(
if $VAULT_OK && vault policy list -format=json >/dev/null 2>&1; then
vault policy list -format=json | jq -r '.[]'
else
SUDO_JQ 'select(.response.auth.policies!=null) | .response.auth.policies[]' | sort -u
fi
)
printf "=== AUDIT === %s\n\n" "$AUDIT"
# ---------- CERT MAPPINGS ----------
echo "=== CERT MAPPINGS (auth/cert/login) ==="
for m in "${CERT_MAPS[@]}"; do
[[ -n "$m" ]] || continue
printf "%-32s " "$m"
PM="[]"; ACN="[]"; DISP=""; MARKER=""
if $VAULT_OK; then
MAPJSON="$(vault read -format=json "auth/cert/certs/$m" 2>/dev/null || true)"
if [[ -n "$MAPJSON" ]]; then
PM="$(jq -c '(.data.policies // [])' <<<"$MAPJSON")"
ACN="$(jq -c '(.data.allowed_common_names // [])' <<<"$MAPJSON")"
DISP="$(jq -r '(.data.display_name // empty)' <<<"$MAPJSON")"
MARKER="$(jq -r '(.data.policies // [])[]? | select(test("^marker-"))' <<<"$MAPJSON" | head -n1 || true)"
fi
fi
if $STRICT; then
if [[ -n "$MARKER" ]]; then
# Marker-Policy hart werten -> Event-Objekt zurückgeben
JQ_TIMES '
select(.request.path=="auth/cert/login") as $e
| ($e.response.auth.policies // []) as $pol
| if (($marker != "") and (($pol | index($marker)) != null)) then $e else empty end
' --arg marker "$MARKER" | summarize_times
else
# Policy-Superset
JQ_TIMES '
select(.request.path=="auth/cert/login") as $e
| ($e.response.auth.policies // []) as $pol
| (reduce $pm[] as $x (true; . and (($pol | index($x)) != null))) as $ok
| if $ok then $e else empty end
' --argjson pm "$PM" | summarize_times
fi
else
# Breite Heuristik: Name → Marker → Policies → CN → Display
JQ_TIMES '
select(.request.path=="auth/cert/login") as $e
| (
((($e.request.data.name // "") == $m))
or
((($marker != "")) and ((($e.response.auth.policies // []) | index($marker)) != null))
or
((reduce $pm[] as $x (true; . and (((($e.response.auth.policies // [])) | index($x)) != null))))
or
((($e.response.auth.metadata.common_name // "") as $cn
| ( ($acn | length) > 0 ) and ( [ $acn[] | select(. == $cn) ] | length > 0 )))
or
((($disp != "")) and (($e.response.auth.display_name // "") == $disp))
) as $ok
| if $ok then $e else empty end
' \
--arg m "$m" \
--arg marker "$MARKER" \
--arg disp "$DISP" \
--argjson pm "$PM" \
--argjson acn "$ACN" | summarize_times
fi
done
# ---------- PKI ROLES ----------
echo; echo "=== PKI ROLES ($MOUNT/issue|sign) ==="
for r in "${PKI_ROLES[@]}"; do
[[ -n "$r" ]] || continue
printf "%-32s " "$r"
JQ_TIMES 'select((.request.path==($m+"/issue/"+$r)) or (.request.path==($m+"/sign/"+$r)))' \
--arg r "$r" --arg m "$MOUNT" | summarize_times
done
# ---------- POLICIES ----------
echo; echo "=== POLICIES (minted on login) ==="
for p in "${POLICIES[@]}"; do
[[ -n "$p" ]] || continue
printf "%-32s " "$p"
JQ_TIMES 'select(.response.auth.policies? and (.response.auth.policies | index($p)))' \
--arg p "$p" | summarize_times
done