151 lines
6.5 KiB
Python
151 lines
6.5 KiB
Python
"""SBOM — Software Bill of Materials (CycloneDX/SPDX) 생성·관리"""
|
|
from __future__ import annotations
|
|
import json, logging, uuid
|
|
from datetime import datetime
|
|
from typing import Optional
|
|
from fastapi import APIRouter, Depends, HTTPException, Response
|
|
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, SBOMRecord, SBOMComponent
|
|
|
|
logger = logging.getLogger(__name__)
|
|
router = APIRouter(prefix="/api/sbom", tags=["SBOM"])
|
|
|
|
|
|
async def _ssh_scan_packages(server_id: int, db) -> list:
|
|
"""에이전트리스 SSH로 서버 패키지 목록 수집."""
|
|
# 실제 구현: paramiko SSH 실행
|
|
# 여기서는 샘플 데이터 반환
|
|
return [
|
|
{"name": "python3", "version": "3.11.0", "type": "runtime"},
|
|
{"name": "fastapi", "version": "0.100.0", "type": "library", "ecosystem": "pypi"},
|
|
{"name": "sqlalchemy", "version": "2.0.0", "type": "library", "ecosystem": "pypi"},
|
|
{"name": "nginx", "version": "1.24.0", "type": "system", "ecosystem": "apt"},
|
|
{"name": "postgresql", "version": "16.0", "type": "system", "ecosystem": "apt"},
|
|
]
|
|
|
|
|
|
def _build_cyclonedx(sbom: SBOMRecord, components: list) -> dict:
|
|
return {
|
|
"bomFormat": "CycloneDX",
|
|
"specVersion": "1.4",
|
|
"serialNumber": f"urn:uuid:{uuid.uuid4()}",
|
|
"version": 1,
|
|
"metadata": {
|
|
"timestamp": datetime.utcnow().isoformat() + "Z",
|
|
"tools": [{"vendor": "GUARDiA", "name": "sbom-scanner", "version": "1.0"}],
|
|
"component": {"type": "container", "name": f"server-{sbom.server_id}",
|
|
"version": "1.0", "bom-ref": f"server-{sbom.server_id}"}
|
|
},
|
|
"components": [
|
|
{"type": "library", "name": c["name"], "version": c["version"],
|
|
"purl": f"pkg:{c.get('ecosystem','generic')}/{c['name']}@{c['version']}"}
|
|
for c in components
|
|
]
|
|
}
|
|
|
|
|
|
class VEXStatement(BaseModel):
|
|
sbom_id: int; component_name: str; cve_id: str
|
|
status: str # not_affected | affected | fixed | under_investigation
|
|
justification: str = ""
|
|
|
|
|
|
@router.post("/generate")
|
|
async def generate_sbom(server_id: int, db: AsyncSession = Depends(get_db),
|
|
user: User = Depends(get_current_user)):
|
|
packages = await _ssh_scan_packages(server_id, db)
|
|
record = SBOMRecord(
|
|
server_id=server_id, format="CycloneDX", spec_version="1.4",
|
|
component_count=len(packages), status="COMPLETED",
|
|
generated_by=user.id, created_at=datetime.utcnow()
|
|
)
|
|
db.add(record); await db.commit(); await db.refresh(record)
|
|
|
|
for pkg in packages:
|
|
db.add(SBOMComponent(
|
|
sbom_id=record.id, name=pkg["name"], version=pkg["version"],
|
|
component_type=pkg.get("type", "library"), ecosystem=pkg.get("ecosystem", ""),
|
|
purl=f"pkg:{pkg.get('ecosystem','generic')}/{pkg['name']}@{pkg['version']}"
|
|
))
|
|
await db.commit()
|
|
return {"sbom_id": record.id, "server_id": server_id, "component_count": len(packages)}
|
|
|
|
|
|
@router.get("/list")
|
|
async def list_sboms(db: AsyncSession = Depends(get_db), user: User = Depends(get_current_user)):
|
|
rows = await db.execute(select(SBOMRecord).order_by(desc(SBOMRecord.created_at)).limit(50))
|
|
return [{"id":s.id,"server_id":s.server_id,"format":s.format,
|
|
"component_count":s.component_count,"status":s.status,"created_at":s.created_at}
|
|
for s in rows.scalars().all()]
|
|
|
|
|
|
@router.get("/{sbom_id}")
|
|
async def get_sbom(sbom_id: int, db: AsyncSession = Depends(get_db),
|
|
user: User = Depends(get_current_user)):
|
|
row = await db.execute(select(SBOMRecord).where(SBOMRecord.id == sbom_id))
|
|
sbom = row.scalar_one_or_none()
|
|
if not sbom: raise HTTPException(404)
|
|
comp_rows = await db.execute(select(SBOMComponent).where(SBOMComponent.sbom_id == sbom_id))
|
|
comps = comp_rows.scalars().all()
|
|
return {
|
|
"id": sbom.id, "server_id": sbom.server_id, "format": sbom.format,
|
|
"created_at": sbom.created_at,
|
|
"components": [{"name":c.name,"version":c.version,"purl":c.purl,"ecosystem":c.ecosystem}
|
|
for c in comps]
|
|
}
|
|
|
|
|
|
@router.get("/{sbom_id}/export")
|
|
async def export_sbom(sbom_id: int, db: AsyncSession = Depends(get_db),
|
|
user: User = Depends(get_current_user)):
|
|
row = await db.execute(select(SBOMRecord).where(SBOMRecord.id == sbom_id))
|
|
sbom = row.scalar_one_or_none()
|
|
if not sbom: raise HTTPException(404)
|
|
comp_rows = await db.execute(select(SBOMComponent).where(SBOMComponent.sbom_id == sbom_id))
|
|
comps = [{"name":c.name,"version":c.version,"type":c.component_type,"ecosystem":c.ecosystem}
|
|
for c in comp_rows.scalars().all()]
|
|
cyclonedx = _build_cyclonedx(sbom, comps)
|
|
return Response(content=json.dumps(cyclonedx, ensure_ascii=False, indent=2),
|
|
media_type="application/json",
|
|
headers={"Content-Disposition": f"attachment; filename=sbom-{sbom_id}.cdx.json"})
|
|
|
|
|
|
@router.post("/{sbom_id}/scan")
|
|
async def scan_vulnerabilities(sbom_id: int, db: AsyncSession = Depends(get_db),
|
|
user: User = Depends(get_current_user)):
|
|
"""SBOM 컴포넌트 → CVE 매핑 (vuln_scan.py 연동)."""
|
|
comp_rows = await db.execute(select(SBOMComponent).where(SBOMComponent.sbom_id == sbom_id))
|
|
comps = comp_rows.scalars().all()
|
|
vuln_count = 0
|
|
for c in comps:
|
|
# 실제: NVD/KISA CVE DB 조회
|
|
if c.name in ("python3", "nginx", "openssl"):
|
|
c.cve_ids = '["CVE-2024-SAMPLE"]'
|
|
vuln_count += 1
|
|
await db.commit()
|
|
return {"sbom_id": sbom_id, "scanned": len(comps), "vulnerable": vuln_count}
|
|
|
|
|
|
@router.get("/dashboard")
|
|
async def sbom_dashboard(db: AsyncSession = Depends(get_db), user: User = Depends(get_current_user)):
|
|
from sqlalchemy import func as sa_func
|
|
total = (await db.execute(select(sa_func.count(SBOMRecord.id)))).scalar() or 0
|
|
return {"total_sboms": total, "format_breakdown": {"CycloneDX": total}}
|
|
|
|
|
|
@router.post("/vex")
|
|
async def create_vex(body: VEXStatement, db: AsyncSession = Depends(get_db),
|
|
user: User = Depends(get_current_user)):
|
|
return {
|
|
"ok": True, "vex": {
|
|
"sbom_id": body.sbom_id, "component": body.component_name,
|
|
"cve": body.cve_id, "status": body.status,
|
|
"justification": body.justification,
|
|
"timestamp": datetime.utcnow().isoformat()
|
|
}
|
|
}
|