""" 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"] != "낮음"], }