"""Zero Trust Network Access — 정책 엔진 + 디바이스 상태 검증""" from __future__ import annotations import json, logging from datetime import datetime from typing import Optional from fastapi import APIRouter, Depends, HTTPException from pydantic import BaseModel from sqlalchemy import select, desc from sqlalchemy.ext.asyncio import AsyncSession from core.auth import get_current_user, require_admin_role from database import get_db from models import User, ZTNAPolicy, ZTNADevice, ZTNAViolation logger = logging.getLogger(__name__) router = APIRouter(prefix="/api/ztna", tags=["Zero Trust"]) class PolicyCreate(BaseModel): name: str; resource: str; allowed_roles: list = [] require_mfa: bool = True; min_trust_score: int = 70 allowed_ips: list = []; require_device_compliant: bool = True is_active: bool = True class DeviceRegister(BaseModel): device_name: str; device_type: str # PC|MOBILE|SERVER os_name: str; os_version: str antivirus_active: bool = False; disk_encrypted: bool = False last_patch_days: int = 999 # 마지막 패치 경과일 class VerifyRequest(BaseModel): user_id: int; device_id: Optional[int] = None resource: str; source_ip: Optional[str] = None mfa_verified: bool = False def _calc_trust_score(device: Optional[ZTNADevice], mfa: bool, source_ip: str) -> int: score = 50 if mfa: score += 20 if device: if device.antivirus_active: score += 10 if device.disk_encrypted: score += 10 if device.last_patch_days <= 30: score += 10 elif device.last_patch_days > 90: score -= 15 return max(0, min(100, score)) @router.get("/policies") async def list_policies(db: AsyncSession = Depends(get_db), user: User = Depends(get_current_user)): rows = await db.execute(select(ZTNAPolicy).order_by(ZTNAPolicy.name)) return [{"id":p.id,"name":p.name,"resource":p.resource,"min_trust_score":p.min_trust_score, "require_mfa":p.require_mfa,"is_active":p.is_active} for p in rows.scalars().all()] @router.post("/policies", status_code=201) async def create_policy(body: PolicyCreate, db: AsyncSession = Depends(get_db), user: User = Depends(require_admin_role)): p = ZTNAPolicy(**body.model_dump(), created_by=user.id, created_at=datetime.utcnow()) db.add(p); await db.commit(); await db.refresh(p) return {"id": p.id} @router.put("/policies/{pid}") async def update_policy(pid: int, body: PolicyCreate, db: AsyncSession = Depends(get_db), user: User = Depends(require_admin_role)): row = await db.execute(select(ZTNAPolicy).where(ZTNAPolicy.id == pid)) p = row.scalar_one_or_none() if not p: raise HTTPException(404) for k, v in body.model_dump().items(): setattr(p, k, v) await db.commit(); return {"ok": True} @router.post("/verify") async def verify_access(body: VerifyRequest, db: AsyncSession = Depends(get_db), user: User = Depends(get_current_user)): """접속 요청 검증 — Zero Trust 결정 엔진.""" # 해당 리소스의 정책 조회 row = await db.execute( select(ZTNAPolicy).where(ZTNAPolicy.resource == body.resource, ZTNAPolicy.is_active == True) ) policy = row.scalar_one_or_none() if not policy: return {"allowed": True, "reason": "정책 없음 — 기본 허용", "trust_score": 50} device = None if body.device_id: dr = await db.execute(select(ZTNADevice).where(ZTNADevice.id == body.device_id)) device = dr.scalar_one_or_none() trust_score = _calc_trust_score(device, body.mfa_verified, body.source_ip or "") reasons = [] if policy.require_mfa and not body.mfa_verified: reasons.append("MFA 미인증") if trust_score < policy.min_trust_score: reasons.append(f"신뢰 점수 부족 ({trust_score}/{policy.min_trust_score})") if policy.require_device_compliant and not device: reasons.append("등록된 디바이스 없음") allowed = len(reasons) == 0 if not allowed: db.add(ZTNAViolation( user_id=body.user_id, resource=body.resource, reason=", ".join(reasons), source_ip=body.source_ip, trust_score=trust_score, created_at=datetime.utcnow() )) await db.commit() return {"allowed": allowed, "trust_score": trust_score, "reasons": reasons, "policy": policy.name} @router.get("/devices") async def list_devices(db: AsyncSession = Depends(get_db), user: User = Depends(get_current_user)): rows = await db.execute(select(ZTNADevice).order_by(desc(ZTNADevice.registered_at))) return [{"id":d.id,"device_name":d.device_name,"device_type":d.device_type, "os_name":d.os_name,"compliant":d.is_compliant} for d in rows.scalars().all()] @router.post("/devices/register", status_code=201) async def register_device(body: DeviceRegister, db: AsyncSession = Depends(get_db), user: User = Depends(get_current_user)): is_compliant = (body.antivirus_active and body.disk_encrypted and body.last_patch_days <= 90) d = ZTNADevice( **body.model_dump(), is_compliant=is_compliant, registered_by=user.id, registered_at=datetime.utcnow() ) db.add(d); await db.commit(); await db.refresh(d) return {"id": d.id, "is_compliant": is_compliant} @router.get("/violations") async def list_violations(limit: int = 50, db: AsyncSession = Depends(get_db), user: User = Depends(require_admin_role)): rows = await db.execute(select(ZTNAViolation).order_by(desc(ZTNAViolation.created_at)).limit(limit)) return [{"id":v.id,"user_id":v.user_id,"resource":v.resource,"reason":v.reason, "trust_score":v.trust_score,"created_at":v.created_at} for v in rows.scalars().all()] @router.get("/segments") async def list_segments(db: AsyncSession = Depends(get_db), user: User = Depends(get_current_user)): """마이크로세그멘테이션 구성 (정책 기반).""" rows = await db.execute(select(ZTNAPolicy).where(ZTNAPolicy.is_active == True)) policies = rows.scalars().all() return [{"segment": p.resource, "min_trust": p.min_trust_score, "require_mfa": p.require_mfa, "require_device": p.require_device_compliant} for p in policies]