From ce46314afbff43caeb8a7afffc208b119ee3fd04 Mon Sep 17 00:00:00 2001 From: Kitos Date: Fri, 6 Feb 2026 16:33:22 +0100 Subject: [PATCH] feat: add complete Docker setup for testing - Update docker-compose.yml with frontend service and healthchecks - Add frontend Dockerfile with dev and production stages - Add nginx.conf for production frontend serving - Add docker-compose.prod.yml for production deployment - Add .env.example with all configuration options - Add init scripts (init.sh, init.ps1) for easy setup --- .env.example | 24 ++++++++ docker-compose.prod.yml | 106 ++++++++++++++++++++++++++++++++ docker-compose.yml | 95 ++++++++++++++++++++++++++--- frontend/Dockerfile | 45 ++++++++++++++ frontend/nginx.conf | 35 +++++++++++ scripts/init.ps1 | 132 ++++++++++++++++++++++++++++++++++++++++ scripts/init.sh | 114 ++++++++++++++++++++++++++++++++++ 7 files changed, 543 insertions(+), 8 deletions(-) create mode 100644 .env.example create mode 100644 docker-compose.prod.yml create mode 100644 frontend/Dockerfile create mode 100644 frontend/nginx.conf create mode 100644 scripts/init.ps1 create mode 100644 scripts/init.sh diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..66d5373 --- /dev/null +++ b/.env.example @@ -0,0 +1,24 @@ +# ============================================================================= +# Aegis Environment Variables +# ============================================================================= +# Copy this file to .env and fill in the values +# ============================================================================= + +# ── Database ───────────────────────────────────────────────────────────────── +DB_USER=postgres +DB_PASSWORD=change-me-in-production +DB_NAME=attackdb + +# ── Security ───────────────────────────────────────────────────────────────── +# IMPORTANT: Generate a strong random key for production +# Example: openssl rand -hex 32 +SECRET_KEY=change-me-in-production-use-a-long-random-string +TOKEN_EXPIRE_MINUTES=60 + +# ── MinIO Object Storage ───────────────────────────────────────────────────── +MINIO_ACCESS_KEY=minioadmin +MINIO_SECRET_KEY=change-me-in-production +MINIO_BUCKET=evidence + +# ── Frontend ───────────────────────────────────────────────────────────────── +FRONTEND_PORT=80 diff --git a/docker-compose.prod.yml b/docker-compose.prod.yml new file mode 100644 index 0000000..3de7212 --- /dev/null +++ b/docker-compose.prod.yml @@ -0,0 +1,106 @@ +# ============================================================================= +# Aegis - Production Docker Compose +# ============================================================================= +# +# Usage: +# docker-compose -f docker-compose.prod.yml up -d --build +# +# Note: Set environment variables in .env file or via environment +# ============================================================================= + +services: + # ── PostgreSQL Database ──────────────────────────────────────────────────── + postgres: + image: postgres:15-alpine + container_name: aegis-postgres + environment: + POSTGRES_USER: ${DB_USER:-postgres} + POSTGRES_PASSWORD: ${DB_PASSWORD:-postgres} + POSTGRES_DB: ${DB_NAME:-attackdb} + volumes: + - postgres_data:/var/lib/postgresql/data + healthcheck: + test: ["CMD-SHELL", "pg_isready -U ${DB_USER:-postgres} -d ${DB_NAME:-attackdb}"] + interval: 5s + timeout: 5s + retries: 5 + restart: always + networks: + - aegis-network + + # ── MinIO Object Storage ─────────────────────────────────────────────────── + minio: + image: minio/minio:latest + container_name: aegis-minio + command: server /data --console-address ":9001" + environment: + MINIO_ROOT_USER: ${MINIO_ACCESS_KEY:-minioadmin} + MINIO_ROOT_PASSWORD: ${MINIO_SECRET_KEY:-minioadmin} + volumes: + - minio_data:/data + healthcheck: + test: ["CMD", "mc", "ready", "local"] + interval: 5s + timeout: 5s + retries: 5 + restart: always + networks: + - aegis-network + + # ── FastAPI Backend ──────────────────────────────────────────────────────── + backend: + build: + context: ./backend + dockerfile: Dockerfile + container_name: aegis-backend + environment: + DATABASE_URL: postgresql://${DB_USER:-postgres}:${DB_PASSWORD:-postgres}@postgres:5432/${DB_NAME:-attackdb} + SECRET_KEY: ${SECRET_KEY:?Set SECRET_KEY in environment} + ALGORITHM: HS256 + ACCESS_TOKEN_EXPIRE_MINUTES: ${TOKEN_EXPIRE_MINUTES:-60} + MINIO_ENDPOINT: minio:9000 + MINIO_ACCESS_KEY: ${MINIO_ACCESS_KEY:-minioadmin} + MINIO_SECRET_KEY: ${MINIO_SECRET_KEY:-minioadmin} + MINIO_BUCKET: ${MINIO_BUCKET:-evidence} + MINIO_SECURE: "false" + depends_on: + postgres: + condition: service_healthy + minio: + condition: service_started + command: uvicorn app.main:app --host 0.0.0.0 --port 8000 --workers 4 + healthcheck: + test: ["CMD", "curl", "-f", "http://localhost:8000/health"] + interval: 10s + timeout: 5s + retries: 5 + restart: always + networks: + - aegis-network + + # ── React Frontend (Production with Nginx) ───────────────────────────────── + frontend: + build: + context: ./frontend + dockerfile: Dockerfile + target: production + container_name: aegis-frontend + ports: + - "${FRONTEND_PORT:-80}:80" + depends_on: + - backend + restart: always + networks: + - aegis-network + +# ── Networks ───────────────────────────────────────────────────────────────── +networks: + aegis-network: + driver: bridge + +# ── Volumes ────────────────────────────────────────────────────────────────── +volumes: + postgres_data: + name: aegis_postgres_data_prod + minio_data: + name: aegis_minio_data_prod diff --git a/docker-compose.yml b/docker-compose.yml index d13b837..f22bec9 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,6 +1,26 @@ +# ============================================================================= +# Aegis - MITRE ATT&CK Coverage Platform +# ============================================================================= +# +# Quick Start: +# docker-compose up -d +# docker-compose exec backend alembic upgrade head +# docker-compose exec backend python -m app.seed +# +# Access: +# - Frontend: http://localhost:5173 +# - Backend API: http://localhost:8000 +# - Swagger UI: http://localhost:8000/docs +# - MinIO Console: http://localhost:9001 (minioadmin/minioadmin) +# +# Default credentials: admin / admin123 +# ============================================================================= + services: + # ── PostgreSQL Database ──────────────────────────────────────────────────── postgres: - image: postgres:15 + image: postgres:15-alpine + container_name: aegis-postgres environment: POSTGRES_USER: postgres POSTGRES_PASSWORD: postgres @@ -9,32 +29,91 @@ services: - "5433:5432" volumes: - postgres_data:/var/lib/postgresql/data + healthcheck: + test: ["CMD-SHELL", "pg_isready -U postgres -d attackdb"] + interval: 5s + timeout: 5s + retries: 5 + restart: unless-stopped + # ── MinIO Object Storage ─────────────────────────────────────────────────── minio: - image: minio/minio + image: minio/minio:latest + container_name: aegis-minio command: server /data --console-address ":9001" environment: MINIO_ROOT_USER: minioadmin MINIO_ROOT_PASSWORD: minioadmin ports: - - "9000:9000" - - "9001:9001" + - "9000:9000" # API + - "9001:9001" # Console volumes: - minio_data:/data + healthcheck: + test: ["CMD", "mc", "ready", "local"] + interval: 5s + timeout: 5s + retries: 5 + restart: unless-stopped + # ── FastAPI Backend ──────────────────────────────────────────────────────── backend: - build: ./backend + build: + context: ./backend + dockerfile: Dockerfile + container_name: aegis-backend ports: - "8000:8000" environment: + # Database DATABASE_URL: postgresql://postgres:postgres@postgres:5432/attackdb + # Security (change in production!) + SECRET_KEY: change-me-in-production-use-a-long-random-string + ALGORITHM: HS256 + ACCESS_TOKEN_EXPIRE_MINUTES: 60 + # MinIO + MINIO_ENDPOINT: minio:9000 + MINIO_ACCESS_KEY: minioadmin + MINIO_SECRET_KEY: minioadmin + MINIO_BUCKET: evidence + MINIO_SECURE: "false" depends_on: - - postgres - - minio - command: uvicorn app.main:app --host 0.0.0.0 --port 8000 --reload + postgres: + condition: service_healthy + minio: + condition: service_started volumes: - ./backend:/app + command: uvicorn app.main:app --host 0.0.0.0 --port 8000 --reload + healthcheck: + test: ["CMD", "curl", "-f", "http://localhost:8000/health"] + interval: 10s + timeout: 5s + retries: 5 + restart: unless-stopped + # ── React Frontend (Development) ─────────────────────────────────────────── + frontend: + build: + context: ./frontend + dockerfile: Dockerfile + target: development + container_name: aegis-frontend + ports: + - "5173:5173" + environment: + # Vite environment variables + VITE_API_URL: http://localhost:8000/api/v1 + depends_on: + - backend + volumes: + - ./frontend:/app + - /app/node_modules # Prevent overwriting node_modules + restart: unless-stopped + +# ── Volumes ────────────────────────────────────────────────────────────────── volumes: postgres_data: + name: aegis_postgres_data minio_data: + name: aegis_minio_data diff --git a/frontend/Dockerfile b/frontend/Dockerfile new file mode 100644 index 0000000..125c230 --- /dev/null +++ b/frontend/Dockerfile @@ -0,0 +1,45 @@ +# ── Development Stage ────────────────────────────────────────────────────── +FROM node:20-alpine AS development + +WORKDIR /app + +# Copy package files +COPY package*.json ./ + +# Install dependencies +RUN npm install + +# Copy source code +COPY . . + +# Expose Vite dev server port +EXPOSE 5173 + +# Start dev server with host binding for Docker +CMD ["npm", "run", "dev", "--", "--host", "0.0.0.0"] + + +# ── Build Stage ──────────────────────────────────────────────────────────── +FROM node:20-alpine AS build + +WORKDIR /app + +COPY package*.json ./ +RUN npm ci + +COPY . . +RUN npm run build + + +# ── Production Stage ─────────────────────────────────────────────────────── +FROM nginx:alpine AS production + +# Copy built files to nginx +COPY --from=build /app/dist /usr/share/nginx/html + +# Copy nginx config for SPA routing +COPY nginx.conf /etc/nginx/conf.d/default.conf + +EXPOSE 80 + +CMD ["nginx", "-g", "daemon off;"] diff --git a/frontend/nginx.conf b/frontend/nginx.conf new file mode 100644 index 0000000..8a043bb --- /dev/null +++ b/frontend/nginx.conf @@ -0,0 +1,35 @@ +server { + listen 80; + server_name localhost; + root /usr/share/nginx/html; + index index.html; + + # Gzip compression + gzip on; + gzip_types text/plain text/css application/json application/javascript text/xml application/xml; + + # SPA routing - serve index.html for all routes + location / { + try_files $uri $uri/ /index.html; + } + + # Cache static assets + location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2)$ { + expires 1y; + add_header Cache-Control "public, immutable"; + } + + # Proxy API requests to backend (for production) + location /api/ { + proxy_pass http://backend:8000/api/; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + } + + # Health endpoint proxy + location /health { + proxy_pass http://backend:8000/health; + } +} diff --git a/scripts/init.ps1 b/scripts/init.ps1 new file mode 100644 index 0000000..d4d3d68 --- /dev/null +++ b/scripts/init.ps1 @@ -0,0 +1,132 @@ +# ============================================================================= +# 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 new file mode 100644 index 0000000..d544eb9 --- /dev/null +++ b/scripts/init.sh @@ -0,0 +1,114 @@ +#!/bin/bash +# ============================================================================= +# Aegis Initialization Script +# ============================================================================= +# This script initializes the Aegis platform after starting Docker containers. +# +# Usage: +# ./scripts/init.sh +# ============================================================================= + +set -e + +echo "╔═══════════════════════════════════════════════════════════════════════╗" +echo "║ Aegis - Platform Initialization ║" +echo "╚═══════════════════════════════════════════════════════════════════════╝" +echo "" + +# Colors for output +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +RED='\033[0;31m' +NC='\033[0m' # No Color + +# Function to print status +print_status() { + echo -e "${GREEN}[✓]${NC} $1" +} + +print_warning() { + echo -e "${YELLOW}[!]${NC} $1" +} + +print_error() { + echo -e "${RED}[✗]${NC} $1" +} + +# Check if Docker is running +if ! docker info > /dev/null 2>&1; then + print_error "Docker is not running. Please start Docker first." + exit 1 +fi + +# Check if containers are running +if ! docker-compose ps | grep -q "aegis-backend"; then + print_warning "Containers not running. Starting them now..." + docker-compose up -d + echo "" + echo "Waiting for services to be healthy..." + sleep 10 +fi + +# Wait for backend to be ready +echo "Waiting for backend to be ready..." +MAX_RETRIES=30 +RETRY_COUNT=0 +until curl -s http://localhost:8000/health > /dev/null 2>&1; do + RETRY_COUNT=$((RETRY_COUNT + 1)) + if [ $RETRY_COUNT -ge $MAX_RETRIES ]; then + print_error "Backend failed to start after $MAX_RETRIES attempts" + exit 1 + fi + echo " Waiting... ($RETRY_COUNT/$MAX_RETRIES)" + sleep 2 +done +print_status "Backend is healthy" + +# Run database migrations +echo "" +echo "Running database migrations..." +docker-compose exec -T backend alembic upgrade head +print_status "Migrations completed" + +# Seed admin user +echo "" +echo "Seeding admin user..." +docker-compose exec -T backend python -m app.seed 2>/dev/null || print_warning "Admin user may already exist" +print_status "Admin user ready" + +# Trigger initial MITRE sync (optional) +echo "" +read -p "Do you want to run initial MITRE ATT&CK sync? (y/N) " -n 1 -r +echo "" +if [[ $REPLY =~ ^[Yy]$ ]]; then + echo "Triggering MITRE sync (this may take a minute)..." + # Get admin token + TOKEN=$(curl -s -X POST "http://localhost:8000/api/v1/auth/login" \ + -d "username=admin&password=admin123" | grep -o '"access_token":"[^"]*' | cut -d'"' -f4) + + if [ -n "$TOKEN" ]; then + curl -s -X POST "http://localhost:8000/api/v1/system/sync-mitre" \ + -H "Authorization: Bearer $TOKEN" > /dev/null + print_status "MITRE sync triggered" + else + print_warning "Could not authenticate. Run sync manually from the System page." + fi +fi + +# Print summary +echo "" +echo "╔═══════════════════════════════════════════════════════════════════════╗" +echo "║ Aegis is ready! ║" +echo "╠═══════════════════════════════════════════════════════════════════════╣" +echo "║ ║" +echo "║ Frontend: http://localhost:5173 ║" +echo "║ Backend API: http://localhost:8000 ║" +echo "║ Swagger UI: http://localhost:8000/docs ║" +echo "║ MinIO Console: http://localhost:9001 ║" +echo "║ ║" +echo "║ Default login: admin / admin123 ║" +echo "║ ║" +echo "║ ⚠️ Change the default password in production! ║" +echo "║ ║" +echo "╚═══════════════════════════════════════════════════════════════════════╝" +echo ""