""" CVE 패치 현황 API (모바일 기능 #82, #83). GET /api/patches/cve — CVE 목록 (severity 필터) GET /api/patches/status — 서버별 패치 적용률 (IP 노출 금지) GET /api/patches/pending — 미적용 패치 목록 GET /api/patches/pii-types — PII 데이터 처리 유형 목록 POST /api/patches/{cve_id}/apply — 패치 적용 SR 자동 생성 """ from __future__ import annotations import hashlib from datetime import datetime, date from typing import List, Optional from fastapi import APIRouter, Depends from pydantic import BaseModel, ConfigDict from sqlalchemy import select, func from sqlalchemy.ext.asyncio import AsyncSession from core.auth import get_current_user from database import get_db from models import AuditLog, SRRequest, SRStatus, SRType, Priority, User router = APIRouter(prefix="/api/patches", tags=["Patches"]) _MOCK_CVE = [ {"id": "CVE-2024-1001", "severity": "critical", "title": "OpenSSL 원격 코드 실행", "affected": "OpenSSL < 3.2.1", "cvss": 9.8, "patch_available": True, "patched_servers": 3, "total_servers": 10}, {"id": "CVE-2024-1002", "severity": "high", "title": "Apache httpd 디렉토리 탐색", "affected": "Apache < 2.4.59", "cvss": 7.5, "patch_available": True, "patched_servers": 8, "total_servers": 10}, {"id": "CVE-2024-1003", "severity": "high", "title": "Linux 커널 권한 상승", "affected": "kernel < 6.8.2", "cvss": 7.8, "patch_available": True, "patched_servers": 5, "total_servers": 10}, {"id": "CVE-2024-1004", "severity": "medium", "title": "SSH 취약 암호화 허용", "affected": "OpenSSH < 9.7", "cvss": 5.3, "patch_available": True, "patched_servers": 9, "total_servers": 10}, {"id": "CVE-2024-1005", "severity": "medium", "title": "Python urllib SSRF", "affected": "Python < 3.12.3", "cvss": 5.9, "patch_available": False, "patched_servers": 0, "total_servers": 10}, ] _MOCK_SERVERS = [ {"name": "WEB-01", "role": "웹서버", "patch_rate": 85, "pending": 2}, {"name": "WEB-02", "role": "웹서버", "patch_rate": 70, "pending": 4}, {"name": "APP-01", "role": "앱서버", "patch_rate": 95, "pending": 1}, {"name": "APP-02", "role": "앱서버", "patch_rate": 60, "pending": 5}, {"name": "DB-01", "role": "DB서버", "patch_rate": 100, "pending": 0}, ] _PII_TYPES = [ {"code": "PII_001", "name": "주민등록번호", "storage": "암호화 DB", "retention": "5년", "status": "compliant"}, {"code": "PII_002", "name": "연락처", "storage": "암호화 DB", "retention": "3년", "status": "compliant"}, {"code": "PII_003", "name": "이메일", "storage": "평문 로그", "retention": "미정", "status": "non_compliant"}, {"code": "PII_004", "name": "IP 주소", "storage": "감사 로그", "retention": "1년", "status": "compliant"}, ] class PatchApplyOut(BaseModel): sr_id: int message: str @router.get("/cve") async def list_cve(severity: Optional[str] = None): data = _MOCK_CVE if severity: data = [c for c in data if c["severity"] == severity] return {"total": len(data), "items": data} @router.get("/status") async def patch_status(): total_rate = round(sum(s["patch_rate"] for s in _MOCK_SERVERS) / len(_MOCK_SERVERS), 1) return {"overall_patch_rate": total_rate, "servers": _MOCK_SERVERS} @router.get("/pending") async def pending_patches(): pending = [c for c in _MOCK_CVE if c["patched_servers"] < c["total_servers"]] return {"total": len(pending), "items": pending} @router.get("/pii-types") async def pii_types(): return {"items": _PII_TYPES} @router.post("/{cve_id}/apply", response_model=PatchApplyOut, status_code=201) async def apply_patch( cve_id: str, db: AsyncSession = Depends(get_db), current_user: User = Depends(get_current_user), ): """패치 적용 SR 자동 생성.""" cve = next((c for c in _MOCK_CVE if c["id"] == cve_id), None) title = f"[패치] {cve['title'] if cve else cve_id} 적용" sr = SRRequest( sr_type=SRType.OTHER, title=title, description=f"CVE ID: {cve_id}\n적용 대상: {cve['affected'] if cve else '전체 서버'}", status=SRStatus.RECEIVED, priority=Priority.HIGH, requested_by=current_user.username, ) db.add(sr) await db.flush() prev = await db.execute( select(AuditLog).order_by(AuditLog.id.desc()).limit(1) ) prev_row = prev.scalar_one_or_none() prev_hash = prev_row.log_hash if prev_row else "0" * 64 ts = datetime.now() raw = f"{prev_hash}|{current_user.username}|PATCH_SR_CREATE|{title}|{ts.isoformat()}" log_hash = hashlib.sha256(raw.encode()).hexdigest() audit = AuditLog( entity_type="sr_request", entity_id=str(sr.sr_id), actor=current_user.username, action="PATCH_SR_CREATE", detail=f"CVE {cve_id} 패치 적용 SR 생성", log_hash=log_hash, prev_hash=prev_hash, created_at=ts, ) db.add(audit) await db.commit() return PatchApplyOut(sr_id=sr.sr_id, message=f"SR #{sr.sr_id} 생성됨")