"""Phase 9: Ownership service — techniques and detection assets.""" import logging from datetime import datetime from typing import Optional from uuid import UUID from sqlalchemy.orm import Session from app.models.ownership_queue import TechniqueOwnership from app.models.detection_lifecycle import DetectionAsset from app.models.technique import Technique from app.domain.exceptions import EntityNotFoundError from app.services import audit_service logger = logging.getLogger(__name__) def _now() -> datetime: return datetime.utcnow() # ── Technique Ownership ─────────────────────────────────────────────────────── def get_technique_ownership(db: Session, technique_id: UUID) -> Optional[TechniqueOwnership]: return db.query(TechniqueOwnership).filter( TechniqueOwnership.technique_id == technique_id ).first() def set_technique_ownership( db: Session, technique_id: UUID, owner_id: Optional[UUID], backup_owner_id: Optional[UUID], team: Optional[str], notes: Optional[str], assigned_by: UUID, ) -> TechniqueOwnership: technique = db.query(Technique).filter(Technique.id == technique_id).first() if not technique: raise EntityNotFoundError("Technique", str(technique_id)) ownership = db.query(TechniqueOwnership).filter( TechniqueOwnership.technique_id == technique_id ).first() if not ownership: ownership = TechniqueOwnership(technique_id=technique_id) db.add(ownership) ownership.owner_id = owner_id ownership.backup_owner_id = backup_owner_id ownership.team = team ownership.notes = notes ownership.assigned_at = _now() ownership.assigned_by = assigned_by ownership.updated_at = _now() db.commit() db.refresh(ownership) audit_service.log_action( db, assigned_by, "TECHNIQUE_OWNERSHIP_SET", "technique_ownership", str(ownership.id), details={"technique_id": str(technique_id), "owner_id": str(owner_id) if owner_id else None, "team": team}, ) return ownership def get_orphan_techniques(db: Session) -> list[dict]: """Techniques with no ownership record or null owner_id.""" owned_ids = db.query(TechniqueOwnership.technique_id).filter( TechniqueOwnership.owner_id.isnot(None) ).subquery() orphans = db.query(Technique).filter( ~Technique.id.in_(owned_ids) ).order_by(Technique.tactic, Technique.mitre_id).all() return [ { "technique_id": str(t.id), "mitre_id": t.mitre_id, "name": t.name, "tactic": t.tactic, } for t in orphans ] # ── Detection Asset Ownership ───────────────────────────────────────────────── def set_asset_ownership( db: Session, asset_id: UUID, owner_id: Optional[UUID], backup_owner_id: Optional[UUID], team: Optional[str], user_id: UUID, ) -> DetectionAsset: asset = db.query(DetectionAsset).filter(DetectionAsset.id == asset_id).first() if not asset: raise EntityNotFoundError("DetectionAsset", str(asset_id)) asset.owner_id = owner_id asset.backup_owner_id = backup_owner_id asset.team = team db.commit() db.refresh(asset) audit_service.log_action( db, user_id, "ASSET_OWNERSHIP_SET", "detection_asset", str(asset_id), details={"owner_id": str(owner_id) if owner_id else None, "team": team}, ) return asset def get_orphan_assets(db: Session) -> list[dict]: """Active detection assets with no owner.""" orphans = db.query(DetectionAsset).filter( DetectionAsset.is_active == True, DetectionAsset.owner_id.is_(None), ).order_by(DetectionAsset.platform, DetectionAsset.name).all() return [ { "asset_id": str(a.id), "name": a.name, "platform": a.platform, "asset_type": a.asset_type, "health_status": a.health_status.value if a.health_status else None, } for a in orphans ] # ── Bulk Assignment ─────────────────────────────────────────────────────────── def bulk_assign_techniques_by_tactic( db: Session, tactic: str, owner_id: Optional[UUID], backup_owner_id: Optional[UUID], team: Optional[str], overwrite: bool, user_id: UUID, ) -> dict: techniques = db.query(Technique).filter(Technique.tactic == tactic).all() assigned = 0 skipped = 0 now = _now() for technique in techniques: existing = db.query(TechniqueOwnership).filter( TechniqueOwnership.technique_id == technique.id ).first() if existing and existing.owner_id and not overwrite: skipped += 1 continue if not existing: existing = TechniqueOwnership(technique_id=technique.id) db.add(existing) existing.owner_id = owner_id existing.backup_owner_id = backup_owner_id existing.team = team existing.assigned_at = now existing.assigned_by = user_id existing.updated_at = now assigned += 1 db.commit() audit_service.log_action( db, user_id, "BULK_OWNERSHIP_ASSIGNED", "technique_ownership", None, details={"tactic": tactic, "assigned": assigned, "skipped": skipped, "team": team}, ) logger.info("Bulk ownership: tactic=%s assigned=%d skipped=%d", tactic, assigned, skipped) return {"assigned_count": assigned, "skipped_count": skipped, "target_type": "technique"} def bulk_assign_assets_by_platform( db: Session, platform: str, owner_id: Optional[UUID], backup_owner_id: Optional[UUID], team: Optional[str], overwrite: bool, user_id: UUID, ) -> dict: assets = db.query(DetectionAsset).filter( DetectionAsset.platform == platform, DetectionAsset.is_active == True, ).all() assigned = 0 skipped = 0 for asset in assets: if asset.owner_id and not overwrite: skipped += 1 continue asset.owner_id = owner_id asset.backup_owner_id = backup_owner_id asset.team = team assigned += 1 db.commit() audit_service.log_action( db, user_id, "BULK_ASSET_OWNERSHIP_ASSIGNED", "detection_asset", None, details={"platform": platform, "assigned": assigned, "skipped": skipped}, ) return {"assigned_count": assigned, "skipped_count": skipped, "target_type": "detection_asset"}