Some checks failed
Aegis CI / lint-and-test (push) Has been cancelled
- Playbooks: versioned Markdown runbooks per technique × type (attack/detect/investigate/respond/hunt) - PlaybookVersion: immutable snapshots on every update; restore to any previous version - LessonLearned: post-mortem records linked to tests/campaigns/attack-paths or manual - Alembic migration b037know (raw SQL, idempotent, no PostgreSQL enums) - Router /api/v1/knowledge: 14 endpoints for playbooks + lessons + stats - Pydantic validators for playbook_type, severity, entity_type (422 on invalid) - Knowledge stats endpoint: totals + breakdown by severity and playbook type - Soft-delete on both resources; include_inactive filter for admin recovery - QA script: 70+ tests across CRUD, versioning, filtering, auth, soft-delete, regression Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
150 lines
5.1 KiB
Python
150 lines
5.1 KiB
Python
"""Phase 11: Knowledge Management schemas — Playbooks + Lessons Learned."""
|
|
|
|
from datetime import datetime
|
|
from typing import List, Optional
|
|
from uuid import UUID
|
|
|
|
from pydantic import BaseModel, ConfigDict, field_validator
|
|
|
|
# ── Constants ─────────────────────────────────────────────────────────────────
|
|
|
|
VALID_PLAYBOOK_TYPES = ["attack", "detect", "investigate", "respond", "hunt"]
|
|
VALID_SEVERITIES = ["critical", "high", "medium", "low", "info"]
|
|
VALID_ENTITY_TYPES = ["test", "campaign", "attack_path", "manual"]
|
|
|
|
|
|
# ══════════════════════════════════════════════════════════════════════════════
|
|
# Playbook schemas
|
|
# ══════════════════════════════════════════════════════════════════════════════
|
|
|
|
class PlaybookCreate(BaseModel):
|
|
technique_id: UUID
|
|
playbook_type: str
|
|
title: str
|
|
content: str = ""
|
|
tools: List[str] = []
|
|
prerequisites: List[str] = []
|
|
change_note: Optional[str] = None
|
|
|
|
@field_validator("playbook_type")
|
|
@classmethod
|
|
def validate_playbook_type(cls, v: str) -> str:
|
|
if v not in VALID_PLAYBOOK_TYPES:
|
|
raise ValueError(
|
|
f"Invalid playbook_type '{v}'. Must be one of: {VALID_PLAYBOOK_TYPES}"
|
|
)
|
|
return v
|
|
|
|
|
|
class PlaybookUpdate(BaseModel):
|
|
title: Optional[str] = None
|
|
content: Optional[str] = None
|
|
tools: Optional[List[str]] = None
|
|
prerequisites: Optional[List[str]] = None
|
|
change_note: Optional[str] = None
|
|
|
|
|
|
class PlaybookVersionOut(BaseModel):
|
|
model_config = ConfigDict(from_attributes=True)
|
|
|
|
id: UUID
|
|
playbook_id: UUID
|
|
version: int
|
|
title: str
|
|
content: str
|
|
tools: List[str] = []
|
|
prerequisites: List[str] = []
|
|
changed_by: Optional[UUID]
|
|
change_note: Optional[str]
|
|
created_at: datetime
|
|
|
|
|
|
class PlaybookOut(BaseModel):
|
|
model_config = ConfigDict(from_attributes=True)
|
|
|
|
id: UUID
|
|
technique_id: UUID
|
|
playbook_type: str
|
|
title: str
|
|
content: str
|
|
version: int
|
|
tools: List[str] = []
|
|
prerequisites: List[str] = []
|
|
created_by: Optional[UUID]
|
|
updated_by: Optional[UUID]
|
|
created_at: datetime
|
|
updated_at: datetime
|
|
is_active: bool
|
|
|
|
|
|
# ══════════════════════════════════════════════════════════════════════════════
|
|
# Lesson Learned schemas
|
|
# ══════════════════════════════════════════════════════════════════════════════
|
|
|
|
class LessonLearnedCreate(BaseModel):
|
|
title: str
|
|
what_happened: str
|
|
root_cause: str
|
|
fix_applied: Optional[str] = None
|
|
severity: str = "medium"
|
|
entity_type: str = "manual"
|
|
entity_id: Optional[UUID] = None
|
|
technique_ids: List[str] = []
|
|
tags: List[str] = []
|
|
|
|
@field_validator("severity")
|
|
@classmethod
|
|
def validate_severity(cls, v: str) -> str:
|
|
if v not in VALID_SEVERITIES:
|
|
raise ValueError(
|
|
f"Invalid severity '{v}'. Must be one of: {VALID_SEVERITIES}"
|
|
)
|
|
return v
|
|
|
|
@field_validator("entity_type")
|
|
@classmethod
|
|
def validate_entity_type(cls, v: str) -> str:
|
|
if v not in VALID_ENTITY_TYPES:
|
|
raise ValueError(
|
|
f"Invalid entity_type '{v}'. Must be one of: {VALID_ENTITY_TYPES}"
|
|
)
|
|
return v
|
|
|
|
|
|
class LessonLearnedUpdate(BaseModel):
|
|
title: Optional[str] = None
|
|
what_happened: Optional[str] = None
|
|
root_cause: Optional[str] = None
|
|
fix_applied: Optional[str] = None
|
|
severity: Optional[str] = None
|
|
technique_ids: Optional[List[str]] = None
|
|
tags: Optional[List[str]] = None
|
|
|
|
@field_validator("severity")
|
|
@classmethod
|
|
def validate_severity(cls, v: Optional[str]) -> Optional[str]:
|
|
if v is not None and v not in VALID_SEVERITIES:
|
|
raise ValueError(
|
|
f"Invalid severity '{v}'. Must be one of: {VALID_SEVERITIES}"
|
|
)
|
|
return v
|
|
|
|
|
|
class LessonLearnedOut(BaseModel):
|
|
model_config = ConfigDict(from_attributes=True)
|
|
|
|
id: UUID
|
|
title: str
|
|
what_happened: str
|
|
root_cause: str
|
|
fix_applied: Optional[str]
|
|
severity: str
|
|
entity_type: str
|
|
entity_id: Optional[UUID]
|
|
technique_ids: List[str] = []
|
|
tags: List[str] = []
|
|
created_by: Optional[UUID]
|
|
created_at: datetime
|
|
updated_at: datetime
|
|
is_active: bool
|