guardia-itsm/routers/patches.py

124 lines
5.0 KiB
Python

"""
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} 생성됨")