#!/usr/bin/env bash
set -Eeuo pipefail

TASK="REDIS01_Q131_SELINUX_LABEL_REPAIR_AND_VERIFY_V3"
EXPECTED_HOST="sqjcredis01"
EXPECTED_IP="10.2.203.112"
FIXED_ENTRY="/root/redis-q131-q132-v2"
FIXED_ENTRY_SHA256="d229ade533bbe22f66f174bbcfccf685b470f00c466a5286649710be14794f5e"
FAILED_CHILD_LOG="/var/backups/zhct-q131-selinux-repair-v2/q131-selinux-repair-v2-20260730T084158Z/operator.log"
FAILED_CHILD_LOG_SHA256="a88ecbb9f29271bea7c030f0553d6b8f3e9e0504223b083e4dc4f02d946256c7"
RUN_ID="q131-selinux-repair-v3-$(date -u +%Y%m%dT%H%M%SZ)"
EVIDENCE_DIR="/var/backups/zhct-q131-selinux-repair-v3/${RUN_ID}"
AUDIT_LOG="${EVIDENCE_DIR}/operator.log"
RESULT_FILE="${EVIDENCE_DIR}/result.env"

FCONTEXT_REGEXES=(
  '/opt/redis/5\.0\.14/bin(/.*)?'
  '/opt/redis/7\.2\.14/bin(/.*)?'
  '/etc/redis(/.*)?'
  '/data/redis5(/.*)?'
  '/data/redis7(/.*)?'
  '/var/log/redis5(/.*)?'
  '/var/log/redis7(/.*)?'
  '/var/run/redis5(/.*)?'
  '/var/run/redis7(/.*)?'
)
FCONTEXT_TYPES=(
  redis_exec_t
  redis_exec_t
  redis_conf_t
  redis_var_lib_t
  redis_var_lib_t
  redis_log_t
  redis_log_t
  redis_var_run_t
  redis_var_run_t
)
RESTORE_PATHS=(
  /opt/redis/5.0.14/bin
  /opt/redis/7.2.14/bin
  /etc/redis
  /data/redis5
  /data/redis7
  /var/log/redis5
  /var/log/redis7
)

timestamp() {
  date '+%Y-%m-%dT%H:%M:%S%z'
}

if test "${1:-}" = "--static-validate"; then
  test "${#FCONTEXT_REGEXES[@]}" -eq "${#FCONTEXT_TYPES[@]}"
  echo "Q131_SELINUX_LABEL_REPAIR_V3_STATIC_VALIDATE=PASS"
  echo "TARGET_HOST=${EXPECTED_HOST}"
  echo "TARGET_IP=${EXPECTED_IP}"
  echo "FCONTEXT_POLICY=KEEP_DESIRED_ADD_MISSING_FAIL_CONFLICT"
  echo "REDIS_UNITS=REDIS5_REDIS7_ONLY"
  echo "AUXILIARY_UNITS=DISABLED_INACTIVE"
  echo "Q132=LOCKED_NOT_EXECUTED"
  exit 0
fi

printf 'BEGIN|%s|%s\n' "$TASK" "$(timestamp)"

fail_before_write() {
  echo "ERROR: $*"
  printf 'END|%s|RC=1|PERSISTENT_WRITE=ZERO\n' "$TASK"
  exit 1
}

test "$(id -u)" -eq 0 || fail_before_write "root is required"
actual_host="$(hostname -s)"
test "$actual_host" = "$EXPECTED_HOST" ||
  fail_before_write "hostname mismatch: actual=${actual_host} expected=${EXPECTED_HOST}"
ip -4 -o addr show scope global | awk '{print $4}' | cut -d/ -f1 |
  grep -Fxq "$EXPECTED_IP" ||
  fail_before_write "expected IPv4 is absent: ${EXPECTED_IP}"
echo "Q131_SELINUX_REPAIR_V3_TARGET_IDENTITY=PASS|host=${actual_host}|ip=${EXPECTED_IP}"

