#!/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 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}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_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}" 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 "" 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" <&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 ────────────────────── 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 "Username :" | sed 's/.*Username : //') 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_USERNAME "$ENV_FILE" 2>/dev/null | cut -d= -f2 || echo "admin") ADMIN_CREDS_USER="${ADMIN_CREDS_USER:-admin}" 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} Username: ${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 ""