Some checks failed
Aegis CI / lint-and-test (push) Has been cancelled
- fix(auth): enforce API key scopes in require_role/require_any_role; attach _api_key_scopes to user on API key auth; add require_scope() dependency — scopes were stored but never enforced (CWE-285) - fix(sso): read SECURE_COOKIES env var for SSO cookie instead of hardcoded secure=False — SAML sessions now respect HTTPS config (CWE-614) - fix(webhooks): SSRF prevention — validate webhook URLs against private and reserved CIDRs at creation/update time (CWE-918) - fix(knowledge): restrict playbook/lesson create, update and restore to admin/red_lead/blue_lead roles — was open to any authenticated user (CWE-284) - fix(alerts): restrict alert acknowledge/resolve/dismiss to admin/lead roles — any user could silence security alerts (CWE-284) - security: delete get_admin_creds.py, check_auth.py, deploy.py scripts containing hardcoded root SSH credentials and production DB access; add scripts/.gitignore to prevent reintroduction (CWE-798)
207 lines
7.3 KiB
Python
207 lines
7.3 KiB
Python
"""Phase 11: Knowledge Management router — Playbooks + Lessons Learned."""
|
|
|
|
from typing import List, Optional
|
|
from uuid import UUID
|
|
|
|
from fastapi import APIRouter, Depends, Query
|
|
from sqlalchemy.orm import Session
|
|
|
|
from app.database import get_db
|
|
from app.dependencies.auth import get_current_user, require_any_role
|
|
from app.schemas.knowledge_schema import (
|
|
PlaybookCreate, PlaybookUpdate, PlaybookOut, PlaybookVersionOut,
|
|
LessonLearnedCreate, LessonLearnedUpdate, LessonLearnedOut,
|
|
)
|
|
from app.services import playbook_service as pb_svc
|
|
from app.services import lesson_learned_service as ll_svc
|
|
|
|
router = APIRouter(prefix="/knowledge", tags=["knowledge"])
|
|
|
|
|
|
# ══════════════════════════════════════════════════════════════════════════════
|
|
# Playbooks
|
|
# ══════════════════════════════════════════════════════════════════════════════
|
|
|
|
@router.get("/playbooks", response_model=List[PlaybookOut])
|
|
def list_playbooks(
|
|
technique_id: Optional[UUID] = None,
|
|
playbook_type: Optional[str] = None,
|
|
include_inactive: bool = False,
|
|
db: Session = Depends(get_db),
|
|
user=Depends(get_current_user),
|
|
):
|
|
return pb_svc.list_playbooks(
|
|
db,
|
|
technique_id=technique_id,
|
|
playbook_type=playbook_type,
|
|
include_inactive=include_inactive,
|
|
)
|
|
|
|
|
|
@router.post("/playbooks", response_model=PlaybookOut, status_code=201)
|
|
def create_playbook(
|
|
body: PlaybookCreate,
|
|
db: Session = Depends(get_db),
|
|
user=Depends(require_any_role("admin", "red_lead", "blue_lead")),
|
|
):
|
|
return pb_svc.create_playbook(db, body.model_dump(), user.id)
|
|
|
|
|
|
@router.get("/playbooks/{playbook_id}", response_model=PlaybookOut)
|
|
def get_playbook(
|
|
playbook_id: UUID,
|
|
db: Session = Depends(get_db),
|
|
user=Depends(get_current_user),
|
|
):
|
|
return pb_svc.get_playbook(db, playbook_id)
|
|
|
|
|
|
@router.patch("/playbooks/{playbook_id}", response_model=PlaybookOut)
|
|
def update_playbook(
|
|
playbook_id: UUID,
|
|
body: PlaybookUpdate,
|
|
db: Session = Depends(get_db),
|
|
user=Depends(require_any_role("admin", "red_lead", "blue_lead")),
|
|
):
|
|
return pb_svc.update_playbook(db, playbook_id, body.model_dump(exclude_unset=True), user.id)
|
|
|
|
|
|
@router.delete("/playbooks/{playbook_id}", status_code=204)
|
|
def delete_playbook(
|
|
playbook_id: UUID,
|
|
db: Session = Depends(get_db),
|
|
user=Depends(require_any_role("admin", "red_lead", "blue_lead")),
|
|
):
|
|
pb_svc.delete_playbook(db, playbook_id, user.id)
|
|
|
|
|
|
# ── Versions ──────────────────────────────────────────────────────────────────
|
|
|
|
@router.get("/playbooks/{playbook_id}/versions", response_model=List[PlaybookVersionOut])
|
|
def list_versions(
|
|
playbook_id: UUID,
|
|
db: Session = Depends(get_db),
|
|
user=Depends(get_current_user),
|
|
):
|
|
return pb_svc.get_playbook_versions(db, playbook_id)
|
|
|
|
|
|
@router.post("/playbooks/{playbook_id}/restore/{version}", response_model=PlaybookOut)
|
|
def restore_version(
|
|
playbook_id: UUID,
|
|
version: int,
|
|
db: Session = Depends(get_db),
|
|
user=Depends(require_any_role("admin", "red_lead", "blue_lead")),
|
|
):
|
|
"""Roll the playbook back to a specific historical version."""
|
|
return pb_svc.restore_version(db, playbook_id, version, user.id)
|
|
|
|
|
|
# ── By technique (convenience) ────────────────────────────────────────────────
|
|
|
|
@router.get(
|
|
"/techniques/{technique_id}/playbooks",
|
|
response_model=List[PlaybookOut],
|
|
)
|
|
def playbooks_for_technique(
|
|
technique_id: UUID,
|
|
db: Session = Depends(get_db),
|
|
user=Depends(get_current_user),
|
|
):
|
|
"""List all active playbooks for a specific technique."""
|
|
return pb_svc.list_playbooks(db, technique_id=technique_id)
|
|
|
|
|
|
@router.get(
|
|
"/techniques/{technique_id}/playbooks/{playbook_type}",
|
|
response_model=PlaybookOut,
|
|
)
|
|
def get_playbook_by_technique_type(
|
|
technique_id: UUID,
|
|
playbook_type: str,
|
|
db: Session = Depends(get_db),
|
|
user=Depends(get_current_user),
|
|
):
|
|
pb = pb_svc.get_playbook_by_technique_type(db, technique_id, playbook_type)
|
|
if not pb:
|
|
from app.domain.errors import EntityNotFoundError
|
|
raise EntityNotFoundError("Playbook", f"{technique_id}/{playbook_type}")
|
|
return pb
|
|
|
|
|
|
# ══════════════════════════════════════════════════════════════════════════════
|
|
# Lessons Learned
|
|
# ══════════════════════════════════════════════════════════════════════════════
|
|
|
|
@router.get("/lessons", response_model=List[LessonLearnedOut])
|
|
def list_lessons(
|
|
entity_type: Optional[str] = None,
|
|
entity_id: Optional[UUID] = None,
|
|
severity: Optional[str] = None,
|
|
tag: Optional[str] = None,
|
|
technique_id: Optional[str] = None,
|
|
include_inactive: bool = False,
|
|
db: Session = Depends(get_db),
|
|
user=Depends(get_current_user),
|
|
):
|
|
return ll_svc.list_lessons_learned(
|
|
db,
|
|
entity_type=entity_type,
|
|
entity_id=entity_id,
|
|
severity=severity,
|
|
tag=tag,
|
|
technique_id=technique_id,
|
|
include_inactive=include_inactive,
|
|
)
|
|
|
|
|
|
@router.post("/lessons", response_model=LessonLearnedOut, status_code=201)
|
|
def create_lesson(
|
|
body: LessonLearnedCreate,
|
|
db: Session = Depends(get_db),
|
|
user=Depends(require_any_role("admin", "red_lead", "blue_lead")),
|
|
):
|
|
return ll_svc.create_lesson_learned(db, body.model_dump(), user.id)
|
|
|
|
|
|
@router.get("/lessons/{lesson_id}", response_model=LessonLearnedOut)
|
|
def get_lesson(
|
|
lesson_id: UUID,
|
|
db: Session = Depends(get_db),
|
|
user=Depends(get_current_user),
|
|
):
|
|
return ll_svc.get_lesson_learned(db, lesson_id)
|
|
|
|
|
|
@router.patch("/lessons/{lesson_id}", response_model=LessonLearnedOut)
|
|
def update_lesson(
|
|
lesson_id: UUID,
|
|
body: LessonLearnedUpdate,
|
|
db: Session = Depends(get_db),
|
|
user=Depends(require_any_role("admin", "red_lead", "blue_lead")),
|
|
):
|
|
return ll_svc.update_lesson_learned(
|
|
db, lesson_id, body.model_dump(exclude_unset=True), user.id
|
|
)
|
|
|
|
|
|
@router.delete("/lessons/{lesson_id}", status_code=204)
|
|
def delete_lesson(
|
|
lesson_id: UUID,
|
|
db: Session = Depends(get_db),
|
|
user=Depends(require_any_role("admin", "red_lead", "blue_lead")),
|
|
):
|
|
"""Soft-delete a lesson (admin / lead only)."""
|
|
ll_svc.delete_lesson_learned(db, lesson_id, user.id)
|
|
|
|
|
|
# ── Stats ─────────────────────────────────────────────────────────────────────
|
|
|
|
@router.get("/stats")
|
|
def knowledge_stats(
|
|
db: Session = Depends(get_db),
|
|
user=Depends(get_current_user),
|
|
):
|
|
"""Summary counts: total playbooks, lessons by severity, playbooks by type."""
|
|
return ll_svc.get_knowledge_stats(db)
|