diff --git a/README.md b/README.md index 13cbc62..3dc1ae3 100644 --- a/README.md +++ b/README.md @@ -1,40 +1,53 @@ -# Aegis - MITRE ATT&CK Coverage Platform +# Aegis — MITRE ATT&CK Coverage Platform Aegis is a comprehensive platform for tracking and managing security coverage against the MITRE ATT&CK framework. It enables security teams to document, validate, and visualize their defensive capabilities against known adversary techniques through a structured Red Team / Blue Team validation workflow. ## Features -- **MITRE ATT&CK Integration**: Automatic synchronization with the MITRE ATT&CK framework via TAXII (with GitHub fallback), scheduled every 24h -- **Red/Blue Validation Workflow**: Structured dual-validation lifecycle for security tests (draft → red_executing → blue_evaluating → in_review → validated/rejected) -- **Test Template Catalog**: Import tests from Atomic Red Team, create custom templates, and instantiate real tests from them -- **Dual Validation**: Independent approval/rejection by Red Lead and Blue Lead before a test is finalized -- **Coverage Tracking**: Track validation status for each technique (validated, partial, not covered, in progress) -- **Evidence Storage**: Secure evidence file storage with SHA256 integrity verification, separated by team (red/blue) -- **In-App Notifications**: Real-time notification bell with polling, automatic alerts on state changes -- **Reports & Export**: Coverage summary, test results, and remediation reports in JSON and CSV formats -- **Remediation Tracking**: Step-by-step remediation assignments with status tracking per test -- **Role-Based Access Control**: Granular permissions for red team, blue team, and leadership roles -- **Intel Monitoring**: Automated scanning for new threat intelligence related to techniques -- **Metrics Dashboard**: Pipeline funnel, team activity, validation rates, and recent tests +### Core (V1) +- **MITRE ATT&CK Integration** — Automatic synchronization via TAXII 2.0 (with GitHub fallback), scheduled every 24h +- **Red/Blue Validation Workflow** — Structured dual-validation lifecycle: draft → red_executing → blue_evaluating → in_review → validated/rejected +- **Dual Validation** — Independent Red Lead / Blue Lead approval before finalization +- **Coverage Tracking** — Per-technique status (validated, partial, not covered, in progress) +- **Evidence Storage** — Secure evidence with SHA256 integrity, separated by team (red/blue) +- **Role-Based Access Control** — Granular permissions for 6 roles (admin, red_tech, blue_tech, red_lead, blue_lead, viewer) + +### Enhanced (V2) +- **Test Template Catalog** — Import from Atomic Red Team, create custom templates, instantiate tests +- **In-App Notifications** — Real-time notification bell with polling and automatic state-change alerts +- **Reports & Export** — Coverage summary, test results, and remediation reports in JSON and CSV +- **Remediation Tracking** — Step-by-step remediation assignments with status tracking +- **Metrics Dashboard** — Pipeline funnel, team activity, validation rates + +### Advanced (V3) +- **Multi-Source Data Import** — Sigma, Elastic, CALDERA, LOLBAS, D3FEND, MITRE CTI threat actors, compliance mappings +- **Detection Rule Tracking** — Import and evaluate Sigma/Elastic detection rules per test +- **ATT&CK Heatmap** — Interactive Navigator-style heatmap with layers, filters, and export +- **Threat Actor Intelligence** — Track intrusion sets and their technique coverage +- **Campaign Management** — Group tests into campaigns with dependencies, scheduling, and recurring execution +- **Compliance Mapping** — Map NIST 800-53 controls to ATT&CK techniques with gap analysis +- **Granular Scoring** — 0–100 scoring for techniques, tactics, actors, and organization with configurable weights +- **Operational Metrics** — MTTD, MTTR, detection efficacy, alert fidelity, coverage velocity +- **Executive Dashboard** — High-level KPIs for leadership (leads + admin) +- **Temporal Comparison** — Coverage snapshots with historical comparison and trend analysis +- **Auto Re-testing** — Automatic retest creation after remediation completion (configurable limit) +- **Performance Optimizations** — Score caching, lazy loading, React.memo, database index optimization ## Red Team / Blue Team Validation Flow ``` -┌─────────────────────────────────────────────────────────────────────────┐ -│ TEST LIFECYCLE │ -│ │ -│ ┌──────┐ ┌──────────────┐ ┌─────────────────┐ ┌───────────┐ │ -│ │ DRAFT│───▶│RED_EXECUTING │───▶│ BLUE_EVALUATING │───▶│ IN_REVIEW │ │ -│ └──────┘ └──────────────┘ └─────────────────┘ └───────────┘ │ -│ │ │ -│ ┌────────────────────┤ │ -│ ▼ ▼ │ -│ ┌──────────┐ ┌──────────┐ │ -│ │ REJECTED │ │VALIDATED │ │ -│ └──────────┘ └──────────┘ │ -│ │ │ -│ └──────▶ Back to DRAFT │ -└─────────────────────────────────────────────────────────────────────────┘ +┌──────┐ ┌──────────────┐ ┌─────────────────┐ ┌───────────┐ +│ DRAFT│───▶│RED_EXECUTING │───▶│ BLUE_EVALUATING │───▶│ IN_REVIEW │ +└──────┘ └──────────────┘ └─────────────────┘ └─────┬─────┘ + │ + ┌───────────────────┤ + ▼ ▼ + ┌──────────┐ ┌──────────┐ + │ REJECTED │ │VALIDATED │ + └────┬─────┘ └──────────┘ + │ │ + └──▶ Back to DRAFT ├──▶ Remediation + └──▶ Auto Re-test ``` ### States @@ -66,23 +79,15 @@ Both Red Lead and Blue Lead must independently vote: | `blue_lead` | Blue team lead | Validate/reject the blue side of tests | | `viewer` | Read-only access | View all data | -## Test Template Catalog - -Tests can be created from predefined templates sourced from: -1. **Atomic Red Team** (Red Canary) — imported via the System admin panel -2. **Custom templates** — created by admins with suggested procedures and remediation -3. **MITRE procedures** — based on MITRE ATT&CK documentation - -Templates include attack procedures, expected detections, suggested tools, severity levels, and suggested remediation steps. When instantiated, these fields are pre-populated into the new test. - ## Tech Stack - **Backend**: FastAPI (Python 3.11) -- **Database**: PostgreSQL 15 +- **Database**: PostgreSQL 15 with UUID primary keys and JSONB columns - **Object Storage**: MinIO (S3-compatible) -- **ORM**: SQLAlchemy with Alembic migrations -- **Frontend**: React 19 + TypeScript + Vite + Tailwind CSS v4 + TanStack Query -- **Scheduler**: APScheduler (MITRE sync, Intel scan, Notification cleanup) +- **ORM**: SQLAlchemy with Alembic migrations (18 migration files) +- **Frontend**: React 19 + TypeScript + Vite 7 + Tailwind CSS v4 + TanStack Query + TanStack Virtual +- **Scheduler**: APScheduler (MITRE sync, Intel scan, Notification cleanup, Snapshots, Recurring campaigns) +- **Charts**: Recharts ## Quick Start @@ -106,21 +111,17 @@ docker-compose up -d 3. Run database migrations: ```bash -docker exec -w /app aegis-backend-1 alembic upgrade head +docker exec aegis-backend alembic upgrade head ``` 4. Seed the admin user: ```bash -docker exec -w /app aegis-backend-1 python -m app.seed +docker exec aegis-backend python -m app.seed ``` -5. Start the frontend: -```bash -cd frontend && npm install && npm run dev -``` - -6. Verify the installation: +5. Access the application: ```bash +# API health check curl http://localhost:8000/health # Expected: {"status":"ok"} @@ -131,13 +132,51 @@ curl http://localhost:8000/health JWT-based authentication. Default admin credentials after seeding: -```bash -curl -X POST http://localhost:8000/api/v1/auth/login \ - -d "username=admin&password=admin123" +``` +Username: admin +Password: admin123 ``` > **Important:** Change the default `admin123` password and `SECRET_KEY` in production. +### Importing Data Sources + +After initial setup, populate the platform with data: + +```bash +# 1. Sync MITRE ATT&CK techniques +curl -X POST http://localhost:8000/api/v1/system/sync-mitre -H "Authorization: Bearer $TOKEN" + +# 2. Import test templates from Atomic Red Team +curl -X POST http://localhost:8000/api/v1/system/import-atomic-red-team -H "Authorization: Bearer $TOKEN" + +# 3. Import additional sources via the Data Sources admin page +# Navigate to System → Data Sources in the UI +``` + +See [docs/DATA_SOURCES.md](docs/DATA_SOURCES.md) for detailed instructions on all data sources. + +### Configuring Scoring Weights + +Default scoring weights can be adjusted via environment variables: + +```env +SCORING_WEIGHT_TESTS=40 +SCORING_WEIGHT_DETECTION_RULES=20 +SCORING_WEIGHT_D3FEND=15 +SCORING_WEIGHT_FRESHNESS=15 +SCORING_WEIGHT_PLATFORM_DIVERSITY=10 +``` + +Or at runtime via the admin API — see [docs/SCORING.md](docs/SCORING.md). + +### Configuring Campaigns + +1. Navigate to **Campaigns** in the sidebar +2. Create a new campaign (custom or from a threat actor) +3. Add tests with dependencies and phases +4. Optionally enable **recurring scheduling** (weekly, monthly, quarterly) + ## Services | Service | Port | Description | @@ -148,6 +187,28 @@ curl -X POST http://localhost:8000/api/v1/auth/login \ | MinIO API | 9000 | S3-compatible object storage | | MinIO Console | 9001 | MinIO web interface | +## Navigation + +``` +📊 Dashboard +📊 Executive Dashboard (leads + admin) +🔲 ATT&CK Matrix (advanced heatmap) +🧪 Tests + ├─ All Tests + ├─ My Pending Tasks + └─ Test Catalog +📋 Campaigns +👤 Threat Actors +📜 Compliance +📈 Comparison (leads + admin) +📄 Reports +⚙️ System (admin only) + ├─ Data Sources + ├─ MITRE Sync + ├─ Users + └─ Audit Log +``` + ## API Documentation Interactive API documentation available at: @@ -155,192 +216,33 @@ Interactive API documentation available at: - **Swagger UI**: http://localhost:8000/docs - **ReDoc**: http://localhost:8000/redoc -## API Endpoints +### API Endpoints -### Auth -| Method | Route | Auth | Description | -|--------|-------|------|-------------| -| POST | `/api/v1/auth/login` | Public | Obtain JWT token | -| GET | `/api/v1/auth/me` | Authenticated | Current user profile | +| Group | Prefix | Key Operations | +|-------|--------|---------------| +| Auth | `/api/v1/auth` | Login, get current user | +| Techniques | `/api/v1/techniques` | CRUD, list with filters, mark reviewed | +| Tests | `/api/v1/tests` | Full Red/Blue workflow, remediation, retest chain | +| Test Templates | `/api/v1/test-templates` | CRUD, import, stats, toggle active | +| Evidence | `/api/v1/tests/{id}/evidence` | Upload evidence, get presigned URLs | +| Campaigns | `/api/v1/campaigns` | CRUD, scheduling, history | +| Threat Actors | `/api/v1/threat-actors` | CRUD, technique mappings | +| Detection Rules | `/api/v1/detection-rules` | List, filter by source/technique | +| D3FEND | `/api/v1/d3fend` | Defensive techniques and mappings | +| Compliance | `/api/v1/compliance` | Frameworks, controls, gaps | +| Scores | `/api/v1/scores` | Technique/tactic/actor/org scores, config | +| Operational Metrics | `/api/v1/metrics/operational` | MTTD, MTTR, trends, team breakdown | +| Heatmap | `/api/v1/heatmap` | ATT&CK Navigator-style data | +| Snapshots | `/api/v1/snapshots` | Create, compare, list snapshots | +| Reports | `/api/v1/reports` | Coverage, results, remediation exports | +| Notifications | `/api/v1/notifications` | List, read, mark all read | +| Metrics | `/api/v1/metrics` | Summary, by-tactic, pipeline, team activity | +| System | `/api/v1/system` | MITRE sync, import, scheduler status | +| Users | `/api/v1/users` | User CRUD (admin) | +| Audit Logs | `/api/v1/audit-logs` | Audit trail (admin) | +| Data Sources | `/api/v1/data-sources` | Data source management (admin) | -### Techniques -| Method | Route | Auth | Description | -|--------|-------|------|-------------| -| GET | `/api/v1/techniques` | Authenticated | List all (filters: tactic, status, review_required) | -| GET | `/api/v1/techniques/{mitre_id}` | Authenticated | Detail with associated tests | -| POST | `/api/v1/techniques` | Admin | Create technique | -| PATCH | `/api/v1/techniques/{mitre_id}` | Admin | Update technique fields | -| PATCH | `/api/v1/techniques/{mitre_id}/review` | Lead, Admin | Mark as reviewed | - -### Tests — Red/Blue Workflow -| Method | Route | Auth | Description | -|--------|-------|------|-------------| -| GET | `/api/v1/tests` | Authenticated | List with filters (state, technique, platform, creator, pending_validation_side) | -| POST | `/api/v1/tests` | Red Tech, Admin | Create test (state=draft) | -| POST | `/api/v1/tests/from-template` | Red Tech, Admin | Create from template (pre-populates fields) | -| GET | `/api/v1/tests/{id}` | Authenticated | Detail with split red/blue evidences | -| PATCH | `/api/v1/tests/{id}` | Creator, Admin | General update (draft/rejected only) | -| PATCH | `/api/v1/tests/{id}/red` | Red Tech, Admin | Red Team fields (draft, red_executing) | -| PATCH | `/api/v1/tests/{id}/blue` | Blue Tech, Admin | Blue Team fields (blue_evaluating) | -| PATCH | `/api/v1/tests/{id}/remediation` | Authenticated | Update remediation fields | -| POST | `/api/v1/tests/{id}/start-execution` | Red Tech, Admin | draft → red_executing | -| POST | `/api/v1/tests/{id}/submit-red` | Red Tech, Admin | red_executing → blue_evaluating | -| POST | `/api/v1/tests/{id}/submit-blue` | Blue Tech, Admin | blue_evaluating → in_review | -| POST | `/api/v1/tests/{id}/validate-red` | Red Lead, Admin | Red Lead approves/rejects | -| POST | `/api/v1/tests/{id}/validate-blue` | Blue Lead, Admin | Blue Lead approves/rejects | -| POST | `/api/v1/tests/{id}/reopen` | Lead, Admin | rejected → draft (clears validation) | -| GET | `/api/v1/tests/{id}/timeline` | Authenticated | Audit-log history for this test | - -### Test Templates -| Method | Route | Auth | Description | -|--------|-------|------|-------------| -| GET | `/api/v1/test-templates` | Authenticated | List templates (filters: source, platform, severity, search, mitre_technique_id) | -| POST | `/api/v1/test-templates` | Admin | Create custom template | -| GET | `/api/v1/test-templates/stats` | Admin | Catalog statistics | -| GET | `/api/v1/test-templates/{id}` | Authenticated | Template detail | -| PATCH | `/api/v1/test-templates/{id}` | Admin | Update template | -| DELETE | `/api/v1/test-templates/{id}` | Admin | Soft-delete (deactivate) | -| POST | `/api/v1/test-templates/{id}/toggle-active` | Admin | Toggle active/inactive | - -### Evidence -| Method | Route | Auth | Description | -|--------|-------|------|-------------| -| POST | `/api/v1/tests/{test_id}/evidence` | Authenticated | Upload evidence (team=red/blue) | -| GET | `/api/v1/evidence/{id}` | Authenticated | Metadata + presigned download URL | - -### Notifications -| Method | Route | Auth | Description | -|--------|-------|------|-------------| -| GET | `/api/v1/notifications` | Authenticated | List notifications (paginated, limit=20) | -| GET | `/api/v1/notifications/unread-count` | Authenticated | Unread notification count | -| PATCH | `/api/v1/notifications/{id}/read` | Authenticated | Mark one as read | -| POST | `/api/v1/notifications/read-all` | Authenticated | Mark all as read | - -### Reports -| Method | Route | Auth | Description | -|--------|-------|------|-------------| -| GET | `/api/v1/reports/coverage-summary` | Authenticated | Full coverage JSON report (filters: tactic, platform) | -| GET | `/api/v1/reports/coverage-csv` | Authenticated | CSV export of coverage | -| GET | `/api/v1/reports/test-results` | Authenticated | Test results report (filters: state, date_from, date_to) | -| GET | `/api/v1/reports/remediation-status` | Authenticated | Remediation status report (filter: status) | - -### Metrics -| Method | Route | Auth | Description | -|--------|-------|------|-------------| -| GET | `/api/v1/metrics/summary` | Authenticated | Global coverage summary | -| GET | `/api/v1/metrics/by-tactic` | Authenticated | Coverage by MITRE tactic | -| GET | `/api/v1/metrics/test-pipeline` | Authenticated | Test counts by pipeline state | -| GET | `/api/v1/metrics/team-activity` | Authenticated | Red/Blue team activity | -| GET | `/api/v1/metrics/validation-rate` | Authenticated | Approval/rejection rates by lead | -| GET | `/api/v1/metrics/recent-tests` | Authenticated | Last 10 updated tests | - -### System (Admin) -| Method | Route | Auth | Description | -|--------|-------|------|-------------| -| POST | `/api/v1/system/sync-mitre` | Admin | Trigger MITRE ATT&CK sync | -| POST | `/api/v1/system/run-intel-scan` | Admin | Trigger threat-intel RSS scan | -| POST | `/api/v1/system/import-atomic-red-team` | Admin | Import Atomic Red Team templates | -| GET | `/api/v1/system/scheduler-status` | Admin | Background scheduler health | - -### Users (Admin) -| Method | Route | Auth | Description | -|--------|-------|------|-------------| -| GET | `/api/v1/users` | Admin | List all users | -| POST | `/api/v1/users` | Admin | Create new user | -| GET | `/api/v1/users/{id}` | Admin | Get user by ID | -| PATCH | `/api/v1/users/{id}` | Admin | Update user | - -### Audit Logs (Admin) -| Method | Route | Auth | Description | -|--------|-------|------|-------------| -| GET | `/api/v1/audit-logs` | Admin | List audit logs (filters: action, entity_type, dates) | -| GET | `/api/v1/audit-logs/actions` | Admin | List distinct action types | -| GET | `/api/v1/audit-logs/entity-types` | Admin | List distinct entity types | - -## Project Structure - -``` -Aegis/ -├── docker-compose.yml -├── backend/ -│ ├── Dockerfile -│ ├── requirements.txt -│ ├── alembic.ini -│ ├── alembic/versions/ # b001–b007 migration files -│ └── app/ -│ ├── main.py # FastAPI app with all routers -│ ├── config.py # Settings from environment -│ ├── database.py # SQLAlchemy engine + session -│ ├── storage.py # MinIO/S3 helpers -│ ├── models/ -│ │ ├── user.py # User with roles -│ │ ├── technique.py # MITRE ATT&CK techniques -│ │ ├── test.py # Tests with Red/Blue + remediation fields -│ │ ├── test_template.py # Template catalog -│ │ ├── evidence.py # Evidence files (team-separated) -│ │ ├── notification.py # In-app notifications -│ │ ├── intel.py # Threat intelligence -│ │ ├── audit.py # Audit logging -│ │ └── enums.py # Shared enumerations -│ ├── schemas/ # Pydantic schemas -│ │ ├── test.py # TestCreate/Red/Blue/Validate/Remediation -│ │ ├── test_template.py # Template CRUD schemas -│ │ ├── notification.py # NotificationOut, UnreadCountOut -│ │ └── metrics.py # Pipeline, TeamActivity, ValidationRate -│ ├── routers/ # API endpoints -│ │ ├── tests.py # Full Red/Blue workflow endpoints -│ │ ├── test_templates.py # Template CRUD + import + stats -│ │ ├── notifications.py # Notification list/read/mark -│ │ ├── reports.py # Coverage/results/remediation reports -│ │ ├── metrics.py # V1 + V2 metrics endpoints -│ │ └── ... # auth, techniques, evidence, system, users, audit -│ ├── services/ -│ │ ├── test_workflow_service.py # State machine + dual validation -│ │ ├── notification_service.py # Create/read/cleanup notifications -│ │ ├── status_service.py # Technique status recalculation -│ │ └── ... # audit, mitre_sync, intel -│ └── jobs/ -│ └── mitre_sync_job.py # Scheduler: MITRE sync, Intel scan, Notification cleanup -├── frontend/src/ -│ ├── App.tsx # Routes including /reports -│ ├── api/ # API clients -│ │ ├── notifications.ts # Notification API -│ │ ├── reports.ts # Report API -│ │ └── ... -│ ├── components/ -│ │ ├── Layout.tsx # Sidebar + header + NotificationBell -│ │ ├── Sidebar.tsx # Collapsible nav with admin section -│ │ ├── NotificationBell.tsx # Bell icon with badge (polls every 30s) -│ │ ├── NotificationDropdown.tsx # Notification list dropdown -│ │ ├── ConfirmDialog.tsx # Reusable confirmation modal -│ │ ├── Toast.tsx # Toast notification system -│ │ └── test-detail/ # Test detail sub-components -│ └── pages/ -│ ├── DashboardPage.tsx # Pipeline funnel, team activity, validation rates -│ ├── TestsPage.tsx # Filters, state counters, pending tasks -│ ├── TestDetailPage.tsx # Red/Blue tabs, validation, evidence -│ ├── TestCatalogPage.tsx # Browse & use templates -│ ├── ReportsPage.tsx # Coverage, results, remediation reports -│ └── SystemPage.tsx # Template admin, import Atomic Red Team -└── backend/tests/ # Test suite - ├── test_workflow.py # Red/Blue workflow tests - ├── test_templates_crud.py # Template CRUD tests - ├── test_metrics_v2.py # V2 metrics tests - └── test_integration_v2.py # Full integration E2E tests -``` - -## Database Schema - -| Table | Description | -|-------|-------------| -| `users` | User accounts with role-based access | -| `techniques` | MITRE ATT&CK techniques with coverage status | -| `tests` | Security tests with Red/Blue fields, dual validation, and remediation | -| `test_templates` | Predefined test catalog (Atomic Red Team, custom) | -| `evidences` | File evidence separated by team (red/blue) | -| `notifications` | In-app notifications with read status | -| `intel_items` | Threat intelligence items linked to techniques | -| `audit_logs` | System-wide audit trail | +See [docs/API.md](docs/API.md) for the full endpoint reference. ## Configuration @@ -354,30 +256,114 @@ Aegis/ | `MINIO_ACCESS_KEY` | `minioadmin` | MinIO access key | | `MINIO_SECRET_KEY` | `minioadmin` | MinIO secret key | | `MINIO_BUCKET` | `evidence` | Evidence bucket | +| `MAX_RETEST_COUNT` | `3` | Max automatic retests per original test | +| `SCORING_WEIGHT_TESTS` | `40` | Weight for test validation component | +| `SCORING_WEIGHT_DETECTION_RULES` | `20` | Weight for detection rules component | +| `SCORING_WEIGHT_D3FEND` | `15` | Weight for D3FEND coverage component | +| `SCORING_WEIGHT_FRESHNESS` | `15` | Weight for freshness component | +| `SCORING_WEIGHT_PLATFORM_DIVERSITY` | `10` | Weight for platform diversity component | + +## Project Structure + +``` +Aegis/ +├── docker-compose.yml +├── docker-compose.prod.yml +├── docs/ +│ ├── API.md # Full API endpoint reference +│ ├── ARCHITECTURE.md # System architecture and DB schema +│ ├── DATA_SOURCES.md # External data source documentation +│ └── SCORING.md # Scoring system and metrics +├── backend/ +│ ├── Dockerfile +│ ├── requirements.txt +│ ├── alembic.ini +│ ├── alembic/versions/ # b001–b018 migration files +│ ├── pytest.ini +│ └── app/ +│ ├── main.py # FastAPI app with all routers + lifespan +│ ├── config.py # Settings from environment +│ ├── database.py # SQLAlchemy engine + session (lazy init) +│ ├── storage.py # MinIO/S3 helpers +│ ├── auth.py # Password hashing + JWT tokens +│ ├── models/ # 18 model files (SQLAlchemy ORM) +│ ├── schemas/ # Pydantic request/response schemas +│ ├── routers/ # 21 API routers +│ ├── services/ # 20 business logic services +│ ├── dependencies/ # Auth dependencies (get_current_user, require_role) +│ └── jobs/ +│ └── mitre_sync_job.py # APScheduler: 5 background jobs +├── frontend/src/ +│ ├── App.tsx # Routes with lazy loading + role protection +│ ├── api/ # 22 API client modules (Axios + TanStack Query) +│ ├── components/ +│ │ ├── Layout.tsx # Sidebar + header + NotificationBell +│ │ ├── Sidebar.tsx # Role-aware collapsible navigation +│ │ ├── heatmap/ # ATT&CK heatmap (6 components) +│ │ ├── compliance/ # Compliance UI (gauge, controls table) +│ │ └── test-detail/ # Test detail sub-components +│ ├── hooks/ +│ │ └── useDebounce.ts # Debounce hook for search inputs +│ ├── context/ +│ │ └── AuthContext.tsx # Auth state management +│ └── pages/ # 21 page components +└── backend/tests/ + ├── conftest.py # SQLite test DB with JSONB/UUID compatibility + ├── fixtures/ # YAML/TOML/JSON test fixtures + ├── test_data_sources.py # Data source parsing tests + ├── test_scoring_and_compliance.py # Scoring + metrics + compliance tests + ├── test_campaigns_and_snapshots.py # Campaign, snapshot, and retest tests + ├── test_workflow.py # Red/Blue workflow tests + ├── test_templates_crud.py # Template CRUD tests + ├── test_metrics_v2.py # V2 metrics tests + └── test_integration_v2.py # Full integration E2E tests +``` ## Development ### Running Migrations ```bash -docker exec -w /app aegis-backend-1 alembic upgrade head -docker exec -w /app aegis-backend-1 alembic revision --autogenerate -m "description" -docker exec -w /app aegis-backend-1 alembic downgrade -1 +docker exec aegis-backend alembic upgrade head +docker exec aegis-backend alembic revision --autogenerate -m "description" +docker exec aegis-backend alembic downgrade -1 ``` ### Running Tests ```bash -# Run standalone tests (no database required) -cd backend && python tests/test_workflow.py -cd backend && python tests/test_templates_crud.py -cd backend && python tests/test_metrics_v2.py -cd backend && python tests/test_integration_v2.py +# Run all V3 tests inside the container (recommended) +docker exec aegis-backend python -m pytest tests/ -v --tb=short -# Run with pytest (requires PostgreSQL) -docker exec -w /app aegis-backend-1 pytest -v +# Run specific test suites +docker exec aegis-backend python -m pytest tests/test_data_sources.py -v +docker exec aegis-backend python -m pytest tests/test_scoring_and_compliance.py -v +docker exec aegis-backend python -m pytest tests/test_campaigns_and_snapshots.py -v + +# Skip integration tests (require network) +docker exec aegis-backend python -m pytest tests/ -v -m "not integration" ``` +### Generating Reports + +```bash +# Coverage summary (JSON) +GET /api/v1/reports/coverage-summary + +# Coverage CSV export +GET /api/v1/reports/coverage-csv + +# Compliance gap analysis +GET /api/v1/compliance/{framework_id}/gaps +``` + +## Further Documentation + +- **[Architecture](docs/ARCHITECTURE.md)** — Database schema, service layer, state machine diagrams +- **[Data Sources](docs/DATA_SOURCES.md)** — All external data sources with import instructions +- **[Scoring](docs/SCORING.md)** — Scoring system explained with examples and configuration +- **[API Reference](docs/API.md)** — Full endpoint documentation + ## License This project is proprietary software. All rights reserved. diff --git a/backend/alembic/versions/b018_add_performance_indexes.py b/backend/alembic/versions/b018_add_performance_indexes.py new file mode 100644 index 0000000..39b1a5a --- /dev/null +++ b/backend/alembic/versions/b018_add_performance_indexes.py @@ -0,0 +1,78 @@ +"""add_performance_indexes + +Revision ID: b018perfidx +Revises: b017scheduling +Create Date: 2026-02-10 06:00:00.000000 + +""" +from typing import Sequence, Union + +from alembic import op + +# revision identifiers, used by Alembic. +revision: str = "b018perfidx" +down_revision: Union[str, None] = "b017scheduling" +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + # Composite index for detection rules filtered by technique + source + op.create_index( + "ix_detection_rules_technique_source", + "detection_rules", + ["mitre_technique_id", "source"], + ) + + # Composite index for snapshot technique states + op.create_index( + "ix_snapshot_technique_states_snap_tech", + "snapshot_technique_states", + ["snapshot_id", "technique_id"], + unique=True, + ) + + # Covering index for tests frequently filtered by technique + state + op.create_index( + "ix_tests_technique_state", + "tests", + ["technique_id", "state"], + ) + + # Audit logs — timestamp-based lookups + op.create_index( + "ix_audit_logs_timestamp", + "audit_logs", + ["timestamp"], + ) + + # Audit logs — entity lookups + op.create_index( + "ix_audit_logs_entity", + "audit_logs", + ["entity_type", "entity_id"], + ) + + # Test detection results — triggered flag for maturity queries + op.create_index( + "ix_test_detection_results_triggered", + "test_detection_results", + ["triggered"], + ) + + # Compliance control mappings — composite for joins + op.create_index( + "ix_compliance_mappings_control_technique", + "compliance_control_mappings", + ["compliance_control_id", "technique_id"], + ) + + +def downgrade() -> None: + op.drop_index("ix_compliance_mappings_control_technique", table_name="compliance_control_mappings") + op.drop_index("ix_test_detection_results_triggered", table_name="test_detection_results") + op.drop_index("ix_audit_logs_entity", table_name="audit_logs") + op.drop_index("ix_audit_logs_timestamp", table_name="audit_logs") + op.drop_index("ix_tests_technique_state", table_name="tests") + op.drop_index("ix_snapshot_technique_states_snap_tech", table_name="snapshot_technique_states") + op.drop_index("ix_detection_rules_technique_source", table_name="detection_rules") diff --git a/backend/app/routers/operational_metrics.py b/backend/app/routers/operational_metrics.py index 3aaf061..344a1ed 100644 --- a/backend/app/routers/operational_metrics.py +++ b/backend/app/routers/operational_metrics.py @@ -27,8 +27,10 @@ def operational_metrics( db: Session = Depends(get_db), current_user: User = Depends(get_current_user), ): - """Get all operational metrics (MTTD, MTTR, Detection Efficacy, etc.).""" - return get_all_operational_metrics(db) + """Get all operational metrics (MTTD, MTTR, etc.) — cached for 5 min.""" + from app.services.score_cache import get_operational_metrics_cached + + return get_operational_metrics_cached(db) # ── GET /metrics/operational/trend ──────────────────────────────────── diff --git a/backend/app/routers/scores.py b/backend/app/routers/scores.py index d17163d..a755ea9 100644 --- a/backend/app/routers/scores.py +++ b/backend/app/routers/scores.py @@ -93,8 +93,10 @@ def score_organization( db: Session = Depends(get_db), current_user: User = Depends(get_current_user), ): - """Get the overall organization security score.""" - return calculate_organization_score(db) + """Get the overall organization security score (cached for 5 min).""" + from app.services.score_cache import get_organization_score_cached + + return get_organization_score_cached(db) # ── GET /scores/history ────────────────────────────────────────────── @@ -170,6 +172,10 @@ def update_scoring_config( if payload.platform_diversity is not None: settings.SCORING_WEIGHT_PLATFORM_DIVERSITY = payload.platform_diversity + # Weights changed — bust the score cache + from app.services.score_cache import invalidate + invalidate() + return { "message": "Scoring config updated", "weights": { diff --git a/backend/app/services/score_cache.py b/backend/app/services/score_cache.py new file mode 100644 index 0000000..67218f7 --- /dev/null +++ b/backend/app/services/score_cache.py @@ -0,0 +1,84 @@ +"""In-memory TTL cache for expensive scoring and metrics calculations. + +The cache is a simple dict with timestamps. It is invalidated when tests +are validated, scores change, or an explicit ``invalidate`` call is made. + +Thread-safe: each worker process has its own dict, and the TTL ensures +stale data does not persist longer than ``CACHE_TTL`` seconds. +""" + +import time +from typing import Any, Optional + +CACHE_TTL = 300 # 5 minutes + +_cache: dict[str, dict[str, Any]] = {} + + +def get(key: str) -> Optional[Any]: + """Return cached value if present and not expired, else None.""" + entry = _cache.get(key) + if entry is None: + return None + if time.time() - entry["ts"] > CACHE_TTL: + _cache.pop(key, None) + return None + return entry["data"] + + +def put(key: str, data: Any) -> None: + """Store *data* under *key* with the current timestamp.""" + _cache[key] = {"data": data, "ts": time.time()} + + +def invalidate(key: Optional[str] = None) -> None: + """Remove one key or clear the whole cache.""" + if key is None: + _cache.clear() + else: + _cache.pop(key, None) + + +# ── High-level helpers ──────────────────────────────────────────────── + + +def get_organization_score_cached(db): + """Cached wrapper around ``calculate_organization_score``.""" + from app.services.scoring_service import calculate_organization_score + + cached = get("org_score") + if cached is not None: + return cached + + result = calculate_organization_score(db) + put("org_score", result) + return result + + +def get_operational_metrics_cached(db): + """Cached wrapper around operational metrics (MTTD, MTTR, efficacy).""" + from app.services.operational_metrics_service import ( + calculate_mttd, + calculate_mttr, + calculate_detection_efficacy, + calculate_alert_fidelity, + calculate_coverage_velocity, + calculate_validation_throughput, + calculate_rejection_rate, + ) + + cached = get("op_metrics") + if cached is not None: + return cached + + result = { + "mttd": calculate_mttd(db), + "mttr": calculate_mttr(db), + "detection_efficacy": calculate_detection_efficacy(db), + "alert_fidelity": calculate_alert_fidelity(db), + "coverage_velocity": calculate_coverage_velocity(db), + "validation_throughput": calculate_validation_throughput(db), + "rejection_rate": calculate_rejection_rate(db), + } + put("op_metrics", result) + return result diff --git a/backend/app/services/test_workflow_service.py b/backend/app/services/test_workflow_service.py index a6205aa..d76d852 100644 --- a/backend/app/services/test_workflow_service.py +++ b/backend/app/services/test_workflow_service.py @@ -288,6 +288,12 @@ def check_dual_validation(db: Session, test: Test) -> Test: elif red_status == "approved" and blue_status == "approved": test.state = TestState.validated db.commit() + # Invalidate cached scores — a validation changes org-level numbers + try: + from app.services.score_cache import invalidate + invalidate() + except Exception: + pass try: notify_test_state_change(db, test, "validated") except Exception: diff --git a/docs/API.md b/docs/API.md index 0c22a32..275784c 100644 --- a/docs/API.md +++ b/docs/API.md @@ -604,3 +604,101 @@ Common HTTP status codes: - `404` - Not Found (resource doesn't exist) - `409` - Conflict (duplicate resource) - `500` - Internal Server Error + +--- + +## V3 Endpoints + +### Campaigns + +| Method | Route | Auth | Description | +|--------|-------|------|-------------| +| GET | `/api/v1/campaigns` | Authenticated | List campaigns (filters: status, type, search; pagination: offset, limit) | +| POST | `/api/v1/campaigns` | Authenticated | Create campaign | +| GET | `/api/v1/campaigns/{id}` | Authenticated | Campaign detail with tests | +| PATCH | `/api/v1/campaigns/{id}` | Creator, Admin | Update campaign | +| DELETE | `/api/v1/campaigns/{id}` | Creator, Admin | Delete campaign | +| POST | `/api/v1/campaigns/{id}/tests` | Authenticated | Add test to campaign | +| DELETE | `/api/v1/campaigns/{id}/tests/{test_id}` | Authenticated | Remove test from campaign | +| PATCH | `/api/v1/campaigns/{id}/schedule` | Authenticated | Set recurring schedule | +| GET | `/api/v1/campaigns/{id}/history` | Authenticated | Execution history for recurring campaigns | + +### Threat Actors + +| Method | Route | Auth | Description | +|--------|-------|------|-------------| +| GET | `/api/v1/threat-actors` | Authenticated | List threat actors (filters: country, motivation, search) | +| GET | `/api/v1/threat-actors/{id}` | Authenticated | Detail with technique mappings | + +### Detection Rules + +| Method | Route | Auth | Description | +|--------|-------|------|-------------| +| GET | `/api/v1/detection-rules` | Authenticated | List rules (filters: source, mitre_technique_id, severity, search) | +| GET | `/api/v1/detection-rules/{id}` | Authenticated | Rule detail | + +### D3FEND (Defensive Techniques) + +| Method | Route | Auth | Description | +|--------|-------|------|-------------| +| GET | `/api/v1/d3fend` | Authenticated | List defensive techniques | +| GET | `/api/v1/d3fend/{id}` | Authenticated | Detail with ATT&CK mappings | + +### Compliance + +| Method | Route | Auth | Description | +|--------|-------|------|-------------| +| GET | `/api/v1/compliance/frameworks` | Authenticated | List compliance frameworks | +| GET | `/api/v1/compliance/{framework_id}/controls` | Authenticated | List controls with coverage status | +| GET | `/api/v1/compliance/{framework_id}/summary` | Authenticated | Coverage summary (total, covered, gaps) | +| GET | `/api/v1/compliance/{framework_id}/gaps` | Authenticated | Gap analysis — uncovered controls | + +### Scores + +| Method | Route | Auth | Description | +|--------|-------|------|-------------| +| GET | `/api/v1/scores/technique/{mitre_id}` | Authenticated | Technique score with detailed breakdown | +| GET | `/api/v1/scores/tactic/{tactic}` | Authenticated | Average tactic score | +| GET | `/api/v1/scores/threat-actor/{id}` | Authenticated | Coverage score against threat actor | +| GET | `/api/v1/scores/organization` | Authenticated | Overall organization score (cached 5 min) | +| GET | `/api/v1/scores/history` | Authenticated | Weekly score history (period: 30d, 90d, 1y) | +| GET | `/api/v1/scores/config` | Admin | Current scoring weights | +| PATCH | `/api/v1/scores/config` | Admin | Update scoring weights | + +### Operational Metrics + +| Method | Route | Auth | Description | +|--------|-------|------|-------------| +| GET | `/api/v1/metrics/operational` | Authenticated | All KPIs (MTTD, MTTR, efficacy, etc.) — cached 5 min | +| GET | `/api/v1/metrics/operational/trend` | Authenticated | Weekly trend (period: 30d, 90d, 1y) | +| GET | `/api/v1/metrics/operational/by-team` | Authenticated | Red vs Blue team breakdown | + +### Heatmap + +| Method | Route | Auth | Description | +|--------|-------|------|-------------| +| GET | `/api/v1/heatmap` | Authenticated | Full ATT&CK Navigator-style heatmap data | + +### Coverage Snapshots + +| Method | Route | Auth | Description | +|--------|-------|------|-------------| +| GET | `/api/v1/snapshots` | Authenticated | List snapshots (pagination: offset, limit) | +| POST | `/api/v1/snapshots` | Authenticated | Create new snapshot | +| GET | `/api/v1/snapshots/{id}` | Authenticated | Snapshot detail with technique states | +| DELETE | `/api/v1/snapshots/{id}` | Admin | Delete snapshot | +| GET | `/api/v1/snapshots/compare` | Authenticated | Compare two snapshots (query: a, b) | + +### Re-testing + +| Method | Route | Auth | Description | +|--------|-------|------|-------------| +| GET | `/api/v1/tests/{id}/retest-chain` | Authenticated | Full retest chain for a test | + +### Data Sources + +| Method | Route | Auth | Description | +|--------|-------|------|-------------| +| GET | `/api/v1/data-sources` | Admin | List all data sources | +| PATCH | `/api/v1/data-sources/{id}` | Admin | Update source config | +| POST | `/api/v1/data-sources/{id}/sync` | Admin | Trigger manual sync | diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md new file mode 100644 index 0000000..b2d6211 --- /dev/null +++ b/docs/ARCHITECTURE.md @@ -0,0 +1,220 @@ +# Aegis — Architecture + +## High-Level Overview + +``` +┌────────────────────┐ ┌─────────────────────┐ +│ React Frontend │──────▶│ FastAPI Backend │ +│ (Vite / TS / TW) │ REST │ (Python 3.11) │ +└────────────────────┘ └──────┬──────┬────────┘ + │ │ + ┌─────────┘ └─────────┐ + ▼ ▼ + ┌─────────────────┐ ┌─────────────────┐ + │ PostgreSQL │ │ MinIO │ + │ (Data Store) │ │ (Object Storage) │ + └─────────────────┘ └─────────────────┘ +``` + +- **Frontend** — React 19 + TypeScript + Tailwind CSS v4 + TanStack Query +- **Backend** — FastAPI with SQLAlchemy ORM + Alembic migrations +- **Database** — PostgreSQL 15 with UUID primary keys and JSONB columns +- **Object Storage** — MinIO (S3-compatible) for evidence files +- **Scheduler** — APScheduler (in-process) for background jobs + +--- + +## Database Schema + +### Core Tables + +| Table | Description | +|-------|-------------| +| `users` | User accounts with role-based access (admin, red_tech, blue_tech, red_lead, blue_lead, viewer) | +| `techniques` | MITRE ATT&CK techniques with coverage status, tactic, platforms (JSONB) | +| `tests` | Security tests with full Red/Blue workflow fields, dual validation, remediation, and retest chain | +| `test_templates` | Predefined test catalog from Atomic Red Team, Sigma, CALDERA, LOLBAS, custom | +| `evidences` | Evidence files separated by team (red/blue) with SHA256 integrity verification | + +### Detection & Defense + +| Table | Description | +|-------|-------------| +| `detection_rules` | Imported detection rules (Sigma, Elastic, custom) linked to ATT&CK techniques | +| `test_detection_results` | Per-test detection rule evaluation results (triggered / not triggered) | +| `test_template_detection_rules` | Template ↔ detection rule associations | +| `defensive_techniques` | MITRE D3FEND defensive techniques | +| `defensive_technique_mappings` | ATT&CK technique ↔ D3FEND defensive technique mappings | + +### Campaigns & Scheduling + +| Table | Description | +|-------|-------------| +| `campaigns` | Test campaign groupings with scheduling (recurring, weekly/monthly/quarterly) | +| `campaign_tests` | Ordered test assignments within campaigns with dependency support | + +### Intelligence & Actors + +| Table | Description | +|-------|-------------| +| `threat_actors` | MITRE CTI intrusion sets with aliases, country, motivation, JSONB targets | +| `threat_actor_techniques` | Threat actor ↔ ATT&CK technique mappings | +| `intel_items` | Threat intelligence items from RSS feeds | + +### Compliance + +| Table | Description | +|-------|-------------| +| `compliance_frameworks` | Compliance frameworks (e.g., NIST 800-53) | +| `compliance_controls` | Individual controls within a framework | +| `compliance_control_mappings` | Control ↔ ATT&CK technique mappings | + +### Operational + +| Table | Description | +|-------|-------------| +| `coverage_snapshots` | Point-in-time coverage status captures with aggregate metrics | +| `snapshot_technique_states` | Normalized per-technique state within a snapshot | +| `audit_logs` | System-wide audit trail with JSONB details | +| `notifications` | In-app notifications with read status | +| `data_sources` | External data source configuration and sync status | + +### Key Relationships + +``` +Technique ──1:N── Test ──1:N── Evidence + │ │ + │ ├── TestDetectionResult ──N:1── DetectionRule + │ └── CampaignTest ──N:1── Campaign + │ + ├── ThreatActorTechnique ──N:1── ThreatActor + ├── DefensiveTechniqueMapping ──N:1── DefensiveTechnique + ├── ComplianceControlMapping ──N:1── ComplianceControl ──N:1── ComplianceFramework + └── SnapshotTechniqueState ──N:1── CoverageSnapshot + +Test ──retest_of──▶ Test (self-referential retest chain) +Campaign ──parent_campaign_id──▶ Campaign (recurring execution history) +``` + +--- + +## Backend Architecture + +### Layered Structure + +``` +routers/ ← HTTP endpoints (input validation, auth, response shaping) + ↓ +services/ ← Business logic (state machines, calculations, imports) + ↓ +models/ ← SQLAlchemy ORM models + ↓ +database.py ← Engine + session management (lazy initialization) +``` + +### Services + +| Service | Responsibility | +|---------|---------------| +| `test_workflow_service` | Test state machine (draft → validated/rejected) with dual validation | +| `scoring_service` | 0–100 scoring for techniques, tactics, actors, organization | +| `score_cache` | In-memory TTL cache (5 min) for expensive score/metric calculations | +| `operational_metrics_service` | MTTD, MTTR, detection efficacy, alert fidelity, coverage velocity | +| `snapshot_service` | Coverage snapshot creation, temporal comparison, cleanup | +| `campaign_service` | Campaign CRUD, progress tracking, circular dependency prevention | +| `campaign_scheduler_service` | Recurring campaign execution (clone + schedule next run) | +| `status_service` | Technique status recalculation from test results | +| `notification_service` | In-app notification CRUD and state-change alerts | +| `audit_service` | Immutable audit trail logging | +| `mitre_sync_service` | MITRE ATT&CK sync via TAXII 2.0 / GitHub fallback | +| `atomic_import_service` | Atomic Red Team template import from GitHub | +| `sigma_import_service` | SigmaHQ detection rule import | +| `elastic_import_service` | Elastic detection rule import (TOML) | +| `caldera_import_service` | CALDERA ability import | +| `lolbas_import_service` | LOLBAS/GTFOBins template import | +| `d3fend_import_service` | MITRE D3FEND defensive technique import | +| `threat_actor_import_service` | MITRE CTI threat actor import (STIX) | +| `compliance_import_service` | NIST 800-53 ↔ ATT&CK mapping import | +| `intel_service` | RSS-based threat intelligence scanning | + +### Scheduled Jobs (APScheduler) + +| Job | Schedule | Description | +|-----|----------|-------------| +| MITRE Sync | Every 24h | Sync ATT&CK techniques from TAXII/GitHub | +| Intel Scan | Every 7 days | Scan RSS feeds for threat intelligence | +| Notification Cleanup | Every 24h | Remove old read notifications | +| Weekly Snapshot | Sundays 00:00 | Create coverage snapshot + cleanup old ones | +| Recurring Campaigns | Every 24h | Check and execute due recurring campaigns | + +--- + +## Test Lifecycle (State Machine) + +``` +┌──────┐ ┌──────────────┐ ┌─────────────────┐ ┌───────────┐ +│ DRAFT│───▶│RED_EXECUTING │───▶│ BLUE_EVALUATING │───▶│ IN_REVIEW │ +└──────┘ └──────────────┘ └─────────────────┘ └─────┬─────┘ + │ + ┌───────────────────┤ + ▼ ▼ + ┌──────────┐ ┌──────────┐ + │ REJECTED │ │VALIDATED │ + └────┬─────┘ └──────────┘ + │ │ + └──▶ Back to DRAFT ├──▶ Remediation + └──▶ Auto Re-test +``` + +**Dual Validation in IN_REVIEW:** +- Red Lead votes approve/reject +- Blue Lead votes approve/reject +- Both approve → VALIDATED +- Either rejects → REJECTED +- One votes, other pending → stays IN_REVIEW + +**Auto Re-testing:** When remediation is completed on a validated test, the system automatically creates a follow-up retest (up to `MAX_RETEST_COUNT` = 3). + +--- + +## Frontend Architecture + +### Key Technologies + +- **React 19** with TypeScript +- **Vite 7** for bundling +- **Tailwind CSS v4** for styling +- **TanStack Query** for server state management +- **TanStack Virtual** for table virtualization +- **React Router v7** for routing +- **Recharts** for charts and visualizations +- **Lucide React** for icons + +### Page Lazy Loading + +All pages except `LoginPage` and `DashboardPage` are lazy-loaded via `React.lazy()` with `` fallbacks for optimal initial bundle size. + +### Role-Based Navigation + +The sidebar dynamically filters navigation items based on the current user's role: + +| Section | Visible to | +|---------|-----------| +| Dashboard | All roles | +| Executive Dashboard | admin, red_lead, blue_lead | +| ATT&CK Matrix | All roles | +| Tests (sub-menu) | All roles | +| Campaigns | All roles | +| Threat Actors | All roles | +| Compliance | All roles | +| Comparison | admin, red_lead, blue_lead | +| Reports | All roles | +| System (admin section) | admin only | + +### Performance Optimizations + +- **React.memo** on `HeatmapCell` (renders 3000+ times in full matrix) +- **useMemo** / **useCallback** for expensive calculations in memoized components +- **useDebounce** hook for search inputs (300ms delay) +- **TanStack Virtual** for large table virtualization (test templates, detection rules, audit logs) +- **Lazy loading** for all non-critical page bundles diff --git a/docs/DATA_SOURCES.md b/docs/DATA_SOURCES.md new file mode 100644 index 0000000..ef5a2a7 --- /dev/null +++ b/docs/DATA_SOURCES.md @@ -0,0 +1,281 @@ +# Aegis — Data Sources + +Aegis imports security data from multiple external sources to populate its test catalog, detection rules, defensive techniques, threat actors, and compliance mappings. This document describes each source, its format, and how to manage imports. + +--- + +## Overview + +| Source | Type | Format | Destination | +|--------|------|--------|-------------| +| MITRE ATT&CK | Techniques | STIX 2.0 (TAXII / GitHub) | `techniques` | +| Atomic Red Team | Test Templates | YAML | `test_templates` | +| SigmaHQ | Detection Rules | YAML | `detection_rules` | +| Elastic Detection Rules | Detection Rules | TOML | `detection_rules` | +| CALDERA | Test Templates | YAML (multi-doc) | `test_templates` | +| LOLBAS | Test Templates | YAML | `test_templates` | +| MITRE D3FEND | Defensive Techniques | JSON-LD | `defensive_techniques` + mappings | +| MITRE CTI | Threat Actors | STIX 2.0 (JSON) | `threat_actors` + technique mappings | +| NIST 800-53 → ATT&CK | Compliance Mappings | STIX 2.0 (JSON) | `compliance_*` tables | + +--- + +## MITRE ATT&CK (Techniques) + +**Repository:** https://github.com/mitre/cti (Enterprise ATT&CK) +**Protocol:** TAXII 2.0 with GitHub JSON fallback +**Format:** STIX 2.0 bundles +**Service:** `mitre_sync_service.py` +**Schedule:** Automatic every 24 hours via APScheduler + +**Extracted fields:** +- `mitre_id` — External ID (e.g., T1059, T1059.001) +- `name` — Technique name +- `description` — Full description +- `tactic` — ATT&CK tactic (execution, persistence, etc.) +- `platforms` — Target platforms (windows, linux, macos, etc.) +- `is_subtechnique` — Whether it's a sub-technique +- `url` — Link to MITRE page + +**Manual trigger:** +```bash +# Via API +curl -X POST http://localhost:8000/api/v1/system/sync-mitre \ + -H "Authorization: Bearer $TOKEN" + +# Via container +docker exec aegis-backend python -c "from app.services.mitre_sync_service import sync_mitre_attack; sync_mitre_attack()" +``` + +**Volume:** ~700 techniques (Enterprise ATT&CK v16) + +**Troubleshooting:** +- If TAXII fails (timeout/rate limit), the service automatically falls back to the GitHub JSON bundle +- Check `data_sources` table for `last_sync_at` and `last_sync_stats` + +--- + +## Atomic Red Team (Test Templates) + +**Repository:** https://github.com/redcanaryco/atomic-red-team +**Format:** YAML (one file per technique under `atomics/T*/T*.yaml`) +**Service:** `atomic_import_service.py` + +**Extracted fields:** +- `name` — Test name +- `description` — What the test does +- `mitre_technique_id` — ATT&CK technique ID +- `attack_commands` — Commands to execute +- `expected_detection` — What should be detected +- `platform` — Target OS +- `severity` — Derived from technique context +- `cleanup_commands` — Cleanup procedures + +**Import:** +```bash +curl -X POST http://localhost:8000/api/v1/system/import-atomic-red-team \ + -H "Authorization: Bearer $TOKEN" +``` + +**Volume:** ~3,500 test templates +**Frequency:** Monthly or after ATT&CK version updates + +--- + +## SigmaHQ (Detection Rules) + +**Repository:** https://github.com/SigmaHQ/sigma +**Format:** YAML (Sigma rule format) +**Service:** `sigma_import_service.py` + +**Extracted fields:** +- `name` — Rule title +- `description` — Rule description +- `query` — Sigma detection logic +- `mitre_technique_id` — Extracted from `tags: attack.tXXXX` +- `severity` — From `level` field (low, medium, high, critical) +- `platforms` — From `logsource.product` +- `source` = `"sigma"` + +**Example Sigma rule tags:** +```yaml +tags: + - attack.execution + - attack.t1059.001 + - attack.defense_evasion + - attack.t1562.001 +``` + +**Volume:** ~3,000 detection rules +**Frequency:** Monthly recommended + +**Troubleshooting:** +- Rules without MITRE technique tags in `attack.tXXXX` format are skipped +- Duplicate detection is by `name` + `source` + `mitre_technique_id` + +--- + +## Elastic Detection Rules + +**Repository:** https://github.com/elastic/detection-rules +**Format:** TOML (one file per rule under `rules/`) +**Service:** `elastic_import_service.py` + +**Extracted fields:** +- `name` — Rule name from `[rule]` +- `description` — Rule description +- `query` — KQL/EQL query +- `mitre_technique_id` — From `[[rule.threat]]` entries +- `severity` — From `rule.severity` +- `rule_type` — eql, query, threshold, etc. +- `source` = `"elastic"` + +**TOML structure:** +```toml +[rule] +name = "Scheduled Task Created via Schtasks" +severity = "medium" +type = "eql" + +[[rule.threat]] +framework = "MITRE ATT&CK" +[[rule.threat.technique]] +id = "T1053" +name = "Scheduled Task/Job" +[[rule.threat.technique.subtechnique]] +id = "T1053.005" +``` + +**Volume:** ~1,200 detection rules +**Frequency:** Quarterly recommended + +--- + +## CALDERA (Test Templates) + +**Repository:** https://github.com/mitre/caldera +**Format:** YAML (multi-document, abilities under `data/abilities/`) +**Service:** `caldera_import_service.py` + +**Extracted fields:** +- `name` — Ability name +- `description` — What the ability does +- `mitre_technique_id` — From `technique.attack_id` +- `tactic` — ATT&CK tactic +- `platforms` — Target platforms +- `attack_commands` — Commands per platform/executor + +**Volume:** ~500 abilities +**Frequency:** Quarterly recommended + +--- + +## LOLBAS (Test Templates) + +**Repository:** https://github.com/LOLBAS-Project/LOLBAS +**Format:** YAML (one file per binary under `yml/OSBinaries/`, `yml/OtherMSBinaries/`, etc.) +**Service:** `lolbas_import_service.py` + +**Extracted fields:** +- `name` — Binary name (e.g., Mshta.exe) +- `mitre_technique_id` — From `Commands[].MitreID` +- `attack_commands` — From `Commands[].Command` +- `description` — From `Commands[].Description` +- `usecase` — From `Commands[].Usecase` + +**Volume:** ~200 living-off-the-land binaries with ~500 commands +**Frequency:** Quarterly recommended + +--- + +## MITRE D3FEND (Defensive Techniques) + +**Repository:** https://d3fend.mitre.org/ +**Format:** JSON-LD (REST API at `https://d3fend.mitre.org/api/`) +**Service:** `d3fend_import_service.py` + +**Extracted fields:** +- `d3fend_id` — D3FEND identifier (e.g., D3-AL, D3-NI) +- `name` — Defensive technique name +- `description` — Definition or comment +- `tactic` — Defensive tactic (Detect, Isolate, Deceive, Evict, Harden) +- ATT&CK ↔ D3FEND mappings stored in `defensive_technique_mappings` + +**Volume:** ~200 defensive techniques +**Frequency:** Annually (D3FEND updates are infrequent) + +--- + +## MITRE CTI — Threat Actors + +**Repository:** https://github.com/mitre/cti (enterprise-attack) +**Format:** STIX 2.0 JSON bundles (`intrusion-set`, `relationship`, `attack-pattern`) +**Service:** `threat_actor_import_service.py` + +**Extracted fields:** +- `name` — Actor name (e.g., APT28) +- `mitre_id` — MITRE group ID (e.g., G0007) +- `aliases` — Known aliases (JSONB array) +- `description` — Full description +- `country` — Attribution (when available) +- `motivation` — Espionage, financial, etc. +- `target_sectors` / `target_regions` — JSONB arrays +- Technique mappings via `relationship` objects + +**Volume:** ~140 threat actors with ~2,000 technique mappings +**Frequency:** Quarterly recommended + +--- + +## NIST 800-53 → ATT&CK (Compliance) + +**Repository:** https://github.com/center-for-threat-informed-defense/attack-control-framework-mappings +**Format:** STIX 2.0 JSON bundles +**Service:** `compliance_import_service.py` + +**Extracted fields:** +- `ComplianceFramework` — Framework name and version +- `ComplianceControl` — Control ID (e.g., AC-2), title, category +- `ComplianceControlMapping` — Control ↔ ATT&CK technique associations + +**Volume:** ~1,000 controls with ~5,000 mappings +**Frequency:** Annually (mappings are versioned with the framework) + +--- + +## Managing Data Sources + +### Admin UI + +Navigate to **System → Data Sources** in the Aegis frontend to: +- View all configured data sources and their sync status +- Trigger manual imports +- Enable/disable individual sources +- View import statistics (imported, updated, errors) + +### API Endpoints + +```bash +# List data sources +GET /api/v1/data-sources + +# Trigger import for a specific source +POST /api/v1/data-sources/{id}/sync + +# Enable/disable a source +PATCH /api/v1/data-sources/{id} +``` + +### Recommended Update Schedule + +| Source | Frequency | Reason | +|--------|-----------|--------| +| MITRE ATT&CK | Automatic (24h) | Core framework, frequent updates | +| Atomic Red Team | Monthly | Active community contributions | +| SigmaHQ | Monthly | Active community contributions | +| Elastic Rules | Quarterly | Major version-aligned releases | +| CALDERA | Quarterly | Less frequent updates | +| LOLBAS | Quarterly | Less frequent updates | +| D3FEND | Annually | Infrequent updates | +| CTI Actors | Quarterly | New groups and campaigns | +| NIST 800-53 | Annually | Framework revision cycles | diff --git a/docs/SCORING.md b/docs/SCORING.md new file mode 100644 index 0000000..08b9a59 --- /dev/null +++ b/docs/SCORING.md @@ -0,0 +1,285 @@ +# Aegis — Scoring System + +Aegis uses a granular 0–100 scoring system to measure security coverage at multiple levels: individual techniques, tactics, threat actors, and the overall organization. + +--- + +## Technique Score (0–100) + +Each ATT&CK technique receives a composite score based on five weighted components: + +| Component | Default Weight | Description | +|-----------|---------------|-------------| +| Tests Validated | 40% | Ratio of detected tests to total validated tests | +| Detection Rules | 20% | Number of active detection rules linked to the technique | +| D3FEND Coverage | 15% | Number of D3FEND defensive techniques mapped | +| Freshness | 15% | How recent the latest validated test is | +| Platform Diversity | 10% | Coverage across different platforms (Windows, Linux, macOS) | + +### Tests Validated Component + +``` +score = (detected_tests / total_validated_tests) × weight +``` + +- Only tests in `validated` state are counted +- `detected` means `detection_result = "detected"` +- Example: 2 detected out of 3 validated → `2/3 × 40 = 26.7` + +### Detection Rules Component + +``` +score = min(active_rules / 3, 1.0) × weight +``` + +- Counts active detection rules linked to the technique's `mitre_id` +- 3+ rules gives full marks (capped at 1.0) +- Example: 2 active rules → `2/3 × 20 = 13.3` + +### D3FEND Coverage Component + +``` +score = min(d3fend_mappings / 2, 1.0) × weight +``` + +- Counts D3FEND defensive technique mappings +- 2+ mappings gives full marks +- Example: 1 mapping → `1/2 × 15 = 7.5` + +### Freshness Component + +``` +days = (now - newest_validated_test.red_validated_at).days +score = max(0, 1.0 - days / 180) × weight +``` + +- 0 days old = full freshness score +- 180+ days old = 0 (completely stale) +- Linear decay between 0 and 180 days +- Example: test is 60 days old → `(1 - 60/180) × 15 = 10.0` + +### Platform Diversity Component + +``` +platforms_covered = unique platforms across validated tests +score = min(platforms_covered / 3, 1.0) × weight +``` + +- Counts unique platforms (windows, linux, macos) from validated tests +- 3+ platforms gives full marks +- Example: windows + linux → `2/3 × 10 = 6.7` + +### Example Calculation + +A technique with: +- 2/3 tests detected, 2 detection rules, 1 D3FEND mapping, 60 days old, 2 platforms + +``` +Tests: (2/3) × 40 = 26.7 +Detection: (2/3) × 20 = 13.3 +D3FEND: (1/2) × 15 = 7.5 +Freshness: (1 - 60/180) × 15 = 10.0 +Platform: (2/3) × 10 = 6.7 + ───── +Total: 64.2 +``` + +--- + +## Configuring Weights + +Weights are configurable via environment variables or the admin API. They must sum to 100. + +### Environment Variables + +```env +SCORING_WEIGHT_TESTS=40 +SCORING_WEIGHT_DETECTION_RULES=20 +SCORING_WEIGHT_D3FEND=15 +SCORING_WEIGHT_FRESHNESS=15 +SCORING_WEIGHT_PLATFORM_DIVERSITY=10 +``` + +### API Configuration + +```bash +# Get current weights +GET /api/v1/scores/config + +# Update weights (admin only) +PATCH /api/v1/scores/config +{ + "tests": 50, + "detection_rules": 20, + "d3fend": 10, + "freshness": 10, + "platform_diversity": 10 +} +``` + +Note: Runtime changes do not persist across restarts. Update the `.env` file or environment variables for permanent changes. + +--- + +## Tactic Score + +The tactic score is the **average** of all technique scores within that tactic: + +``` +tactic_score = mean(technique_scores for techniques in tactic) +``` + +Also provides: +- `techniques_total` — number of techniques in the tactic +- `techniques_evaluated` — techniques with score > 0 +- `techniques_by_status` — count by status (validated, partial, not_covered, not_evaluated) + +### API + +```bash +GET /api/v1/scores/tactic/execution +GET /api/v1/scores/tactic/persistence +``` + +--- + +## Threat Actor Coverage Score + +Measures how well the organization is covered against a specific threat actor: + +``` +actor_score = mean(technique_scores for techniques used by actor) +``` + +Also provides: +- `techniques_total` — techniques attributed to the actor +- `techniques_covered` — techniques with score > 0 +- `coverage_percentage` — percentage of techniques covered +- `uncovered_techniques` — list of technique IDs with score = 0 + +### API + +```bash +GET /api/v1/scores/threat-actor/{actor_id} +``` + +--- + +## Organization Score + +The top-level organizational security score is a weighted average of four sub-scores: + +| Sub-score | Weight | Description | +|-----------|--------|-------------| +| Total Coverage | 40% | Average technique score across all evaluated techniques | +| Critical Coverage | 25% | Average score for techniques with high/critical severity templates | +| Detection Maturity | 20% | `(triggered_rules / total_active_rules) × 100` | +| Response Readiness | 15% | `(remediation_completed / remediation_total) × 100` | + +``` +org_score = total_coverage × 0.4 + + critical_coverage × 0.25 + + detection_maturity × 0.2 + + response_readiness × 0.15 +``` + +### Caching + +The organization score is cached in-memory for 5 minutes. The cache is automatically invalidated when: +- A test is validated (state → `validated`) +- Scoring weights are updated via the API + +### API + +```bash +GET /api/v1/scores/organization +``` + +--- + +## Operational Metrics + +In addition to coverage scores, Aegis tracks operational KPIs: + +### Mean Time to Detect (MTTD) + +Time from test execution start (`start_execution` audit entry) to red team submission (`submit_red`). + +``` +MTTD = mean(submit_red.timestamp - start_execution.timestamp) for all tests +``` + +### Mean Time to Respond (MTTR) + +Time from blue team evaluation (`blue_validated_at`) to remediation completion (`update_remediation` audit entry). + +``` +MTTR = mean(update_remediation.timestamp - blue_validated_at) for remediated tests +``` + +### Detection Efficacy + +``` +efficacy = (detected_tests / total_validated_tests) × 100 +``` + +### Alert Fidelity + +Ratio of true positive detections to total detection rule evaluations. + +### Coverage Velocity + +Rate at which new techniques are being covered over time (techniques covered per week). + +### Validation Throughput + +Number of tests moving through the pipeline per time period. + +### Rejection Rate + +Percentage of tests rejected during dual validation. + +### API + +```bash +# All operational metrics +GET /api/v1/metrics/operational + +# Weekly trend data +GET /api/v1/metrics/operational/trend?period=90d + +# Breakdown by team +GET /api/v1/metrics/operational/by-team +``` + +--- + +## Score History + +Weekly score snapshots for trend analysis: + +```bash +GET /api/v1/scores/history?period=90d +# Returns weekly data points with: date, overall_score, total_coverage, +# critical_coverage, detection_maturity, response_readiness +``` + +Periods: `30d`, `90d`, `1y` + +--- + +## Coverage Snapshots + +Point-in-time captures of the complete coverage state for historical comparison: + +```bash +# Create a snapshot +POST /api/v1/snapshots +{ "name": "Q1 2026 Baseline" } + +# Compare two snapshots +GET /api/v1/snapshots/compare?a={snapshot_id_a}&b={snapshot_id_b} +# Returns: score_delta, improved techniques, worsened techniques, unchanged count +``` + +Automatic weekly snapshots are created every Sunday at 00:00 by the scheduler, with old snapshots cleaned up to keep the last 52 (one year). diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index e41c525..0d72f77 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -1,28 +1,34 @@ +import React, { Suspense } from "react"; import { Routes, Route, Navigate } from "react-router-dom"; -import LoginPage from "./pages/LoginPage"; -import DashboardPage from "./pages/DashboardPage"; -import TechniquesPage from "./pages/TechniquesPage"; -import MatrixPage from "./pages/MatrixPage"; -import ExecutiveDashboardPage from "./pages/ExecutiveDashboardPage"; -import CompliancePage from "./pages/CompliancePage"; -import TechniqueDetailPage from "./pages/TechniqueDetailPage"; -import TestsPage from "./pages/TestsPage"; -import TestCreatePage from "./pages/TestCreatePage"; -import TestDetailPage from "./pages/TestDetailPage"; -import TestCatalogPage from "./pages/TestCatalogPage"; -import ReportsPage from "./pages/ReportsPage"; -import SystemPage from "./pages/SystemPage"; -import UsersPage from "./pages/UsersPage"; -import AuditLogPage from "./pages/AuditLogPage"; -import DataSourcesPage from "./pages/DataSourcesPage"; -import ThreatActorsPage from "./pages/ThreatActorsPage"; -import ThreatActorDetailPage from "./pages/ThreatActorDetailPage"; -import CampaignsPage from "./pages/CampaignsPage"; -import CampaignDetailPage from "./pages/CampaignDetailPage"; -import ComparisonPage from "./pages/ComparisonPage"; +import LoadingSpinner from "./components/LoadingSpinner"; import Layout from "./components/Layout"; import ProtectedRoute from "./components/ProtectedRoute"; +/* ── Eagerly loaded (core pages) ──────────────────────────────────── */ +import LoginPage from "./pages/LoginPage"; +import DashboardPage from "./pages/DashboardPage"; + +/* ── Lazy loaded (V1-V3 pages) ────────────────────────────────────── */ +const TechniquesPage = React.lazy(() => import("./pages/TechniquesPage")); +const MatrixPage = React.lazy(() => import("./pages/MatrixPage")); +const ExecutiveDashboardPage = React.lazy(() => import("./pages/ExecutiveDashboardPage")); +const CompliancePage = React.lazy(() => import("./pages/CompliancePage")); +const TechniqueDetailPage = React.lazy(() => import("./pages/TechniqueDetailPage")); +const TestsPage = React.lazy(() => import("./pages/TestsPage")); +const TestCreatePage = React.lazy(() => import("./pages/TestCreatePage")); +const TestDetailPage = React.lazy(() => import("./pages/TestDetailPage")); +const TestCatalogPage = React.lazy(() => import("./pages/TestCatalogPage")); +const ReportsPage = React.lazy(() => import("./pages/ReportsPage")); +const SystemPage = React.lazy(() => import("./pages/SystemPage")); +const UsersPage = React.lazy(() => import("./pages/UsersPage")); +const AuditLogPage = React.lazy(() => import("./pages/AuditLogPage")); +const DataSourcesPage = React.lazy(() => import("./pages/DataSourcesPage")); +const ThreatActorsPage = React.lazy(() => import("./pages/ThreatActorsPage")); +const ThreatActorDetailPage = React.lazy(() => import("./pages/ThreatActorDetailPage")); +const CampaignsPage = React.lazy(() => import("./pages/CampaignsPage")); +const CampaignDetailPage = React.lazy(() => import("./pages/CampaignDetailPage")); +const ComparisonPage = React.lazy(() => import("./pages/ComparisonPage")); + export default function App() { return ( @@ -37,35 +43,61 @@ export default function App() { } > + {/* ── Core ─────────────────────────────────────────────── */} } /> - } /> - } /> + + }>} /> + }>} /> + + }>} /> + + {/* ── Executive Dashboard (leads + admin) ──────────────── */} - + }> } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> + + {/* ── Tests ────────────────────────────────────────────── */} + }>} /> + }>} /> + }>} /> + }>} /> + }>} /> + + {/* ── Campaigns ────────────────────────────────────────── */} + }>} /> + }>} /> + + {/* ── Threat Actors ────────────────────────────────────── */} + }>} /> + }>} /> + + {/* ── Compliance ───────────────────────────────────────── */} + }>} /> + + {/* ── Comparison (leads + admin) ───────────────────────── */} + + }> + + } + /> + + {/* ── Reports ──────────────────────────────────────────── */} + }>} /> + + {/* ── System (admin only) ──────────────────────────────── */} - + }> } /> @@ -73,7 +105,7 @@ export default function App() { path="/users" element={ - + }> } /> @@ -81,7 +113,7 @@ export default function App() { path="/audit" element={ - + }> } /> @@ -89,7 +121,7 @@ export default function App() { path="/data-sources" element={ - + }> } /> diff --git a/frontend/src/components/Sidebar.tsx b/frontend/src/components/Sidebar.tsx index d5aee29..16d841e 100644 --- a/frontend/src/components/Sidebar.tsx +++ b/frontend/src/components/Sidebar.tsx @@ -19,6 +19,7 @@ import { Gauge, ShieldCheck, GitCompareArrows, + ScrollText, } from "lucide-react"; import { useAuth } from "../context/AuthContext"; @@ -26,13 +27,15 @@ interface NavItem { to: string; label: string; icon: React.FC<{ className?: string }>; + /** Roles allowed to see this item. undefined = everyone. */ + roles?: string[]; children?: NavItem[]; } const mainLinks: NavItem[] = [ { to: "/dashboard", label: "Dashboard", icon: LayoutDashboard }, - { to: "/techniques", label: "ATT&CK Matrix", icon: Shield }, - { to: "/matrix", label: "Advanced Heatmap", icon: Grid3X3 }, + { to: "/executive-dashboard", label: "Executive Dashboard", icon: Gauge, roles: ["admin", "red_lead", "blue_lead"] }, + { to: "/matrix", label: "ATT&CK Matrix", icon: Grid3X3 }, { to: "/tests", label: "Tests", @@ -43,19 +46,18 @@ const mainLinks: NavItem[] = [ { to: "/test-catalog", label: "Test Catalog", icon: BookOpen }, ], }, - { to: "/executive-dashboard", label: "Executive Dashboard", icon: Gauge }, - { to: "/reports", label: "Reports", icon: BarChart3 }, - { to: "/threat-actors", label: "Threat Actors", icon: Crosshair }, { to: "/campaigns", label: "Campaigns", icon: Zap }, - { to: "/comparison", label: "Comparison", icon: GitCompareArrows }, + { to: "/threat-actors", label: "Threat Actors", icon: Crosshair }, { to: "/compliance", label: "Compliance", icon: ShieldCheck }, + { to: "/comparison", label: "Comparison", icon: GitCompareArrows, roles: ["admin", "red_lead", "blue_lead"] }, + { to: "/reports", label: "Reports", icon: BarChart3 }, ]; -const adminLinks: NavItem[] = [ +const systemLinks: NavItem[] = [ + { to: "/data-sources", label: "Data Sources", icon: Database }, + { to: "/system", label: "MITRE Sync", icon: ScrollText }, { to: "/users", label: "Users", icon: Users }, { to: "/audit", label: "Audit Log", icon: FileText }, - { to: "/data-sources", label: "Data Sources", icon: Database }, - { to: "/system", label: "System", icon: Settings }, ]; function SidebarLink({ item }: { item: NavItem }) { @@ -117,7 +119,15 @@ function SidebarLink({ item }: { item: NavItem }) { export default function Sidebar() { const { user } = useAuth(); - const isAdmin = user?.role === "admin"; + const role = user?.role ?? ""; + const isAdmin = role === "admin"; + + /** Returns true when the current user is allowed to see `item`. */ + const canSee = (item: NavItem) => { + if (!item.roles) return true; // no restriction + if (isAdmin) return true; // admin sees everything + return item.roles.includes(role); + }; return ( diff --git a/frontend/src/components/heatmap/HeatmapCell.tsx b/frontend/src/components/heatmap/HeatmapCell.tsx index 7dfa494..66f2654 100644 --- a/frontend/src/components/heatmap/HeatmapCell.tsx +++ b/frontend/src/components/heatmap/HeatmapCell.tsx @@ -1,4 +1,4 @@ -import { useState } from "react"; +import React, { useState, useMemo, useCallback } from "react"; import type { HeatmapTechnique } from "../../api/heatmap"; import HeatmapTooltip from "./HeatmapTooltip"; @@ -8,7 +8,12 @@ interface HeatmapCellProps { onClick: (techniqueId: string) => void; } -export default function HeatmapCell({ technique, size, onClick }: HeatmapCellProps) { +/** + * Memoized heatmap cell — this component renders 3000+ times in the + * full ATT&CK matrix, so React.memo prevents unnecessary re-renders + * when only sibling cells change. + */ +const HeatmapCell = React.memo(function HeatmapCell({ technique, size, onClick }: HeatmapCellProps) { const [showTooltip, setShowTooltip] = useState(false); const sizeClasses = { @@ -20,21 +25,28 @@ export default function HeatmapCell({ technique, size, onClick }: HeatmapCellPro const bgColor = technique.enabled ? technique.color : "transparent"; const isDisabled = !technique.enabled; - // Determine text color based on background brightness - const getTextColor = (hex: string): string => { + // Memoize text color (derived from background hex) + const textColor = useMemo(() => { + const hex = bgColor; if (!hex || hex === "transparent" || hex === "") return "text-gray-600"; const r = parseInt(hex.slice(1, 3), 16); const g = parseInt(hex.slice(3, 5), 16); const b = parseInt(hex.slice(5, 7), 16); const brightness = (r * 299 + g * 587 + b * 114) / 1000; return brightness > 128 ? "text-gray-900" : "text-white"; - }; + }, [bgColor]); - // Status indicators - const hasTests = technique.metadata.find((m) => m.name === "tests_count"); - const testsCount = hasTests ? parseInt(hasTests.value, 10) : 0; - const reviewRequired = technique.comment?.toLowerCase().includes("review"); - const isValidated = technique.score >= 100; + // Status indicators — memoized + const { testsCount, reviewRequired, isValidated } = useMemo(() => { + const hasTests = technique.metadata.find((m) => m.name === "tests_count"); + return { + testsCount: hasTests ? parseInt(hasTests.value, 10) : 0, + reviewRequired: technique.comment?.toLowerCase().includes("review") ?? false, + isValidated: technique.score >= 100, + }; + }, [technique.metadata, technique.comment, technique.score]); + + const handleClick = useCallback(() => onClick(technique.techniqueID), [onClick, technique.techniqueID]); return (
setShowTooltip(false)} >
); -} +}); + +export default HeatmapCell; diff --git a/frontend/src/hooks/useDebounce.ts b/frontend/src/hooks/useDebounce.ts new file mode 100644 index 0000000..f432d94 --- /dev/null +++ b/frontend/src/hooks/useDebounce.ts @@ -0,0 +1,24 @@ +import { useState, useEffect } from "react"; + +/** + * Debounce a value — useful for search inputs that trigger API calls. + * + * @param value The raw value to debounce. + * @param delay Delay in milliseconds (default 300ms). + * @returns The debounced value that updates only after `delay` ms of inactivity. + * + * @example + * const [search, setSearch] = useState(""); + * const debouncedSearch = useDebounce(search, 300); + * // use `debouncedSearch` in a TanStack Query key + */ +export function useDebounce(value: T, delay = 300): T { + const [debounced, setDebounced] = useState(value); + + useEffect(() => { + const timer = setTimeout(() => setDebounced(value), delay); + return () => clearTimeout(timer); + }, [value, delay]); + + return debounced; +}