"""CRUD router for MITRE ATT&CK Techniques. Uses the TechniqueRepository for data access and domain exceptions for error signaling. The error_handler middleware maps domain exceptions to HTTP responses automatically. """ # Import APIRouter, Depends, Query, status from fastapi from fastapi import APIRouter, Depends, Query, status # Import Session from sqlalchemy.orm from sqlalchemy.orm import Session # Import get_db from app.database from app.database import get_db # Import get_current_user, require_any_role, require_role from app.dependencies.auth from app.dependencies.auth import get_current_user, require_any_role, require_role # Import get_technique_repository from app.dependencies.repositories from app.dependencies.repositories import get_technique_repository # Import TechniqueEntity from app.domain.entities.technique from app.domain.entities.technique import TechniqueEntity # Import TechniqueStatus from app.domain.enums from app.domain.enums import TechniqueStatus # Import DuplicateEntityError, EntityNotFoundError from app.domain.errors from app.domain.errors import DuplicateEntityError, EntityNotFoundError # Import UnitOfWork from app.domain.unit_of_work from app.domain.unit_of_work import UnitOfWork # Import from app.infrastructure.persistence.repositories.sa_technique_repository from app.infrastructure.persistence.repositories.sa_technique_repository import ( SATechniqueRepository, ) # Import User from app.models.user from app.models.user import User # Import from app.schemas.technique from app.schemas.technique import ( TechniqueCreate, TechniqueOut, TechniqueSummary, TechniqueUpdate, ) # Import log_action from app.services.audit_service from app.services.audit_service import log_action # Import get_technique_detail from app.services.technique_query_service from app.services.technique_query_service import get_technique_detail # Assign router = APIRouter(prefix="/techniques", tags=["techniques"]) router = APIRouter(prefix="/techniques", tags=["techniques"]) # --------------------------------------------------------------------------- # GET /techniques — list (with optional filters) # --------------------------------------------------------------------------- @router.get("", response_model=list[TechniqueSummary]) # Define function list_techniques def list_techniques( # Entry: tactic tactic: str | None = Query(None, description="Filter by tactic name"), # Entry: status_global status_global: TechniqueStatus | None = Query( None, alias="status", description="Filter by global status" ), # Entry: review_required review_required: bool | None = Query(None, description="Filter by review flag"), # Entry: repo repo: SATechniqueRepository = Depends(get_technique_repository), # Entry: current_user current_user: User = Depends(get_current_user), ) -> list: """Return a lightweight list of techniques, optionally filtered.""" # Return repo.list_all( return repo.list_all( # Keyword argument: tactic tactic=tactic, # Keyword argument: status status=status_global, # Keyword argument: review_required review_required=review_required, ) # --------------------------------------------------------------------------- # GET /techniques/{mitre_id} — detail (with tests + D3FEND) # --------------------------------------------------------------------------- @router.get("/{mitre_id}") # Define function get_technique def get_technique( # Entry: mitre_id mitre_id: str, # Entry: db db: Session = Depends(get_db), # Entry: current_user current_user: User = Depends(get_current_user), ) -> dict: """Return full details for a single technique, including its tests and D3FEND defenses.""" # Return get_technique_detail(db, mitre_id) return get_technique_detail(db, mitre_id) # --------------------------------------------------------------------------- # POST /techniques — create (admin only) # --------------------------------------------------------------------------- @router.post( # Literal argument value "", # Keyword argument: response_model response_model=TechniqueOut, # Keyword argument: status_code status_code=status.HTTP_201_CREATED, ) # Define function create_technique def create_technique( # Entry: payload payload: TechniqueCreate, # Entry: db db: Session = Depends(get_db), # Entry: repo repo: SATechniqueRepository = Depends(get_technique_repository), # Entry: current_user current_user: User = Depends(require_role("admin")), ) -> TechniqueOut: """Create a new technique manually.""" # Check: repo.exists_by_mitre_id(payload.mitre_id) if repo.exists_by_mitre_id(payload.mitre_id): # Raise DuplicateEntityError raise DuplicateEntityError("Technique", "mitre_id", payload.mitre_id) # Assign entity = TechniqueEntity.create( entity = TechniqueEntity.create( # Keyword argument: mitre_id mitre_id=payload.mitre_id, # Keyword argument: name name=payload.name, # Keyword argument: description description=payload.description, # Keyword argument: tactic tactic=payload.tactic, # Keyword argument: platforms platforms=payload.platforms, ) # Open context manager with UnitOfWork(db) as uow: # Assign saved = repo.save(entity) saved = repo.save(entity) # Call log_action() log_action( db, # Keyword argument: user_id user_id=current_user.id, # Keyword argument: action action="create_technique", # Keyword argument: entity_type entity_type="technique", # Keyword argument: entity_id entity_id=saved.id, # Keyword argument: details details={"mitre_id": saved.mitre_id, "name": saved.name}, ) # Call uow.commit() uow.commit() # Return saved return saved # --------------------------------------------------------------------------- # PATCH /techniques/{mitre_id} — update (admin only) # --------------------------------------------------------------------------- @router.patch("/{mitre_id}", response_model=TechniqueOut) # Define function update_technique def update_technique( # Entry: mitre_id mitre_id: str, # Entry: payload payload: TechniqueUpdate, # Entry: db db: Session = Depends(get_db), # Entry: repo repo: SATechniqueRepository = Depends(get_technique_repository), # Entry: current_user current_user: User = Depends(require_role("admin")), ) -> TechniqueOut: """Update one or more fields of an existing technique.""" # Assign entity = repo.find_by_mitre_id(mitre_id) entity = repo.find_by_mitre_id(mitre_id) # Check: entity is None if entity is None: # Raise EntityNotFoundError raise EntityNotFoundError("Technique", mitre_id) # Assign update_data = payload.model_dump(exclude_unset=True) update_data = payload.model_dump(exclude_unset=True) # Iterate over update_data.items() for field, value in update_data.items(): # Call setattr() setattr(entity, field, value) # Open context manager with UnitOfWork(db) as uow: # Assign saved = repo.save(entity) saved = repo.save(entity) # Call log_action() log_action( db, # Keyword argument: user_id user_id=current_user.id, # Keyword argument: action action="update_technique", # Keyword argument: entity_type entity_type="technique", # Keyword argument: entity_id entity_id=saved.id, # Keyword argument: details details={"mitre_id": mitre_id, "updated_fields": list(update_data.keys())}, ) # Call uow.commit() uow.commit() # Return saved return saved # --------------------------------------------------------------------------- # PATCH /techniques/{mitre_id}/review — mark as reviewed (leads + admin) # --------------------------------------------------------------------------- @router.patch("/{mitre_id}/review", response_model=TechniqueOut) # Define function review_technique def review_technique( # Entry: mitre_id mitre_id: str, # Entry: db db: Session = Depends(get_db), # Entry: repo repo: SATechniqueRepository = Depends(get_technique_repository), # Entry: current_user current_user: User = Depends(require_any_role("red_lead", "blue_lead")), ) -> TechniqueOut: """Mark a technique as reviewed. Sets ``review_required`` to *False* and records the current timestamp in ``last_review_date``. """ # Assign entity = repo.find_by_mitre_id(mitre_id) entity = repo.find_by_mitre_id(mitre_id) # Check: entity is None if entity is None: # Raise EntityNotFoundError raise EntityNotFoundError("Technique", mitre_id) # Call entity.mark_reviewed() entity.mark_reviewed() # Open context manager with UnitOfWork(db) as uow: # Assign saved = repo.save(entity) saved = repo.save(entity) # Call log_action() log_action( db, # Keyword argument: user_id user_id=current_user.id, # Keyword argument: action action="review_technique", # Keyword argument: entity_type entity_type="technique", # Keyword argument: entity_id entity_id=saved.id, # Keyword argument: details details={"mitre_id": mitre_id}, ) # Call uow.commit() uow.commit() # Return saved return saved