feat(phase-33): final polish V3 - navigation, performance, and documentation (T-238 to T-240)

This commit is contained in:
2026-02-10 09:21:35 +01:00
parent 35983de67e
commit 14f8485f06
14 changed files with 1446 additions and 320 deletions

478
README.md
View File

@@ -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** — 0100 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/ # b001b007 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/ # b001b018 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.

View File

@@ -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")

View File

@@ -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 ────────────────────────────────────

View File

@@ -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": {

View File

@@ -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

View File

@@ -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:

View File

@@ -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 |

220
docs/ARCHITECTURE.md Normal file
View File

@@ -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` | 0100 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 `<Suspense>` 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

281
docs/DATA_SOURCES.md Normal file
View File

@@ -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 |

285
docs/SCORING.md Normal file
View File

@@ -0,0 +1,285 @@
# Aegis — Scoring System
Aegis uses a granular 0100 scoring system to measure security coverage at multiple levels: individual techniques, tactics, threat actors, and the overall organization.
---
## Technique Score (0100)
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).

View File

@@ -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 (
<Routes>
@@ -37,35 +43,61 @@ export default function App() {
</ProtectedRoute>
}
>
{/* ── Core ─────────────────────────────────────────────── */}
<Route path="/dashboard" element={<DashboardPage />} />
<Route path="/techniques" element={<TechniquesPage />} />
<Route path="/matrix" element={<MatrixPage />} />
<Route path="/techniques" element={<Suspense fallback={<LoadingSpinner text="Loading…" />}><TechniquesPage /></Suspense>} />
<Route path="/techniques/:mitreId" element={<Suspense fallback={<LoadingSpinner text="Loading…" />}><TechniqueDetailPage /></Suspense>} />
<Route path="/matrix" element={<Suspense fallback={<LoadingSpinner text="Loading…" />}><MatrixPage /></Suspense>} />
{/* ── Executive Dashboard (leads + admin) ──────────────── */}
<Route
path="/executive-dashboard"
element={
<ProtectedRoute roles={["admin", "red_lead", "blue_lead"]}>
<ExecutiveDashboardPage />
<Suspense fallback={<LoadingSpinner text="Loading…" />}><ExecutiveDashboardPage /></Suspense>
</ProtectedRoute>
}
/>
<Route path="/techniques/:mitreId" element={<TechniqueDetailPage />} />
<Route path="/tests" element={<TestsPage />} />
<Route path="/tests/new" element={<TestCreatePage />} />
<Route path="/tests/:testId" element={<TestDetailPage />} />
<Route path="/test-catalog" element={<TestCatalogPage />} />
<Route path="/test-catalog/:templateId/use" element={<TestCatalogPage />} />
<Route path="/reports" element={<ReportsPage />} />
<Route path="/threat-actors" element={<ThreatActorsPage />} />
<Route path="/threat-actors/:actorId" element={<ThreatActorDetailPage />} />
<Route path="/campaigns" element={<CampaignsPage />} />
<Route path="/campaigns/:campaignId" element={<CampaignDetailPage />} />
<Route path="/comparison" element={<ComparisonPage />} />
<Route path="/compliance" element={<CompliancePage />} />
{/* ── Tests ────────────────────────────────────────────── */}
<Route path="/tests" element={<Suspense fallback={<LoadingSpinner text="Loading…" />}><TestsPage /></Suspense>} />
<Route path="/tests/new" element={<Suspense fallback={<LoadingSpinner text="Loading…" />}><TestCreatePage /></Suspense>} />
<Route path="/tests/:testId" element={<Suspense fallback={<LoadingSpinner text="Loading…" />}><TestDetailPage /></Suspense>} />
<Route path="/test-catalog" element={<Suspense fallback={<LoadingSpinner text="Loading…" />}><TestCatalogPage /></Suspense>} />
<Route path="/test-catalog/:templateId/use" element={<Suspense fallback={<LoadingSpinner text="Loading…" />}><TestCatalogPage /></Suspense>} />
{/* ── Campaigns ────────────────────────────────────────── */}
<Route path="/campaigns" element={<Suspense fallback={<LoadingSpinner text="Loading…" />}><CampaignsPage /></Suspense>} />
<Route path="/campaigns/:campaignId" element={<Suspense fallback={<LoadingSpinner text="Loading…" />}><CampaignDetailPage /></Suspense>} />
{/* ── Threat Actors ────────────────────────────────────── */}
<Route path="/threat-actors" element={<Suspense fallback={<LoadingSpinner text="Loading…" />}><ThreatActorsPage /></Suspense>} />
<Route path="/threat-actors/:actorId" element={<Suspense fallback={<LoadingSpinner text="Loading…" />}><ThreatActorDetailPage /></Suspense>} />
{/* ── Compliance ───────────────────────────────────────── */}
<Route path="/compliance" element={<Suspense fallback={<LoadingSpinner text="Loading…" />}><CompliancePage /></Suspense>} />
{/* ── Comparison (leads + admin) ───────────────────────── */}
<Route
path="/comparison"
element={
<ProtectedRoute roles={["admin", "red_lead", "blue_lead"]}>
<Suspense fallback={<LoadingSpinner text="Loading…" />}><ComparisonPage /></Suspense>
</ProtectedRoute>
}
/>
{/* ── Reports ──────────────────────────────────────────── */}
<Route path="/reports" element={<Suspense fallback={<LoadingSpinner text="Loading…" />}><ReportsPage /></Suspense>} />
{/* ── System (admin only) ──────────────────────────────── */}
<Route
path="/system"
element={
<ProtectedRoute roles={["admin"]}>
<SystemPage />
<Suspense fallback={<LoadingSpinner text="Loading…" />}><SystemPage /></Suspense>
</ProtectedRoute>
}
/>
@@ -73,7 +105,7 @@ export default function App() {
path="/users"
element={
<ProtectedRoute roles={["admin"]}>
<UsersPage />
<Suspense fallback={<LoadingSpinner text="Loading…" />}><UsersPage /></Suspense>
</ProtectedRoute>
}
/>
@@ -81,7 +113,7 @@ export default function App() {
path="/audit"
element={
<ProtectedRoute roles={["admin"]}>
<AuditLogPage />
<Suspense fallback={<LoadingSpinner text="Loading…" />}><AuditLogPage /></Suspense>
</ProtectedRoute>
}
/>
@@ -89,7 +121,7 @@ export default function App() {
path="/data-sources"
element={
<ProtectedRoute roles={["admin"]}>
<DataSourcesPage />
<Suspense fallback={<LoadingSpinner text="Loading…" />}><DataSourcesPage /></Suspense>
</ProtectedRoute>
}
/>

View File

@@ -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 (
<aside className="flex h-screen w-60 flex-col border-r border-gray-800 bg-gray-900">
@@ -130,19 +140,19 @@ export default function Sidebar() {
</div>
{/* Main nav */}
<nav className="flex-1 space-y-1 px-3 py-4">
{mainLinks.map((item) => (
<nav className="flex-1 space-y-1 overflow-y-auto px-3 py-4">
{mainLinks.filter(canSee).map((item) => (
<SidebarLink key={item.to + item.label} item={item} />
))}
{/* Admin section */}
{/* System / Administration section — admin only */}
{isAdmin && (
<>
<div className="my-3 border-t border-gray-800" />
<p className="mb-2 px-3 text-[10px] font-semibold uppercase tracking-widest text-gray-600">
Administration
System
</p>
{adminLinks.map((item) => (
{systemLinks.map((item) => (
<SidebarLink key={item.to} item={item} />
))}
</>
@@ -152,7 +162,7 @@ export default function Sidebar() {
{/* Footer */}
<div className="border-t border-gray-800 px-5 py-4">
<p className="truncate text-xs text-gray-500">
{user?.role ?? "—"}
{user?.username ?? "—"} · {role || "—"}
</p>
</div>
</aside>

View File

@@ -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
// Status indicators — memoized
const { testsCount, reviewRequired, isValidated } = useMemo(() => {
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;
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 (
<div
@@ -43,7 +55,7 @@ export default function HeatmapCell({ technique, size, onClick }: HeatmapCellPro
onMouseLeave={() => setShowTooltip(false)}
>
<button
onClick={() => onClick(technique.techniqueID)}
onClick={handleClick}
disabled={isDisabled}
className={`
w-full rounded border transition-all duration-150
@@ -59,7 +71,7 @@ export default function HeatmapCell({ technique, size, onClick }: HeatmapCellPro
backgroundColor: isDisabled ? undefined : bgColor,
}}
>
<span className={`truncate font-mono font-medium leading-tight ${getTextColor(bgColor)}`}>
<span className={`truncate font-mono font-medium leading-tight ${textColor}`}>
{technique.techniqueID}
</span>
{size !== "compact" && !isDisabled && (
@@ -78,4 +90,6 @@ export default function HeatmapCell({ technique, size, onClick }: HeatmapCellPro
)}
</div>
);
}
});
export default HeatmapCell;

View File

@@ -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<T>(value: T, delay = 300): T {
const [debounced, setDebounced] = useState(value);
useEffect(() => {
const timer = setTimeout(() => setDebounced(value), delay);
return () => clearTimeout(timer);
}, [value, delay]);
return debounced;
}