212 lines
7.8 KiB
Python
212 lines
7.8 KiB
Python
"""
|
|
골든 구성(Golden Config) 정의·버전 관리
|
|
|
|
서버 유형별 기대 설정값을 정의하고 버전을 관리한다.
|
|
내장 CSAP/보안 템플릿 포함.
|
|
|
|
엔드포인트:
|
|
GET /api/goldenconfig/ — 골든 구성 목록
|
|
POST /api/goldenconfig/ — 새 골든 구성 생성
|
|
GET /api/goldenconfig/{id} — 상세 조회
|
|
PUT /api/goldenconfig/{id} — 수정
|
|
DELETE /api/goldenconfig/{id} — 삭제
|
|
GET /api/goldenconfig/templates — 내장 템플릿 목록
|
|
POST /api/goldenconfig/apply-template — 템플릿 적용
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
from datetime import datetime
|
|
from typing import Optional
|
|
|
|
from fastapi import APIRouter, Depends, HTTPException
|
|
from pydantic import BaseModel, Field
|
|
from sqlalchemy import select
|
|
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, GoldenConfig
|
|
|
|
router = APIRouter(prefix="/api/goldenconfig", tags=["골든 구성"])
|
|
|
|
# 내장 보안 템플릿 (CSAP/행안부 보안지침 기반)
|
|
BUILTIN_TEMPLATES = {
|
|
"linux_security_baseline": {
|
|
"name": "Linux 보안 기준선 (행안부 지침)",
|
|
"server_type": "LINUX",
|
|
"items": [
|
|
{"key": "ssh_root_login", "cmd": "grep '^PermitRootLogin' /etc/ssh/sshd_config 2>/dev/null",
|
|
"expected": "PermitRootLogin no", "severity": "HIGH", "description": "SSH root 직접 접속 금지",
|
|
"auto_fix": "sed -i 's/.*PermitRootLogin.*/PermitRootLogin no/' /etc/ssh/sshd_config && systemctl restart sshd"},
|
|
{"key": "password_max_days", "cmd": "grep '^PASS_MAX_DAYS' /etc/login.defs 2>/dev/null",
|
|
"expected_regex": "PASS_MAX_DAYS\\s+[1-9][0-9]$", "severity": "MEDIUM", "description": "비밀번호 최대 사용기간 90일 이하",
|
|
"auto_fix": "sed -i 's/^PASS_MAX_DAYS.*/PASS_MAX_DAYS 90/' /etc/login.defs"},
|
|
{"key": "password_min_days", "cmd": "grep '^PASS_MIN_DAYS' /etc/login.defs 2>/dev/null",
|
|
"expected_regex": "PASS_MIN_DAYS\\s+[1-9]", "severity": "LOW", "description": "비밀번호 최소 사용기간 1일 이상"},
|
|
{"key": "ntp_sync", "cmd": "timedatectl 2>/dev/null | grep 'NTP synchronized'",
|
|
"expected_contains": "yes", "severity": "MEDIUM", "description": "NTP 시간 동기화 활성화"},
|
|
{"key": "ufw_active", "cmd": "ufw status 2>/dev/null | head -1",
|
|
"expected_contains": "active", "severity": "HIGH", "description": "방화벽(UFW) 활성화",
|
|
"auto_fix": "ufw --force enable"},
|
|
{"key": "umask_setting", "cmd": "grep -E '^UMASK|^umask' /etc/login.defs /etc/profile 2>/dev/null | head -1",
|
|
"expected_contains": "022", "severity": "MEDIUM", "description": "UMASK 022 이상 설정"},
|
|
{"key": "passwd_perm", "cmd": "stat -c '%a' /etc/passwd 2>/dev/null",
|
|
"expected": "644", "severity": "HIGH", "description": "/etc/passwd 권한 644"},
|
|
{"key": "shadow_perm", "cmd": "stat -c '%a' /etc/shadow 2>/dev/null",
|
|
"expected": "640", "severity": "HIGH", "description": "/etc/shadow 권한 640"},
|
|
]
|
|
},
|
|
"web_server_baseline": {
|
|
"name": "웹서버 보안 기준선",
|
|
"server_type": "WEB",
|
|
"items": [
|
|
{"key": "nginx_version_hide", "cmd": "grep 'server_tokens' /etc/nginx/nginx.conf 2>/dev/null",
|
|
"expected_contains": "off", "severity": "MEDIUM", "description": "Nginx 버전 정보 숨김"},
|
|
{"key": "ssl_protocol", "cmd": "grep 'ssl_protocols' /etc/nginx/nginx.conf 2>/dev/null",
|
|
"expected_contains": "TLSv1.2", "severity": "HIGH", "description": "TLS 1.2 이상 사용"},
|
|
{"key": "http_headers", "cmd": "curl -sI http://localhost 2>/dev/null | grep -i 'x-powered-by\\|server'",
|
|
"expected_not_contains": "PHP", "severity": "MEDIUM", "description": "서버 기술 정보 미노출"},
|
|
]
|
|
},
|
|
}
|
|
|
|
|
|
class GoldenConfigCreate(BaseModel):
|
|
name: str = Field(..., max_length=200)
|
|
server_type: str = Field(..., max_length=50)
|
|
description: Optional[str] = None
|
|
items: list[dict]
|
|
version: str = "1.0"
|
|
|
|
|
|
class ApplyTemplateRequest(BaseModel):
|
|
template_keys: list[str]
|
|
|
|
|
|
@router.get("/templates")
|
|
async def list_templates(_: User = Depends(get_current_user)):
|
|
return [
|
|
{"key": k, "name": v["name"], "server_type": v["server_type"],
|
|
"item_count": len(v["items"])}
|
|
for k, v in BUILTIN_TEMPLATES.items()
|
|
]
|
|
|
|
|
|
@router.post("/apply-template")
|
|
async def apply_template(
|
|
req: ApplyTemplateRequest,
|
|
db: AsyncSession = Depends(get_db),
|
|
user: User = Depends(require_admin_role),
|
|
):
|
|
created = []
|
|
for key in req.template_keys:
|
|
tpl = BUILTIN_TEMPLATES.get(key)
|
|
if not tpl:
|
|
continue
|
|
existing = await db.execute(
|
|
select(GoldenConfig).where(
|
|
GoldenConfig.tenant_id == user.tenant_id,
|
|
GoldenConfig.name == tpl["name"],
|
|
)
|
|
)
|
|
if existing.scalar_one_or_none():
|
|
continue
|
|
cfg = GoldenConfig(
|
|
tenant_id=user.tenant_id,
|
|
name=tpl["name"],
|
|
server_type=tpl["server_type"],
|
|
items_json=json.dumps(tpl["items"], ensure_ascii=False),
|
|
version="1.0",
|
|
is_active=True,
|
|
created_at=datetime.utcnow(),
|
|
)
|
|
db.add(cfg)
|
|
created.append(tpl["name"])
|
|
await db.commit()
|
|
return {"ok": True, "created": created}
|
|
|
|
|
|
@router.get("/")
|
|
async def list_golden_configs(
|
|
db: AsyncSession = Depends(get_db),
|
|
user: User = Depends(get_current_user),
|
|
):
|
|
rows = await db.execute(
|
|
select(GoldenConfig).where(
|
|
GoldenConfig.tenant_id == user.tenant_id,
|
|
GoldenConfig.is_active == True,
|
|
)
|
|
)
|
|
cfgs = rows.scalars().all()
|
|
return [
|
|
{"id": c.id, "name": c.name, "server_type": c.server_type,
|
|
"version": c.version,
|
|
"item_count": len(json.loads(c.items_json or "[]")),
|
|
"created_at": c.created_at}
|
|
for c in cfgs
|
|
]
|
|
|
|
|
|
@router.post("/")
|
|
async def create_golden_config(
|
|
req: GoldenConfigCreate,
|
|
db: AsyncSession = Depends(get_db),
|
|
user: User = Depends(require_admin_role),
|
|
):
|
|
cfg = GoldenConfig(
|
|
tenant_id=user.tenant_id,
|
|
name=req.name, server_type=req.server_type,
|
|
description=req.description,
|
|
items_json=json.dumps(req.items, ensure_ascii=False),
|
|
version=req.version, is_active=True,
|
|
created_at=datetime.utcnow(),
|
|
)
|
|
db.add(cfg)
|
|
await db.commit()
|
|
await db.refresh(cfg)
|
|
return {"ok": True, "id": cfg.id}
|
|
|
|
|
|
@router.get("/{config_id}")
|
|
async def get_golden_config(
|
|
config_id: int,
|
|
db: AsyncSession = Depends(get_db),
|
|
user: User = Depends(get_current_user),
|
|
):
|
|
row = await db.execute(
|
|
select(GoldenConfig).where(
|
|
GoldenConfig.id == config_id,
|
|
GoldenConfig.tenant_id == user.tenant_id,
|
|
)
|
|
)
|
|
cfg = row.scalar_one_or_none()
|
|
if not cfg:
|
|
raise HTTPException(404)
|
|
return {
|
|
"id": cfg.id, "name": cfg.name, "server_type": cfg.server_type,
|
|
"version": cfg.version,
|
|
"items": json.loads(cfg.items_json or "[]"),
|
|
"created_at": cfg.created_at,
|
|
}
|
|
|
|
|
|
@router.delete("/{config_id}")
|
|
async def delete_golden_config(
|
|
config_id: int,
|
|
db: AsyncSession = Depends(get_db),
|
|
user: User = Depends(require_admin_role),
|
|
):
|
|
row = await db.execute(
|
|
select(GoldenConfig).where(
|
|
GoldenConfig.id == config_id,
|
|
GoldenConfig.tenant_id == user.tenant_id,
|
|
)
|
|
)
|
|
cfg = row.scalar_one_or_none()
|
|
if not cfg:
|
|
raise HTTPException(404)
|
|
cfg.is_active = False
|
|
await db.commit()
|
|
return {"ok": True}
|