Move layer dispatch, entity-not-found checks, and validation from router to heatmap_service. Router now only validates requests, calls service, and formats responses (no HTTPException, no business logic). Service raises EntityNotFoundError/BusinessRuleViolation instead of returning None. Add build_navigator_export() for centralized dispatch. 29 new tests (253 total, 0 failures).
validate_as_red/blue_lead now delegate to TestEntity. check_dual_validation routes through entity instead of assigning test.state directly. Side effects dispatched via domain events. Entity raises InvalidOperationError for backward compat. Removed 4 dead V1 xfail tests, fixed 2 real test issues. 224 passed, 0 xfailed.
- Add test_test_entity.py with 46 pure unit tests covering the full domain entity
- Fix _FakeSettings in 11 test files (REPORT_TEMPLATES_DIR, JIRA, TEMPO)
- Fix stale db.commit assertions to db.flush after UoW refactor
- Add missing mock fields for TestEntity.from_orm compatibility
- Make database.py skip pool args for SQLite in test environment
- Disable slowapi rate limiter in test client fixture
- Inject test engine into app.database to fix threading errors
- Update role assertions to match current require_any_role policy
- Mark 6 legacy V1 endpoint tests as xfail (replaced by V2 workflow)
str() on models.enums.TestState produces 'TestState.red_executing' instead of 'red_executing'. Use .value to extract the plain string before constructing the domain TestState.
transition_state() now hydrates a TestEntity from the ORM model and delegates state validation to entity.transition_to(). The entity is authoritative for which transitions are valid; VALID_TRANSITIONS and can_transition() are kept for backward compatibility.
Also adds public transition_to() method to TestEntity as the stable API surface for callers that need a single validated transition without lifecycle side-effects.
- Create domain/errors.py as canonical error hierarchy: DomainError, InvalidStateTransition, PermissionViolation, BusinessRuleViolation, EntityNotFoundError, DuplicateEntityError
- InvalidOperationError now inherits from BusinessRuleViolation for semantic consistency
- Convert domain/exceptions.py to backward-compatible re-export shim with legacy aliases (DomainException, InvalidTransitionError, AuthorizationError)
- Update error_handler.py to import from domain/errors.py and map all new error types
- Update main.py to register DomainError (new base) as the exception handler root
- Add UniqueConstraint(test_id, detection_rule_id) named uq_tdr_test_rule to TestDetectionResult model
- Alembic b025: safely deduplicate existing rows before creating constraint
- Create heatmap_service.py with all layer-building logic (coverage, threat-actor, detection-rules, campaign)
- Service is framework-agnostic: no FastAPI imports, no HTTPException, no db.commit()
- Fix N+1 in coverage and threat-actor layers: bulk-fetch test_counts and rule_counts with GROUP BY
- Router reduced from 528 to 140 lines: validates request, calls service, returns response
- Add UnitOfWork context manager in domain/unit_of_work.py with commit/rollback/flush API and auto-rollback on exception
- Remove all db.commit() from test_workflow_service (8 calls), notification_service (4 calls), status_service (1 call)
- Services now only stage changes via db.add/db.flush; caller owns the transaction boundary
- Update routers/tests.py: wrap 9 workflow endpoints in UnitOfWork context managers
- Update routers/notifications.py: wrap mark_as_read and mark_all_as_read in UnitOfWork
- Replace per-technique calculate_technique_score loop with bulk_technique_scores() from scoring_service
- Snapshot creation now runs ~10 fixed queries instead of N*5+N*5 (was ~2000+ for 200 techniques)
- Add bulk_technique_scores() that pre-fetches all scoring data in 5 aggregated GROUP BY queries instead of N*5 per-technique queries
- Rewrite calculate_organization_score to use bulk data (N*5+5 queries -> 10 fixed queries)
- Rewrite calculate_tactic_score and calculate_actor_coverage_score to use bulk data
- Preserve calculate_technique_score single-technique API for router-level calls
- Declare __table_args__ on Test with 5 indexes: technique_id, state, created_at, (technique_id,state), (state,created_at)
- Declare __table_args__ on AuditLog with 3 indexes: (entity_type,entity_id), timestamp, (entity_type,entity_id,action)
- Alembic b024: create only the 2 new indexes (ix_tests_created_at, ix_tests_state_created_at); existing indexes from b005/b018/b019 are preserved
- Model index names aligned with existing migration names to prevent duplicates
- Replace default=datetime.utcnow with server_default=func.now() across all 16 models (17 columns) for consistent, timezone-aware timestamps from PostgreSQL
- Upgrade DateTime columns to DateTime(timezone=True) for timestamptz storage
- Configure SQLAlchemy engine pool: pool_size=20, max_overflow=10, pool_recycle=3600, pool_pre_ping=True
- Remove unused datetime imports from model files
- Add must_change_password field to User model with migration b023
- Add POST /auth/change-password endpoint with password policy validation
- Add require_password_changed dependency to block requests until password is changed
- Add ChangePasswordModal with live password policy checklist (forced on first login)
- Show password policy in CreateUserModal and EditUserModal
- Fix backend permissions: tests, campaigns, templates, reports, evidence, worklogs
- red_tech/blue_tech: execute only, cannot create tests/campaigns/templates
- red_lead/blue_lead: create/edit tests/campaigns/templates, generate reports, no system access
- viewer: read-only everywhere, can generate reports
- Fix frontend role checks across TestDetailPage, TestDetailHeader, TeamTabs, TestsPage, CampaignsPage, CampaignDetailPage, Sidebar
Tarea 4.1 — OSINT Enrichment:
- Add OsintItem model with source_type, severity, CVSS metadata, review flag
- Add Alembic migration b022 with osint_items table and optimized indexes
- Add osint_enrichment_service with NVD API integration, deduplication, rate limiting
- Add OSINT router: GET /osint/items, /osint/summary, /osint/technique/{id}
- Add POST /osint/items/{id}/review to mark items as reviewed
- Add POST /osint/enrich/{technique_id} for manual single-technique enrichment
- Techniques with new CVEs are automatically flagged review_required=True
- Register weekly enrichment job in APScheduler
- Add NVD_API_KEY config setting for optional increased rate limits
Tarea 4.2 — Stale Coverage Detection:
- Add stale_detection_service that flags techniques with no validated test
in the last N days, or never-validated but with a coverage status
- Configurable threshold via STALE_THRESHOLD_DAYS setting (default 365)
- Register daily stale detection job in APScheduler
- Only flags techniques not already marked review_required
Pause/Resume timer:
- Add paused_at, red_paused_seconds, blue_paused_seconds fields to Test model
- Add pause_timer/resume_timer workflow functions with accumulated pause tracking
- Auto-resume on phase submit; subtract paused time from worklog duration
- Add POST /tests/{id}/pause-timer and resume-timer endpoints
- Update LiveTimer component with pause/resume button and paused visual state
- Wire pause/resume mutations through TestDetailPage and TestDetailHeader
Professional Reporting Engine - Fase 2:
- Add ReportEngine service with Jinja2 HTML rendering, WeasyPrint PDF, and docxtpl DOCX
- Add corporate CSS stylesheet with cover page, data tables, stats grid, findings
- Create purple_campaign, coverage_report, and executive_summary HTML templates
- Add report_generation_service collecting domain data for each report type
- Add professional_reports router: GET /reports/generate/purple-campaign/{id}, coverage-summary, executive-summary
- Add analytics router with flat JSON endpoints for PowerBI: /coverage, /tests, /trends, /operators
- Add advanced_metrics router: /coverage-by-tactic, /never-tested, /avg-validation-time, /detection-rate-trend
- Add weasyprint and docxtpl to requirements.txt
- Add REPORT_TEMPLATES_DIR, REPORT_OUTPUT_DIR, COMPANY_NAME, COMPANY_LOGO_PATH to config
- Add red_started_at/blue_started_at timing fields to Test model with Alembic migration
- Modify workflow transitions to auto-create integrity-hashed worklogs: Start Execution records red_started_at, Submit to Blue Team stops Red timer and creates worklog then starts Blue timer, Submit for Review stops Blue timer and creates worklog
- Auto-sync worklogs to Tempo when test has a Jira link
- Add LiveTimer component showing real-time elapsed counter during active phases
- Clear timing fields on test reopen
- Fix campaign test management: replace broken navigate-to-tests flow with AddTestToCampaignModal that lets users search and add existing tests directly from the campaign detail page
Replace all sa.Enum / op.create_table / ALTER TABLE approach with a
single op.execute() containing raw DDL. This sidesteps every
SQLAlchemy hook (enum auto-create, default cast conflicts) by letting
PostgreSQL handle CREATE TYPE IF NOT EXISTS, CREATE TABLE IF NOT
EXISTS, and CREATE INDEX IF NOT EXISTS directly.
Replace sa.Enum column types with raw SQL DO $$ IF NOT EXISTS blocks
for enum creation, then sa.Text columns + ALTER TYPE USING casts.
This completely bypasses SQLAlchemy's _on_table_create hook that
triggers CREATE TYPE without checkfirst, causing DuplicateObject
on PostgreSQL when the entrypoint retries after a failed migration.