160 lines
8.6 KiB
Bash
160 lines
8.6 KiB
Bash
#!/bin/sh
|
||
# Идемпотентная настройка Zitadel Actions v2 для журнала ИБ: Target (restAsync) → access-collector
|
||
# и Execution'ы на события из EVENTS_FILE. С флагом --remove (или MODE=remove) снимает Target
|
||
# с этих Execution'ов и удаляет его.
|
||
#
|
||
# Без set -x: заголовки с PAT лежат в файле $TMP/headers и в вывод не попадают.
|
||
# Ответы API с signingKey наружу не выводятся: коллектор подпись не проверяет. При переходе на приёмник
|
||
# с проверкой HMAC ключ из ответа CreateTarget нужно будет записать во Vault, а не в лог.
|
||
#
|
||
# Код выхода ≠ 0 при любой ошибке; остальные события при этом всё равно обрабатываются.
|
||
set -eu
|
||
|
||
MODE="${MODE:-apply}"
|
||
[ "${1:-}" = "--remove" ] && MODE=remove
|
||
|
||
: "${ZITADEL_API:?ZITADEL_API не задан (http://<service>:8080)}"
|
||
: "${ZITADEL_HOST:?ZITADEL_HOST не задан (внешний домен инстанса, ExternalDomain)}"
|
||
TARGET_NAME="${TARGET_NAME:-security-audit-idp-events}"
|
||
TARGET_ENDPOINT="${TARGET_ENDPOINT:-http://access-collector.security-audit.svc.cluster.local:8088/zitadel/events}"
|
||
TARGET_TIMEOUT="${TARGET_TIMEOUT:-10s}"
|
||
EVENTS_FILE="${EVENTS_FILE:-/config/events.txt}"
|
||
PAT_FILE="${PAT_FILE:-/vault/secrets/zitadel-pat}"
|
||
CURL_MAX_TIME="${CURL_MAX_TIME:-20}"
|
||
CONNECT_ATTEMPTS="${CONNECT_ATTEMPTS:-10}"
|
||
CONNECT_PAUSE="${CONNECT_PAUSE:-15}"
|
||
ISTIO_QUIT_ON_EXIT="${ISTIO_QUIT_ON_EXIT:-false}"
|
||
TMP="${TMPDIR:-/tmp}"
|
||
|
||
# Job в mesh: istio-proxy сам не завершится, без quitquitquit под останется Running.
|
||
quit_proxy() {
|
||
if [ "$ISTIO_QUIT_ON_EXIT" = "true" ]; then
|
||
curl -fsS --max-time 5 -X POST http://127.0.0.1:15020/quitquitquit >/dev/null 2>&1 || true
|
||
fi
|
||
}
|
||
trap quit_proxy EXIT
|
||
|
||
log() { printf '%s\n' "$*"; }
|
||
err() { printf 'ОШИБКА: %s\n' "$*" >&2; }
|
||
|
||
[ -r "$PAT_FILE" ] || { err "нет файла PAT $PAT_FILE (Vault Agent)"; exit 1; }
|
||
[ -r "$EVENTS_FILE" ] || { err "нет списка событий $EVENTS_FILE"; exit 1; }
|
||
|
||
# Инстанс выбирается по x-zitadel-instance-host (InstanceHostHeaders Zitadel), Host остаётся именем Service:
|
||
# по нему istio-proxy находит маршрут и шлёт запрос с mTLS. X-Forwarded-Proto — инстанс с ExternalSecure.
|
||
umask 077
|
||
{
|
||
printf 'Authorization: Bearer %s\n' "$(tr -d '\r\n' <"$PAT_FILE")"
|
||
printf 'x-zitadel-instance-host: %s\n' "$ZITADEL_HOST"
|
||
printf 'x-zitadel-public-host: %s\n' "$ZITADEL_HOST"
|
||
printf 'X-Forwarded-Proto: https\n'
|
||
printf 'Content-Type: application/json\n'
|
||
printf 'Accept: application/json\n'
|
||
} >"$TMP/headers"
|
||
|
||
# api METHOD PATH [BODY]: тело ответа в stdout при 2xx; иначе код и текст ошибки в stderr, return 1.
|
||
api() {
|
||
_method=$1 _path=$2
|
||
if [ $# -ge 3 ]; then set -- --data "$3"; else set --; fi
|
||
_code=$(curl -sS -o "$TMP/resp" -w '%{http_code}' --max-time "$CURL_MAX_TIME" \
|
||
-X "$_method" -H @"$TMP/headers" "$@" "$ZITADEL_API$_path") || _code=000
|
||
case "$_code" in
|
||
2??) cat "$TMP/resp" ;;
|
||
*) err "$_method $_path → HTTP $_code: $(head -c 300 "$TMP/resp" 2>/dev/null | tr '\n' ' ')"; return 1 ;;
|
||
esac
|
||
}
|
||
|
||
# --- 1. Доступность API (Zitadel и коллектор поднимаются раньше Job'а, но без dependsOn) ---
|
||
attempt=1
|
||
until curl -fsS -o /dev/null --max-time "$CURL_MAX_TIME" -H @"$TMP/headers" "$ZITADEL_API/debug/ready"; do
|
||
if [ "$attempt" -ge "$CONNECT_ATTEMPTS" ]; then
|
||
err "Zitadel недоступен: $ZITADEL_API (попыток: $attempt)"; exit 1
|
||
fi
|
||
log "Zitadel не готов, попытка $attempt/$CONNECT_ATTEMPTS, пауза ${CONNECT_PAUSE}s"
|
||
attempt=$((attempt + 1))
|
||
sleep "$CONNECT_PAUSE"
|
||
done
|
||
|
||
# --- 2. Типы событий инстанса: в лог — для сверки списка, и для проверки имён ниже ---
|
||
# Ответ сначала в переменную: в конвейере api | jq код ошибки api потерялся бы (set -e смотрит на jq).
|
||
resp=$(api POST /admin/v1/events/types/_search '{}')
|
||
printf '%s' "$resp" | jq -r '.eventTypes[]?.type' | sort -u >"$TMP/known-events"
|
||
log "Типы событий Zitadel ($(wc -l <"$TMP/known-events")):"
|
||
sed 's/^/ known: /' "$TMP/known-events"
|
||
|
||
sed -e 's/#.*//' -e 's/[[:space:]]//g' "$EVENTS_FILE" | grep -v '^$' | sort -u >"$TMP/events"
|
||
|
||
# --- 3. Target ---
|
||
search=$(jq -nc --arg n "$TARGET_NAME" \
|
||
'{filters: [{targetNameFilter: {targetName: $n, method: "TEXT_FILTER_METHOD_EQUALS"}}]}')
|
||
resp=$(api POST /v2/actions/targets/search "$search")
|
||
printf '%s' "$resp" | jq -c --arg n "$TARGET_NAME" '[.targets[]? | select(.name == $n)
|
||
| {id, endpoint, timeout, async: has("restAsync")}]' >"$TMP/targets"
|
||
found=$(jq length "$TMP/targets")
|
||
if [ "$found" -gt 1 ]; then
|
||
err "Target'ов с именем $TARGET_NAME: $found, ожидался один — разберитесь вручную"; exit 1
|
||
fi
|
||
target_id=$(jq -r '.[0].id // empty' "$TMP/targets")
|
||
|
||
if [ "$MODE" = "apply" ]; then
|
||
want=$(jq -nc --arg e "$TARGET_ENDPOINT" --arg t "$TARGET_TIMEOUT" \
|
||
'{restAsync: {}, endpoint: $e, timeout: $t}')
|
||
if [ -z "$target_id" ]; then
|
||
# Из ответа берётся только id: signingKey не выводится и не сохраняется.
|
||
resp=$(api POST /v2/actions/targets "$(printf '%s' "$want" | jq -c --arg n "$TARGET_NAME" '. + {name: $n}')")
|
||
target_id=$(printf '%s' "$resp" | jq -r '.id // empty')
|
||
[ -n "$target_id" ] || { err "CreateTarget не вернул id"; exit 1; }
|
||
log "Target $TARGET_NAME: создан, id=$target_id"
|
||
elif jq -e --arg e "$TARGET_ENDPOINT" --arg t "$TARGET_TIMEOUT" \
|
||
'.[0] | .async and .endpoint == $e and .timeout == $t' "$TMP/targets" >/dev/null; then
|
||
log "Target $TARGET_NAME: без изменений, id=$target_id"
|
||
else
|
||
api POST "/v2/actions/targets/$target_id" "$want" >/dev/null
|
||
log "Target $TARGET_NAME: обновлён (тип/endpoint/timeout), id=$target_id"
|
||
fi
|
||
elif [ -z "$target_id" ]; then
|
||
log "Target $TARGET_NAME не найден — снимать нечего"; exit 0
|
||
fi
|
||
|
||
# --- 4. Execution'ы: чужие targets сохраняются, свой добавляется в конец (или убирается при --remove) ---
|
||
rc=0; n_created=0; n_extended=0; n_same=0; n_removed=0; n_err=0
|
||
while IFS= read -r ev; do
|
||
if ! grep -qxF "$ev" "$TMP/known-events"; then
|
||
log " $ev: ошибка — такого типа нет в Zitadel"; n_err=$((n_err + 1)); rc=1; continue
|
||
fi
|
||
query=$(jq -nc --arg e "$ev" '{filters: [{inConditionsFilter: {conditions: [{event: {event: $e}}]}}]}')
|
||
if ! resp=$(api POST /v2/actions/executions/search "$query"); then
|
||
log " $ev: ошибка чтения"; n_err=$((n_err + 1)); rc=1; continue
|
||
fi
|
||
current=$(printf '%s' "$resp" \
|
||
| jq -c --arg e "$ev" '[.executions[]? | select(.condition.event.event == $e) | .targets[]?]')
|
||
has=$(printf '%s' "$current" | jq --arg t "$target_id" 'index($t) != null')
|
||
if [ "$MODE" = "apply" ]; then
|
||
if [ "$has" = "true" ]; then log " $ev: без изменений"; n_same=$((n_same + 1)); continue; fi
|
||
targets=$(printf '%s' "$current" | jq -c --arg t "$target_id" '. + [$t]')
|
||
if [ "$current" = "[]" ]; then status="создано"; else status="дополнено (были другие targets)"; fi
|
||
else
|
||
if [ "$has" != "true" ]; then log " $ev: без изменений"; n_same=$((n_same + 1)); continue; fi
|
||
targets=$(printf '%s' "$current" | jq -c --arg t "$target_id" 'map(select(. != $t))')
|
||
status="target снят"
|
||
fi
|
||
body=$(jq -nc --arg e "$ev" --argjson t "$targets" '{condition: {event: {event: $e}}, targets: $t}')
|
||
if api PUT /v2/actions/executions "$body" >/dev/null; then
|
||
log " $ev: $status"
|
||
case "$status" in
|
||
создано) n_created=$((n_created + 1)) ;;
|
||
дополнено*) n_extended=$((n_extended + 1)) ;;
|
||
*) n_removed=$((n_removed + 1)) ;;
|
||
esac
|
||
else
|
||
log " $ev: ошибка записи"; n_err=$((n_err + 1)); rc=1
|
||
fi
|
||
done <"$TMP/events"
|
||
|
||
if [ "$MODE" = "remove" ] && [ "$rc" -eq 0 ]; then
|
||
api DELETE "/v2/actions/targets/$target_id" >/dev/null && log "Target $TARGET_NAME удалён, id=$target_id" || rc=1
|
||
fi
|
||
|
||
log "Итог ($MODE): target=$target_id, событий=$(wc -l <"$TMP/events"), создано=$n_created, дополнено=$n_extended, снято=$n_removed, без изменений=$n_same, ошибок=$n_err"
|
||
exit "$rc"
|