"""CRUD router for TestTemplates — predefined test catalog. Endpoints --------- GET /test-templates — list with filters + pagination GET /test-templates/stats — catalog statistics (admin) GET /test-templates/{id} — detail POST /test-templates — create custom (admin) PATCH /test-templates/{id} — update (admin) PATCH /test-templates/{id}/toggle-active — toggle active/inactive (admin) DELETE /test-templates/{id} — soft delete (admin) GET /test-templates/by-technique/{mitre_id} — templates for a MITRE technique Filters (GET /test-templates) ----------------------------- - source: atomic_red_team | mitre | custom - platform: windows | linux | macos - severity: low | medium | high | critical - mitre_technique_id: filter by specific technique - search: full-text search across name and description - is_active: true | false (default only active) - offset / limit: pagination (default limit=50) """ import uuid from typing import Optional from fastapi import APIRouter, Depends, Query, status from sqlalchemy.orm import Session from app.database import get_db from app.dependencies.auth import get_current_user, require_any_role from app.domain.unit_of_work import UnitOfWork from app.models.technique import Technique from app.models.user import User from app.schemas.test_template import ( TestTemplateCreate, TestTemplateOut, TestTemplateSummary, ) from app.services.audit_service import log_action from app.services.test_template_service import ( bulk_activate, create_template as create_template_svc, get_template_or_raise, get_template_stats, get_templates_by_technique as templates_by_technique, list_templates, soft_delete_template, toggle_template_active as toggle_template_active_svc, update_template as update_template_svc, ) router = APIRouter(prefix="/test-templates", tags=["test-templates"]) # --------------------------------------------------------------------------- # GET /test-templates — list with filters + pagination # --------------------------------------------------------------------------- @router.get("", response_model=list[TestTemplateSummary]) def _list_templates_handler( source: Optional[str] = Query(None, description="Filter by source (atomic_red_team, mitre, custom)"), platform: Optional[str] = Query(None, description="Filter by platform (windows, linux, macos)"), severity: Optional[str] = Query(None, description="Filter by severity (low, medium, high, critical)"), mitre_technique_id: Optional[str] = Query(None, description="Filter by MITRE technique ID"), search: Optional[str] = Query(None, description="Search in name and description"), is_active: Optional[bool] = Query(None, description="Filter by active status (true/false). Omit to return all."), offset: int = Query(0, ge=0), limit: int = Query(50, ge=1, le=200), db: Session = Depends(get_db), current_user: User = Depends(get_current_user), ): """Return a paginated, filterable list of test templates.""" return list_templates( db, source=source, platform=platform, severity=severity, mitre_technique_id=mitre_technique_id, search=search, is_active=is_active, offset=offset, limit=limit, ) # --------------------------------------------------------------------------- # GET /test-templates/stats — catalog statistics (admin) # --------------------------------------------------------------------------- @router.get("/stats") def template_stats( db: Session = Depends(get_db), current_user: User = Depends(require_any_role("red_lead", "blue_lead")), ): """Return catalog statistics: active, by_source, by_platform.""" return get_template_stats(db) # --------------------------------------------------------------------------- # PATCH /test-templates/bulk-activate — activate/deactivate all (admin) # --------------------------------------------------------------------------- @router.patch("/bulk-activate") def bulk_activate_templates( activate: bool = Query(True, description="True to activate all, False to deactivate all"), db: Session = Depends(get_db), current_user: User = Depends(require_any_role("red_lead", "blue_lead")), ): """Set all templates to active or inactive.""" count = bulk_activate(db, activate=activate) with UnitOfWork(db) as uow: log_action( db, user_id=current_user.id, action="bulk_activate_templates" if activate else "bulk_deactivate_templates", entity_type="test_template", entity_id=None, details={"affected": count, "is_active": activate}, ) uow.commit() return { "detail": f"{'Activated' if activate else 'Deactivated'} {count} templates", "affected": count, "is_active": activate, } # --------------------------------------------------------------------------- # GET /test-templates/by-technique/{mitre_id} # --------------------------------------------------------------------------- @router.get("/by-technique/{mitre_id}", response_model=list[TestTemplateSummary]) def _templates_by_technique_handler( mitre_id: str, db: Session = Depends(get_db), current_user: User = Depends(get_current_user), ): """Return all active templates mapped to a specific MITRE technique.""" return templates_by_technique(db, mitre_id) # --------------------------------------------------------------------------- # GET /test-templates/{id} — detail # --------------------------------------------------------------------------- @router.get("/{template_id}", response_model=TestTemplateOut) def get_template( template_id: uuid.UUID, db: Session = Depends(get_db), current_user: User = Depends(get_current_user), ): """Return full details for a single test template.""" return get_template_or_raise(db, template_id) # --------------------------------------------------------------------------- # POST /test-templates — create (admin only) # --------------------------------------------------------------------------- @router.post( "", response_model=TestTemplateOut, status_code=status.HTTP_201_CREATED, ) def create_template( payload: TestTemplateCreate, db: Session = Depends(get_db), current_user: User = Depends(require_any_role("red_lead", "blue_lead")), ): """Create a custom test template.""" template = create_template_svc(db, **payload.model_dump()) with UnitOfWork(db) as uow: # Flag the associated technique for review — new template available if template.mitre_technique_id: technique = ( db.query(Technique) .filter(Technique.mitre_id == template.mitre_technique_id) .first() ) if technique: technique.review_required = True log_action( db, user_id=current_user.id, action="create_test_template", entity_type="test_template", entity_id=template.id, details={ "name": template.name, "source": template.source, "mitre_technique_id": template.mitre_technique_id, }, ) uow.commit() db.refresh(template) return template # --------------------------------------------------------------------------- # PATCH /test-templates/{id} — update (admin only) # --------------------------------------------------------------------------- @router.patch("/{template_id}", response_model=TestTemplateOut) def update_template( template_id: uuid.UUID, payload: TestTemplateCreate, db: Session = Depends(get_db), current_user: User = Depends(require_any_role("red_lead", "blue_lead")), ): """Update fields of an existing test template.""" template = update_template_svc(db, template_id, **payload.model_dump(exclude_unset=True)) with UnitOfWork(db) as uow: log_action( db, user_id=current_user.id, action="update_test_template", entity_type="test_template", entity_id=template.id, details={"updated_fields": list(payload.model_dump(exclude_unset=True).keys())}, ) uow.commit() db.refresh(template) return template # --------------------------------------------------------------------------- # PATCH /test-templates/{id}/toggle-active — toggle active/inactive (admin) # --------------------------------------------------------------------------- @router.patch("/{template_id}/toggle-active", response_model=TestTemplateOut) def toggle_template_active( template_id: uuid.UUID, db: Session = Depends(get_db), current_user: User = Depends(require_any_role("red_lead", "blue_lead")), ): """Toggle a template between active and inactive (is_active = not is_active).""" template = toggle_template_active_svc(db, template_id) with UnitOfWork(db) as uow: log_action( db, user_id=current_user.id, action="toggle_test_template", entity_type="test_template", entity_id=template.id, details={"name": template.name, "is_active": template.is_active}, ) uow.commit() db.refresh(template) return template # --------------------------------------------------------------------------- # DELETE /test-templates/{id} — soft delete (admin only) # --------------------------------------------------------------------------- @router.delete("/{template_id}", status_code=status.HTTP_200_OK) def delete_template( template_id: uuid.UUID, db: Session = Depends(get_db), current_user: User = Depends(require_any_role("red_lead", "blue_lead")), ): """Soft-delete a test template by setting ``is_active=False``.""" template = get_template_or_raise(db, template_id) soft_delete_template(db, template_id) with UnitOfWork(db) as uow: log_action( db, user_id=current_user.id, action="delete_test_template", entity_type="test_template", entity_id=template.id, details={"name": template.name}, ) uow.commit() return {"detail": "Test template deactivated"}