for command_name in semanage restorecon matchpathcon getenforce systemctl ss runuser sha256sum rpm; do
  command -v "$command_name" >/dev/null 2>&1 ||
    fail_before_write "required command is missing: ${command_name}"
done
test "$(getenforce)" = "Enforcing" || fail_before_write "SELinux is not Enforcing"
rpm -q policycoreutils-python-utils >/dev/null 2>&1 ||
  fail_before_write "policycoreutils-python-utils is not installed"

test -f "$FIXED_ENTRY" && test ! -L "$FIXED_ENTRY" ||
  fail_before_write "fixed Q131 entry is absent or unsafe"
printf '%s  %s\n' "$FIXED_ENTRY_SHA256" "$FIXED_ENTRY" | sha256sum -c - ||
  fail_before_write "fixed Q131 entry checksum mismatch"
test -f "$FAILED_CHILD_LOG" && test ! -L "$FAILED_CHILD_LOG" ||
  fail_before_write "failed child audit is absent or unsafe"
printf '%s  %s\n' "$FAILED_CHILD_LOG_SHA256" "$FAILED_CHILD_LOG" | sha256sum -c - ||
  fail_before_write "failed child audit checksum mismatch"
grep -Fq "ValueError: 文件 spec /run/redis5(/.*)? 与等效规则 '/run /var/run' 冲突" "$FAILED_CHILD_LOG" ||
  fail_before_write "expected /run equivalence failure evidence is absent"
grep -Fq 'Q131_SELINUX_REPAIR_V2_ROLLBACK=SAFE_QUIET_STATE' "$FAILED_CHILD_LOG" ||
  fail_before_write "safe rollback receipt is absent"

for unit in redis5.service redis7.service sentinel5.service sentinel7.service orchestrator.service; do
  test "$(systemctl is-enabled "$unit" 2>/dev/null || true)" = "disabled" ||
    fail_before_write "safe quiet state requires disabled unit: ${unit}"
  test "$(systemctl is-active "$unit" 2>/dev/null || true)" = "inactive" ||
    fail_before_write "safe quiet state requires inactive unit: ${unit}"
done
if ss -lntupH |
  grep -E ':(6379|6387|26379|26387|3000|10008)([[:space:]]|$)' >/dev/null; then
    fail_before_write "target listener exists before v3 repair"
fi

for path in /opt/redis/5.0.14/bin/redis-server /opt/redis/7.2.14/bin/redis-server; do
  test -f "$path" && test ! -L "$path" ||
    fail_before_write "Redis executable is absent or unsafe: ${path}"
  case "$(stat -c '%C' "$path")" in
    *:user_tmp_t:*) ;;
    *) fail_before_write "unexpected executable context before v3 repair: ${path}" ;;
  esac
done
for config in /etc/redis/redis5.conf /etc/redis/redis7.conf; do
  test -f "$config" && test ! -L "$config" ||
    fail_before_write "Redis config is absent or unsafe: ${config}"
  test "$(stat -c '%U:%G:%a' "$config")" = "root:redis:640" ||
    fail_before_write "Redis config metadata mismatch: ${config}"
  if grep -Eq '__[A-Z0-9_]+__' "$config"; then
    fail_before_write "unresolved placeholder in Redis config: ${config}"
  fi
done
for secret in /etc/zhct/secrets/redis5.password /etc/zhct/secrets/redis7.password; do
  test -f "$secret" && test ! -L "$secret" && test -s "$secret" ||
    fail_before_write "secret metadata gate failed: ${secret}"
  test "$(stat -c '%U:%G:%a' "$secret")" = "root:root:600" ||
    fail_before_write "secret metadata mismatch: ${secret}"
done
echo "Q131_SECRET_METADATA_GATE=PASS|values=NOT_OUTPUT_NOT_HASHED"

