PurpleOPS BAS - Architecture
┌─────────────────────────────────────────────────────────────┐
│ PurpleOPS BAS │
│ Purple Team Platform │
└─────────────────────────────────────────────────────────────┘
┌──────────────┐ ┌──────────────┐ ┌──────────────┐
│ Frontend │───▶│ Backend │───▶│ Postgres │
│ (ClojureScript) │ │(Clojure/Ring)│ │ Database │
└──────────────┘ └──────────────┘ └──────────────┘
│
▼
┌──────────────┐
│ Orchestrator │
│ (core.async) │
└──────────────┘
│
┌──────────────────┼──────────────────┐
▼ ▼ ▼
┌─────────┐ ┌─────────┐ ┌─────────┐
│ Planner│ │Executor │ │ Critic │
└─────────┘ └─────────┘ └─────────┘
│
▼
┌─────────┐
│Reporter │
└─────────┘
Frontend (ClojureScript + Reagent + re-frame)
Location : frontend/
Tech : ClojureScript, Reagent (React wrapper), re-frame (state management)
Build : shadow-cljs
Serves : Dashboard, Exercise management, Scenario library, Reports
Port : 8080 (Nginx in Docker)
Backend (Clojure + Ring + Reitit)
Location : backend/
Tech : Clojure, Ring, Reitit, Integrant
DB : Postgres via next.jdbc + HikariCP
Auth : JWT + RBAC (Buddy)
API : RESTful endpoints
Port : 3000
Orchestrator (Clojure + core.async)
Location : orchestrator/
Tech : Clojure, core.async
Pipeline : Planner → Executor → Critic → Reporter
Port : 3001 (health check)
Loads scenario from DB
Validates allowlist and authorization
Creates execution plan
Safety first : blocks dangerous actions
Executes actions from plan
Multimethod dispatch by action type
Respects timeouts
Logs all actions
Imports detections from connectors
Compares expected vs actual
Calculates metrics (detection rate, TTD, TTC)
Identifies gaps
Generates final report
Persists results to DB
Creates recommendations
Schema : migrations/
Tables : users, exercises, scenarios, runs, detections, evidences, audit_log
Indexes : Optimized for queries
Migrations : Migratus
Location : connectors/
Purpose : Import telemetry from external tools
Types : SIEM, EDR, Logs
Formats : JSON, API calls, file imports
control-plane (172.20.0.0/24)
├── frontend:80
├── backend:3000
├── postgres:5432
└── orchestrator:3001
lab-net (172.21.0.0/24)
└── (for scenario execution)
control-plane : Management and API
lab-net : Scenario execution (isolated from production)
Authentication & Authorization
Login : POST /api/auth/login → JWT token
JWT : Signed with HS256, expires in 8h
RBAC : 4 roles (admin, purple-lead, analyst, viewer)
Middleware : wrap-auth, wrap-require-role
Create Exercise
↓
[pending] → (requires allowlist + proof-of-authorization)
↓
Approve (by purple-lead/admin)
↓
[approved]
↓
Start Execution
↓
[running] → Orchestrator processes
↓
[completed/failed]
Allowlist Validation : Every target must be in scope
Proof of Authorization : Ticket ID required
Action Blacklist : Blocks dangerous keywords (exploit, malware, etc.)
Production Check : Rejects targets with "prod" in name
Audit Log : All actions logged with user, timestamp, IP
Table: audit_log
Captures: user_id, action, details, timestamp, ip_address
Middleware: wrap-audit-log (automatic)
1. User creates exercise (UI)
↓
2. POST /api/exercises (Backend)
↓
3. Validation (allowlist, proof-of-auth)
↓
4. Saved as [pending] (DB)
↓
5. Purple Lead approves (UI)
↓
6. POST /api/exercises/:id/approve (Backend)
↓
7. Status → [approved] (DB)
↓
8. User starts exercise (UI)
↓
9. POST /api/exercises/:id/start (Backend)
↓
10. Create run record (DB)
↓
11. Submit to orchestrator (async)
↓
12. Orchestrator pipeline:
Planner → Executor → Critic → Reporter
↓
13. Results saved to DB
↓
14. User views report (UI)
Containers (Alpine-based)
Backend:
Builder (clojure:temurin-21-alpine) → uberjar
Runtime (eclipse-temurin:21-jre-alpine) → non-root user
Orchestrator:
Builder (clojure:temurin-21-alpine) → uberjar
Runtime (eclipse-temurin:21-jre-alpine) → non-root user
Frontend:
Builder (node:20-alpine) → shadow-cljs build
Runtime (nginx:alpine) → static files
Non-root users (1001, 1002)
Read-only filesystems (where possible)
Minimal capabilities
Healthchecks (every 15-30s)
Resource limits (CPU, memory)
# Backend
DATABASE_URL=jdbc:postgresql://...
JWT_SECRET=...
ENVIRONMENT=production
LOG_LEVEL=info
# Orchestrator
DATABASE_URL=jdbc:postgresql://...
BACKEND_URL=http://backend:3000
backend/resources/config.edn
Aero-based with profiles (:dev, :production)
Monitoring & Observability
Backend: GET /health
Orchestrator: GET /health (port 3001)
Frontend: Nginx status
Structured logging (timbre)
Volumes: backend-logs, orchestrator-logs
View: docker compose logs -f <service>
Prometheus endpoint: /metrics
Grafana dashboard
Backend: Stateless (can scale with load balancer)
Orchestrator: Single instance (or use queue for distribution)
Database: Read replicas for queries
Connection pooling (HikariCP)
Async execution (core.async)
Caching (can add Redis)
# Backend REPL
cd backend && clj -M:dev
# Frontend watch
cd frontend && npm run watch
# Database migrations
clj -M:migratus migrate
# Tests
clj -M:test
Layer
Technology
Frontend
ClojureScript, Reagent, re-frame, shadow-cljs
Backend
Clojure, Ring, Reitit, Integrant
Orchestrator
Clojure, core.async
Database
PostgreSQL 16
Auth
JWT (Buddy)
Containers
Docker, Alpine Linux
Web Server
Nginx (frontend), Jetty (backend)
Logging
timbre
HTTP Client
clj-http
JSON
Cheshire