Files
Aegis/scripts/install.sh
T
kitos 504dfc52f5
Aegis CI / lint-and-test (push) Has been cancelled
Snyk Security Scan / Python vulnerabilities (backend) (push) Has been cancelled
Snyk Security Scan / npm vulnerabilities (frontend) (push) Has been cancelled
Snyk Security Scan / Docker image vulnerabilities (backend) (push) Has been cancelled
feat(auth): make email the unique login identifier
Login is now by email, not username. username still exists internally
(JWT sub claim, audit logs, Jira actor attribution, SSO provisioning all
still key off it) but is now always kept equal to email everywhere a user
is created or their email changes — never a separately-chosen value.

- User.email is now unique + NOT NULL (migration b067 backfills any
  missing/blank email from username first, so existing rows — notably
  the seeded admin, which historically had none — never violate it).
- /auth/login and the (unused but updated for consistency)
  authenticate_user() now query by email.
- create_user (legacy, unreferenced but kept) and
  create_user_without_password both derive username from email.
- update_user keeps username in sync when email changes, and rejects
  duplicate emails.
- seed.py reads ADMIN_EMAIL (new env var, wired through install.sh and
  docker-compose.prod.yml) for the initial admin; falls back to an
  email-shaped ADMIN_USERNAME or a placeholder that's flagged for the
  operator to fix.
- admin_config.py's import bundle now matches/creates users by email,
  skipping (not crashing on) entries with no email.
