199 lines
6.5 KiB
Bash
Executable File
199 lines
6.5 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
# Preflight checks before sofiia-console deploy.
|
|
#
|
|
# Exit codes:
|
|
# 0 = OK
|
|
# 1 = FAIL (or WARN in STRICT=1)
|
|
# 2 = WARN-only (STRICT=0)
|
|
|
|
set -euo pipefail
|
|
|
|
SOFIIA_URL="${SOFIIA_URL:-http://127.0.0.1:8002}"
|
|
STRICT="${STRICT:-0}"
|
|
|
|
RED='\033[0;31m'
|
|
GREEN='\033[0;32m'
|
|
YELLOW='\033[1;33m'
|
|
NC='\033[0m'
|
|
|
|
PASS=0
|
|
WARN=0
|
|
FAIL=0
|
|
|
|
_pass() { echo -e "${GREEN}PASS${NC}: $1"; PASS=$((PASS+1)); }
|
|
_warn() { echo -e "${YELLOW}WARN${NC}: $1"; WARN=$((WARN+1)); }
|
|
_fail() { echo -e "${RED}FAIL${NC}: $1"; FAIL=$((FAIL+1)); }
|
|
_info() { echo "INFO: $1"; }
|
|
_section() { echo -e "\n-- $1 --"; }
|
|
|
|
for cmd in bash curl rg python3; do
|
|
command -v "$cmd" >/dev/null 2>&1 || { echo "Missing prerequisite: $cmd"; exit 1; }
|
|
done
|
|
|
|
echo "Sofiia Console Preflight"
|
|
echo " SOFIIA_URL = ${SOFIIA_URL}"
|
|
echo " STRICT = ${STRICT}"
|
|
echo " $(date -u '+%Y-%m-%dT%H:%M:%SZ')"
|
|
|
|
_section "1) Secrets exposure scan (tracked files, best-effort)"
|
|
if git rev-parse --is-inside-work-tree >/dev/null 2>&1; then
|
|
mapfile -t scan_files < <(git ls-files | rg '(^|/)(docker-compose.*\.ya?ml|\.env(\..*)?$|config/.*\.ya?ml)$' || true)
|
|
if [ "${#scan_files[@]}" -eq 0 ]; then
|
|
_warn "No tracked compose/env/config files found for secrets scan"
|
|
else
|
|
# Patterns focus on obvious accidental leaks in committed files.
|
|
secret_pattern='(OPENAI_API_KEY\s*=\s*[^\s$]|DEEPSEEK_API_KEY\s*=\s*[^\s$]|ANTHROPIC_API_KEY\s*=\s*[^\s$]|TELEGRAM_BOT_TOKEN\s*=\s*[^\s$]|SUPERVISOR_API_KEY\s*=\s*[^\s$]|ROUTER_API_KEY\s*=\s*[^\s$]|Authorization:\s*Bearer\s+[A-Za-z0-9._-]+|ghp_[A-Za-z0-9]{20,}|xox[baprs]-[A-Za-z0-9-]{20,}|AKIA[0-9A-Z]{16}|sk-[A-Za-z0-9_-]{20,})'
|
|
if mapfile -t exposed < <(rg --files-with-matches -e "$secret_pattern" -- "${scan_files[@]}" 2>/dev/null); then
|
|
if [ "${#exposed[@]}" -gt 0 ]; then
|
|
_fail "Potential secrets detected in tracked files:"
|
|
for f in "${exposed[@]}"; do
|
|
echo " - $f"
|
|
done
|
|
else
|
|
_pass "No obvious plaintext secrets found in tracked compose/env/config files"
|
|
fi
|
|
else
|
|
_warn "Secrets scan command returned non-zero (review manually)"
|
|
fi
|
|
fi
|
|
else
|
|
_warn "Not inside git repo; skipped tracked-file secrets scan"
|
|
fi
|
|
|
|
_section "2) Required environment variables (presence only)"
|
|
required_env=("ROUTER_URL" "SOFIIA_DATA_DIR")
|
|
if [ "${SOFIIA_IDEMPOTENCY_BACKEND:-inmemory}" = "redis" ] || [ "${SOFIIA_RATE_LIMIT_BACKEND:-inmemory}" = "redis" ]; then
|
|
required_env+=("SOFIIA_REDIS_URL")
|
|
fi
|
|
|
|
for var_name in "${required_env[@]}"; do
|
|
if [ -n "${!var_name:-}" ]; then
|
|
_pass "env ${var_name} is set"
|
|
else
|
|
_warn "env ${var_name} is not set"
|
|
fi
|
|
done
|
|
|
|
_section "3) Reachability (health + metrics)"
|
|
if curl -fsS --max-time 5 "${SOFIIA_URL}/api/health" >/dev/null 2>&1; then
|
|
_pass "GET ${SOFIIA_URL}/api/health reachable"
|
|
else
|
|
_warn "GET ${SOFIIA_URL}/api/health unreachable (service may be down pre-deploy)"
|
|
fi
|
|
|
|
if curl -fsS --max-time 5 "${SOFIIA_URL}/metrics" >/dev/null 2>&1; then
|
|
_pass "GET ${SOFIIA_URL}/metrics reachable"
|
|
else
|
|
_warn "GET ${SOFIIA_URL}/metrics unreachable (service may be down pre-deploy)"
|
|
fi
|
|
|
|
_section "4) Filesystem / data dir writable"
|
|
DATA_DIR="${SOFIIA_DATA_DIR:-/app/data}"
|
|
if mkdir -p "${DATA_DIR}" 2>/dev/null; then
|
|
probe="${DATA_DIR}/.preflight_write_probe_$$"
|
|
if ( : > "${probe}" ) 2>/dev/null; then
|
|
rm -f "${probe}" || true
|
|
_pass "SOFIIA_DATA_DIR is writable (${DATA_DIR})"
|
|
else
|
|
_fail "SOFIIA_DATA_DIR is not writable (${DATA_DIR})"
|
|
fi
|
|
else
|
|
_fail "Cannot create SOFIIA_DATA_DIR (${DATA_DIR})"
|
|
fi
|
|
|
|
_section "5) Audit DB checks"
|
|
RETENTION_RAW="${SOFIIA_AUDIT_RETENTION_DAYS:-}"
|
|
if [ -n "${RETENTION_RAW}" ]; then
|
|
if [[ "${RETENTION_RAW}" =~ ^[0-9]+$ ]] && [ "${RETENTION_RAW}" -gt 0 ] 2>/dev/null; then
|
|
_pass "SOFIIA_AUDIT_RETENTION_DAYS=${RETENTION_RAW} (valid)"
|
|
else
|
|
_warn "SOFIIA_AUDIT_RETENTION_DAYS must be integer > 0 (got: ${RETENTION_RAW})"
|
|
fi
|
|
else
|
|
_info "SOFIIA_AUDIT_RETENTION_DAYS not set; default 90 will apply"
|
|
fi
|
|
|
|
DB_PATH="${DATA_DIR}/sofiia.db"
|
|
if [ -f "${DB_PATH}" ]; then
|
|
db_size_bytes=""
|
|
if stat -f%z "${DB_PATH}" >/dev/null 2>&1; then
|
|
db_size_bytes="$(stat -f%z "${DB_PATH}")"
|
|
elif stat -c%s "${DB_PATH}" >/dev/null 2>&1; then
|
|
db_size_bytes="$(stat -c%s "${DB_PATH}")"
|
|
elif command -v du >/dev/null 2>&1; then
|
|
db_size_bytes="$(du -k "${DB_PATH}" | awk '{print $1 * 1024}')"
|
|
fi
|
|
if [ -n "${db_size_bytes}" ] && [[ "${db_size_bytes}" =~ ^[0-9]+$ ]]; then
|
|
threshold_bytes="${SOFIIA_AUDIT_DB_WARN_BYTES:-1073741824}" # 1GB default
|
|
if [[ "${threshold_bytes}" =~ ^[0-9]+$ ]] && [ "${db_size_bytes}" -gt "${threshold_bytes}" ]; then
|
|
_warn "sofiia.db size is high (${db_size_bytes} bytes > ${threshold_bytes} bytes)"
|
|
else
|
|
_pass "sofiia.db size check OK (${db_size_bytes} bytes)"
|
|
fi
|
|
else
|
|
_warn "Could not determine sofiia.db size (stat/du unavailable)"
|
|
fi
|
|
|
|
if command -v sqlite3 >/dev/null 2>&1; then
|
|
if sqlite3 "${DB_PATH}" "SELECT 1 FROM audit_events LIMIT 1;" >/dev/null 2>&1; then
|
|
_pass "audit_events table exists"
|
|
else
|
|
_warn "audit_events table check failed (table missing or DB not ready)"
|
|
fi
|
|
else
|
|
_warn "sqlite3 not installed; skipped audit_events table check"
|
|
fi
|
|
else
|
|
_warn "DB file not found at ${DB_PATH} (fresh instance or path mismatch)"
|
|
fi
|
|
|
|
_section "6) Redis connectivity (if redis backend enabled)"
|
|
if [ "${SOFIIA_IDEMPOTENCY_BACKEND:-inmemory}" = "redis" ] || [ "${SOFIIA_RATE_LIMIT_BACKEND:-inmemory}" = "redis" ]; then
|
|
REDIS_URL="${SOFIIA_REDIS_URL:-}"
|
|
if [ -z "${REDIS_URL}" ]; then
|
|
_fail "Redis backend enabled but SOFIIA_REDIS_URL is empty"
|
|
else
|
|
if python3 - <<'PYEOF' >/dev/null 2>&1
|
|
import os
|
|
url = os.getenv("SOFIIA_REDIS_URL", "")
|
|
if not url:
|
|
raise SystemExit(2)
|
|
try:
|
|
import redis
|
|
c = redis.Redis.from_url(url, socket_connect_timeout=2, socket_timeout=2, decode_responses=True)
|
|
if c.ping():
|
|
raise SystemExit(0)
|
|
raise SystemExit(1)
|
|
except Exception:
|
|
raise SystemExit(1)
|
|
PYEOF
|
|
then
|
|
_pass "Redis ping OK (redis-python)"
|
|
elif command -v redis-cli >/dev/null 2>&1 && redis-cli -u "${REDIS_URL}" --no-auth-warning ping >/dev/null 2>&1; then
|
|
_pass "Redis ping OK (redis-cli)"
|
|
else
|
|
_fail "Redis ping failed (backend=redis)"
|
|
fi
|
|
fi
|
|
else
|
|
_pass "Redis connectivity check skipped (backend != redis)"
|
|
fi
|
|
|
|
echo ""
|
|
echo "========================================"
|
|
echo "Preflight summary: PASS=${PASS} WARN=${WARN} FAIL=${FAIL}"
|
|
echo "========================================"
|
|
|
|
if [ "${FAIL}" -gt 0 ]; then
|
|
exit 1
|
|
fi
|
|
|
|
if [ "${WARN}" -gt 0 ]; then
|
|
if [ "${STRICT}" = "1" ]; then
|
|
exit 1
|
|
fi
|
|
exit 2
|
|
fi
|
|
|
|
exit 0
|