Revoke tokens by jti in a dedicated Redis DB, honor TTL from JWT exp on logout, reject revoked tokens in get_current_user, and add FakeRedis-backed API tests.
Add Redis 7 to Docker Compose with healthcheck and persistence, separate logical DBs for blacklist and cache, singleton redis client helpers, and unit tests with fakeredis.
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.