guardia-itsm/routers/ztna.py
2026-06-03 08:04:03 +09:00

157 lines
6.3 KiB
Python

"""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]