124 lines
5.0 KiB
Python
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} 생성됨")
|