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), }