- sso_service.py always sets username = email for SSO-provisioned users.
- LoginPage/auth.ts updated to email input/copy (wire field name stays
  'username' — that's the OAuth2PasswordRequestForm spec, not the value).
2026-07-23 14:39:36 +02:00

573 lines
27 KiB
Bash

#!/bin/bash
# =============================================================================
# Aegis - Interactive Production Installer
# =============================================================================
# Sets up the Aegis platform for production with an interactive wizard
# that configures all environment variables.
#
# Usage:
# chmod +x scripts/install.sh
# ./scripts/install.sh
#
# Prerequisites:
# - Docker and Docker Compose installed
# - Port 80 (or chosen port) available
# =============================================================================
set -e
# Always run from the project root (parent of scripts/)
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
PROJECT_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
cd "$PROJECT_ROOT"
# ── Colors & helpers ──────────────────────────────────────────────────────
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
RED='\033[0;31m'
CYAN='\033[0;36m'
BOLD='\033[1m'
DIM='\033[2m'
NC='\033[0m'
print_ok() { echo -e "${GREEN}[OK]${NC} $1"; }
print_warn() { echo -e "${YELLOW}[!]${NC} $1"; }
print_error() { echo -e "${RED}[X]${NC} $1"; }
print_info() { echo -e "${CYAN}[i]${NC} $1"; }
print_header() { echo -e "\n${BOLD}── $1 ──${NC}\n"; }
print_prompt() { echo -en "${CYAN}>>>${NC} $1"; }
# Generate a cryptographically secure random string
gen_secret() {
python3 -c "import secrets; print(secrets.token_hex($1))" 2>/dev/null \
|| openssl rand -hex "$1" 2>/dev/null \
|| head -c "$1" /dev/urandom | od -An -tx1 | tr -d ' \n'
}
gen_password() {
python3 -c "import secrets; print(secrets.token_urlsafe($1))" 2>/dev/null \
|| openssl rand -base64 "$1" 2>/dev/null \
|| head -c "$1" /dev/urandom | base64 | tr -d '=/+' | head -c "$1"
}
# ── Banner ────────────────────────────────────────────────────────────────
clear 2>/dev/null || true
echo ""
echo -e "${BOLD}"
echo " ╔══════════════════════════════════════════════════════════╗"
echo " ║ ║"
echo " ║ Aegis - Installation Wizard ║"
echo " ║ MITRE ATT&CK Coverage Platform ║"
echo " ║ ║"
echo " ╚══════════════════════════════════════════════════════════╝"
echo -e "${NC}"
# ═════════════════════════════════════════════════════════════════════════
# STEP 1: Check prerequisites
# ═════════════════════════════════════════════════════════════════════════
print_header "Step 1/5 - Checking prerequisites"
if ! command -v docker &> /dev/null; then
print_error "Docker is not installed. Please install Docker first."
echo " https://docs.docker.com/engine/install/"
exit 1
fi
print_ok "Docker found: $(docker --version | head -1)"
if ! docker info > /dev/null 2>&1; then
print_error "Docker daemon is not running. Please start Docker."
exit 1
fi
print_ok "Docker daemon is running"
if docker compose version > /dev/null 2>&1; then
COMPOSE_CMD="docker compose"
elif command -v docker-compose &> /dev/null; then
COMPOSE_CMD="docker-compose"
else
print_error "Docker Compose is not installed."
echo " https://docs.docker.com/compose/install/"
exit 1
fi
print_ok "Docker Compose found ($COMPOSE_CMD)"
# Auto-detect Docker API version
DOCKER_SERVER_API=$(docker version --format '{{.Server.APIVersion}}' 2>/dev/null || echo "")
if [ -n "$DOCKER_SERVER_API" ]; then
export DOCKER_API_VERSION="$DOCKER_SERVER_API"
fi
# ═════════════════════════════════════════════════════════════════════════
# STEP 2: Interactive configuration
# ═════════════════════════════════════════════════════════════════════════
print_header "Step 2/5 - Configuration"
ENV_FILE=".env"
SKIP_CONFIG=false
if [ -f "$ENV_FILE" ]; then
print_warn "An existing .env file was found."
echo ""
print_prompt "Do you want to reconfigure? (y/N): "
read -r REPLY
if [[ ! $REPLY =~ ^[Yy]$ ]]; then
print_info "Keeping existing configuration."
SKIP_CONFIG=true
# Older installs predate PLATFORM_URL (added when email links moved
# off the dev default) — backfill it from CORS_ORIGINS so upgrading
# in place doesn't break the backend's required-env-var check.
if ! grep -q '^PLATFORM_URL=' "$ENV_FILE" 2>/dev/null; then
EXISTING_ORIGIN=$(grep '^CORS_ORIGINS=' "$ENV_FILE" 2>/dev/null | cut -d= -f2-)
if [ -n "$EXISTING_ORIGIN" ]; then
echo "PLATFORM_URL=${EXISTING_ORIGIN}" >> "$ENV_FILE"
print_info "Added missing PLATFORM_URL=${EXISTING_ORIGIN} to existing .env"
fi
fi
fi
fi
if [ "$SKIP_CONFIG" = false ]; then
echo -e " ${DIM}Answer the following questions to configure Aegis."
echo -e " Press Enter to accept the default value shown in [brackets].${NC}"
echo ""
# ── Domain / URL ──────────────────────────────────────────────────
echo -e " ${BOLD}1. Domain Configuration${NC}"
echo -e " ${DIM}The domain where Aegis will be accessible."
echo -e " Examples: aegis.example.com, 192.168.1.100, localhost${NC}"
echo ""
print_prompt "Domain or IP [localhost]: "
read -r INPUT_DOMAIN
DOMAIN="${INPUT_DOMAIN:-localhost}"
# ── Protocol ──────────────────────────────────────────────────────
if [ "$DOMAIN" = "localhost" ] || [ "$DOMAIN" = "127.0.0.1" ]; then
PROTOCOL="http"
print_info "Using HTTP for local deployment"
else
echo ""
print_prompt "Are you using HTTPS/SSL? (Y/n): "
read -r REPLY
if [[ $REPLY =~ ^[Nn]$ ]]; then
PROTOCOL="http"
else
PROTOCOL="https"
fi
fi
# ── Port ──────────────────────────────────────────────────────────
if [ "$PROTOCOL" = "https" ]; then
DEFAULT_PORT=443
else
DEFAULT_PORT=80
fi
echo ""
echo -e " ${BOLD}2. Port${NC}"
print_prompt "Frontend port [$DEFAULT_PORT]: "
read -r INPUT_PORT
FRONTEND_PORT="${INPUT_PORT:-$DEFAULT_PORT}"
# Build the full origin URL for CORS
if { [ "$PROTOCOL" = "https" ] && [ "$FRONTEND_PORT" = "443" ]; } || \
{ [ "$PROTOCOL" = "http" ] && [ "$FRONTEND_PORT" = "80" ]; }; then
ORIGIN_URL="${PROTOCOL}://${DOMAIN}"
else
ORIGIN_URL="${PROTOCOL}://${DOMAIN}:${FRONTEND_PORT}"
fi
# ── Admin account ─────────────────────────────────────────────────
echo ""
echo -e " ${BOLD}3. Admin Account${NC}"
echo -e " ${DIM}The initial administrator account for Aegis.${NC}"
echo ""
print_prompt "Admin username [admin]: "
read -r INPUT_ADMIN_USER
ADMIN_USERNAME="${INPUT_ADMIN_USER:-admin}"
echo ""
echo -e " ${DIM}Email is what you'll actually log in with (the unique identifier).${NC}"
print_prompt "Admin email: "
read -r INPUT_ADMIN_EMAIL
while [ -z "$INPUT_ADMIN_EMAIL" ]; do
print_warn "An admin email is required."
print_prompt "Admin email: "
read -r INPUT_ADMIN_EMAIL
done
ADMIN_EMAIL="$INPUT_ADMIN_EMAIL"
echo ""
echo -e " ${DIM}Leave empty to auto-generate a secure password."
echo -e " The password will be shown in the installation summary.${NC}"
print_prompt "Admin password [auto-generate]: "
read -rs INPUT_ADMIN_PASS
echo ""
ADMIN_PASSWORD="${INPUT_ADMIN_PASS}"
if [ -z "$ADMIN_PASSWORD" ]; then
ADMIN_PASSWORD=$(gen_password 18)
ADMIN_PW_GENERATED=true
print_info "Password will be auto-generated"
else
ADMIN_PW_GENERATED=false
print_ok "Password set"
fi
# ── Database ──────────────────────────────────────────────────────
echo ""
echo -e " ${BOLD}4. Database${NC}"
print_prompt "Database name [attackdb]: "
read -r INPUT_DB_NAME
DB_NAME="${INPUT_DB_NAME:-attackdb}"
print_prompt "Database user [postgres]: "
read -r INPUT_DB_USER
DB_USER="${INPUT_DB_USER:-postgres}"
echo -e " ${DIM}Leave empty to auto-generate a secure password.${NC}"
print_prompt "Database password [auto-generate]: "
read -rs INPUT_DB_PASS
echo ""
if [ -z "$INPUT_DB_PASS" ]; then
DB_PASSWORD=$(gen_password 24)
print_info "Database password auto-generated"
else
DB_PASSWORD="$INPUT_DB_PASS"
print_ok "Database password set"
fi
# ── Token expiry ──────────────────────────────────────────────────
echo ""
echo -e " ${BOLD}5. Session Duration${NC}"
print_prompt "Token expiry in minutes [60]: "
read -r INPUT_TOKEN_EXP
TOKEN_EXPIRE_MINUTES="${INPUT_TOKEN_EXP:-60}"
# ── MITRE sync ────────────────────────────────────────────────────
echo ""
echo -e " ${BOLD}6. Initial Data${NC}"
print_prompt "Run MITRE ATT&CK sync after install? (Y/n): "
read -r INPUT_SYNC
if [[ $INPUT_SYNC =~ ^[Nn]$ ]]; then
RUN_MITRE_SYNC=false
else
RUN_MITRE_SYNC=true
fi
# ── Generate secrets ──────────────────────────────────────────────
SECRET_KEY=$(gen_secret 32)
MINIO_ACCESS=$(gen_secret 8)
MINIO_SECRET=$(gen_password 24)
# ── Show summary before writing ──────────────────────────────────
echo ""
echo -e "${BOLD} ┌──────────────────────────────────────────────────────┐${NC}"
echo -e "${BOLD} │ Configuration Summary │${NC}"
echo -e "${BOLD} ├──────────────────────────────────────────────────────┤${NC}"
echo -e " │ URL: ${CYAN}${ORIGIN_URL}${NC}"
echo -e " │ Admin user: ${CYAN}${ADMIN_USERNAME}${NC}"
echo -e " │ Admin email: ${CYAN}${ADMIN_EMAIL}${NC}"
if [ "$ADMIN_PW_GENERATED" = true ]; then
echo -e " │ Admin pass: ${CYAN}(auto-generated)${NC}"
else
echo -e " │ Admin pass: ${CYAN}(custom)${NC}"
fi
echo -e " │ Database: ${CYAN}${DB_USER}@${DB_NAME}${NC}"
echo -e " │ Port: ${CYAN}${FRONTEND_PORT}${NC}"
echo -e " │ Session TTL: ${CYAN}${TOKEN_EXPIRE_MINUTES} min${NC}"
echo -e " │ MITRE sync: ${CYAN}$([ "$RUN_MITRE_SYNC" = true ] && echo "yes" || echo "no")${NC}"
echo -e "${BOLD} └──────────────────────────────────────────────────────┘${NC}"
echo ""
# Warn about data loss if containers/volumes already exist
if $COMPOSE_CMD -f docker-compose.prod.yml ps -q 2>/dev/null | grep -q . || \
docker volume ls --format '{{.Name}}' 2>/dev/null | grep -qi 'postgres'; then
echo ""
print_warn "Existing database volumes detected."
print_warn "Proceeding will RESET the database — all existing data will be lost."
fi
print_prompt "Proceed with these settings? (Y/n): "
read -r CONFIRM
if [[ $CONFIRM =~ ^[Nn]$ ]]; then
print_warn "Installation cancelled. Run the script again to reconfigure."
exit 0
fi
# ── Write .env ────────────────────────────────────────────────────
cat > "$ENV_FILE" <<ENVEOF
# =============================================================================
# Aegis Production Environment
# Generated by install.sh on $(date -u +"%Y-%m-%d %H:%M:%S UTC")
# =============================================================================
# ── Database ─────────────────────────────────────────────────────────────────
DB_USER=${DB_USER}
DB_PASSWORD=${DB_PASSWORD}
DB_NAME=${DB_NAME}
# ── Security ─────────────────────────────────────────────────────────────────
SECRET_KEY=${SECRET_KEY}
TOKEN_EXPIRE_MINUTES=${TOKEN_EXPIRE_MINUTES}
# ── Initial Admin Account ────────────────────────────────────────────────────
ADMIN_USERNAME=${ADMIN_USERNAME}
ADMIN_EMAIL=${ADMIN_EMAIL}
ADMIN_PASSWORD=${ADMIN_PASSWORD}
# ── MinIO Object Storage ─────────────────────────────────────────────────────
MINIO_ACCESS_KEY=${MINIO_ACCESS}
MINIO_SECRET_KEY=${MINIO_SECRET}
MINIO_BUCKET=evidence
# ── CORS (allowed frontend origins) ─────────────────────────────────────────
CORS_ORIGINS=${ORIGIN_URL}
# ── Emails ────────────────────────────────────────────────────────────────────
# Base URL used to build links in outbound emails (set-password, etc).
PLATFORM_URL=${ORIGIN_URL}
# ── Frontend ─────────────────────────────────────────────────────────────────
FRONTEND_PORT=${FRONTEND_PORT}
# ── Environment ──────────────────────────────────────────────────────────────
AEGIS_ENV=production
ENVEOF
print_ok ".env file created with secure configuration"
fi # end SKIP_CONFIG
# ═════════════════════════════════════════════════════════════════════════
# STEP 3: Build and start
# ═════════════════════════════════════════════════════════════════════════
print_header "Step 3/5 - Building and starting containers"
print_info "This may take several minutes on first run..."
# When reconfiguring, remove old volumes so Postgres re-initializes with the new password.
# Without this, Postgres ignores the new DB_PASSWORD because it only sets credentials
# on first initialization (empty volume) — leaving the backend unable to authenticate.
if [ "$SKIP_CONFIG" = false ]; then
print_info "Removing existing volumes to apply new credentials..."
$COMPOSE_CMD -f docker-compose.prod.yml down -v > /dev/null 2>&1 || true
fi
if ! $COMPOSE_CMD -f docker-compose.prod.yml up -d --build 2>&1; then
print_error "Failed to build/start containers. Check the output above."
exit 1
fi
print_ok "Containers started"
# ═════════════════════════════════════════════════════════════════════════
# STEP 4: Wait for services
# ═════════════════════════════════════════════════════════════════════════
print_header "Step 4/5 - Waiting for services to be ready"
# Wait for PostgreSQL
echo -en " PostgreSQL ..."
MAX_RETRIES=30
RETRY=0
until docker exec aegis-postgres pg_isready -U postgres > /dev/null 2>&1; do
RETRY=$((RETRY + 1))
if [ $RETRY -ge $MAX_RETRIES ]; then
echo ""
print_error "PostgreSQL failed to start. Check: docker logs aegis-postgres"
exit 1
fi
echo -n "."
sleep 2
done
echo -e " ${GREEN}ready${NC}"
# Wait for backend (runs migrations + seed)
echo -en " Backend (migrations + seed) ..."
RETRY=0
until docker exec aegis-backend curl -sf http://localhost:8000/health > /dev/null 2>&1; do
RETRY=$((RETRY + 1))
if [ $RETRY -ge 90 ]; then
echo ""
print_error "Backend failed to start after 3 minutes."
echo " Check: docker logs aegis-backend"
exit 1
fi
echo -n "."
sleep 2
done
echo -e " ${GREEN}ready${NC}"
# Wait for frontend
FRONTEND_PORT=$(grep FRONTEND_PORT "$ENV_FILE" 2>/dev/null | cut -d= -f2 || echo "80")
FRONTEND_PORT=${FRONTEND_PORT:-80}
echo -en " Frontend ..."
RETRY=0
until curl -sf "http://localhost:${FRONTEND_PORT}" > /dev/null 2>&1; do
RETRY=$((RETRY + 1))
if [ $RETRY -ge 30 ]; then
echo ""
print_error "Frontend failed to start. Check: docker logs aegis-frontend"
exit 1
fi
echo -n "."
sleep 2
done
echo -e " ${GREEN}ready${NC}"
print_ok "All services are running"
# ── Extract admin credentials from backend logs ──────────────────────
# Login is by email (the unique identifier) — ADMIN_CREDS_USER holds that
# email despite the variable name, kept for minimal diff below.
ADMIN_CREDS_USER=""
ADMIN_CREDS_PASS=""
# Try to extract the credentials from the backend startup logs
LOG_OUTPUT=$(docker logs aegis-backend 2>&1 | tail -20)
if echo "$LOG_OUTPUT" | grep -q "Initial Admin User Created"; then
ADMIN_CREDS_USER=$(echo "$LOG_OUTPUT" | grep "Login (email) :" | sed 's/.*Login (email) : //')
ADMIN_CREDS_PASS=$(echo "$LOG_OUTPUT" | grep "Password :" | sed 's/.*Password : //')
fi
# Fallback: if we set it via env, use those values
if [ -z "$ADMIN_CREDS_USER" ]; then
ADMIN_CREDS_USER=$(grep '^ADMIN_EMAIL=' "$ENV_FILE" 2>/dev/null | cut -d= -f2-)
fi
if [ -z "$ADMIN_CREDS_PASS" ] || [ "$ADMIN_CREDS_PASS" = "(set via ADMIN_PASSWORD env var)" ]; then
ADMIN_CREDS_PASS=$(grep ADMIN_PASSWORD "$ENV_FILE" 2>/dev/null | cut -d= -f2 || echo "")
fi
# ═════════════════════════════════════════════════════════════════════════
# STEP 5: Initial data sync (optional)
# ═════════════════════════════════════════════════════════════════════════
# Re-read RUN_MITRE_SYNC if we skipped config
if [ "$SKIP_CONFIG" = true ]; then
echo ""
print_prompt "Run initial MITRE ATT&CK sync? (~700 techniques, 1-2 min) (Y/n): "
read -r INPUT_SYNC
if [[ $INPUT_SYNC =~ ^[Nn]$ ]]; then
RUN_MITRE_SYNC=false
else
RUN_MITRE_SYNC=true
fi
fi
if [ "$RUN_MITRE_SYNC" = true ]; then
print_header "Step 5/5 - Initial data sync"
print_info "Authenticating with backend..."
# Authenticate via backend container (most reliable)
TOKEN=$(docker exec aegis-backend curl -sf -X POST "http://localhost:8000/api/v1/auth/login" \
-H "Content-Type: application/x-www-form-urlencoded" \
-d "username=${ADMIN_CREDS_USER}&password=${ADMIN_CREDS_PASS}" 2>/dev/null | \
python3 -c "import sys,json; print(json.load(sys.stdin).get('access_token',''))" 2>/dev/null || echo "")
if [ -n "$TOKEN" ] && [ "$TOKEN" != "" ]; then
# MITRE ATT&CK sync
print_info "Syncing MITRE ATT&CK techniques (1-2 minutes)..."
SYNC_RESULT=$(docker exec aegis-backend curl -sf --max-time 300 \
-X POST "http://localhost:8000/api/v1/system/sync-mitre" \
-H "Authorization: Bearer $TOKEN" 2>/dev/null || echo "error")
if [ "$SYNC_RESULT" != "error" ]; then
NEW_TECHNIQUES=$(echo "$SYNC_RESULT" | python3 -c "import sys,json; print(json.load(sys.stdin).get('new',0))" 2>/dev/null || echo "?")
print_ok "MITRE sync completed ($NEW_TECHNIQUES techniques imported)"
else
print_warn "MITRE sync timed out. You can retry from the System page."
fi
# Data sources sync
print_info "Syncing data sources (Atomic Red Team, SigmaHQ, etc.)..."
SOURCES=$(docker exec aegis-backend curl -sf "http://localhost:8000/api/v1/data-sources" \
-H "Authorization: Bearer $TOKEN" 2>/dev/null | \
python3 -c "import sys,json; [print(s['id']) for s in json.load(sys.stdin)]" 2>/dev/null || echo "")
SYNC_COUNT=0
for source_id in $SOURCES; do
docker exec aegis-backend curl -sf --max-time 120 \
-X POST "http://localhost:8000/api/v1/data-sources/${source_id}/sync" \
-H "Authorization: Bearer $TOKEN" > /dev/null 2>&1 && SYNC_COUNT=$((SYNC_COUNT + 1)) || true
done
if [ "$SYNC_COUNT" -gt 0 ]; then
print_ok "Data sources synced ($SYNC_COUNT sources)"
fi
else
print_warn "Could not authenticate. Run MITRE sync from the System page."
fi
else
print_header "Step 5/5 - Skipping data sync"
print_info "You can import data later from the System page."
fi
# ═════════════════════════════════════════════════════════════════════════
# FINAL SUMMARY
# ═════════════════════════════════════════════════════════════════════════
# Build the access URL
ORIGIN_URL=$(grep CORS_ORIGINS "$ENV_FILE" 2>/dev/null | cut -d= -f2 || echo "http://localhost")
SERVER_IP=$(hostname -I 2>/dev/null | awk '{print $1}' || echo "localhost")
echo ""
echo ""
echo -e "${BOLD}╔══════════════════════════════════════════════════════════════╗${NC}"
echo -e "${BOLD}║ ║${NC}"
echo -e "${BOLD}${GREEN}Aegis is ready!${NC}${BOLD}${NC}"
echo -e "${BOLD}║ ║${NC}"
echo -e "${BOLD}╠══════════════════════════════════════════════════════════════╣${NC}"
echo -e "${BOLD}${NC} ${BOLD}${NC}"
echo -e "${BOLD}${NC} ${CYAN}Application${NC} ${ORIGIN_URL}"
echo -e "${BOLD}${NC} ${CYAN}Local IP${NC} http://${SERVER_IP}:${FRONTEND_PORT}"
echo -e "${BOLD}${NC} ${BOLD}${NC}"
echo -e "${BOLD}╠══════════════════════════════════════════════════════════════╣${NC}"
echo -e "${BOLD}${NC} ${BOLD}${NC}"
echo -e "${BOLD}${NC} ${CYAN}Admin Login${NC} ${BOLD}${NC}"
echo -e "${BOLD}${NC} Email: ${GREEN}${ADMIN_CREDS_USER}${NC}"
if [ -n "$ADMIN_CREDS_PASS" ]; then
echo -e "${BOLD}${NC} Password: ${GREEN}${ADMIN_CREDS_PASS}${NC}"
else
echo -e "${BOLD}${NC} Password: ${YELLOW}(check: docker logs aegis-backend | grep Password)${NC}"
fi
echo -e "${BOLD}${NC} ${BOLD}${NC}"
echo -e "${BOLD}╠══════════════════════════════════════════════════════════════╣${NC}"
echo -e "${BOLD}${NC} ${BOLD}${NC}"
echo -e "${BOLD}${NC} ${YELLOW}Important:${NC} ${BOLD}${NC}"
echo -e "${BOLD}${NC} - Save the admin password now if auto-generated ${BOLD}${NC}"
echo -e "${BOLD}${NC} - Set up HTTPS/TLS for internet-facing deployments ${BOLD}${NC}"
echo -e "${BOLD}${NC} - Configure firewall rules as needed ${BOLD}${NC}"
echo -e "${BOLD}${NC} - Set up regular database backups ${BOLD}${NC}"
echo -e "${BOLD}${NC} ${BOLD}${NC}"
echo -e "${BOLD}╚══════════════════════════════════════════════════════════════╝${NC}"
echo ""
echo -e "${BOLD}Useful commands:${NC}"
echo -e " ${DIM}View logs${NC} docker logs -f aegis-backend"
echo -e " ${DIM}Stop${NC} $COMPOSE_CMD -f docker-compose.prod.yml down"
echo -e " ${DIM}Restart${NC} $COMPOSE_CMD -f docker-compose.prod.yml restart"
echo -e " ${DIM}Update${NC} $COMPOSE_CMD -f docker-compose.prod.yml up -d --build"
echo -e " ${DIM}DB backup${NC} docker exec aegis-postgres pg_dump -U postgres ${DB_NAME:-attackdb} > backup.sql"
echo -e " ${DIM}Reconfigure${NC} ./scripts/install.sh"
echo ""