guardia-itsm/routers/ai_soc.py
2026-06-06 08:13:57 +09:00

439 lines
14 KiB
Python

from __future__ import annotations
from datetime import datetime, timedelta
from typing import Optional, Any
import httpx
from fastapi import APIRouter, Depends, HTTPException, Query
from pydantic import BaseModel
from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession
from core.auth import get_current_user
from database import get_db
from models import User, SecurityEvent, SOARPlaybook, ThreatIntel
router = APIRouter(prefix="/api/soc", tags=["AI-SOC"])
def _tenant(user: User) -> str:
return user.inst_code or str(user.id)
async def _ollama(prompt: str) -> str:
try:
async with httpx.AsyncClient(timeout=30) as c:
r = await c.post(
"http://localhost:11434/api/generate",
json={"model": "llama3", "prompt": prompt, "stream": False},
)
return r.json().get("response", "분석 결과 없음")
except Exception:
return "AI 분석 불가 (Ollama 연결 실패)"
# ── Pydantic 스키마 ────────────────────────────────────────────────────────────
class EventIn(BaseModel):
event_type: str
source_ip: Optional[str] = None
severity: str = "MEDIUM"
description: Optional[str] = None
raw_log: Optional[str] = None
class EventOut(BaseModel):
model_config = {"from_attributes": True}
id: int
event_type: str
source_ip: Optional[str]
severity: str
description: Optional[str]
status: str
created_at: datetime
class ThreatIn(BaseModel):
ioc_type: str
ioc_value: str
threat_type: Optional[str] = None
confidence: float = 0.5
source: Optional[str] = None
expires_days: Optional[int] = None
class PlaybookIn(BaseModel):
name: str
trigger_condition: dict[str, Any]
actions: list[dict[str, Any]]
class PlaybookOut(BaseModel):
model_config = {"from_attributes": True}
id: int
name: str
trigger_condition: Any
actions: Any
is_active: bool
run_count: int
created_at: datetime
class CveImpactIn(BaseModel):
cve_id: str
cvss_score: float
affected_software: Optional[str] = None
# ── 엔드포인트 ─────────────────────────────────────────────────────────────────
@router.post("/events", summary="보안 이벤트 수집")
async def ingest_event(
body: EventIn,
db: AsyncSession = Depends(get_db),
current_user: User = Depends(get_current_user),
):
tid = _tenant(current_user)
valid_severities = {"CRITICAL", "HIGH", "MEDIUM", "LOW"}
if body.severity not in valid_severities:
raise HTTPException(400, f"severity는 {valid_severities} 중 하나여야 합니다")
# 위협 인텔리전스 IP 매칭
matched_threat = None
if body.source_ip:
matched = (await db.execute(
select(ThreatIntel).where(
ThreatIntel.tenant_id == tid,
ThreatIntel.ioc_type == "IP",
ThreatIntel.ioc_value == body.source_ip,
)
)).scalar_one_or_none()
if matched:
matched_threat = matched.threat_type
body.severity = "CRITICAL"
event = SecurityEvent(
tenant_id=tid,
event_type=body.event_type,
source_ip=body.source_ip,
severity=body.severity,
description=body.description,
raw_log=body.raw_log,
)
db.add(event)
await db.commit()
await db.refresh(event)
return {
"id": event.id,
"severity": event.severity,
"threat_matched": matched_threat,
"message": "이벤트 수집 완료",
}
@router.get("/events", summary="보안 이벤트 목록")
async def list_events(
status: Optional[str] = None,
severity: Optional[str] = None,
hours: int = Query(24, ge=1, le=720),
limit: int = Query(50, ge=1, le=500),
db: AsyncSession = Depends(get_db),
current_user: User = Depends(get_current_user),
):
tid = _tenant(current_user)
since = datetime.utcnow() - timedelta(hours=hours)
q = select(SecurityEvent).where(
SecurityEvent.tenant_id == tid,
SecurityEvent.created_at >= since,
)
if status:
q = q.where(SecurityEvent.status == status)
if severity:
q = q.where(SecurityEvent.severity == severity)
q = q.order_by(SecurityEvent.created_at.desc()).limit(limit)
rows = (await db.execute(q)).scalars().all()
return [EventOut.model_validate(r) for r in rows]
@router.post("/correlate", summary="AI 이벤트 상관분석")
async def correlate_events(
hours: int = Query(6, ge=1, le=72),
db: AsyncSession = Depends(get_db),
current_user: User = Depends(get_current_user),
):
tid = _tenant(current_user)
since = datetime.utcnow() - timedelta(hours=hours)
events = (await db.execute(
select(SecurityEvent).where(
SecurityEvent.tenant_id == tid,
SecurityEvent.created_at >= since,
).limit(100)
)).scalars().all()
if not events:
return {"correlation_groups": [], "message": "분석할 이벤트 없음"}
# IP 기반 규칙 상관관계
ip_groups: dict[str, list] = {}
for e in events:
key = e.source_ip or "unknown"
ip_groups.setdefault(key, []).append({"id": e.id, "type": e.event_type, "severity": e.severity})
summary = "\n".join(
f"- IP {ip}: {len(evs)}건 ({', '.join(set(e['type'] for e in evs))})"
for ip, evs in ip_groups.items() if len(evs) > 1
)
prompt = (
f"보안 이벤트 상관분석 ({len(events)}건, 최근 {hours}시간):\n{summary or '단일 이벤트'}\n"
f"공격 패턴, 캠페인 연관성, 권고 조치를 분석하시오."
)
analysis = await _ollama(prompt)
for e in events:
if e.source_ip and ip_groups.get(e.source_ip, []):
e.correlation_id = f"corr-{e.source_ip.replace('.', '-')}"
await db.commit()
return {
"event_count": len(events),
"correlation_groups": [
{"source_ip": ip, "event_count": len(evs), "events": evs}
for ip, evs in ip_groups.items() if len(evs) > 1
],
"ai_analysis": analysis,
}
@router.get("/threats", summary="위협 인텔리전스 피드 목록")
async def list_threats(
active_only: bool = True,
ioc_type: Optional[str] = None,
db: AsyncSession = Depends(get_db),
current_user: User = Depends(get_current_user),
):
tid = _tenant(current_user)
q = select(ThreatIntel).where(ThreatIntel.tenant_id == tid)
if active_only:
q = q.where(
(ThreatIntel.expires_at == None) | (ThreatIntel.expires_at > datetime.utcnow())
)
if ioc_type:
q = q.where(ThreatIntel.ioc_type == ioc_type)
rows = (await db.execute(q)).scalars().all()
return [
{
"id": r.id,
"ioc_type": r.ioc_type,
"ioc_value": r.ioc_value,
"threat_type": r.threat_type,
"confidence": r.confidence,
"source": r.source,
"expires_at": r.expires_at,
}
for r in rows
]
@router.post("/threats", summary="위협 인텔리전스 등록")
async def create_threat(
body: ThreatIn,
db: AsyncSession = Depends(get_db),
current_user: User = Depends(get_current_user),
):
tid = _tenant(current_user)
expires_at = None
if body.expires_days:
expires_at = datetime.utcnow() + timedelta(days=body.expires_days)
intel = ThreatIntel(
tenant_id=tid,
ioc_type=body.ioc_type,
ioc_value=body.ioc_value,
threat_type=body.threat_type,
confidence=body.confidence,
source=body.source,
expires_at=expires_at,
)
db.add(intel)
await db.commit()
await db.refresh(intel)
return {"id": intel.id, "message": "위협 인텔리전스 등록 완료"}
@router.post("/playbooks", summary="SOAR 플레이북 정의 등록")
async def create_playbook(
body: PlaybookIn,
db: AsyncSession = Depends(get_db),
current_user: User = Depends(get_current_user),
):
tid = _tenant(current_user)
pb = SOARPlaybook(
tenant_id=tid,
name=body.name,
trigger_condition=body.trigger_condition,
actions=body.actions,
)
db.add(pb)
await db.commit()
await db.refresh(pb)
return PlaybookOut.model_validate(pb)
@router.get("/playbooks", summary="플레이북 목록")
async def list_playbooks(
db: AsyncSession = Depends(get_db),
current_user: User = Depends(get_current_user),
):
tid = _tenant(current_user)
rows = (await db.execute(
select(SOARPlaybook).where(SOARPlaybook.tenant_id == tid, SOARPlaybook.is_active == True)
)).scalars().all()
return [PlaybookOut.model_validate(r) for r in rows]
@router.post("/playbooks/{playbook_id}/run", summary="플레이북 자동 실행")
async def run_playbook(
playbook_id: int,
event_id: Optional[int] = None,
db: AsyncSession = Depends(get_db),
current_user: User = Depends(get_current_user),
):
tid = _tenant(current_user)
pb = (await db.execute(
select(SOARPlaybook).where(SOARPlaybook.tenant_id == tid, SOARPlaybook.id == playbook_id)
)).scalar_one_or_none()
if not pb:
raise HTTPException(404, "플레이북을 찾을 수 없습니다")
actions_result = []
for action in (pb.actions or []):
action_type = action.get("type", "unknown")
actions_result.append({
"action": action_type,
"status": "EXECUTED",
"timestamp": datetime.utcnow().isoformat(),
})
pb.run_count = (pb.run_count or 0) + 1
await db.commit()
return {
"playbook_id": playbook_id,
"playbook_name": pb.name,
"event_id": event_id,
"actions_executed": len(actions_result),
"results": actions_result,
"run_count": pb.run_count,
}
@router.get("/incidents/{incident_id}/timeline", summary="보안 인시던트 타임라인 자동 재구성")
async def incident_timeline(
incident_id: str,
db: AsyncSession = Depends(get_db),
current_user: User = Depends(get_current_user),
):
tid = _tenant(current_user)
events = (await db.execute(
select(SecurityEvent).where(
SecurityEvent.tenant_id == tid,
SecurityEvent.correlation_id == incident_id,
).order_by(SecurityEvent.created_at.asc())
)).scalars().all()
if not events:
raise HTTPException(404, "해당 인시던트 이벤트를 찾을 수 없습니다")
timeline = [
{
"timestamp": e.created_at.isoformat(),
"event_type": e.event_type,
"source_ip": e.source_ip,
"severity": e.severity,
"description": e.description,
}
for e in events
]
summary_text = "\n".join(
f"{t['timestamp']}: {t['event_type']} ({t['severity']})" for t in timeline
)
prompt = f"보안 인시던트 타임라인 재구성:\n{summary_text}\n공격 흐름을 단계별로 설명하고 대응 조치를 제시하시오."
narrative = await _ollama(prompt)
return {
"incident_id": incident_id,
"event_count": len(events),
"timeline": timeline,
"ai_narrative": narrative,
}
@router.post("/cve-impact", summary="CVE→자산 영향도 분석 + 패치 우선순위")
async def cve_impact(
body: CveImpactIn,
db: AsyncSession = Depends(get_db),
current_user: User = Depends(get_current_user),
):
# CVSS 기반 패치 우선순위 결정
cvss = body.cvss_score
if cvss >= 9.0:
priority = "P1"
sla_days = 3
elif cvss >= 7.0:
priority = "P2"
sla_days = 7
elif cvss >= 4.0:
priority = "P3"
sla_days = 30
else:
priority = "P4"
sla_days = 90
prompt = (
f"CVE 영향도 분석:\n"
f"- CVE ID: {body.cve_id}\n"
f"- CVSS 점수: {body.cvss_score}\n"
f"- 영향 소프트웨어: {body.affected_software or '미지정'}\n"
f"영향받는 시스템 유형, 익스플로잇 가능성, 패치 적용 방법을 간략히 설명하시오."
)
analysis = await _ollama(prompt)
return {
"cve_id": body.cve_id,
"cvss_score": body.cvss_score,
"patch_priority": priority,
"sla_days": sla_days,
"due_date": (datetime.utcnow() + timedelta(days=sla_days)).strftime("%Y-%m-%d"),
"affected_software": body.affected_software,
"ai_analysis": analysis,
}
@router.get("/dashboard", summary="SOC 대시보드")
async def soc_dashboard(
hours: int = Query(24, ge=1, le=720),
db: AsyncSession = Depends(get_db),
current_user: User = Depends(get_current_user),
):
tid = _tenant(current_user)
since = datetime.utcnow() - timedelta(hours=hours)
events = (await db.execute(
select(SecurityEvent).where(
SecurityEvent.tenant_id == tid,
SecurityEvent.created_at >= since,
)
)).scalars().all()
by_severity: dict[str, int] = {}
by_status: dict[str, int] = {}
for e in events:
by_severity[e.severity] = by_severity.get(e.severity, 0) + 1
by_status[e.status] = by_status.get(e.status, 0) + 1
threat_count = (await db.execute(
select(ThreatIntel).where(ThreatIntel.tenant_id == tid)
)).scalars().all()
return {
"period_hours": hours,
"total_events": len(events),
"by_severity": by_severity,
"by_status": by_status,
"active_threats": len(threat_count),
}