"""Pydantic schemas for Phase 9: Ownership & Revalidation Queue.""" from datetime import datetime from typing import Optional from uuid import UUID from pydantic import BaseModel, ConfigDict, field_validator # ── Technique Ownership ─────────────────────────────────────────────────────── class TechniqueOwnershipSet(BaseModel): """Set (create or replace) ownership for a technique.""" owner_id: Optional[UUID] = None backup_owner_id: Optional[UUID] = None team: Optional[str] = None notes: Optional[str] = None class TechniqueOwnershipOut(BaseModel): model_config = ConfigDict(from_attributes=True) id: UUID technique_id: UUID owner_id: Optional[UUID] = None backup_owner_id: Optional[UUID] = None team: Optional[str] = None notes: Optional[str] = None assigned_at: Optional[datetime] = None assigned_by: Optional[UUID] = None created_at: Optional[datetime] = None updated_at: Optional[datetime] = None class DetectionAssetOwnershipPatch(BaseModel): """Update ownership fields on a detection asset.""" owner_id: Optional[UUID] = None backup_owner_id: Optional[UUID] = None team: Optional[str] = None # ── Bulk Assignment ─────────────────────────────────────────────────────────── class BulkAssignRequest(BaseModel): """Bulk-assign ownership by tactic, platform, or team filter.""" owner_id: Optional[UUID] = None backup_owner_id: Optional[UUID] = None team: Optional[str] = None # Filters — at least one must be set tactic: Optional[str] = None # assign all techniques with this tactic platform: Optional[str] = None # assign all detection assets with this platform overwrite: bool = False # overwrite existing assignments class BulkAssignResult(BaseModel): assigned_count: int skipped_count: int target_type: str # "technique" or "detection_asset" # ── Revalidation Queue ──────────────────────────────────────────────────────── class QueueItemPatch(BaseModel): """Update a revalidation queue item.""" status: Optional[str] = None assigned_to: Optional[UUID] = None priority: Optional[str] = None due_date: Optional[datetime] = None @field_validator("status") @classmethod def validate_status(cls, v): from app.models.ownership_queue import QueueStatus if v is not None: try: QueueStatus(v) except ValueError: raise ValueError(f"Invalid status: {v}") return v @field_validator("priority") @classmethod def validate_priority(cls, v): from app.models.ownership_queue import QueuePriority if v is not None: try: QueuePriority(v) except ValueError: raise ValueError(f"Invalid priority: {v}") return v class QueueItemCreate(BaseModel): """Manually create a queue item.""" technique_id: Optional[UUID] = None detection_asset_id: Optional[UUID] = None priority: str = "medium" reason: str = "manual" reason_detail: Optional[str] = None assigned_to: Optional[UUID] = None due_date: Optional[datetime] = None @field_validator("reason") @classmethod def validate_reason(cls, v): from app.models.ownership_queue import QueueReason try: QueueReason(v) except ValueError: valid = [e.value for e in QueueReason] raise ValueError(f"Invalid reason '{v}'. Must be one of: {valid}") return v @field_validator("priority") @classmethod def validate_priority(cls, v): from app.models.ownership_queue import QueuePriority try: QueuePriority(v) except ValueError: valid = [e.value for e in QueuePriority] raise ValueError(f"Invalid priority '{v}'. Must be one of: {valid}") return v class QueueItemOut(BaseModel): model_config = ConfigDict(from_attributes=True) id: UUID technique_id: Optional[UUID] = None detection_asset_id: Optional[UUID] = None priority: str reason: str reason_detail: Optional[str] = None status: str assigned_to: Optional[UUID] = None due_date: Optional[datetime] = None created_at: Optional[datetime] = None completed_at: Optional[datetime] = None dismissed_at: Optional[datetime] = None completed_by: Optional[UUID] = None extra: Optional[dict] = None # ── Analyst Dashboard ───────────────────────────────────────────────────────── class AnalystDashboard(BaseModel): """Personalised daily workday view for an analyst.""" my_pending_items: list[QueueItemOut] expiring_validations_7d: list[dict] recent_infra_changes: list[dict] my_low_confidence_techniques: list[dict] summary: dict