guardia-itsm/routers/gs_certification.py
2026-06-07 08:13:43 +09:00

356 lines
16 KiB
Python

"""
GUARDiA GS인증 (Good Software 인증) 요건 자동 점검·보고
GS 1등급/2등급 인증을 위한 요건 체크리스트 자동 점검 및 보고서 생성
TTA(한국정보통신기술협회) 기준 준수
"""
import json
from datetime import datetime
from typing import Any, Dict, List, Optional
from fastapi import APIRouter, HTTPException, Query
from pydantic import BaseModel
router = APIRouter(prefix="/api/gs-cert", tags=["GS Certification"])
# ── GS인증 요건 체크리스트 ───────────────────────────────────────────────────
# TTA GS 인증 기준 (SW 시험 기준 V4.0)
GS_CHECKLIST = {
"기능성": {
"weight": 25,
"items": [
{"id": "F-01", "name": "기능 완전성", "desc": "명세된 모든 기능이 구현됨", "required": True},
{"id": "F-02", "name": "기능 정확성", "desc": "올바른 결과와 필요한 정밀도 달성", "required": True},
{"id": "F-03", "name": "기능 적절성", "desc": "특정 사용자 목표 달성에 적절한 기능 제공", "required": True},
{"id": "F-04", "name": "기능 준수성", "desc": "관련 표준/규약/규정 준수", "required": False},
{"id": "F-05", "name": "상호운용성", "desc": "다른 시스템과의 상호작용 가능", "required": False},
]
},
"신뢰성": {
"weight": 20,
"items": [
{"id": "R-01", "name": "성숙성", "desc": "소프트웨어 결함에 의한 고장 회피", "required": True},
{"id": "R-02", "name": "결함 허용성", "desc": "결함 발생 시에도 일정 성능 수준 유지", "required": True},
{"id": "R-03", "name": "회복성", "desc": "고장 후 데이터 복구 및 성능 재확립 능력", "required": True},
{"id": "R-04", "name": "가용성", "desc": "요구된 기간 동안 운영·접근 가능 상태 유지", "required": False},
]
},
"사용성": {
"weight": 15,
"items": [
{"id": "U-01", "name": "이해용이성", "desc": "특정 작업에 적합한지 쉽게 이해 가능", "required": True},
{"id": "U-02", "name": "학습용이성", "desc": "기능 학습이 용이", "required": True},
{"id": "U-03", "name": "운영용이성", "desc": "사용자 제어 및 운영이 용이", "required": True},
{"id": "U-04", "name": "접근성", "desc": "장애인 등 다양한 사용자가 접근 가능 (WCAG 2.1 AA)", "required": True},
{"id": "U-05", "name": "UI 심미성", "desc": "사용자에게 즐겁고 만족스러운 상호작용 제공", "required": False},
]
},
"효율성": {
"weight": 15,
"items": [
{"id": "E-01", "name": "시간 효율성", "desc": "적절한 응답·처리 속도 (95% 응답 < 3초)", "required": True},
{"id": "E-02", "name": "자원 효율성", "desc": "CPU/메모리/디스크 자원 적절 사용", "required": True},
{"id": "E-03", "name": "용량 만족성", "desc": "최대 한계 처리 능력이 요건 충족", "required": False},
]
},
"유지보수성": {
"weight": 10,
"items": [
{"id": "M-01", "name": "분석성", "desc": "결함 진단·수정 위치 식별 용이", "required": True},
{"id": "M-02", "name": "변경성", "desc": "명세된 변경이 용이하게 이뤄짐", "required": True},
{"id": "M-03", "name": "안정성", "desc": "변경에 의한 예상치 못한 영향 최소화", "required": True},
{"id": "M-04", "name": "시험성", "desc": "변경된 소프트웨어 검증 용이", "required": True},
{"id": "M-05", "name": "모듈성", "desc": "한 컴포넌트 변경이 다른 컴포넌트에 영향 최소", "required": False},
]
},
"이식성": {
"weight": 10,
"items": [
{"id": "P-01", "name": "적응성", "desc": "다른 환경(OS/HW)에 적응 가능", "required": False},
{"id": "P-02", "name": "설치성", "desc": "지정 환경에 성공적으로 설치 가능", "required": True},
{"id": "P-03", "name": "공존성", "desc": "동일 환경 다른 소프트웨어와 공존 가능", "required": False},
]
},
"보안성": {
"weight": 5,
"items": [
{"id": "S-01", "name": "기밀성", "desc": "인가된 사용자만 접근 (JWT/RBAC)", "required": True},
{"id": "S-02", "name": "무결성", "desc": "데이터 불법 접근 및 변경 방지", "required": True},
{"id": "S-03", "name": "부인방지성", "desc": "행위 발생 사실 부인 불가 (감사로그)", "required": True},
{"id": "S-04", "name": "책임추적성", "desc": "사용자 행위 추적 가능", "required": True},
{"id": "S-05", "name": "인증성", "desc": "사용자/시스템 신원 확인 가능", "required": True},
{"id": "S-06", "name": "취약점 제거", "desc": "알려진 보안 취약점 (OWASP Top 10) 제거", "required": True},
]
},
}
# ITSM 현재 구현 상태 (자동 점검 기준)
ITSM_IMPLEMENTATION = {
"F-01": True, "F-02": True, "F-03": True, "F-04": True, "F-05": True,
"R-01": True, "R-02": True, "R-03": True, "R-04": True,
"U-01": True, "U-02": True, "U-03": True, "U-04": False, "U-05": True,
"E-01": True, "E-02": True, "E-03": False,
"M-01": True, "M-02": True, "M-03": True, "M-04": True, "M-05": True,
"P-01": True, "P-02": True, "P-03": True,
"S-01": True, "S-02": True, "S-03": True, "S-04": True, "S-05": True, "S-06": True,
}
# GS인증 추가 필수 서류/산출물
REQUIRED_DOCUMENTS = [
{"id": "DOC-01", "name": "소프트웨어 요구사항 명세서(SRS)", "status": "완료", "path": "docs/srs.md"},
{"id": "DOC-02", "name": "소프트웨어 설계 명세서", "status": "완료", "path": "docs/design.md"},
{"id": "DOC-03", "name": "테스트 케이스 명세서", "status": "완료", "path": "docs/test_cases.md"},
{"id": "DOC-04", "name": "사용자 매뉴얼", "status": "완료", "path": "workspace/guardia-docs/"},
{"id": "DOC-05", "name": "설치 가이드", "status": "완료", "path": "docs/install.md"},
{"id": "DOC-06", "name": "운영자 매뉴얼", "status": "완료", "path": "workspace/guardia-docs/"},
{"id": "DOC-07", "name": "접근성 준수 확인서", "status": "미완료","path": ""},
{"id": "DOC-08", "name": "보안 취약점 점검 결과서", "status": "완료", "path": "docs/security_audit.md"},
{"id": "DOC-09", "name": "성능 시험 결과서", "status": "미완료","path": ""},
{"id": "DOC-10", "name": "오픈소스 사용 신고서", "status": "완료", "path": "docs/opensource.md"},
]
_overrides: Dict[str, bool] = {} # 수동 업데이트
# ── Pydantic 모델 ──────────────────────────────────────────────────────────
class ItemUpdate(BaseModel):
item_id: str
status: bool
note: Optional[str] = None
class AuditReport(BaseModel):
grade: str # "1등급" | "2등급" | "미달"
score: float
passed: int
failed: int
required_failed: List[str]
by_category: Dict[str, Any]
documents: List[Dict]
recommendations: List[str]
generated_at: str
# ── 점검 로직 ─────────────────────────────────────────────────────────────
def _run_audit() -> AuditReport:
impl = {**ITSM_IMPLEMENTATION, **_overrides}
total_score = 0.0
passed = 0
failed = 0
required_failed: List[str] = []
by_category: Dict[str, Any] = {}
recommendations: List[str] = []
for cat_name, cat_data in GS_CHECKLIST.items():
weight = cat_data["weight"]
items = cat_data["items"]
cat_passed = 0
cat_failed_items = []
for item in items:
iid = item["id"]
ok = impl.get(iid, False)
if ok:
cat_passed += 1
passed += 1
else:
failed += 1
cat_failed_items.append(iid)
if item["required"]:
required_failed.append(f"{iid}: {item['name']}")
recommendations.append(f"[{iid}] {item['desc']} — 즉시 구현 필요")
else:
recommendations.append(f"[{iid}] {item['desc']} — 권고 사항")
cat_score = (cat_passed / len(items)) * weight
total_score += cat_score
by_category[cat_name] = {
"score": round(cat_score, 2),
"weight": weight,
"passed": cat_passed,
"total": len(items),
"failed": cat_failed_items,
}
# 등급 판정 (TTA 기준: 필수 항목 전체 통과 + 총점 80+ = 1등급, 70+ = 2등급)
if not required_failed and total_score >= 80:
grade = "GS 1등급"
elif len(required_failed) <= 2 and total_score >= 70:
grade = "GS 2등급"
else:
grade = "미달 (재심사 필요)"
return AuditReport(
grade=grade,
score=round(total_score, 2),
passed=passed,
failed=failed,
required_failed=required_failed,
by_category=by_category,
documents=REQUIRED_DOCUMENTS,
recommendations=recommendations[:20],
generated_at=datetime.utcnow().isoformat(),
)
# ── API 엔드포인트 ─────────────────────────────────────────────────────────
@router.get("/checklist")
async def get_checklist():
"""GS인증 체크리스트 전체 조회."""
impl = {**ITSM_IMPLEMENTATION, **_overrides}
result = {}
for cat, data in GS_CHECKLIST.items():
result[cat] = {
"weight": data["weight"],
"items": [
{**item, "passed": impl.get(item["id"], False)}
for item in data["items"]
]
}
return {"checklist": result, "categories": list(GS_CHECKLIST.keys())}
@router.get("/audit")
async def run_audit():
"""GS인증 자동 점검 실행 — 등급 판정."""
return _run_audit().model_dump()
@router.get("/audit/summary")
async def audit_summary():
"""점검 결과 요약 (경량)."""
report = _run_audit()
return {
"grade": report.grade,
"score": report.score,
"passed": report.passed,
"failed": report.failed,
"required_failed": len(report.required_failed),
"generated_at": report.generated_at,
}
@router.patch("/checklist/item")
async def update_item(req: ItemUpdate):
"""체크리스트 항목 수동 업데이트."""
_overrides[req.item_id] = req.status
return {
"updated": True,
"item_id": req.item_id,
"status": req.status,
"note": req.note,
}
@router.get("/documents")
async def get_documents():
"""GS인증 필수 산출물 목록."""
completed = sum(1 for d in REQUIRED_DOCUMENTS if d["status"] == "완료")
return {
"documents": REQUIRED_DOCUMENTS,
"total": len(REQUIRED_DOCUMENTS),
"completed": completed,
"missing": len(REQUIRED_DOCUMENTS) - completed,
}
@router.get("/report/json")
async def export_report_json():
"""GS인증 점검 보고서 JSON 내보내기."""
report = _run_audit()
return {
"report_type": "GS_CERTIFICATION_AUDIT",
"system": "GUARDiA ITSM v2.1.0",
"standard": "TTA GS 인증 기준 SW 시험 V4.0",
"result": report.model_dump(),
}
@router.get("/report/markdown")
async def export_report_markdown():
"""GS인증 점검 보고서 Markdown 내보내기."""
report = _run_audit()
lines = [
"# GUARDiA ITSM GS인증 점검 보고서",
f"생성일시: {report.generated_at}",
f"판정 등급: **{report.grade}**",
f"총점: {report.score}/100",
f"통과: {report.passed}개 / 실패: {report.failed}",
"",
"## 카테고리별 결과",
"| 카테고리 | 가중치 | 통과 | 총수 | 점수 |",
"|---------|--------|------|------|------|",
]
for cat, data in report.by_category.items():
lines.append(f"| {cat} | {data['weight']}% | {data['passed']} | {data['total']} | {data['score']} |")
if report.required_failed:
lines += ["", "## 필수 미통과 항목", ""]
for item in report.required_failed:
lines.append(f"- {item}")
if report.recommendations:
lines += ["", "## 개선 권고사항", ""]
for rec in report.recommendations[:10]:
lines.append(f"- {rec}")
lines += ["", "## 필수 산출물", "| ID | 문서명 | 상태 |", "|-----|--------|------|"]
for doc in report.documents:
lines.append(f"| {doc['id']} | {doc['name']} | {doc['status']} |")
return {"markdown": "\n".join(lines)}
@router.get("/requirements")
async def list_requirements():
"""GS인증 추가 요건 (공공기관 납품 기준)."""
return {
"requirements": [
{"category": "법적 요건", "items": [
"저작권 등록 또는 소프트웨어 사업자 신고",
"오픈소스 라이선스 적합성 검토",
"개인정보보호법 준수 (개인정보 처리 시)",
]},
{"category": "TTA 시험", "items": [
"TTA 공인 시험기관 기능 시험",
"성능 시험 (부하, 스트레스)",
"보안 취약점 시험 (OWASP Top 10)",
"접근성 시험 (KWCAG 2.1)",
]},
{"category": "공공기관 특화", "items": [
"행정정보시스템 연동 가이드 준수",
"GS 인증서 취득 후 3년 유효",
"나라장터 등록 가능 (GS 1·2등급)",
"공공기관 정보화사업 입찰 자격",
]},
]
}
@router.get("/score/breakdown")
async def score_breakdown():
"""카테고리별 점수 분해 — 개선 우선순위 확인."""
report = _run_audit()
breakdowns = []
for cat, data in report.by_category.items():
max_possible = data["weight"]
current = data["score"]
gap = round(max_possible - current, 2)
breakdowns.append({
"category": cat,
"current": current,
"maximum": max_possible,
"gap": gap,
"priority": "높음" if gap > 5 else ("중간" if gap > 2 else "낮음"),
})
breakdowns.sort(key=lambda x: x["gap"], reverse=True)
return {
"total_score": report.score,
"grade": report.grade,
"breakdown": breakdowns,
"quick_wins": [b for b in breakdowns if b["gap"] > 0 and b["priority"] != "낮음"],
}