157 lines
6.3 KiB
Python
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]
|