diff --git a/README.md b/README.md index 74b58d5..fa887c5 100644 --- a/README.md +++ b/README.md @@ -95,38 +95,40 @@ Both Red Lead and Blue Lead must independently vote: - Docker and Docker Compose - Git +- Linux / macOS (or WSL on Windows) -### Installation +### Production Deployment + +The recommended way to deploy Aegis in production: -1. Clone the repository: ```bash git clone cd Aegis +chmod +x scripts/install.sh +./scripts/install.sh ``` -2. Start all services: +The install script will automatically: +- Generate a `.env` file with secure random secrets +- Build and start all containers (PostgreSQL, MinIO, Backend, Frontend) +- Run database migrations +- Seed the admin user and data sources +- Optionally run the initial MITRE ATT&CK sync + +Access the application at **http://your-server:80**. + +### Development Setup + +For local development with hot-reload: + ```bash +git clone +cd Aegis docker-compose up -d +./scripts/init.sh ``` -3. Run database migrations: -```bash -docker exec aegis-backend alembic upgrade head -``` - -4. Seed the admin user: -```bash -docker exec aegis-backend python -m app.seed -``` - -5. Access the application: -```bash -# API health check -curl http://localhost:8000/health -# Expected: {"status":"ok"} - -# Open http://localhost:5173 — Aegis login page -``` +Access at **http://localhost:5173** (frontend dev server) and **http://localhost:8000/docs** (API docs). ### Authentication @@ -137,27 +139,32 @@ Username: admin Password: admin123 ``` -> **Important:** Change the default `admin123` password and `SECRET_KEY` in production. +> **Important:** Change the default `admin123` password immediately after first login. ### Importing Data Sources -After initial setup, the entrypoint script automatically seeds the initial data sources (Atomic Red Team, SigmaHQ, CALDERA, LOLBAS, GTFOBins, D3FEND). You can then sync each source from the UI: +On startup, the backend automatically seeds the initial data sources (Atomic Red Team, SigmaHQ, CALDERA, LOLBAS, GTFOBins, D3FEND). You can then sync each source from the UI: -1. Navigate to **System > Data Sources** in the admin panel +1. Navigate to **Data Sources** in the sidebar 2. Click **Sync** on each data source to import its content -3. Trigger a **MITRE ATT&CK Sync** from the **System > MITRE Sync** page +3. Trigger a **MITRE ATT&CK Sync** from the **System** page Alternatively, use the API: ```bash # Sync MITRE ATT&CK techniques -curl -X POST http://localhost:8000/api/v1/system/sync-mitre -H "Authorization: Bearer $TOKEN" +curl -X POST http://your-server/api/v1/system/sync-mitre -H "Authorization: Bearer $TOKEN" # Sync all data sources at once -curl -X POST http://localhost:8000/api/v1/data-sources/sync-all -H "Authorization: Bearer $TOKEN" +curl -X POST http://your-server/api/v1/data-sources/sync-all -H "Authorization: Bearer $TOKEN" ``` -See [docs/DATA_SOURCES.md](docs/DATA_SOURCES.md) for detailed instructions on all data sources. +### Production Considerations + +- **HTTPS/TLS:** For internet-facing deployments, place a reverse proxy with TLS in front (e.g., Traefik, Caddy, or Nginx with Let's Encrypt). +- **Backups:** Set up regular PostgreSQL backups: `docker exec aegis-postgres pg_dump -U postgres attackdb > backup.sql` +- **Updates:** To update, pull the latest code and run: `docker compose -f docker-compose.prod.yml up -d --build` +- **Firewall:** Only expose port 80/443. All other services (DB, MinIO, backend) are internal only. ### Configuring Scoring Weights diff --git a/backend/Dockerfile b/backend/Dockerfile index b80452d..0303694 100644 --- a/backend/Dockerfile +++ b/backend/Dockerfile @@ -16,8 +16,8 @@ RUN pip install --no-cache-dir -r requirements.txt # Copy application code COPY . . -# Make entrypoint executable -RUN chmod +x /app/entrypoint.sh +# Make entrypoints executable +RUN chmod +x /app/entrypoint.sh /app/entrypoint.prod.sh # Expose port EXPOSE 8000 diff --git a/backend/entrypoint.prod.sh b/backend/entrypoint.prod.sh new file mode 100644 index 0000000..e7410cc --- /dev/null +++ b/backend/entrypoint.prod.sh @@ -0,0 +1,14 @@ +#!/bin/sh +set -e + +echo "=== [PROD] Running Alembic migrations ===" +alembic upgrade head + +echo "=== [PROD] Seeding admin user ===" +python -m app.seed + +echo "=== [PROD] Seeding data sources ===" +python -m app.seed_data_sources + +echo "=== [PROD] Starting uvicorn (production) ===" +exec uvicorn app.main:app --host 0.0.0.0 --port 8000 --workers 4 diff --git a/docker-compose.prod.yml b/docker-compose.prod.yml index 3de7212..b1a2be7 100644 --- a/docker-compose.prod.yml +++ b/docker-compose.prod.yml @@ -68,7 +68,7 @@ services: condition: service_healthy minio: condition: service_started - command: uvicorn app.main:app --host 0.0.0.0 --port 8000 --workers 4 + command: sh /app/entrypoint.prod.sh healthcheck: test: ["CMD", "curl", "-f", "http://localhost:8000/health"] interval: 10s diff --git a/frontend/src/api/client.ts b/frontend/src/api/client.ts index 44ff2f4..94a60e6 100644 --- a/frontend/src/api/client.ts +++ b/frontend/src/api/client.ts @@ -1,7 +1,9 @@ import axios, { type AxiosError } from "axios"; +const API_BASE_URL = import.meta.env.VITE_API_URL || "/api/v1"; + const client = axios.create({ - baseURL: "http://localhost:8000/api/v1", + baseURL: API_BASE_URL, headers: { "Content-Type": "application/json" }, }); diff --git a/frontend/src/pages/TestCreatePage.tsx b/frontend/src/pages/TestCreatePage.tsx index 3b1076b..819d336 100644 --- a/frontend/src/pages/TestCreatePage.tsx +++ b/frontend/src/pages/TestCreatePage.tsx @@ -19,7 +19,8 @@ export default function TestCreatePage() { queryFn: async () => { // We need to find the mitre_id from the technique list // This is a workaround since we get UUID but need mitre_id - const response = await fetch(`http://localhost:8000/api/v1/techniques`, { + const apiBase = import.meta.env.VITE_API_URL || "/api/v1"; + const response = await fetch(`${apiBase}/techniques`, { headers: { Authorization: `Bearer ${localStorage.getItem("token")}`, }, diff --git a/scripts/init.ps1 b/scripts/init.ps1 deleted file mode 100644 index d4d3d68..0000000 --- a/scripts/init.ps1 +++ /dev/null @@ -1,132 +0,0 @@ -# ============================================================================= -# Aegis Initialization Script (PowerShell) -# ============================================================================= -# This script initializes the Aegis platform after starting Docker containers. -# -# Usage: -# .\scripts\init.ps1 -# ============================================================================= - -$ErrorActionPreference = "Stop" - -Write-Host "╔═══════════════════════════════════════════════════════════════════════╗" -ForegroundColor Cyan -Write-Host "║ Aegis - Platform Initialization ║" -ForegroundColor Cyan -Write-Host "╚═══════════════════════════════════════════════════════════════════════╝" -ForegroundColor Cyan -Write-Host "" - -function Write-Status { - param([string]$Message) - Write-Host "[✓] $Message" -ForegroundColor Green -} - -function Write-Warning { - param([string]$Message) - Write-Host "[!] $Message" -ForegroundColor Yellow -} - -function Write-Error { - param([string]$Message) - Write-Host "[✗] $Message" -ForegroundColor Red -} - -# Check if Docker is running -try { - docker info | Out-Null -} catch { - Write-Error "Docker is not running. Please start Docker first." - exit 1 -} - -# Check if containers are running -$backendRunning = docker-compose ps | Select-String "aegis-backend" -if (-not $backendRunning) { - Write-Warning "Containers not running. Starting them now..." - docker-compose up -d - Write-Host "" - Write-Host "Waiting for services to be healthy..." - Start-Sleep -Seconds 10 -} - -# Wait for backend to be ready -Write-Host "Waiting for backend to be ready..." -$maxRetries = 30 -$retryCount = 0 -$ready = $false - -while (-not $ready -and $retryCount -lt $maxRetries) { - try { - $response = Invoke-WebRequest -Uri "http://localhost:8000/health" -UseBasicParsing -TimeoutSec 2 - if ($response.StatusCode -eq 200) { - $ready = $true - } - } catch { - $retryCount++ - Write-Host " Waiting... ($retryCount/$maxRetries)" - Start-Sleep -Seconds 2 - } -} - -if (-not $ready) { - Write-Error "Backend failed to start after $maxRetries attempts" - exit 1 -} -Write-Status "Backend is healthy" - -# Run database migrations -Write-Host "" -Write-Host "Running database migrations..." -docker-compose exec -T backend alembic upgrade head -Write-Status "Migrations completed" - -# Seed admin user -Write-Host "" -Write-Host "Seeding admin user..." -try { - docker-compose exec -T backend python -m app.seed 2>$null - Write-Status "Admin user ready" -} catch { - Write-Warning "Admin user may already exist" -} - -# Ask about MITRE sync -Write-Host "" -$runSync = Read-Host "Do you want to run initial MITRE ATT&CK sync? (y/N)" -if ($runSync -eq "y" -or $runSync -eq "Y") { - Write-Host "Triggering MITRE sync (this may take a minute)..." - - try { - # Get admin token - $loginBody = "username=admin&password=admin123" - $loginResponse = Invoke-RestMethod -Uri "http://localhost:8000/api/v1/auth/login" ` - -Method Post -Body $loginBody -ContentType "application/x-www-form-urlencoded" - - $token = $loginResponse.access_token - - if ($token) { - $headers = @{ "Authorization" = "Bearer $token" } - Invoke-RestMethod -Uri "http://localhost:8000/api/v1/system/sync-mitre" ` - -Method Post -Headers $headers | Out-Null - Write-Status "MITRE sync triggered" - } - } catch { - Write-Warning "Could not authenticate. Run sync manually from the System page." - } -} - -# Print summary -Write-Host "" -Write-Host "╔═══════════════════════════════════════════════════════════════════════╗" -ForegroundColor Cyan -Write-Host "║ Aegis is ready! ║" -ForegroundColor Cyan -Write-Host "╠═══════════════════════════════════════════════════════════════════════╣" -ForegroundColor Cyan -Write-Host "║ ║" -ForegroundColor Cyan -Write-Host "║ Frontend: http://localhost:5173 ║" -ForegroundColor White -Write-Host "║ Backend API: http://localhost:8000 ║" -ForegroundColor White -Write-Host "║ Swagger UI: http://localhost:8000/docs ║" -ForegroundColor White -Write-Host "║ MinIO Console: http://localhost:9001 ║" -ForegroundColor White -Write-Host "║ ║" -ForegroundColor Cyan -Write-Host "║ Default login: admin / admin123 ║" -ForegroundColor Yellow -Write-Host "║ ║" -ForegroundColor Cyan -Write-Host "║ ⚠️ Change the default password in production! ║" -ForegroundColor Yellow -Write-Host "║ ║" -ForegroundColor Cyan -Write-Host "╚═══════════════════════════════════════════════════════════════════════╝" -ForegroundColor Cyan -Write-Host "" diff --git a/scripts/init.sh b/scripts/init.sh index d544eb9..5a94b13 100644 --- a/scripts/init.sh +++ b/scripts/init.sh @@ -1,9 +1,10 @@ #!/bin/bash # ============================================================================= -# Aegis Initialization Script +# Aegis Development Initialization Script # ============================================================================= -# This script initializes the Aegis platform after starting Docker containers. -# +# This script initializes the Aegis platform for local development. +# For production, use: ./scripts/install.sh +# # Usage: # ./scripts/init.sh # ============================================================================= diff --git a/scripts/install.sh b/scripts/install.sh new file mode 100644 index 0000000..9054c95 --- /dev/null +++ b/scripts/install.sh @@ -0,0 +1,249 @@ +#!/bin/bash +# ============================================================================= +# Aegis - Production Installation Script +# ============================================================================= +# This script sets up the Aegis platform for production. +# +# Usage: +# chmod +x scripts/install.sh +# ./scripts/install.sh +# +# Prerequisites: +# - Docker and Docker Compose installed +# - Port 80 (or FRONTEND_PORT) available +# ============================================================================= + +set -e + +# Colors +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +RED='\033[0;31m' +CYAN='\033[0;36m' +BOLD='\033[1m' +NC='\033[0m' + +print_ok() { echo -e "${GREEN}[✓]${NC} $1"; } +print_warn() { echo -e "${YELLOW}[!]${NC} $1"; } +print_error() { echo -e "${RED}[✗]${NC} $1"; } +print_info() { echo -e "${CYAN}[i]${NC} $1"; } +print_header() { echo -e "\n${BOLD}── $1 ──${NC}"; } + +echo "" +echo "╔═══════════════════════════════════════════════════════════════╗" +echo "║ Aegis - Production Installation ║" +echo "╚═══════════════════════════════════════════════════════════════╝" +echo "" + +# ── 1. Check prerequisites ────────────────────────────────────────── + +print_header "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" + +# Check for docker compose (v2 plugin or standalone) +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)" + +# ── 2. Setup .env file ────────────────────────────────────────────── + +print_header "Environment configuration" + +ENV_FILE=".env" + +if [ -f "$ENV_FILE" ]; then + print_warn ".env file already exists" + read -p " Overwrite with new values? (y/N) " -n 1 -r + echo "" + if [[ ! $REPLY =~ ^[Yy]$ ]]; then + print_info "Keeping existing .env file" + SKIP_ENV=true + fi +fi + +if [ "${SKIP_ENV}" != "true" ]; then + # Generate secure secrets + SECRET_KEY=$(python3 -c "import secrets; print(secrets.token_hex(32))" 2>/dev/null || openssl rand -hex 32 2>/dev/null || head -c 64 /dev/urandom | od -An -tx1 | tr -d ' \n') + DB_PASSWORD=$(python3 -c "import secrets; print(secrets.token_urlsafe(24))" 2>/dev/null || openssl rand -base64 24 2>/dev/null || head -c 24 /dev/urandom | base64) + MINIO_SECRET=$(python3 -c "import secrets; print(secrets.token_urlsafe(24))" 2>/dev/null || openssl rand -base64 24 2>/dev/null || head -c 24 /dev/urandom | base64) + + cat > "$ENV_FILE" <&1 | while IFS= read -r line; do + echo " $line" +done + +print_ok "Containers started" + +# ── 4. Wait for services to be healthy ─────────────────────────────── + +print_header "Waiting for services" + +# Wait for postgres +print_info "Waiting for 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 + print_error "PostgreSQL failed to start after $MAX_RETRIES attempts" + echo " Check logs: docker logs aegis-postgres" + exit 1 + fi + sleep 2 +done +print_ok "PostgreSQL is ready" + +# Wait for backend (which runs migrations + seed on startup) +print_info "Waiting for backend (running migrations and seeds)..." +RETRY=0 +until curl -sf http://localhost:8000/health > /dev/null 2>&1 || \ + docker exec aegis-backend curl -sf http://localhost:8000/health > /dev/null 2>&1; do + RETRY=$((RETRY + 1)) + if [ $RETRY -ge 60 ]; then + print_error "Backend failed to start after 120 seconds" + echo " Check logs: docker logs aegis-backend" + exit 1 + fi + sleep 2 +done +print_ok "Backend is ready (migrations and seeds completed)" + +# Wait for frontend +print_info "Waiting for frontend..." +RETRY=0 +FRONTEND_PORT=$(grep FRONTEND_PORT "$ENV_FILE" 2>/dev/null | cut -d= -f2 || echo "80") +FRONTEND_PORT=${FRONTEND_PORT:-80} +until curl -sf "http://localhost:${FRONTEND_PORT}" > /dev/null 2>&1; do + RETRY=$((RETRY + 1)) + if [ $RETRY -ge 30 ]; then + print_error "Frontend failed to start after 60 seconds" + echo " Check logs: docker logs aegis-frontend" + exit 1 + fi + sleep 2 +done +print_ok "Frontend is ready" + +# ── 5. Trigger MITRE ATT&CK sync ──────────────────────────────────── + +print_header "Initial data sync" + +echo "" +read -p "Run initial MITRE ATT&CK sync? This imports ~700 techniques. (Y/n) " -n 1 -r +echo "" +if [[ ! $REPLY =~ ^[Nn]$ ]]; then + print_info "Authenticating..." + + # Get admin token + TOKEN=$(curl -sf -X POST "http://localhost:${FRONTEND_PORT}/api/v1/auth/login" \ + -H "Content-Type: application/x-www-form-urlencoded" \ + -d "username=admin&password=admin123" | python3 -c "import sys,json; print(json.load(sys.stdin).get('access_token',''))" 2>/dev/null || echo "") + + if [ -n "$TOKEN" ] && [ "$TOKEN" != "" ]; then + print_info "Syncing MITRE ATT&CK data (this takes 1-2 minutes)..." + SYNC_RESULT=$(curl -sf -X POST "http://localhost:${FRONTEND_PORT}/api/v1/system/sync-mitre" \ + -H "Authorization: Bearer $TOKEN" 2>/dev/null || echo "error") + + if [ "$SYNC_RESULT" != "error" ]; then + print_ok "MITRE ATT&CK sync completed" + else + print_warn "MITRE sync may have timed out. Check the System page in the UI." + fi + + # Sync data sources + print_info "Syncing data sources (Atomic Red Team, SigmaHQ, etc.)..." + for source_id in $(curl -sf "http://localhost:${FRONTEND_PORT}/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); do + + curl -sf -X POST "http://localhost:${FRONTEND_PORT}/api/v1/data-sources/${source_id}/sync" \ + -H "Authorization: Bearer $TOKEN" > /dev/null 2>&1 || true + done + print_ok "Data source sync triggered" + else + print_warn "Could not authenticate. Run MITRE sync manually from the System page." + print_info "Default credentials: admin / admin123" + fi +else + print_info "Skipping MITRE sync. You can do this later from the System page." +fi + +# ── 6. Summary ─────────────────────────────────────────────────────── + +# Get the server's IP +SERVER_IP=$(hostname -I 2>/dev/null | awk '{print $1}' || echo "localhost") + +echo "" +echo "╔═══════════════════════════════════════════════════════════════╗" +echo "║ Aegis is ready! ║" +echo "╠═══════════════════════════════════════════════════════════════╣" +echo "║ ║" +echo "║ Application: http://${SERVER_IP}:${FRONTEND_PORT} " +echo "║ API Docs: http://${SERVER_IP}:${FRONTEND_PORT}/api/v1/docs " +echo "║ ║" +echo "║ Default login: admin / admin123 ║" +echo "║ ║" +echo "║ ⚠ IMPORTANT: ║" +echo "║ • Change the default password immediately ║" +echo "║ • Set up HTTPS/TLS for internet-facing deployments ║" +echo "║ • Configure firewall rules as needed ║" +echo "║ • Set up regular database backups ║" +echo "║ ║" +echo "╚═══════════════════════════════════════════════════════════════╝" +echo "" +echo "Useful commands:" +echo " View logs: docker logs -f aegis-backend" +echo " Stop: $COMPOSE_CMD -f docker-compose.prod.yml down" +echo " Restart: $COMPOSE_CMD -f docker-compose.prod.yml restart" +echo " Update: $COMPOSE_CMD -f docker-compose.prod.yml up -d --build" +echo " DB backup: docker exec aegis-postgres pg_dump -U postgres attackdb > backup.sql" +echo ""