policy_fcontexts="$(semanage fcontext -l 2>&1)"
for required_type in redis_exec_t redis_conf_t redis_var_lib_t redis_log_t redis_var_run_t; do
  grep -Fq ":${required_type}:" <<<"$policy_fcontexts" ||
    fail_before_write "required SELinux type is unavailable: ${required_type}"
done

FCONTEXT_ACTIONS=()
for ((i=0; i<${#FCONTEXT_REGEXES[@]}; i++)); do
  regex="${FCONTEXT_REGEXES[$i]}"
  type="${FCONTEXT_TYPES[$i]}"
  matches="$(grep -F -- "$regex" <<<"$policy_fcontexts" || true)"
  if test -z "$matches"; then
    FCONTEXT_ACTIONS+=(ADD_MISSING)
  elif grep -Fv ":${type}:" <<<"$matches" | grep -q .; then
    fail_before_write "existing fcontext conflicts with required type: ${regex} -> ${type}"
  else
    FCONTEXT_ACTIONS+=(KEEP_EXISTING_DESIRED)
  fi
done

port_table="$(semanage port -l 2>&1)"
grep -E '^redis_port_t[[:space:]]+tcp[[:space:]].*(^|[[:space:],])6379([[:space:],]|$)' \
  <<<"$port_table" >/dev/null ||
  fail_before_write "tcp/6379 is not assigned to redis_port_t"
port_6387_owner="$(
  grep -E '(^|[[:space:],])6387([[:space:],]|$)' <<<"$port_table" |
    awk '{print $1}' | sort -u | paste -sd ',' - || true
)"
case "$port_6387_owner" in
  "") PORT_6387_ACTION=ADD ;;
  redis_port_t) PORT_6387_ACTION=KEEP ;;
  *) fail_before_write "tcp/6387 belongs to another SELinux port type: ${port_6387_owner}" ;;
esac

