feat(knowledge): Phase 11 — Knowledge Management (Playbooks + Lessons Learned) [FASE-11]
Some checks failed
Aegis CI / lint-and-test (push) Has been cancelled
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>
This commit is contained in:
149
backend/app/schemas/knowledge_schema.py
Normal file
149
backend/app/schemas/knowledge_schema.py
Normal file
@@ -0,0 +1,149 @@
|
||||
"""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
|
||||
Reference in New Issue
Block a user