feat(ownership): Phase 9 — Ownership & Daily Operations [FASE-9]
Some checks failed
Aegis CI / lint-and-test (push) Has been cancelled
Some checks failed
Aegis CI / lint-and-test (push) Has been cancelled
Backend:
- TechniqueOwnership model: per-technique owner, backup owner, team
- RevalidationQueueItem model: prioritised analyst work queue
(critical/high/medium/low, reasons: validation_expired/infra_change/
osint_alert/mitre_update/rule_modified/low_confidence/manual)
- Migration b035ownerq: creates technique_ownerships and
revalidation_queue_items tables with full indexes
Services:
- ownership_service: set/get technique ownership, bulk assign by tactic
or platform, orphan reports for techniques and assets
- revalidation_queue_service: smart queue generation (scans expired
validations, low-confidence techniques, recent infra changes),
list/create/update queue items, analyst dashboard
Router /api/v1/ownership:
GET/PUT /ownership/techniques/{id} — technique ownership
PATCH /ownership/assets/{id} — asset ownership
GET /ownership/orphans/techniques — orphan report
GET /ownership/orphans/assets — orphan report
POST /ownership/bulk-assign — bulk by tactic/platform
GET/POST /ownership/queue — revalidation queue CRUD
PATCH /ownership/queue/{id} — update item status/assignee
POST /ownership/queue/generate — scan & generate items
GET /ownership/analyst-dashboard — personalised daily view
Scheduler: queue_generation job daily at 02:30 (after decay engine)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
136
backend/app/models/ownership_queue.py
Normal file
136
backend/app/models/ownership_queue.py
Normal file
@@ -0,0 +1,136 @@
|
||||
"""Phase 9: Ownership & Revalidation Queue models."""
|
||||
|
||||
import enum
|
||||
import uuid
|
||||
from datetime import datetime
|
||||
|
||||
from sqlalchemy import Column, DateTime, Enum, ForeignKey, Index, String, Text
|
||||
from sqlalchemy.dialects.postgresql import UUID, JSONB
|
||||
from sqlalchemy.orm import relationship
|
||||
|
||||
from app.database import Base
|
||||
|
||||
|
||||
class QueuePriority(str, enum.Enum):
|
||||
critical = "critical"
|
||||
high = "high"
|
||||
medium = "medium"
|
||||
low = "low"
|
||||
|
||||
|
||||
class QueueStatus(str, enum.Enum):
|
||||
pending = "pending"
|
||||
in_progress = "in_progress"
|
||||
completed = "completed"
|
||||
dismissed = "dismissed"
|
||||
|
||||
|
||||
class QueueReason(str, enum.Enum):
|
||||
validation_expired = "validation_expired"
|
||||
infra_change = "infra_change"
|
||||
osint_alert = "osint_alert"
|
||||
mitre_update = "mitre_update"
|
||||
rule_modified = "rule_modified"
|
||||
low_confidence = "low_confidence"
|
||||
manual = "manual"
|
||||
|
||||
|
||||
class TechniqueOwnership(Base):
|
||||
"""Ownership assignment for a MITRE technique."""
|
||||
|
||||
__tablename__ = "technique_ownerships"
|
||||
|
||||
id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
|
||||
technique_id = Column(
|
||||
UUID(as_uuid=True),
|
||||
ForeignKey("techniques.id", ondelete="CASCADE"),
|
||||
nullable=False,
|
||||
unique=True,
|
||||
)
|
||||
owner_id = Column(
|
||||
UUID(as_uuid=True),
|
||||
ForeignKey("users.id", ondelete="SET NULL"),
|
||||
nullable=True,
|
||||
)
|
||||
backup_owner_id = Column(
|
||||
UUID(as_uuid=True),
|
||||
ForeignKey("users.id", ondelete="SET NULL"),
|
||||
nullable=True,
|
||||
)
|
||||
team = Column(String(200), nullable=True)
|
||||
notes = Column(Text, nullable=True)
|
||||
assigned_at = Column(DateTime, nullable=True)
|
||||
assigned_by = Column(
|
||||
UUID(as_uuid=True),
|
||||
ForeignKey("users.id", ondelete="SET NULL"),
|
||||
nullable=True,
|
||||
)
|
||||
created_at = Column(DateTime, default=datetime.utcnow)
|
||||
updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
|
||||
|
||||
technique = relationship("Technique", foreign_keys=[technique_id])
|
||||
owner = relationship("User", foreign_keys=[owner_id])
|
||||
backup_owner = relationship("User", foreign_keys=[backup_owner_id])
|
||||
|
||||
|
||||
class RevalidationQueueItem(Base):
|
||||
"""A prioritised work item for the analyst's daily queue."""
|
||||
|
||||
__tablename__ = "revalidation_queue_items"
|
||||
|
||||
id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
|
||||
technique_id = Column(
|
||||
UUID(as_uuid=True),
|
||||
ForeignKey("techniques.id", ondelete="CASCADE"),
|
||||
nullable=True,
|
||||
)
|
||||
detection_asset_id = Column(
|
||||
UUID(as_uuid=True),
|
||||
ForeignKey("detection_assets.id", ondelete="CASCADE"),
|
||||
nullable=True,
|
||||
)
|
||||
|
||||
priority = Column(
|
||||
Enum(QueuePriority, name="queue_priority"),
|
||||
nullable=False,
|
||||
default=QueuePriority.medium,
|
||||
)
|
||||
reason = Column(
|
||||
Enum(QueueReason, name="queue_reason"),
|
||||
nullable=False,
|
||||
)
|
||||
reason_detail = Column(Text, nullable=True)
|
||||
status = Column(
|
||||
Enum(QueueStatus, name="queue_status"),
|
||||
nullable=False,
|
||||
default=QueueStatus.pending,
|
||||
)
|
||||
|
||||
assigned_to = Column(
|
||||
UUID(as_uuid=True),
|
||||
ForeignKey("users.id", ondelete="SET NULL"),
|
||||
nullable=True,
|
||||
)
|
||||
due_date = Column(DateTime, nullable=True)
|
||||
created_at = Column(DateTime, default=datetime.utcnow)
|
||||
completed_at = Column(DateTime, nullable=True)
|
||||
dismissed_at = Column(DateTime, nullable=True)
|
||||
completed_by = Column(
|
||||
UUID(as_uuid=True),
|
||||
ForeignKey("users.id", ondelete="SET NULL"),
|
||||
nullable=True,
|
||||
)
|
||||
extra = Column(JSONB, nullable=True) # arbitrary metadata
|
||||
|
||||
technique = relationship("Technique", foreign_keys=[technique_id])
|
||||
detection_asset = relationship("DetectionAsset", foreign_keys=[detection_asset_id])
|
||||
assignee = relationship("User", foreign_keys=[assigned_to])
|
||||
|
||||
|
||||
# Indexes
|
||||
Index("ix_rqueue_status", RevalidationQueueItem.status)
|
||||
Index("ix_rqueue_priority", RevalidationQueueItem.priority)
|
||||
Index("ix_rqueue_assigned_to", RevalidationQueueItem.assigned_to)
|
||||
Index("ix_rqueue_technique_id", RevalidationQueueItem.technique_id)
|
||||
Index("ix_rqueue_asset_id", RevalidationQueueItem.detection_asset_id)
|
||||
Index("ix_techown_owner_id", TechniqueOwnership.owner_id)
|
||||
Reference in New Issue
Block a user