echo "=== FCONTEXT PLAN ==="
for ((i=0; i<${#FCONTEXT_REGEXES[@]}; i++)); do
  printf 'FCONTEXT|%s|%s|%s\n' \
    "${FCONTEXT_ACTIONS[$i]}" "${FCONTEXT_TYPES[$i]}" "${FCONTEXT_REGEXES[$i]}"
done
echo "PORT_6387_ACTION=${PORT_6387_ACTION}"
echo "Q131_SELINUX_REPAIR_V3_PREWRITE_GATE=PASS"

install -d -o root -g root -m 0700 "$EVIDENCE_DIR"
exec 3>&1 4>&2
exec >"$AUDIT_LOG" 2>&1

mutation_started=0
success=0
port_added=0
ADDED_REGEXES=()

write_result() {
  local state="$1" detail="$2" temp_result
  temp_result="$(mktemp "${EVIDENCE_DIR}/.result.XXXXXX")"
  {
    printf 'state=%s\n' "$state"
    printf 'detail=%s\n' "$detail"
    printf 'run_id=%s\n' "$RUN_ID"
    printf 'target_host=%s\n' "$EXPECTED_HOST"
    printf 'target_ip=%s\n' "$EXPECTED_IP"
    printf 'redis_units=redis5,redis7\n'
    printf 'auxiliary_units=disabled_inactive\n'
    printf 'q132=locked_not_executed\n'
  } >"$temp_result"
  install -o root -g root -m 0600 "$temp_result" "$RESULT_FILE"
  rm -f -- "$temp_result"
}

rollback_repair() {
  set +e
  echo "=== AUTOMATIC SAFE ROLLBACK ==="
  systemctl disable --now redis5.service redis7.service
  systemctl disable --now sentinel5.service sentinel7.service orchestrator.service
  systemctl reset-failed redis5.service redis7.service
  if test "$port_added" -eq 1; then
    semanage port -d -p tcp 6387
  fi
  for ((j=${#ADDED_REGEXES[@]}-1; j>=0; j--)); do
    semanage fcontext -d "${ADDED_REGEXES[$j]}"
  done
  restorecon -RF "${RESTORE_PATHS[@]}"
  chcon -R -t user_tmp_t /opt/redis/5.0.14/bin /opt/redis/7.2.14/bin
  echo "Q131_SELINUX_REPAIR_V3_ROLLBACK=SAFE_QUIET_STATE"
  set -e
}

finalize() {
  local rc=$?
  trap - EXIT
  if test "$rc" -ne 0 && test "$mutation_started" -eq 1 && test "$success" -eq 0; then
    rollback_repair
    write_result FAIL "v3_repair_or_verify_rc_${rc}"
  fi
  if test "$rc" -eq 0; then
    marker="PASS_ROLE_ACTIVATED_LOCAL_ONLY"
  else
    marker="FAIL_ROLLED_BACK_SAFE_QUIET"
  fi
  printf 'Q131_SELINUX_LABEL_REPAIR_V3=%s\n' "$marker"
  printf 'END|%s|RC=%s|AUDIT_LOG=%s\n' "$TASK" "$rc" "$AUDIT_LOG"
  printf 'Q131_SELINUX_LABEL_REPAIR_V3=%s\n' "$marker" >&3
  printf 'END|%s|RC=%s|AUDIT_LOG=%s\n' "$TASK" "$rc" "$AUDIT_LOG" >&3
}
trap finalize EXIT

echo "AUDIT_BEGIN|${TASK}|$(timestamp)"
echo "RUN_ID=${RUN_ID}"
echo "=== PRE-REPAIR CONTEXTS ==="
ls -Zd "${RESTORE_PATHS[@]}"
mutation_started=1

for ((i=0; i<${#FCONTEXT_REGEXES[@]}; i++)); do
  if test "${FCONTEXT_ACTIONS[$i]}" = "ADD_MISSING"; then
    semanage fcontext -a -t "${FCONTEXT_TYPES[$i]}" "${FCONTEXT_REGEXES[$i]}"
    ADDED_REGEXES+=("${FCONTEXT_REGEXES[$i]}")
  fi
done
if test "$PORT_6387_ACTION" = "ADD"; then
  semanage port -a -t redis_port_t -p tcp 6387
  port_added=1
fi

restorecon -RFv "${RESTORE_PATHS[@]}"
echo "=== POST-REPAIR CONTEXTS ==="
ls -Zd "${RESTORE_PATHS[@]}"
for path in /opt/redis/5.0.14/bin/redis-server /opt/redis/7.2.14/bin/redis-server; do
  case "$(stat -c '%C' "$path")" in
    *:redis_exec_t:*) ;;
    *) echo "ERROR: redis_exec_t was not applied: ${path}"; exit 1 ;;
  esac
done

systemctl reset-failed redis5.service redis7.service
systemctl enable --now redis5.service redis7.service
systemctl disable --now sentinel5.service sentinel7.service orchestrator.service

"$FIXED_ENTRY" q131 verify --run-id "${RUN_ID}-verify"
"$FIXED_ENTRY" q131 archive --source-run-id "${RUN_ID}-verify"

test "$(systemctl is-enabled redis5.service)" = "enabled"
test "$(systemctl is-active redis5.service)" = "active"
test "$(systemctl is-enabled redis7.service)" = "enabled"
test "$(systemctl is-active redis7.service)" = "active"
for unit in sentinel5.service sentinel7.service orchestrator.service; do
  test "$(systemctl is-enabled "$unit" 2>/dev/null || true)" = "disabled"
  test "$(systemctl is-active "$unit" 2>/dev/null || true)" = "inactive"
done

write_result PASS ROLE_ACTIVATED_LOCAL_ONLY
success=1
echo "Q131_SELINUX_LABEL_REPAIR_V3=PASS_ROLE_ACTIVATED_LOCAL_ONLY"
echo "Q132=LOCKED_NOT_EXECUTED"
echo "SENTINEL_HA=NOT_CONFIGURED"
echo "VIP=DISABLED"
echo "SECRET_VALUES=NOT_OUTPUT_NOT_HASHED"
