462 lines
16 KiB
Bash
Executable file
462 lines
16 KiB
Bash
Executable file
#!/usr/bin/env bash
|
||
set -Eeuo pipefail
|
||
|
||
# vault-inventory-report.sh — Inventory & Relations Report for HashiCorp Vault
|
||
# - Auth mounts
|
||
# - Cert-Auth mappings (inkl. debug-policy detection)
|
||
# - AppRole roles (robust, still works ohne List-Rechte)
|
||
# - Policies: Pfade & pki-test/(issue|sign)/<role> Referenzen (+ Wildcards)
|
||
# - Cross-Refs: Cert/AppRole ↔ Policy, Policy → PKI-Role
|
||
# - PKI roles: USED zuerst (mit referenzierenden Policies), dann FREE
|
||
# - Missing policy detection für Cert/AppRole
|
||
# - JSON report + optional Graphviz .dot
|
||
|
||
# ===== Pretty logs =====
|
||
if [[ -t 1 ]]; then B=$'\e[1m'; R=$'\e[0m'; else B= R=; fi
|
||
ts(){ date +"%Y-%m-%d %H:%M:%S"; }
|
||
info(){ echo -e "🟦 ${B}[$(ts)]${R} $*"; }
|
||
ok(){ echo -e "🟩 ${B}[$(ts)]${R} $*"; }
|
||
warn(){ echo -e "🟨 ${B}[$(ts)]${R} $*"; }
|
||
err(){ echo -e "🟥 ${B}[$(ts)]${R} $*" >&2; }
|
||
|
||
need(){ command -v "$1" >/dev/null 2>&1 || { err "missing tool: $1"; exit 2; }; }
|
||
need vault
|
||
need jq
|
||
need grep
|
||
need sed
|
||
need awk
|
||
need sort
|
||
|
||
STAMP="$(date +%F_%H%M%S)"
|
||
OUT_JSON="/tmp/vault-inventory-${STAMP}.json"
|
||
OUT_DOT="/tmp/vault-inventory-${STAMP}.dot"
|
||
|
||
# ===== temp files registry =====
|
||
TEMPS=()
|
||
mk(){ local f; f="$(mktemp)"; TEMPS+=("$f"); echo -n "$f"; }
|
||
cleanup(){ local t; for t in "${TEMPS[@]:-}"; do rm -f "$t" 2>/dev/null || true; done; }
|
||
trap cleanup EXIT
|
||
|
||
CERT_ND="$(mk)" # NDJSON (cert mappings)
|
||
APPROLE_ND="$(mk)" # NDJSON (approles)
|
||
PKI_ND="$(mk)" # NDJSON (pki roles)
|
||
POL_RAW_DIR="$(mktemp -d)"; TEMPS+=("$POL_RAW_DIR")
|
||
POL_ND="$(mk)" # NDJSON (policy metadata)
|
||
|
||
# helper: NDJSON -> JSON-Array robust (leer => [])
|
||
ndjson_to_array() {
|
||
local in="$1" out="$2"
|
||
if [[ -s "$in" ]]; then
|
||
jq -s '.' "$in" > "$out"
|
||
else
|
||
printf '[]' > "$out"
|
||
fi
|
||
}
|
||
|
||
# ===== Reachability =====
|
||
if ! S="$(vault status -format=json 2>/dev/null)"; then
|
||
err "Vault nicht erreichbar (VAULT_ADDR/VAULT_CACERT korrekt gesetzt?)"
|
||
exit 1
|
||
fi
|
||
VERS="$(jq -r '.version' <<<"$S")"
|
||
SEALED="$(jq -r '.sealed' <<<"$S")"
|
||
ok "Vault erreichbar (version=${VERS}, sealed=${SEALED})"
|
||
|
||
# ===== Auth mounts (kurz) =====
|
||
if AM="$(vault auth list -detailed -format=json 2>/dev/null)"; then
|
||
info "Auth-Mounts (kurz):"
|
||
jq -r 'to_entries[] | " 📌 \(.key)\tplugin=\(.value.type)\taccessor=\(.value.accessor)"' <<<"$AM"
|
||
else
|
||
warn "konnte Auth-Mounts nicht lesen"
|
||
fi
|
||
|
||
# ===== Cert-Auth Mappings =====
|
||
CERT_NAMES=()
|
||
if MAPS="$(vault list -format=json auth/cert/certs 2>/dev/null)"; then
|
||
info "Cert-Auth Mappings:"
|
||
if [[ "$MAPS" != "null" ]]; then
|
||
mapfile -t CERT_NAMES < <(jq -r '.[]' <<<"$MAPS")
|
||
for c in "${CERT_NAMES[@]}"; do
|
||
if ! J="$(vault read -format=json "auth/cert/certs/${c}" 2>/dev/null)"; then
|
||
warn " 🔐 ${c} (lesen fehlgeschlagen)"
|
||
continue
|
||
fi
|
||
disp="$(jq -r '.data.display_name // ""' <<<"$J")"
|
||
ttl="$(jq -r '.data.token_ttl // 0' <<<"$J")"
|
||
maxt="$(jq -r '.data.token_max_ttl // 0' <<<"$J")"
|
||
has_dbg="$(jq -r '((.data.policies // []) | index("debug-policy")) | if . == null then "false" else "true" end' <<<"$J")"
|
||
dbg_txt="NEIN"; [[ "$has_dbg" == "true" ]] && dbg_txt="JA"
|
||
echo " 🔐 ${c} (display=${disp:-"-"}, ttl=${ttl}s, max_ttl=${maxt}s) 🔎 debug-policy=${dbg_txt}"
|
||
acn="$(jq -r '(.data.allowed_common_names // []) | join(",")' <<<"$J")"
|
||
pol="$(jq -r '(.data.policies // []) | join(",")' <<<"$J")"
|
||
[[ -n "$acn" ]] && echo " ↳ allowed_common_names: ${acn}"
|
||
[[ -n "$pol" ]] && echo " ↳ policies: ${pol}"
|
||
|
||
jq -c --arg name "$c" '
|
||
{
|
||
name: $name,
|
||
display_name: (.data.display_name // null),
|
||
token_ttl: (.data.token_ttl // 0),
|
||
token_max_ttl: (.data.token_max_ttl // 0),
|
||
policies: (.data.policies // []),
|
||
allowed_common_names: (.data.allowed_common_names // []),
|
||
debug_policy: (((.data.policies // []) | index("debug-policy")) != null)
|
||
}
|
||
' <<<"$J" >>"$CERT_ND"
|
||
done
|
||
else
|
||
echo " (keine Einträge)"
|
||
fi
|
||
else
|
||
warn "konnte Cert-Auth-Mappings nicht auflisten"
|
||
fi
|
||
|
||
# ===== AppRole Rollen =====
|
||
APPROLE_NAMES=()
|
||
if ARLIST="$(vault list -format=json auth/approle/role 2>/dev/null)"; then
|
||
info "AppRole-Rollen:"
|
||
if [[ "$ARLIST" != "null" ]]; then
|
||
mapfile -t APPROLE_NAMES < <(jq -r '.[]' <<<"$ARLIST")
|
||
for r in "${APPROLE_NAMES[@]}"; do
|
||
if ! JR="$(vault read -format=json "auth/approle/role/${r}" 2>/dev/null)"; then
|
||
warn " 🔑 ${r} (lesen fehlgeschlagen)"
|
||
continue
|
||
fi
|
||
bsi="$(jq -r '.data.bind_secret_id // false' <<<"$JR")"
|
||
tpol="$(jq -r '(.data.token_policies // []) | join(",")' <<<"$JR")"
|
||
ttype="$(jq -r '.data.token_type // ""' <<<"$JR")"
|
||
period="$(jq -r '.data.period // "0"' <<<"$JR")"
|
||
sidttl="$(jq -r '.data.secret_id_ttl // 0' <<<"$JR")"
|
||
echo " 🔑 ${r} (bind_secret_id=${bsi}, token_type=${ttype}, period=${period}, secret_id_ttl=${sidttl})"
|
||
[[ -n "$tpol" ]] && echo " ↳ token_policies: ${tpol}"
|
||
|
||
jq -c --arg name "$r" '
|
||
{
|
||
name: $name,
|
||
bind_secret_id: (.data.bind_secret_id // false),
|
||
token_policies: (.data.token_policies // []),
|
||
token_type: (.data.token_type // null),
|
||
period: (.data.period // null),
|
||
secret_id_ttl: (.data.secret_id_ttl // 0)
|
||
}
|
||
' <<<"$JR" >>"$APPROLE_ND"
|
||
done
|
||
else
|
||
echo " (keine Einträge)"
|
||
fi
|
||
else
|
||
# leiser, neutraler Fallback (keine Rechte ODER keine Einträge)
|
||
info "AppRole-Rollen: (keine Einträge oder keine Berechtigung)"
|
||
fi
|
||
|
||
# ===== Policies einsammeln (HCL dump) =====
|
||
POLS=()
|
||
if PLIST="$(vault policy list -format=json 2>/dev/null)"; then
|
||
mapfile -t POLS < <(jq -r '.[]' <<<"$PLIST")
|
||
for p in "${POLS[@]}"; do
|
||
vault policy read "$p" >"$POL_RAW_DIR/$p.hcl" 2>/dev/null || true
|
||
done
|
||
else
|
||
warn "konnte Policies nicht auflisten – Policy-Analyse ggf. unvollständig"
|
||
fi
|
||
|
||
# ===== Policies normalisieren (name, paths[], pki_issue_roles[], wildcard_issue_sign) =====
|
||
for p in "${POLS[@]:-}"; do
|
||
f="$POL_RAW_DIR/$p.hcl"
|
||
[[ -s "$f" ]] || continue
|
||
|
||
PPATHS=()
|
||
PROLES=()
|
||
HAS_WILDCARD=false
|
||
|
||
# alle path-Zeilen extrahieren (nur für Anzeige/Zwecke)
|
||
mapfile -t PPATHS < <(
|
||
grep -E '^[[:space:]]*path[[:space:]]*"' "$f" 2>/dev/null \
|
||
| sed -E 's/^[[:space:]]*path[[:space:]]*"([^"]+)".*/\1/' \
|
||
| sort -u
|
||
) || true
|
||
|
||
# issue|sign Rollen-Namen extrahieren (ohne *) – Mount fest: pki-test
|
||
mapfile -t PROLES < <(
|
||
grep -Eo 'pki-test/(issue|sign)/([A-Za-z0-9._-]+|\*)' "$f" 2>/dev/null \
|
||
| awk -F/ '{print $NF}' \
|
||
| grep -v '^\*$' \
|
||
| sort -u
|
||
) || true
|
||
|
||
# Wildcard vorhanden?
|
||
if grep -Eq 'pki-test/(issue|sign)/\*' "$f" 2>/dev/null; then
|
||
HAS_WILDCARD=true
|
||
fi
|
||
|
||
# JSON bauen
|
||
if ((${#PPATHS[@]})); then PATHS_JSON="$(printf '%s\n' "${PPATHS[@]}" | jq -R . | jq -s .)"; else PATHS_JSON='[]'; fi
|
||
if ((${#PROLES[@]})); then ROLES_JSON="$(printf '%s\n' "${PROLES[@]}" | jq -R . | jq -s .)"; else ROLES_JSON='[]'; fi
|
||
|
||
jq -nc \
|
||
--arg name "$p" \
|
||
--argjson paths "$PATHS_JSON" \
|
||
--argjson issue_roles "$ROLES_JSON" \
|
||
--argjson wildcard "$HAS_WILDCARD" \
|
||
'{name:$name, paths:$paths, pki_issue_roles:$issue_roles, wildcard_issue_sign:$wildcard}' \
|
||
>>"$POL_ND"
|
||
done
|
||
|
||
# ===== PKI Rollen (pki-test/roles) + Policy-Ref-Analyse (USED -> FREE, deutlich) =====
|
||
MOUNT="pki-test" # zentral – bei Bedarf später dynamisieren
|
||
|
||
PKI_ROLE_NAMES=()
|
||
if PRLIST="$(vault list -format=json ${MOUNT}/roles 2>/dev/null)"; then
|
||
info "PKI-Rollen (${MOUNT}/roles) — nach Policy-Referenzen sortiert:"
|
||
if [[ "$PRLIST" != "null" ]]; then
|
||
mapfile -t PKI_ROLE_NAMES < <(jq -r '.[]' <<<"$PRLIST")
|
||
for r in "${PKI_ROLE_NAMES[@]}"; do
|
||
if ! JR="$(vault read -format=json "${MOUNT}/roles/${r}" 2>/dev/null)"; then
|
||
warn " 📜 ${r} (lesen fehlgeschlagen)"
|
||
continue
|
||
fi
|
||
|
||
sflag="$(jq -r '.data.server_flag // false' <<<"$JR")"
|
||
cflag="$(jq -r '.data.client_flag // false' <<<"$JR")"
|
||
adom="$(jq -r '(.data.allowed_domains // []) | join(",")' <<<"$JR")"
|
||
asub="$(jq -r '.data.allow_subdomains // false' <<<"$JR")"
|
||
abare="$(jq -r '.data.allow_bare_domains // false' <<<"$JR")"
|
||
mttl="$(jq -r '.data.max_ttl // 0' <<<"$JR")"
|
||
|
||
# Referenzen: issue ODER sign, inkl. Wildcard
|
||
refs=()
|
||
if compgen -G "$POL_RAW_DIR/*.hcl" >/dev/null 2>&1; then
|
||
while IFS= read -r f; do
|
||
refs+=( "$(basename "${f%.hcl}")" )
|
||
done < <(grep -l -E "${MOUNT}/(issue|sign)/(${r}([^A-Za-z0-9._-]|$)|\*)" "$POL_RAW_DIR"/*.hcl 2>/dev/null || true)
|
||
fi
|
||
ref_count=${#refs[@]}
|
||
|
||
# JSON für Gesamtreport
|
||
if (( ref_count == 0 )); then
|
||
REFS_JSON="[]"
|
||
else
|
||
REFS_JSON="$(printf '%s\n' "${refs[@]}" | jq -R . | jq -s .)"
|
||
fi
|
||
|
||
jq -c \
|
||
--arg name "$r" \
|
||
--argjson refs "$REFS_JSON" \
|
||
--argjson refcount "$ref_count" \
|
||
'
|
||
{
|
||
name: $name,
|
||
server_flag: (.data.server_flag // false),
|
||
client_flag: (.data.client_flag // false),
|
||
allowed_domains: (.data.allowed_domains // []),
|
||
allow_subdomains: (.data.allow_subdomains // false),
|
||
allow_bare_domains: (.data.allow_bare_domains // false),
|
||
max_ttl: (.data.max_ttl // 0),
|
||
policy_refs: $refs,
|
||
policy_ref_count: $refcount
|
||
}' <<<"$JR" >>"$PKI_ND"
|
||
done
|
||
|
||
# USED zuerst (absteigend), dann FREE
|
||
USED_JSON="$(jq -s 'map(select(.policy_ref_count > 0)) | sort_by(-.policy_ref_count, .name)' "$PKI_ND")"
|
||
FREE_JSON="$(jq -s 'map(select(.policy_ref_count == 0)) | sort_by(.name)' "$PKI_ND")"
|
||
USED_CNT="$(jq -r 'length' <<<"$USED_JSON")"
|
||
FREE_CNT="$(jq -r 'length' <<<"$FREE_JSON")"
|
||
TOTAL=$(( USED_CNT + FREE_CNT ))
|
||
|
||
# USED
|
||
if (( USED_CNT > 0 )); then
|
||
echo " 🔗 USED (${USED_CNT}/${TOTAL}):"
|
||
jq -r '
|
||
.[] |
|
||
" 🔗 \(.name) [\(.policy_ref_count)] via: " + ((.policy_refs // []) | join(", ")) +
|
||
(
|
||
if ((.allowed_domains // []) | length) > 0
|
||
then "\n ↳ domains: " + ((.allowed_domains // []) | join(",")) +
|
||
" (subdomains=" + ((.allow_subdomains // false)|tostring) +
|
||
", bare=" + ((.allow_bare_domains // false)|tostring) + ")"
|
||
else ""
|
||
end
|
||
) +
|
||
"\n ↳ flags: server=" + ((.server_flag // false)|tostring) +
|
||
", client=" + ((.client_flag // false)|tostring) +
|
||
", max_ttl=" + ((.max_ttl // 0)|tostring)
|
||
' <<<"$USED_JSON"
|
||
fi
|
||
|
||
# FREE
|
||
if (( FREE_CNT > 0 )); then
|
||
echo " 🧹 FREE (${FREE_CNT}/${TOTAL}) — keine Policy-Referenzen:"
|
||
jq -r '
|
||
.[] |
|
||
" 🧹 " + .name + " (NO POLICY)" +
|
||
(
|
||
if ((.allowed_domains // []) | length) > 0
|
||
then "\n ↳ domains: " + ((.allowed_domains // []) | join(",")) +
|
||
" (subdomains=" + ((.allow_subdomains // false)|tostring) +
|
||
", bare=" + ((.allow_bare_domains // false)|tostring) + ")"
|
||
else ""
|
||
end
|
||
) +
|
||
"\n ↳ flags: server=" + ((.server_flag // false)|tostring) +
|
||
", client=" + ((.client_flag // false)|tostring) +
|
||
", max_ttl=" + ((.max_ttl // 0)|tostring)
|
||
' <<<"$FREE_JSON"
|
||
fi
|
||
|
||
# kompakte Summary
|
||
info "Policy-Ref-Analyse (${MOUNT}):"
|
||
echo " • total=${TOTAL}, used=${USED_CNT}, free=${FREE_CNT}"
|
||
if (( USED_CNT > 0 )); then
|
||
echo " • TOP USED:"
|
||
jq -r '.[] | " - \(.name) (\(.policy_ref_count)) -> " + ((.policy_refs // []) | join(", "))' <<<"$USED_JSON"
|
||
fi
|
||
else
|
||
echo " (keine Einträge)"
|
||
fi
|
||
else
|
||
warn "konnte PKI-Rollen nicht auflisten (${MOUNT}/roles)"
|
||
fi
|
||
|
||
# ===== Arrays für JSON/Enrichment (robust, auch wenn ND leer) =====
|
||
CERT_ARR="$(mk)"; ndjson_to_array "$CERT_ND" "$CERT_ARR"
|
||
APPROLE_ARR="$(mk)"; ndjson_to_array "$APPROLE_ND" "$APPROLE_ARR"
|
||
PKI_ARR="$(mk)"; ndjson_to_array "$PKI_ND" "$PKI_ARR"
|
||
POL_ARR="$(mk)"; ndjson_to_array "$POL_ND" "$POL_ARR"
|
||
|
||
# Policy-Namen (Set)
|
||
POL_NAMES_JSON="$(mk)"
|
||
printf '%s\n' "${POLS[@]:-}" | jq -R . | jq -s . > "$POL_NAMES_JSON"
|
||
|
||
# ===== Policies mit Reverse-Refs anreichern =====
|
||
POL_ENRICHED="$(mk)"
|
||
jq -n \
|
||
--slurpfile pol "$POL_ARR" \
|
||
--slurpfile cert "$CERT_ARR" \
|
||
--slurpfile ar "$APPROLE_ARR" \
|
||
'
|
||
($pol[0] // []) as $P
|
||
| ($cert[0] // []) as $C
|
||
| ($ar[0] // []) as $A
|
||
| [ foreach $P[] as $p ({};
|
||
{
|
||
name: $p.name,
|
||
paths: ($p.paths // []),
|
||
pki_issue_roles: ($p.pki_issue_roles // []),
|
||
wildcard_issue_sign: ($p.wildcard_issue_sign // false),
|
||
referenced_by: {
|
||
cert_mappings: (
|
||
$C | map(select((.policies // []) | index($p.name))) | map(.name)
|
||
),
|
||
approle_roles: (
|
||
$A | map(select((.token_policies // []) | index($p.name))) | map(.name)
|
||
)
|
||
}
|
||
}
|
||
) ]
|
||
' > "$POL_ENRICHED"
|
||
|
||
# ===== Missing policies (Cert/AppRole) =====
|
||
ALL_POLS="$(cat "$POL_NAMES_JSON")"
|
||
|
||
CERT_MISSING="$(mk)"
|
||
jq -n \
|
||
--slurpfile cert "$CERT_ARR" \
|
||
--argjson have "$ALL_POLS" '
|
||
($cert[0] // [])
|
||
| [ .[] as $c
|
||
| ($c.policies // []) as $ps
|
||
| [ $ps[] | select(($have | index(.)) == null) ] as $missing
|
||
| select(($missing | length) > 0)
|
||
| {name:$c.name, missing_policies:$missing}
|
||
]' > "$CERT_MISSING"
|
||
|
||
APPROLE_MISSING="$(mk)"
|
||
jq -n \
|
||
--slurpfile ar "$APPROLE_ARR" \
|
||
--argjson have "$ALL_POLS" '
|
||
($ar[0] // [])
|
||
| [ .[] as $a
|
||
| ($a.token_policies // []) as $ps
|
||
| [ $ps[] | select(($have | index(.)) == null) ] as $missing
|
||
| select(($missing | length) > 0)
|
||
| {name:$a.name, missing_policies:$missing}
|
||
]' > "$APPROLE_MISSING"
|
||
|
||
# ===== Graphviz DOT (optional visualization) =====
|
||
{
|
||
echo 'digraph vault_inventory {'
|
||
echo ' rankdir=LR;'
|
||
echo ' node [shape=box, style=rounded];'
|
||
echo ' subgraph cluster_cert { label="Cert-Auth Mappings"; style=dashed; color=gray;'
|
||
jq -r '.[].name | " \"" + . + "\""' "$CERT_ARR"
|
||
echo ' }'
|
||
echo ' subgraph cluster_ar { label="AppRole Roles"; style=dashed; color=gray;'
|
||
jq -r '.[].name | " \"" + . + "\""' "$APPROLE_ARR"
|
||
echo ' }'
|
||
echo ' subgraph cluster_pol { label="Policies"; style=dashed; color=gray;'
|
||
jq -r '.[].name | " \"" + . + "\""' "$POL_ENRICHED"
|
||
echo ' }'
|
||
echo ' subgraph cluster_pki { label="PKI Roles"; style=dashed; color=gray;'
|
||
jq -r '.[].name | " \"" + . + "\""' "$PKI_ARR"
|
||
echo ' }'
|
||
# edges: Cert -> Policy
|
||
jq -r '
|
||
.[] as $c
|
||
| ($c.policies // [])
|
||
| .[] as $p
|
||
| " \"" + $c.name + "\" -> \"" + $p + "\""
|
||
' "$CERT_ARR"
|
||
# edges: AppRole -> Policy
|
||
jq -r '
|
||
.[] as $a
|
||
| ($a.token_policies // [])
|
||
| .[] as $p
|
||
| " \"" + $a.name + "\" -> \"" + $p + "\""
|
||
' "$APPROLE_ARR"
|
||
# edges: Policy -> PKI role (via pki-test/(issue|sign)/<role>)
|
||
jq -r '
|
||
.[] as $p
|
||
| ($p.pki_issue_roles // [])
|
||
| .[] as $r
|
||
| " \"" + $p.name + "\" -> \"" + $r + "\""
|
||
' "$POL_ENRICHED"
|
||
echo '}'
|
||
} > "$OUT_DOT"
|
||
|
||
# ===== JSON-Report bauen (nutzt die *Array*-Dateien, nicht die ND-Files) =====
|
||
jq -n \
|
||
--arg generated "$(date -Iseconds)" \
|
||
--slurpfile cert "$CERT_ARR" \
|
||
--slurpfile approle "$APPROLE_ARR" \
|
||
--slurpfile pki "$PKI_ARR" \
|
||
--slurpfile policies "$POL_ENRICHED" \
|
||
--slurpfile cert_missing "$CERT_MISSING" \
|
||
--slurpfile approle_missing "$APPROLE_MISSING" \
|
||
'{
|
||
generated: $generated,
|
||
cert_mappings: ($cert[0] // []),
|
||
approle_roles: ($approle[0] // []),
|
||
pki_roles: ($pki[0] // []),
|
||
policies: ($policies[0] // []),
|
||
missing: {
|
||
cert_mappings: ($cert_missing[0] // []),
|
||
approle_roles: ($approle_missing[0] // [])
|
||
}
|
||
}' > "$OUT_JSON"
|
||
|
||
ok "Report geschrieben: ${OUT_JSON}"
|
||
echo " 🔎 Quick-Check:"
|
||
jq -r '[
|
||
"cert_mappings=\(.cert_mappings|length)",
|
||
"approle_roles=\(.approle_roles|length)",
|
||
"policies=\(.policies|length)",
|
||
"pki_roles=\(.pki_roles|length)",
|
||
("pki_roles_used=" + ((.pki_roles | map(select((.policy_ref_count // 0) > 0)) | length)|tostring)),
|
||
("pki_roles_free=" + ((.pki_roles | map(select((.policy_ref_count // 0) == 0)) | length)|tostring)),
|
||
("policies_refd_by_cert=" + ((.policies | map(select(.referenced_by.cert_mappings|length>0)) | length)|tostring)),
|
||
("policies_refd_by_approle=" + ((.policies | map(select(.referenced_by.approle_roles|length>0)) | length)|tostring))
|
||
] | " • " + (join(", "))' "$OUT_JSON"
|
||
|
||
echo " 🗺 DOT Graph: ${OUT_DOT} (optional: dot -Tpng \"$OUT_DOT\" -o /tmp/vault-inventory-${STAMP}.png)"
|
||
|