feat: Add Response Orchestrator — autonomous adaptive defense (closed… #79
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: CD - Continuous Deployment | |
| on: | |
| push: | |
| branches: [ master, main ] | |
| tags: | |
| - 'v*' | |
| workflow_dispatch: | |
| env: | |
| REGISTRY: ghcr.io | |
| # Force lowercase for Docker image compatibility | |
| IMAGE_NAME: zhadyz/ai_soc | |
| jobs: | |
| # ============================================================================ | |
| # Build and Push Docker Images | |
| # ============================================================================ | |
| build-and-push: | |
| name: Build & Push Images | |
| runs-on: ubuntu-latest | |
| permissions: | |
| contents: read | |
| packages: write | |
| strategy: | |
| matrix: | |
| service: | |
| - alert-triage | |
| - rag-service | |
| - log-summarization | |
| - ml-inference | |
| steps: | |
| - name: Checkout code | |
| uses: actions/checkout@v4 | |
| - name: Set up Docker Buildx | |
| uses: docker/setup-buildx-action@v3 | |
| - name: Log in to Container Registry | |
| uses: docker/login-action@v3 | |
| with: | |
| registry: ${{ env.REGISTRY }} | |
| username: ${{ github.actor }} | |
| password: ${{ secrets.GITHUB_TOKEN }} | |
| - name: Extract metadata | |
| id: meta | |
| uses: docker/metadata-action@v5 | |
| with: | |
| images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}/${{ matrix.service }} | |
| tags: | | |
| type=ref,event=branch | |
| type=ref,event=pr | |
| type=semver,pattern={{version}} | |
| type=semver,pattern={{major}}.{{minor}} | |
| type=sha,prefix={{branch}}- | |
| - name: Build and push Docker image | |
| uses: docker/build-push-action@v5 | |
| if: hashFiles(format('services/{0}/Dockerfile', matrix.service)) != '' | |
| with: | |
| context: services/${{ matrix.service }} | |
| push: true | |
| tags: ${{ steps.meta.outputs.tags }} | |
| labels: ${{ steps.meta.outputs.labels }} | |
| cache-from: type=gha | |
| cache-to: type=gha,mode=max | |
| # ============================================================================ | |
| # Security Scanning | |
| # ============================================================================ | |
| security-scan: | |
| name: Security Scan Images | |
| runs-on: ubuntu-latest | |
| needs: build-and-push | |
| permissions: | |
| contents: read | |
| security-events: write | |
| strategy: | |
| matrix: | |
| service: | |
| - alert-triage | |
| - rag-service | |
| steps: | |
| - name: Run Trivy security scan | |
| uses: aquasecurity/trivy-action@master | |
| continue-on-error: true # Don't fail if image doesn't exist | |
| with: | |
| image-ref: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}/${{ matrix.service }}:${{ github.sha }} | |
| format: 'sarif' | |
| output: 'trivy-${{ matrix.service }}.sarif' | |
| - name: Upload scan results | |
| uses: github/codeql-action/upload-sarif@v4 | |
| if: always() # Upload even if scan step failed/skipped | |
| continue-on-error: true | |
| with: | |
| sarif_file: 'trivy-${{ matrix.service }}.sarif' | |
| # ============================================================================ | |
| # Deploy to Staging | |
| # Build AI service images, start them in CI, run smoke + integration tests | |
| # ============================================================================ | |
| deploy-staging: | |
| name: Deploy to Staging | |
| runs-on: ubuntu-latest | |
| needs: [build-and-push, security-scan] | |
| steps: | |
| - name: Checkout code | |
| uses: actions/checkout@v4 | |
| - name: Set up Docker Compose | |
| run: | | |
| # Docker Compose v2 is bundled with Docker on ubuntu-latest - verify | |
| docker compose version | |
| - name: Create minimal .env for CI | |
| run: | | |
| cat > .env <<'EOF' | |
| INDEXER_USERNAME=admin | |
| INDEXER_PASSWORD=SecurePassword1! | |
| API_PASSWORD=SecurePassword1! | |
| WAZUH_API_PASSWORD=SecurePassword1! | |
| KIBANA_PASSWORD=SecurePassword1! | |
| EOF | |
| - name: Build AI service images | |
| run: | | |
| docker compose -f docker-compose/ai-services.yml build --parallel | |
| env: | |
| DOCKER_BUILDKIT: 1 | |
| - name: Start AI services (no SIEM in CI - resource constraints) | |
| run: | | |
| # Start ChromaDB first (RAG service dependency) | |
| docker compose -f docker-compose/ai-services.yml up -d chromadb | |
| echo "Waiting for ChromaDB..." | |
| timeout 60 bash -c 'until docker compose -f docker-compose/ai-services.yml ps chromadb | grep -q "healthy\|Up"; do sleep 3; done' || true | |
| # Start AI services without Ollama dependency (Ollama too large for CI) | |
| # Use --no-deps to skip Ollama health check requirement | |
| docker compose -f docker-compose/ai-services.yml up -d --no-deps ml-inference rag-service | |
| docker compose -f docker-compose/ai-services.yml up -d --no-deps alert-triage | |
| echo "AI services started (Ollama skipped in CI)" | |
| - name: Wait for services to be healthy | |
| run: | | |
| echo "Waiting for AI services to pass health checks (max 120s)..." | |
| max_wait=120 | |
| elapsed=0 | |
| services=("ml-inference:8500" "rag-service:8300" "alert-triage:8100") | |
| for svc_port in "${services[@]}"; do | |
| svc="${svc_port%%:*}" | |
| port="${svc_port##*:}" | |
| wait=0 | |
| echo -n " Waiting for $svc..." | |
| while [[ $wait -lt $max_wait ]]; do | |
| if curl -sf "http://localhost:${port}/health" > /dev/null 2>&1; then | |
| echo " healthy (${wait}s)" | |
| break | |
| fi | |
| sleep 5 | |
| wait=$((wait + 5)) | |
| done | |
| if [[ $wait -ge $max_wait ]]; then | |
| echo " TIMEOUT after ${max_wait}s" | |
| fi | |
| done | |
| - name: Run smoke tests | |
| run: | | |
| echo "=== Smoke Tests ===" | |
| PASS=0 | |
| FAIL=0 | |
| check_health() { | |
| local name="$1" | |
| local url="$2" | |
| if curl -sf --max-time 10 "$url" > /dev/null; then | |
| echo " PASS: $name ($url)" | |
| PASS=$((PASS + 1)) | |
| else | |
| echo " FAIL: $name ($url)" | |
| FAIL=$((FAIL + 1)) | |
| fi | |
| } | |
| check_health "ML Inference health" "http://localhost:8500/health" | |
| check_health "RAG Service health" "http://localhost:8300/health" | |
| check_health "Alert Triage health" "http://localhost:8100/health" | |
| check_health "RAG collections" "http://localhost:8300/collections" | |
| check_health "ML models list" "http://localhost:8500/models" | |
| echo "" | |
| echo "Smoke tests: ${PASS} passed, ${FAIL} failed" | |
| if [[ $FAIL -gt 0 ]]; then | |
| echo "::warning::${FAIL} smoke test(s) failed" | |
| fi | |
| - name: Run integration tests | |
| run: | | |
| echo "=== Integration Tests ===" | |
| # Test ML inference prediction endpoint | |
| echo "Testing ML inference prediction..." | |
| features=$(python3 -c "import json; print(json.dumps({'features': [0.1]*78, 'model_name': 'random_forest'}))") | |
| ml_response=$(curl -sf -X POST \ | |
| -H "Content-Type: application/json" \ | |
| -d "$features" \ | |
| "http://localhost:8500/predict" || echo "ERROR") | |
| if echo "$ml_response" | python3 -c "import sys, json; d = json.load(sys.stdin); assert 'prediction' in d, 'missing prediction field'" 2>/dev/null; then | |
| echo " PASS: ML prediction returns valid response" | |
| else | |
| echo " WARN: ML prediction - response: $ml_response" | |
| fi | |
| # Test RAG retrieval endpoint | |
| echo "Testing RAG retrieval..." | |
| rag_response=$(curl -sf -X POST \ | |
| -H "Content-Type: application/json" \ | |
| -d '{"query":"brute force attack","collection":"mitre_attack","top_k":3}' \ | |
| "http://localhost:8300/retrieve" || echo "ERROR") | |
| if echo "$rag_response" | python3 -c "import sys, json; d = json.load(sys.stdin); assert 'results' in d, 'missing results field'" 2>/dev/null; then | |
| echo " PASS: RAG retrieval returns valid response" | |
| else | |
| echo " WARN: RAG retrieval - response: $rag_response" | |
| fi | |
| # Test alert triage root endpoint | |
| echo "Testing Alert Triage API..." | |
| triage_response=$(curl -sf "http://localhost:8100/" || echo "ERROR") | |
| if [[ "$triage_response" != "ERROR" ]]; then | |
| echo " PASS: Alert Triage API responding" | |
| else | |
| echo " WARN: Alert Triage API not responding" | |
| fi | |
| echo "Integration tests complete." | |
| - name: Show container logs on failure | |
| if: failure() | |
| run: | | |
| echo "=== Container Status ===" | |
| docker compose -f docker-compose/ai-services.yml ps | |
| echo "" | |
| echo "=== ML Inference Logs ===" | |
| docker compose -f docker-compose/ai-services.yml logs ml-inference --tail=50 || true | |
| echo "" | |
| echo "=== RAG Service Logs ===" | |
| docker compose -f docker-compose/ai-services.yml logs rag-service --tail=50 || true | |
| echo "" | |
| echo "=== Alert Triage Logs ===" | |
| docker compose -f docker-compose/ai-services.yml logs alert-triage --tail=50 || true | |
| - name: Tear down staging services | |
| if: always() | |
| run: | | |
| docker compose -f docker-compose/ai-services.yml down --volumes --remove-orphans || true | |
| echo "Staging environment torn down." | |
| # ============================================================================ | |
| # Deploy to Production (on tag only) | |
| # Build/push images to GHCR and create GitHub Release | |
| # ============================================================================ | |
| deploy-production: | |
| name: Deploy to Production | |
| runs-on: ubuntu-latest | |
| needs: deploy-staging | |
| if: startsWith(github.ref, 'refs/tags/v') | |
| permissions: | |
| contents: write | |
| packages: write | |
| steps: | |
| - name: Checkout code | |
| uses: actions/checkout@v4 | |
| with: | |
| fetch-depth: 0 # Need full history for changelog generation | |
| - name: Extract version from tag | |
| id: version | |
| run: | | |
| TAG="${{ github.ref_name }}" | |
| echo "tag=$TAG" >> $GITHUB_OUTPUT | |
| echo "version=${TAG#v}" >> $GITHUB_OUTPUT | |
| - name: Log in to Container Registry | |
| uses: docker/login-action@v3 | |
| with: | |
| registry: ${{ env.REGISTRY }} | |
| username: ${{ github.actor }} | |
| password: ${{ secrets.GITHUB_TOKEN }} | |
| - name: Verify production images were pushed | |
| run: | | |
| echo "Verifying images exist in GHCR for tag ${{ steps.version.outputs.tag }}..." | |
| services=("alert-triage" "rag-service" "ml-inference") | |
| for svc in "${services[@]}"; do | |
| image="${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}/${svc}:${{ steps.version.outputs.tag }}" | |
| echo " Checking: $image" | |
| # Pull to verify (will fail gracefully if not found) | |
| docker pull "$image" 2>/dev/null && echo " Found" || echo " Not yet available (built by build-and-push job)" | |
| done | |
| - name: Generate changelog | |
| id: changelog | |
| run: | | |
| # Get commits since last tag | |
| PREV_TAG=$(git describe --tags --abbrev=0 HEAD~1 2>/dev/null || git rev-list --max-parents=0 HEAD) | |
| COMMITS=$(git log "${PREV_TAG}..HEAD" --oneline --no-merges 2>/dev/null | head -30 || echo "Initial release") | |
| echo "Previous tag: $PREV_TAG" | |
| echo "Commits since previous tag:" | |
| echo "$COMMITS" | |
| # Write to file for multiline handling | |
| { | |
| echo "changelog<<EOF" | |
| if [[ -n "$COMMITS" ]]; then | |
| echo "$COMMITS" | while IFS= read -r line; do | |
| echo "- ${line}" | |
| done | |
| else | |
| echo "- See commit history for changes" | |
| fi | |
| echo "EOF" | |
| } >> $GITHUB_OUTPUT | |
| - name: Create GitHub Release | |
| uses: softprops/action-gh-release@v2 | |
| with: | |
| tag_name: ${{ steps.version.outputs.tag }} | |
| name: "AI-SOC ${{ steps.version.outputs.tag }}" | |
| draft: false | |
| prerelease: false | |
| body: | | |
| ## AI-SOC Release ${{ steps.version.outputs.tag }} | |
| AI-Augmented Security Operations Center - automated threat detection and response platform. | |
| ### What's Changed | |
| ${{ steps.changelog.outputs.changelog }} | |
| ### Services | |
| | Service | Port | Description | | |
| |---------|------|-------------| | |
| | Alert Triage | 8100 | LLM-powered alert analysis with MITRE ATT&CK mapping | | |
| | RAG Service | 8300 | Security knowledge base (CVEs, runbooks, MITRE) | | |
| | ML Inference | 8500 | Anomaly detection and threat classification | | |
| | Wazuh Integration | 8002 | SIEM alert forwarding and enrichment | | |
| ### Docker Images (GHCR) | |
| ``` | |
| ghcr.io/${{ env.IMAGE_NAME }}/alert-triage:${{ steps.version.outputs.tag }} | |
| ghcr.io/${{ env.IMAGE_NAME }}/rag-service:${{ steps.version.outputs.tag }} | |
| ghcr.io/${{ env.IMAGE_NAME }}/ml-inference:${{ steps.version.outputs.tag }} | |
| ``` | |
| ### Deployment Instructions | |
| **Quick Deploy (Linux/macOS):** | |
| ```bash | |
| git clone https://github.com/${{ github.repository }}.git | |
| cd $(basename ${{ github.repository }}) | |
| git checkout ${{ steps.version.outputs.tag }} | |
| chmod +x deploy-ai-soc.sh | |
| ./deploy-ai-soc.sh | |
| ``` | |
| **Quick Deploy (Windows):** | |
| ```powershell | |
| git clone https://github.com/${{ github.repository }}.git | |
| cd (Split-Path ${{ github.repository }} -Leaf) | |
| git checkout ${{ steps.version.outputs.tag }} | |
| .\deploy-ai-soc.ps1 | |
| ``` | |
| **Requirements:** | |
| - Docker Desktop 24+ with Docker Compose v2 | |
| - 8GB+ RAM, 20GB+ disk space | |
| - Internet access for Ollama model download | |
| ### Access URLs (after deployment) | |
| - Wazuh Dashboard: https://localhost:443 | |
| - Grafana: http://localhost:3001 | |
| - Alert Triage API: http://localhost:8100/docs | |
| - RAG Service API: http://localhost:8300/docs | |
| env: | |
| GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| # ============================================================================ | |
| # Rollback on Failure | |
| # ============================================================================ | |
| rollback: | |
| name: Rollback on Failure | |
| runs-on: ubuntu-latest | |
| needs: [deploy-staging, deploy-production] | |
| if: failure() | |
| steps: | |
| - name: Checkout code | |
| uses: actions/checkout@v4 | |
| - name: Tear down any leftover staging services | |
| run: | | |
| echo "Deployment failed - tearing down staging environment..." | |
| docker compose -f docker-compose/ai-services.yml down --volumes --remove-orphans 2>/dev/null || true | |
| echo "Staging environment cleaned up." | |
| - name: Post failure summary | |
| run: | | |
| echo "## Deployment Failure Summary" >> $GITHUB_STEP_SUMMARY | |
| echo "" >> $GITHUB_STEP_SUMMARY | |
| echo "**Branch/Tag:** ${{ github.ref_name }}" >> $GITHUB_STEP_SUMMARY | |
| echo "**Commit:** ${{ github.sha }}" >> $GITHUB_STEP_SUMMARY | |
| echo "**Triggered by:** ${{ github.actor }}" >> $GITHUB_STEP_SUMMARY | |
| echo "" >> $GITHUB_STEP_SUMMARY | |
| echo "### Failed Jobs" >> $GITHUB_STEP_SUMMARY | |
| echo "One or more deployment jobs failed. Review the workflow run for details." >> $GITHUB_STEP_SUMMARY | |
| echo "" >> $GITHUB_STEP_SUMMARY | |
| echo "### Recovery Steps" >> $GITHUB_STEP_SUMMARY | |
| echo "1. Review logs for the failed job" >> $GITHUB_STEP_SUMMARY | |
| echo "2. Fix the issue and push a new commit" >> $GITHUB_STEP_SUMMARY | |
| echo "3. Re-run the workflow via Actions > Re-run failed jobs" >> $GITHUB_STEP_SUMMARY |