Some checks failed
Aegis CI / lint-and-test (push) Has been cancelled
PostureSnapshot model, Alembic migration (b039exec), schemas, service aggregating all phases (coverage/risk/operations/knowledge/MTTD), and router at /api/v1/dashboard with executive view, KPIs, coverage-by-tactic, posture-history, posture-snapshot, and activity-feed endpoints. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
69 lines
3.6 KiB
Python
69 lines
3.6 KiB
Python
"""Phase 13: Executive Dashboard — PostureSnapshot model."""
|
||
|
||
import uuid
|
||
from datetime import datetime
|
||
|
||
from sqlalchemy import (
|
||
Boolean, Column, Date, DateTime, Float, ForeignKey,
|
||
Index, Integer, UniqueConstraint,
|
||
)
|
||
from sqlalchemy.dialects.postgresql import UUID, JSONB
|
||
from sqlalchemy.orm import relationship
|
||
|
||
from app.database import Base
|
||
|
||
|
||
class PostureSnapshot(Base):
|
||
"""
|
||
Daily point-in-time capture of the organisation's security posture.
|
||
|
||
Aggregates data from all phases (coverage, risk, ownership, knowledge,
|
||
attack-paths) into a single row that can be trended over time.
|
||
"""
|
||
|
||
__tablename__ = "posture_snapshots"
|
||
|
||
id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
|
||
snapshot_date = Column(Date, nullable=False) # one per calendar day
|
||
|
||
# ── Coverage ──────────────────────────────────────────────────────────────
|
||
total_techniques = Column(Integer, nullable=False, default=0)
|
||
validated_count = Column(Integer, nullable=False, default=0)
|
||
partial_count = Column(Integer, nullable=False, default=0)
|
||
not_covered_count = Column(Integer, nullable=False, default=0)
|
||
coverage_pct = Column(Float, nullable=False, default=0.0) # 0–100
|
||
|
||
# ── Risk ─────────────────────────────────────────────────────────────────
|
||
avg_risk_score = Column(Float, nullable=False, default=0.0)
|
||
critical_count = Column(Integer, nullable=False, default=0)
|
||
high_count = Column(Integer, nullable=False, default=0)
|
||
medium_count = Column(Integer, nullable=False, default=0)
|
||
low_count = Column(Integer, nullable=False, default=0)
|
||
|
||
# ── Operations ────────────────────────────────────────────────────────────
|
||
open_queue_items = Column(Integer, nullable=False, default=0)
|
||
orphan_techniques = Column(Integer, nullable=False, default=0)
|
||
|
||
# ── Knowledge ─────────────────────────────────────────────────────────────
|
||
playbook_count = Column(Integer, nullable=False, default=0)
|
||
lesson_count = Column(Integer, nullable=False, default=0)
|
||
|
||
# ── MTTD (from attack-path executions completed in last 30 d) ────────────
|
||
mttd_avg_seconds = Column(Float, nullable=True) # None if no data
|
||
executions_30d = Column(Integer, nullable=False, default=0)
|
||
detection_rate_30d = Column(Float, nullable=True) # avg across executions
|
||
|
||
# ── Meta ─────────────────────────────────────────────────────────────────
|
||
created_by = Column(
|
||
UUID(as_uuid=True), ForeignKey("users.id", ondelete="SET NULL"), nullable=True
|
||
)
|
||
created_at = Column(DateTime, default=datetime.utcnow)
|
||
extra = Column(JSONB, nullable=True) # full breakdown / by-tactic
|
||
|
||
creator = relationship("User", foreign_keys=[created_by])
|
||
|
||
__table_args__ = (
|
||
UniqueConstraint("snapshot_date", name="uq_posture_snapshot_date"),
|
||
Index("ix_posture_snapshots_date", "snapshot_date"),
|
||
)
|