diff --git a/backend-compliance/compliance/api/isms_routes.py b/backend-compliance/compliance/api/isms_routes.py index 4d98912..1cabd54 100644 --- a/backend-compliance/compliance/api/isms_routes.py +++ b/backend-compliance/compliance/api/isms_routes.py @@ -58,8 +58,8 @@ from compliance.services.isms_governance_service import ( from compliance.services.isms_findings_service import AuditFindingService, CAPAService from compliance.services.isms_assessment_service import ( ManagementReviewService, InternalAuditService, AuditTrailService, - ReadinessCheckService, OverviewService, ) +from compliance.services.isms_readiness_service import ReadinessCheckService, OverviewService router = APIRouter(prefix="/isms", tags=["ISMS"]) diff --git a/backend-compliance/compliance/services/isms_assessment_service.py b/backend-compliance/compliance/services/isms_assessment_service.py index c1f0aa7..b4f0c41 100644 --- a/backend-compliance/compliance/services/isms_assessment_service.py +++ b/backend-compliance/compliance/services/isms_assessment_service.py @@ -1,9 +1,9 @@ # mypy: disable-error-code="arg-type,assignment,union-attr,no-any-return" """ -ISMS Assessment service -- Management Reviews, Internal Audits, Readiness, -Audit Trail, and ISO 27001 Overview. +ISMS Assessment service -- Management Reviews, Internal Audits, Audit Trail. Phase 1 Step 4: extracted from ``compliance.api.isms_routes``. +Readiness Check and Overview live in ``isms_readiness_service``. """ from datetime import datetime, date, timezone @@ -12,31 +12,13 @@ from typing import Optional from sqlalchemy.orm import Session from compliance.db.models import ( - ISMSScopeDB, - ISMSContextDB, - ISMSPolicyDB, - SecurityObjectiveDB, - StatementOfApplicabilityDB, - AuditFindingDB, - CorrectiveActionDB, ManagementReviewDB, InternalAuditDB, AuditTrailDB, - ISMSReadinessCheckDB, - ApprovalStatusEnum, - FindingTypeEnum, - FindingStatusEnum, ) from compliance.domain import NotFoundError from compliance.services.isms_governance_service import generate_id, log_audit_trail -from compliance.schemas.isms_audit import ( - PotentialFinding, - ISMSReadinessCheckResponse, - ISO27001ChapterStatus, - ISO27001OverviewResponse, - AuditTrailResponse, - PaginationMeta, -) +from compliance.schemas.isms_audit import PaginationMeta # ============================================================================ @@ -244,18 +226,18 @@ class AuditTrailService: page: int = 1, page_size: int = 50, ) -> dict: - query = db.query(AuditTrailDB) + q = db.query(AuditTrailDB) if entity_type: - query = query.filter(AuditTrailDB.entity_type == entity_type) + q = q.filter(AuditTrailDB.entity_type == entity_type) if entity_id: - query = query.filter(AuditTrailDB.entity_id == entity_id) + q = q.filter(AuditTrailDB.entity_id == entity_id) if performed_by: - query = query.filter(AuditTrailDB.performed_by == performed_by) + q = q.filter(AuditTrailDB.performed_by == performed_by) if action: - query = query.filter(AuditTrailDB.action == action) - total = query.count() + q = q.filter(AuditTrailDB.action == action) + total = q.count() entries = ( - query.order_by(AuditTrailDB.performed_at.desc()) + q.order_by(AuditTrailDB.performed_at.desc()) .offset((page - 1) * page_size) .limit(page_size) .all() @@ -273,367 +255,3 @@ class AuditTrailService: has_prev=page > 1, ), } - - -# ============================================================================ -# Readiness Check -# ============================================================================ - - -class ReadinessCheckService: - """Business logic for the ISMS Readiness Check.""" - - @staticmethod - def run(db: Session, triggered_by: str) -> ISMSReadinessCheckResponse: - potential_majors: list = [] - potential_minors: list = [] - improvement_opportunities: list = [] - - # Chapter 4: Context - scope = db.query(ISMSScopeDB).filter(ISMSScopeDB.status == ApprovalStatusEnum.APPROVED).first() - if not scope: - potential_majors.append(PotentialFinding( - check="ISMS Scope not approved", status="fail", - recommendation="Approve ISMS scope with top management signature", iso_reference="4.3", - )) - context = db.query(ISMSContextDB).filter(ISMSContextDB.status == ApprovalStatusEnum.APPROVED).first() - if not context: - potential_majors.append(PotentialFinding( - check="ISMS Context not documented", status="fail", - recommendation="Document and approve context analysis (4.1, 4.2)", iso_reference="4.1, 4.2", - )) - - # Chapter 5: Leadership - master_policy = db.query(ISMSPolicyDB).filter( - ISMSPolicyDB.policy_type == "master", ISMSPolicyDB.status == ApprovalStatusEnum.APPROVED, - ).first() - if not master_policy: - potential_majors.append(PotentialFinding( - check="Information Security Policy not approved", status="fail", - recommendation="Create and approve master ISMS policy", iso_reference="5.2", - )) - - # Chapter 6: Risk Assessment - from compliance.db.models import RiskDB - risks_without_treatment = db.query(RiskDB).filter( - RiskDB.status == "open", RiskDB.treatment_plan is None, - ).count() - if risks_without_treatment > 0: - potential_majors.append(PotentialFinding( - check=f"{risks_without_treatment} risks without treatment plan", status="fail", - recommendation="Define risk treatment for all identified risks", iso_reference="6.1.2", - )) - - # Chapter 6: Objectives - objectives = db.query(SecurityObjectiveDB).filter(SecurityObjectiveDB.status == "active").count() - if objectives == 0: - potential_majors.append(PotentialFinding( - check="No security objectives defined", status="fail", - recommendation="Define measurable security objectives", iso_reference="6.2", - )) - - # SoA - soa_total = db.query(StatementOfApplicabilityDB).count() - soa_unapproved = db.query(StatementOfApplicabilityDB).filter( - StatementOfApplicabilityDB.approved_at is None, - ).count() - if soa_total == 0: - potential_majors.append(PotentialFinding( - check="Statement of Applicability not created", status="fail", - recommendation="Create SoA for all 93 Annex A controls", iso_reference="Annex A", - )) - elif soa_unapproved > 0: - potential_minors.append(PotentialFinding( - check=f"{soa_unapproved} SoA entries not approved", status="warning", - recommendation="Review and approve all SoA entries", iso_reference="Annex A", - )) - - # Chapter 9: Internal Audit - last_year = date.today().replace(year=date.today().year - 1) - internal_audit = db.query(InternalAuditDB).filter( - InternalAuditDB.status == "completed", InternalAuditDB.actual_end_date >= last_year, - ).first() - if not internal_audit: - potential_majors.append(PotentialFinding( - check="No internal audit in last 12 months", status="fail", - recommendation="Conduct internal audit before certification", iso_reference="9.2", - )) - - # Chapter 9: Management Review - mgmt_review = db.query(ManagementReviewDB).filter( - ManagementReviewDB.status == "approved", ManagementReviewDB.review_date >= last_year, - ).first() - if not mgmt_review: - potential_majors.append(PotentialFinding( - check="No management review in last 12 months", status="fail", - recommendation="Conduct and approve management review", iso_reference="9.3", - )) - - # Chapter 10: Open Findings - open_majors = db.query(AuditFindingDB).filter( - AuditFindingDB.finding_type == FindingTypeEnum.MAJOR, - AuditFindingDB.status != FindingStatusEnum.CLOSED, - ).count() - if open_majors > 0: - potential_majors.append(PotentialFinding( - check=f"{open_majors} open major finding(s)", status="fail", - recommendation="Close all major findings before certification", iso_reference="10.1", - )) - open_minors = db.query(AuditFindingDB).filter( - AuditFindingDB.finding_type == FindingTypeEnum.MINOR, - AuditFindingDB.status != FindingStatusEnum.CLOSED, - ).count() - if open_minors > 0: - potential_minors.append(PotentialFinding( - check=f"{open_minors} open minor finding(s)", status="warning", - recommendation="Address minor findings or have CAPA in progress", iso_reference="10.1", - )) - - # Calculate scores - total_checks = 10 - passed_checks = total_checks - len(potential_majors) - readiness_score = (passed_checks / total_checks) * 100 - certification_possible = len(potential_majors) == 0 - if certification_possible: - overall_status = "ready" if len(potential_minors) == 0 else "at_risk" - else: - overall_status = "not_ready" - - def get_chapter_status(has_major: bool, has_minor: bool) -> str: - if has_major: - return "fail" - elif has_minor: - return "warning" - return "pass" - - chapter_4_majors = any("4." in (f.iso_reference or "") for f in potential_majors) - chapter_5_majors = any("5." in (f.iso_reference or "") for f in potential_majors) - chapter_6_majors = any("6." in (f.iso_reference or "") for f in potential_majors) - chapter_9_majors = any("9." in (f.iso_reference or "") for f in potential_majors) - chapter_10_majors = any("10." in (f.iso_reference or "") for f in potential_majors) - - priority_actions = [f.recommendation for f in potential_majors[:5]] - - check = ISMSReadinessCheckDB( - id=generate_id(), - check_date=datetime.now(timezone.utc), - triggered_by=triggered_by, - overall_status=overall_status, - certification_possible=certification_possible, - chapter_4_status=get_chapter_status(chapter_4_majors, False), - chapter_5_status=get_chapter_status(chapter_5_majors, False), - chapter_6_status=get_chapter_status(chapter_6_majors, False), - chapter_7_status=get_chapter_status( - any("7." in (f.iso_reference or "") for f in potential_majors), - any("7." in (f.iso_reference or "") for f in potential_minors), - ), - chapter_8_status=get_chapter_status( - any("8." in (f.iso_reference or "") for f in potential_majors), - any("8." in (f.iso_reference or "") for f in potential_minors), - ), - chapter_9_status=get_chapter_status(chapter_9_majors, False), - chapter_10_status=get_chapter_status(chapter_10_majors, False), - potential_majors=[f.model_dump() for f in potential_majors], - potential_minors=[f.model_dump() for f in potential_minors], - improvement_opportunities=[f.model_dump() for f in improvement_opportunities], - readiness_score=readiness_score, - priority_actions=priority_actions, - ) - db.add(check) - db.commit() - db.refresh(check) - - return ISMSReadinessCheckResponse( - id=check.id, - check_date=check.check_date, - triggered_by=check.triggered_by, - overall_status=check.overall_status, - certification_possible=check.certification_possible, - chapter_4_status=check.chapter_4_status, - chapter_5_status=check.chapter_5_status, - chapter_6_status=check.chapter_6_status, - chapter_7_status=check.chapter_7_status, - chapter_8_status=check.chapter_8_status, - chapter_9_status=check.chapter_9_status, - chapter_10_status=check.chapter_10_status, - potential_majors=potential_majors, - potential_minors=potential_minors, - improvement_opportunities=improvement_opportunities, - readiness_score=check.readiness_score, - documentation_score=None, - implementation_score=None, - evidence_score=None, - priority_actions=priority_actions, - ) - - @staticmethod - def get_latest(db: Session) -> ISMSReadinessCheckDB: - check = ( - db.query(ISMSReadinessCheckDB) - .order_by(ISMSReadinessCheckDB.check_date.desc()) - .first() - ) - if not check: - raise NotFoundError("No readiness check found. Run one first.") - return check - - -# ============================================================================ -# ISO 27001 Overview -# ============================================================================ - - -class OverviewService: - """Business logic for the ISO 27001 overview dashboard.""" - - @staticmethod - def get_overview(db: Session) -> ISO27001OverviewResponse: - scope = db.query(ISMSScopeDB).filter(ISMSScopeDB.status == ApprovalStatusEnum.APPROVED).first() - scope_approved = scope is not None - - soa_total = db.query(StatementOfApplicabilityDB).count() - soa_approved = db.query(StatementOfApplicabilityDB).filter( - StatementOfApplicabilityDB.approved_at.isnot(None), - ).count() - soa_all_approved = soa_total > 0 and soa_approved == soa_total - - last_year = date.today().replace(year=date.today().year - 1) - - last_mgmt_review = ( - db.query(ManagementReviewDB) - .filter(ManagementReviewDB.status == "approved") - .order_by(ManagementReviewDB.review_date.desc()) - .first() - ) - last_internal_audit = ( - db.query(InternalAuditDB) - .filter(InternalAuditDB.status == "completed") - .order_by(InternalAuditDB.actual_end_date.desc()) - .first() - ) - - open_majors = db.query(AuditFindingDB).filter( - AuditFindingDB.finding_type == FindingTypeEnum.MAJOR, - AuditFindingDB.status != FindingStatusEnum.CLOSED, - ).count() - open_minors = db.query(AuditFindingDB).filter( - AuditFindingDB.finding_type == FindingTypeEnum.MINOR, - AuditFindingDB.status != FindingStatusEnum.CLOSED, - ).count() - - policies_total = db.query(ISMSPolicyDB).count() - policies_approved = db.query(ISMSPolicyDB).filter( - ISMSPolicyDB.status == ApprovalStatusEnum.APPROVED, - ).count() - - objectives_total = db.query(SecurityObjectiveDB).count() - objectives_achieved = db.query(SecurityObjectiveDB).filter( - SecurityObjectiveDB.status == "achieved", - ).count() - - has_any_data = any([ - scope_approved, soa_total > 0, policies_total > 0, - objectives_total > 0, last_mgmt_review is not None, - last_internal_audit is not None, - ]) - - if not has_any_data: - certification_readiness = 0.0 - else: - readiness_factors = [ - scope_approved, - soa_all_approved, - last_mgmt_review is not None and last_mgmt_review.review_date >= last_year, - last_internal_audit is not None and (last_internal_audit.actual_end_date or date.min) >= last_year, - open_majors == 0 and (soa_total > 0 or policies_total > 0), - policies_total > 0 and policies_approved >= policies_total * 0.8, - objectives_total > 0, - ] - certification_readiness = sum(readiness_factors) / len(readiness_factors) * 100 - - if not has_any_data: - overall_status = "not_started" - elif open_majors > 0: - overall_status = "not_ready" - elif certification_readiness >= 80: - overall_status = "ready" - else: - overall_status = "at_risk" - - def _chapter_status(has_positive_evidence: bool, has_issues: bool) -> str: - if not has_positive_evidence: - return "not_started" - return "compliant" if not has_issues else "non_compliant" - - ch9_parts = sum([last_mgmt_review is not None, last_internal_audit is not None]) - ch9_pct = (ch9_parts / 2) * 100 - - capa_total = db.query(AuditFindingDB).count() - ch10_has_data = capa_total > 0 - ch10_pct = 100.0 if (ch10_has_data and open_majors == 0) else (50.0 if ch10_has_data else 0.0) - - chapters = [ - ISO27001ChapterStatus( - chapter="4", title="Kontext der Organisation", - status=_chapter_status(scope_approved, False), - completion_percentage=100.0 if scope_approved else 0.0, - open_findings=0, - key_documents=["ISMS Scope", "Context Analysis"] if scope_approved else [], - last_reviewed=scope.approved_at if scope else None, - ), - ISO27001ChapterStatus( - chapter="5", title="Fuehrung", - status=_chapter_status(policies_total > 0, policies_approved < policies_total), - completion_percentage=(policies_approved / max(policies_total, 1)) * 100 if policies_total > 0 else 0.0, - open_findings=0, - key_documents=[f"Policy {i+1}" for i in range(min(policies_approved, 3))] if policies_approved > 0 else [], - last_reviewed=None, - ), - ISO27001ChapterStatus( - chapter="6", title="Planung", - status=_chapter_status(objectives_total > 0, False), - completion_percentage=75.0 if objectives_total > 0 else 0.0, - open_findings=0, - key_documents=["Risk Register", "Security Objectives"] if objectives_total > 0 else [], - last_reviewed=None, - ), - ISO27001ChapterStatus( - chapter="9", title="Bewertung der Leistung", - status=_chapter_status(ch9_parts > 0, open_majors + open_minors > 0), - completion_percentage=ch9_pct, - open_findings=open_majors + open_minors, - key_documents=( - (["Internal Audit Report"] if last_internal_audit else []) - + (["Management Review Minutes"] if last_mgmt_review else []) - ), - last_reviewed=last_mgmt_review.approved_at if last_mgmt_review else None, - ), - ISO27001ChapterStatus( - chapter="10", title="Verbesserung", - status=_chapter_status(ch10_has_data, open_majors > 0), - completion_percentage=ch10_pct, - open_findings=open_majors, - key_documents=["CAPA Register"] if ch10_has_data else [], - last_reviewed=None, - ), - ] - - return ISO27001OverviewResponse( - overall_status=overall_status, - certification_readiness=certification_readiness, - chapters=chapters, - scope_approved=scope_approved, - soa_approved=soa_all_approved, - last_management_review=last_mgmt_review.approved_at if last_mgmt_review else None, - last_internal_audit=( - datetime.combine(last_internal_audit.actual_end_date, datetime.min.time()) - if last_internal_audit and last_internal_audit.actual_end_date - else None - ), - open_major_findings=open_majors, - open_minor_findings=open_minors, - policies_count=policies_total, - policies_approved=policies_approved, - objectives_count=objectives_total, - objectives_achieved=objectives_achieved, - ) diff --git a/backend-compliance/compliance/services/isms_readiness_service.py b/backend-compliance/compliance/services/isms_readiness_service.py new file mode 100644 index 0000000..41e387a --- /dev/null +++ b/backend-compliance/compliance/services/isms_readiness_service.py @@ -0,0 +1,400 @@ +# mypy: disable-error-code="arg-type,assignment,union-attr,no-any-return" +""" +ISMS Readiness & Overview service -- Readiness Check and ISO 27001 Overview. + +Phase 1 Step 4: extracted from ``compliance.api.isms_routes`` via +``compliance.services.isms_assessment_service``. +""" + +from datetime import datetime, date, timezone +from typing import List + +from sqlalchemy.orm import Session + +from compliance.db.models import ( + ISMSScopeDB, + ISMSContextDB, + ISMSPolicyDB, + SecurityObjectiveDB, + StatementOfApplicabilityDB, + AuditFindingDB, + ManagementReviewDB, + InternalAuditDB, + ISMSReadinessCheckDB, + ApprovalStatusEnum, + FindingTypeEnum, + FindingStatusEnum, +) +from compliance.domain import NotFoundError +from compliance.services.isms_governance_service import generate_id +from compliance.schemas.isms_audit import ( + PotentialFinding, + ISMSReadinessCheckResponse, + ISO27001ChapterStatus, + ISO27001OverviewResponse, +) + + +# ============================================================================ +# Readiness Check +# ============================================================================ + + +def _get_chapter_status(has_major: bool, has_minor: bool) -> str: + if has_major: + return "fail" + elif has_minor: + return "warning" + return "pass" + + +class ReadinessCheckService: + """Business logic for the ISMS Readiness Check.""" + + @staticmethod + def run(db: Session, triggered_by: str) -> ISMSReadinessCheckResponse: + potential_majors: List[PotentialFinding] = [] + potential_minors: List[PotentialFinding] = [] + improvement_opportunities: List[PotentialFinding] = [] + + # Chapter 4: Context + scope = db.query(ISMSScopeDB).filter(ISMSScopeDB.status == ApprovalStatusEnum.APPROVED).first() + if not scope: + potential_majors.append(PotentialFinding( + check="ISMS Scope not approved", status="fail", + recommendation="Approve ISMS scope with top management signature", iso_reference="4.3", + )) + context = db.query(ISMSContextDB).filter(ISMSContextDB.status == ApprovalStatusEnum.APPROVED).first() + if not context: + potential_majors.append(PotentialFinding( + check="ISMS Context not documented", status="fail", + recommendation="Document and approve context analysis (4.1, 4.2)", iso_reference="4.1, 4.2", + )) + + # Chapter 5: Leadership + master_policy = db.query(ISMSPolicyDB).filter( + ISMSPolicyDB.policy_type == "master", ISMSPolicyDB.status == ApprovalStatusEnum.APPROVED, + ).first() + if not master_policy: + potential_majors.append(PotentialFinding( + check="Information Security Policy not approved", status="fail", + recommendation="Create and approve master ISMS policy", iso_reference="5.2", + )) + + # Chapter 6: Risk Assessment + from compliance.db.models import RiskDB + risks_without_treatment = db.query(RiskDB).filter( + RiskDB.status == "open", RiskDB.treatment_plan is None, + ).count() + if risks_without_treatment > 0: + potential_majors.append(PotentialFinding( + check=f"{risks_without_treatment} risks without treatment plan", status="fail", + recommendation="Define risk treatment for all identified risks", iso_reference="6.1.2", + )) + + # Chapter 6: Objectives + objectives = db.query(SecurityObjectiveDB).filter(SecurityObjectiveDB.status == "active").count() + if objectives == 0: + potential_majors.append(PotentialFinding( + check="No security objectives defined", status="fail", + recommendation="Define measurable security objectives", iso_reference="6.2", + )) + + # SoA + soa_total = db.query(StatementOfApplicabilityDB).count() + soa_unapproved = db.query(StatementOfApplicabilityDB).filter( + StatementOfApplicabilityDB.approved_at is None, + ).count() + if soa_total == 0: + potential_majors.append(PotentialFinding( + check="Statement of Applicability not created", status="fail", + recommendation="Create SoA for all 93 Annex A controls", iso_reference="Annex A", + )) + elif soa_unapproved > 0: + potential_minors.append(PotentialFinding( + check=f"{soa_unapproved} SoA entries not approved", status="warning", + recommendation="Review and approve all SoA entries", iso_reference="Annex A", + )) + + # Chapter 9: Internal Audit + last_year = date.today().replace(year=date.today().year - 1) + internal_audit = db.query(InternalAuditDB).filter( + InternalAuditDB.status == "completed", InternalAuditDB.actual_end_date >= last_year, + ).first() + if not internal_audit: + potential_majors.append(PotentialFinding( + check="No internal audit in last 12 months", status="fail", + recommendation="Conduct internal audit before certification", iso_reference="9.2", + )) + + # Chapter 9: Management Review + mgmt_review = db.query(ManagementReviewDB).filter( + ManagementReviewDB.status == "approved", ManagementReviewDB.review_date >= last_year, + ).first() + if not mgmt_review: + potential_majors.append(PotentialFinding( + check="No management review in last 12 months", status="fail", + recommendation="Conduct and approve management review", iso_reference="9.3", + )) + + # Chapter 10: Open Findings + open_majors = db.query(AuditFindingDB).filter( + AuditFindingDB.finding_type == FindingTypeEnum.MAJOR, + AuditFindingDB.status != FindingStatusEnum.CLOSED, + ).count() + if open_majors > 0: + potential_majors.append(PotentialFinding( + check=f"{open_majors} open major finding(s)", status="fail", + recommendation="Close all major findings before certification", iso_reference="10.1", + )) + open_minors = db.query(AuditFindingDB).filter( + AuditFindingDB.finding_type == FindingTypeEnum.MINOR, + AuditFindingDB.status != FindingStatusEnum.CLOSED, + ).count() + if open_minors > 0: + potential_minors.append(PotentialFinding( + check=f"{open_minors} open minor finding(s)", status="warning", + recommendation="Address minor findings or have CAPA in progress", iso_reference="10.1", + )) + + # Calculate scores + total_checks = 10 + passed_checks = total_checks - len(potential_majors) + readiness_score = (passed_checks / total_checks) * 100 + certification_possible = len(potential_majors) == 0 + if certification_possible: + overall_status = "ready" if len(potential_minors) == 0 else "at_risk" + else: + overall_status = "not_ready" + + ch4m = any("4." in (f.iso_reference or "") for f in potential_majors) + ch5m = any("5." in (f.iso_reference or "") for f in potential_majors) + ch6m = any("6." in (f.iso_reference or "") for f in potential_majors) + ch9m = any("9." in (f.iso_reference or "") for f in potential_majors) + ch10m = any("10." in (f.iso_reference or "") for f in potential_majors) + + priority_actions = [f.recommendation for f in potential_majors[:5]] + + check = ISMSReadinessCheckDB( + id=generate_id(), + check_date=datetime.now(timezone.utc), + triggered_by=triggered_by, + overall_status=overall_status, + certification_possible=certification_possible, + chapter_4_status=_get_chapter_status(ch4m, False), + chapter_5_status=_get_chapter_status(ch5m, False), + chapter_6_status=_get_chapter_status(ch6m, False), + chapter_7_status=_get_chapter_status( + any("7." in (f.iso_reference or "") for f in potential_majors), + any("7." in (f.iso_reference or "") for f in potential_minors), + ), + chapter_8_status=_get_chapter_status( + any("8." in (f.iso_reference or "") for f in potential_majors), + any("8." in (f.iso_reference or "") for f in potential_minors), + ), + chapter_9_status=_get_chapter_status(ch9m, False), + chapter_10_status=_get_chapter_status(ch10m, False), + potential_majors=[f.model_dump() for f in potential_majors], + potential_minors=[f.model_dump() for f in potential_minors], + improvement_opportunities=[f.model_dump() for f in improvement_opportunities], + readiness_score=readiness_score, + priority_actions=priority_actions, + ) + db.add(check) + db.commit() + db.refresh(check) + + return ISMSReadinessCheckResponse( + id=check.id, + check_date=check.check_date, + triggered_by=check.triggered_by, + overall_status=check.overall_status, + certification_possible=check.certification_possible, + chapter_4_status=check.chapter_4_status, + chapter_5_status=check.chapter_5_status, + chapter_6_status=check.chapter_6_status, + chapter_7_status=check.chapter_7_status, + chapter_8_status=check.chapter_8_status, + chapter_9_status=check.chapter_9_status, + chapter_10_status=check.chapter_10_status, + potential_majors=potential_majors, + potential_minors=potential_minors, + improvement_opportunities=improvement_opportunities, + readiness_score=check.readiness_score, + documentation_score=None, + implementation_score=None, + evidence_score=None, + priority_actions=priority_actions, + ) + + @staticmethod + def get_latest(db: Session) -> ISMSReadinessCheckDB: + check = ( + db.query(ISMSReadinessCheckDB) + .order_by(ISMSReadinessCheckDB.check_date.desc()) + .first() + ) + if not check: + raise NotFoundError("No readiness check found. Run one first.") + return check + + +# ============================================================================ +# ISO 27001 Overview +# ============================================================================ + + +class OverviewService: + """Business logic for the ISO 27001 overview dashboard.""" + + @staticmethod + def get_overview(db: Session) -> ISO27001OverviewResponse: + scope = db.query(ISMSScopeDB).filter(ISMSScopeDB.status == ApprovalStatusEnum.APPROVED).first() + scope_approved = scope is not None + + soa_total = db.query(StatementOfApplicabilityDB).count() + soa_approved = db.query(StatementOfApplicabilityDB).filter( + StatementOfApplicabilityDB.approved_at.isnot(None), + ).count() + soa_all_approved = soa_total > 0 and soa_approved == soa_total + + last_year = date.today().replace(year=date.today().year - 1) + + last_mgmt_review = ( + db.query(ManagementReviewDB) + .filter(ManagementReviewDB.status == "approved") + .order_by(ManagementReviewDB.review_date.desc()) + .first() + ) + last_internal_audit = ( + db.query(InternalAuditDB) + .filter(InternalAuditDB.status == "completed") + .order_by(InternalAuditDB.actual_end_date.desc()) + .first() + ) + + open_majors = db.query(AuditFindingDB).filter( + AuditFindingDB.finding_type == FindingTypeEnum.MAJOR, + AuditFindingDB.status != FindingStatusEnum.CLOSED, + ).count() + open_minors = db.query(AuditFindingDB).filter( + AuditFindingDB.finding_type == FindingTypeEnum.MINOR, + AuditFindingDB.status != FindingStatusEnum.CLOSED, + ).count() + + policies_total = db.query(ISMSPolicyDB).count() + policies_approved = db.query(ISMSPolicyDB).filter( + ISMSPolicyDB.status == ApprovalStatusEnum.APPROVED, + ).count() + + objectives_total = db.query(SecurityObjectiveDB).count() + objectives_achieved = db.query(SecurityObjectiveDB).filter( + SecurityObjectiveDB.status == "achieved", + ).count() + + has_any_data = any([ + scope_approved, soa_total > 0, policies_total > 0, + objectives_total > 0, last_mgmt_review is not None, + last_internal_audit is not None, + ]) + + if not has_any_data: + certification_readiness = 0.0 + else: + readiness_factors = [ + scope_approved, + soa_all_approved, + last_mgmt_review is not None and last_mgmt_review.review_date >= last_year, + last_internal_audit is not None and (last_internal_audit.actual_end_date or date.min) >= last_year, + open_majors == 0 and (soa_total > 0 or policies_total > 0), + policies_total > 0 and policies_approved >= policies_total * 0.8, + objectives_total > 0, + ] + certification_readiness = sum(readiness_factors) / len(readiness_factors) * 100 + + if not has_any_data: + overall_status = "not_started" + elif open_majors > 0: + overall_status = "not_ready" + elif certification_readiness >= 80: + overall_status = "ready" + else: + overall_status = "at_risk" + + def _ch_status(has_positive: bool, has_issues: bool) -> str: + if not has_positive: + return "not_started" + return "compliant" if not has_issues else "non_compliant" + + ch9_parts = sum([last_mgmt_review is not None, last_internal_audit is not None]) + ch9_pct = (ch9_parts / 2) * 100 + + capa_total = db.query(AuditFindingDB).count() + ch10_has_data = capa_total > 0 + ch10_pct = 100.0 if (ch10_has_data and open_majors == 0) else (50.0 if ch10_has_data else 0.0) + + chapters = [ + ISO27001ChapterStatus( + chapter="4", title="Kontext der Organisation", + status=_ch_status(scope_approved, False), + completion_percentage=100.0 if scope_approved else 0.0, + open_findings=0, + key_documents=["ISMS Scope", "Context Analysis"] if scope_approved else [], + last_reviewed=scope.approved_at if scope else None, + ), + ISO27001ChapterStatus( + chapter="5", title="Fuehrung", + status=_ch_status(policies_total > 0, policies_approved < policies_total), + completion_percentage=(policies_approved / max(policies_total, 1)) * 100 if policies_total > 0 else 0.0, + open_findings=0, + key_documents=[f"Policy {i+1}" for i in range(min(policies_approved, 3))] if policies_approved > 0 else [], + last_reviewed=None, + ), + ISO27001ChapterStatus( + chapter="6", title="Planung", + status=_ch_status(objectives_total > 0, False), + completion_percentage=75.0 if objectives_total > 0 else 0.0, + open_findings=0, + key_documents=["Risk Register", "Security Objectives"] if objectives_total > 0 else [], + last_reviewed=None, + ), + ISO27001ChapterStatus( + chapter="9", title="Bewertung der Leistung", + status=_ch_status(ch9_parts > 0, open_majors + open_minors > 0), + completion_percentage=ch9_pct, + open_findings=open_majors + open_minors, + key_documents=( + (["Internal Audit Report"] if last_internal_audit else []) + + (["Management Review Minutes"] if last_mgmt_review else []) + ), + last_reviewed=last_mgmt_review.approved_at if last_mgmt_review else None, + ), + ISO27001ChapterStatus( + chapter="10", title="Verbesserung", + status=_ch_status(ch10_has_data, open_majors > 0), + completion_percentage=ch10_pct, + open_findings=open_majors, + key_documents=["CAPA Register"] if ch10_has_data else [], + last_reviewed=None, + ), + ] + + return ISO27001OverviewResponse( + overall_status=overall_status, + certification_readiness=certification_readiness, + chapters=chapters, + scope_approved=scope_approved, + soa_approved=soa_all_approved, + last_management_review=last_mgmt_review.approved_at if last_mgmt_review else None, + last_internal_audit=( + datetime.combine(last_internal_audit.actual_end_date, datetime.min.time()) + if last_internal_audit and last_internal_audit.actual_end_date + else None + ), + open_major_findings=open_majors, + open_minor_findings=open_minors, + policies_count=policies_total, + policies_approved=policies_approved, + objectives_count=objectives_total, + objectives_achieved=objectives_achieved, + ) diff --git a/backend-compliance/tests/contracts/openapi.baseline.json b/backend-compliance/tests/contracts/openapi.baseline.json index 5117cde..8d653a9 100644 --- a/backend-compliance/tests/contracts/openapi.baseline.json +++ b/backend-compliance/tests/contracts/openapi.baseline.json @@ -19510,280 +19510,6 @@ "title": "ConsentCreate", "type": "object" }, - "compliance__api__notfallplan_routes__IncidentCreate": { - "properties": { - "affected_data_categories": { - "default": [], - "items": {}, - "title": "Affected Data Categories", - "type": "array" - }, - "art34_justification": { - "anyOf": [ - { - "type": "string" - }, - { - "type": "null" - } - ], - "title": "Art34 Justification" - }, - "art34_required": { - "default": false, - "title": "Art34 Required", - "type": "boolean" - }, - "description": { - "anyOf": [ - { - "type": "string" - }, - { - "type": "null" - } - ], - "title": "Description" - }, - "detected_by": { - "anyOf": [ - { - "type": "string" - }, - { - "type": "null" - } - ], - "title": "Detected By" - }, - "estimated_affected_persons": { - "default": 0, - "title": "Estimated Affected Persons", - "type": "integer" - }, - "measures": { - "default": [], - "items": {}, - "title": "Measures", - "type": "array" - }, - "severity": { - "default": "medium", - "title": "Severity", - "type": "string" - }, - "status": { - "default": "detected", - "title": "Status", - "type": "string" - }, - "title": { - "title": "Title", - "type": "string" - } - }, - "required": [ - "title" - ], - "title": "IncidentCreate", - "type": "object" - }, - "compliance__api__notfallplan_routes__IncidentUpdate": { - "properties": { - "affected_data_categories": { - "anyOf": [ - { - "items": {}, - "type": "array" - }, - { - "type": "null" - } - ], - "title": "Affected Data Categories" - }, - "art34_justification": { - "anyOf": [ - { - "type": "string" - }, - { - "type": "null" - } - ], - "title": "Art34 Justification" - }, - "art34_required": { - "anyOf": [ - { - "type": "boolean" - }, - { - "type": "null" - } - ], - "title": "Art34 Required" - }, - "closed_at": { - "anyOf": [ - { - "type": "string" - }, - { - "type": "null" - } - ], - "title": "Closed At" - }, - "closed_by": { - "anyOf": [ - { - "type": "string" - }, - { - "type": "null" - } - ], - "title": "Closed By" - }, - "description": { - "anyOf": [ - { - "type": "string" - }, - { - "type": "null" - } - ], - "title": "Description" - }, - "detected_by": { - "anyOf": [ - { - "type": "string" - }, - { - "type": "null" - } - ], - "title": "Detected By" - }, - "estimated_affected_persons": { - "anyOf": [ - { - "type": "integer" - }, - { - "type": "null" - } - ], - "title": "Estimated Affected Persons" - }, - "lessons_learned": { - "anyOf": [ - { - "type": "string" - }, - { - "type": "null" - } - ], - "title": "Lessons Learned" - }, - "measures": { - "anyOf": [ - { - "items": {}, - "type": "array" - }, - { - "type": "null" - } - ], - "title": "Measures" - }, - "notified_affected_at": { - "anyOf": [ - { - "type": "string" - }, - { - "type": "null" - } - ], - "title": "Notified Affected At" - }, - "reported_to_authority_at": { - "anyOf": [ - { - "type": "string" - }, - { - "type": "null" - } - ], - "title": "Reported To Authority At" - }, - "severity": { - "anyOf": [ - { - "type": "string" - }, - { - "type": "null" - } - ], - "title": "Severity" - }, - "status": { - "anyOf": [ - { - "type": "string" - }, - { - "type": "null" - } - ], - "title": "Status" - }, - "title": { - "anyOf": [ - { - "type": "string" - }, - { - "type": "null" - } - ], - "title": "Title" - } - }, - "title": "IncidentUpdate", - "type": "object" - }, - "compliance__api__notfallplan_routes__TemplateCreate": { - "properties": { - "content": { - "title": "Content", - "type": "string" - }, - "title": { - "title": "Title", - "type": "string" - }, - "type": { - "default": "art33", - "title": "Type", - "type": "string" - } - }, - "required": [ - "title", - "content" - ], - "title": "TemplateCreate", - "type": "object" - }, "compliance__schemas__banner__ConsentCreate": { "description": "Request body for recording a device consent.", "properties": { @@ -20308,6 +20034,280 @@ }, "title": "VersionUpdate", "type": "object" + }, + "compliance__schemas__notfallplan__IncidentCreate": { + "properties": { + "affected_data_categories": { + "default": [], + "items": {}, + "title": "Affected Data Categories", + "type": "array" + }, + "art34_justification": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Art34 Justification" + }, + "art34_required": { + "default": false, + "title": "Art34 Required", + "type": "boolean" + }, + "description": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Description" + }, + "detected_by": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Detected By" + }, + "estimated_affected_persons": { + "default": 0, + "title": "Estimated Affected Persons", + "type": "integer" + }, + "measures": { + "default": [], + "items": {}, + "title": "Measures", + "type": "array" + }, + "severity": { + "default": "medium", + "title": "Severity", + "type": "string" + }, + "status": { + "default": "detected", + "title": "Status", + "type": "string" + }, + "title": { + "title": "Title", + "type": "string" + } + }, + "required": [ + "title" + ], + "title": "IncidentCreate", + "type": "object" + }, + "compliance__schemas__notfallplan__IncidentUpdate": { + "properties": { + "affected_data_categories": { + "anyOf": [ + { + "items": {}, + "type": "array" + }, + { + "type": "null" + } + ], + "title": "Affected Data Categories" + }, + "art34_justification": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Art34 Justification" + }, + "art34_required": { + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ], + "title": "Art34 Required" + }, + "closed_at": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Closed At" + }, + "closed_by": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Closed By" + }, + "description": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Description" + }, + "detected_by": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Detected By" + }, + "estimated_affected_persons": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "title": "Estimated Affected Persons" + }, + "lessons_learned": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Lessons Learned" + }, + "measures": { + "anyOf": [ + { + "items": {}, + "type": "array" + }, + { + "type": "null" + } + ], + "title": "Measures" + }, + "notified_affected_at": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Notified Affected At" + }, + "reported_to_authority_at": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Reported To Authority At" + }, + "severity": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Severity" + }, + "status": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Status" + }, + "title": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Title" + } + }, + "title": "IncidentUpdate", + "type": "object" + }, + "compliance__schemas__notfallplan__TemplateCreate": { + "properties": { + "content": { + "title": "Content", + "type": "string" + }, + "title": { + "title": "Title", + "type": "string" + }, + "type": { + "default": "art33", + "title": "Type", + "type": "string" + } + }, + "required": [ + "title", + "content" + ], + "title": "TemplateCreate", + "type": "object" } } }, @@ -24901,7 +24901,6 @@ }, "/api/compliance/dsr": { "get": { - "description": "Liste aller DSRs mit Filtern.", "operationId": "list_dsrs_api_compliance_dsr_get", "parameters": [ { @@ -25093,7 +25092,6 @@ ] }, "post": { - "description": "Erstellt eine neue Betroffenenanfrage.", "operationId": "create_dsr_api_compliance_dsr_post", "parameters": [ { @@ -25152,7 +25150,6 @@ }, "/api/compliance/dsr/deadlines/process": { "post": { - "description": "Verarbeitet Fristen und markiert ueberfaellige DSRs.", "operationId": "process_deadlines_api_compliance_dsr_deadlines_process_post", "parameters": [ { @@ -25201,7 +25198,6 @@ }, "/api/compliance/dsr/export": { "get": { - "description": "Exportiert alle DSRs als CSV oder JSON.", "operationId": "export_dsrs_api_compliance_dsr_export_get", "parameters": [ { @@ -25261,7 +25257,6 @@ }, "/api/compliance/dsr/stats": { "get": { - "description": "Dashboard-Statistiken fuer DSRs.", "operationId": "get_dsr_stats_api_compliance_dsr_stats_get", "parameters": [ { @@ -25310,7 +25305,6 @@ }, "/api/compliance/dsr/template-versions/{version_id}/publish": { "put": { - "description": "Veroeffentlicht eine Vorlagen-Version.", "operationId": "publish_template_version_api_compliance_dsr_template_versions__version_id__publish_put", "parameters": [ { @@ -25368,7 +25362,6 @@ }, "/api/compliance/dsr/templates": { "get": { - "description": "Gibt alle DSR-Vorlagen zurueck.", "operationId": "get_templates_api_compliance_dsr_templates_get", "parameters": [ { @@ -25417,7 +25410,6 @@ }, "/api/compliance/dsr/templates/published": { "get": { - "description": "Gibt publizierte Vorlagen zurueck.", "operationId": "get_published_templates_api_compliance_dsr_templates_published_get", "parameters": [ { @@ -25492,7 +25484,6 @@ }, "/api/compliance/dsr/templates/{template_id}/versions": { "get": { - "description": "Gibt alle Versionen einer Vorlage zurueck.", "operationId": "get_template_versions_api_compliance_dsr_templates__template_id__versions_get", "parameters": [ { @@ -25548,7 +25539,6 @@ ] }, "post": { - "description": "Erstellt eine neue Version einer Vorlage.", "operationId": "create_template_version_api_compliance_dsr_templates__template_id__versions_post", "parameters": [ { @@ -25616,7 +25606,6 @@ }, "/api/compliance/dsr/{dsr_id}": { "delete": { - "description": "Storniert eine DSR (Soft Delete \u2192 Status cancelled).", "operationId": "delete_dsr_api_compliance_dsr__dsr_id__delete", "parameters": [ { @@ -25672,7 +25661,6 @@ ] }, "get": { - "description": "Detail einer Betroffenenanfrage.", "operationId": "get_dsr_api_compliance_dsr__dsr_id__get", "parameters": [ { @@ -25728,7 +25716,6 @@ ] }, "put": { - "description": "Aktualisiert eine Betroffenenanfrage.", "operationId": "update_dsr_api_compliance_dsr__dsr_id__put", "parameters": [ { @@ -25796,7 +25783,6 @@ }, "/api/compliance/dsr/{dsr_id}/assign": { "post": { - "description": "Weist eine DSR einem Bearbeiter zu.", "operationId": "assign_dsr_api_compliance_dsr__dsr_id__assign_post", "parameters": [ { @@ -25864,7 +25850,6 @@ }, "/api/compliance/dsr/{dsr_id}/communicate": { "post": { - "description": "Sendet eine Kommunikation.", "operationId": "send_communication_api_compliance_dsr__dsr_id__communicate_post", "parameters": [ { @@ -25932,7 +25917,6 @@ }, "/api/compliance/dsr/{dsr_id}/communications": { "get": { - "description": "Gibt die Kommunikationshistorie zurueck.", "operationId": "get_communications_api_compliance_dsr__dsr_id__communications_get", "parameters": [ { @@ -25990,7 +25974,6 @@ }, "/api/compliance/dsr/{dsr_id}/complete": { "post": { - "description": "Schliesst eine DSR erfolgreich ab.", "operationId": "complete_dsr_api_compliance_dsr__dsr_id__complete_post", "parameters": [ { @@ -26058,7 +26041,6 @@ }, "/api/compliance/dsr/{dsr_id}/exception-checks": { "get": { - "description": "Gibt die Art. 17(3) Ausnahmepruefungen zurueck.", "operationId": "get_exception_checks_api_compliance_dsr__dsr_id__exception_checks_get", "parameters": [ { @@ -26116,7 +26098,6 @@ }, "/api/compliance/dsr/{dsr_id}/exception-checks/init": { "post": { - "description": "Initialisiert die Art. 17(3) Ausnahmepruefungen fuer eine Loeschanfrage.", "operationId": "init_exception_checks_api_compliance_dsr__dsr_id__exception_checks_init_post", "parameters": [ { @@ -26174,7 +26155,6 @@ }, "/api/compliance/dsr/{dsr_id}/exception-checks/{check_id}": { "put": { - "description": "Aktualisiert eine einzelne Ausnahmepruefung.", "operationId": "update_exception_check_api_compliance_dsr__dsr_id__exception_checks__check_id__put", "parameters": [ { @@ -26251,7 +26231,6 @@ }, "/api/compliance/dsr/{dsr_id}/extend": { "post": { - "description": "Verlaengert die Bearbeitungsfrist (Art. 12 Abs. 3 DSGVO).", "operationId": "extend_deadline_api_compliance_dsr__dsr_id__extend_post", "parameters": [ { @@ -26319,7 +26298,6 @@ }, "/api/compliance/dsr/{dsr_id}/history": { "get": { - "description": "Gibt die Status-Historie zurueck.", "operationId": "get_history_api_compliance_dsr__dsr_id__history_get", "parameters": [ { @@ -26377,7 +26355,6 @@ }, "/api/compliance/dsr/{dsr_id}/reject": { "post": { - "description": "Lehnt eine DSR mit Rechtsgrundlage ab.", "operationId": "reject_dsr_api_compliance_dsr__dsr_id__reject_post", "parameters": [ { @@ -26445,7 +26422,6 @@ }, "/api/compliance/dsr/{dsr_id}/status": { "post": { - "description": "Aendert den Status einer DSR.", "operationId": "change_status_api_compliance_dsr__dsr_id__status_post", "parameters": [ { @@ -26513,7 +26489,6 @@ }, "/api/compliance/dsr/{dsr_id}/verify-identity": { "post": { - "description": "Verifiziert die Identitaet des Antragstellers.", "operationId": "verify_identity_api_compliance_dsr__dsr_id__verify_identity_post", "parameters": [ { @@ -31558,7 +31533,7 @@ ] }, "post": { - "description": "Create a new audit finding.\n\nFinding types:\n- major: Blocks certification, requires immediate CAPA\n- minor: Requires CAPA within deadline\n- ofi: Opportunity for improvement (no mandatory action)\n- positive: Good practice observation", + "description": "Create a new audit finding.", "operationId": "create_finding_api_compliance_isms_findings_post", "requestBody": { "content": { @@ -31664,7 +31639,7 @@ }, "/api/compliance/isms/findings/{finding_id}/close": { "post": { - "description": "Close an audit finding after verification.\n\nRequires:\n- All CAPAs to be completed and verified\n- Verification evidence documenting the fix", + "description": "Close an audit finding after verification.", "operationId": "close_finding_api_compliance_isms_findings__finding_id__close_post", "parameters": [ { @@ -32407,7 +32382,7 @@ }, "/api/compliance/isms/overview": { "get": { - "description": "Get complete ISO 27001 compliance overview.\n\nShows status of all chapters, key metrics, and readiness for certification.", + "description": "Get complete ISO 27001 compliance overview.", "operationId": "get_iso27001_overview_api_compliance_isms_overview_get", "responses": { "200": { @@ -32697,7 +32672,7 @@ }, "/api/compliance/isms/readiness-check": { "post": { - "description": "Run ISMS readiness check.\n\nIdentifies potential Major/Minor findings BEFORE external audit.\nThis helps achieve ISO 27001 certification on the first attempt.", + "description": "Run ISMS readiness check before external audit.", "operationId": "run_readiness_check_api_compliance_isms_readiness_check_post", "requestBody": { "content": { @@ -32763,7 +32738,7 @@ }, "/api/compliance/isms/scope": { "get": { - "description": "Get the current ISMS scope.\n\nThe scope defines the boundaries and applicability of the ISMS.\nOnly one active scope should exist at a time.", + "description": "Get the current ISMS scope.", "operationId": "get_isms_scope_api_compliance_isms_scope_get", "responses": { "200": { @@ -32784,7 +32759,7 @@ ] }, "post": { - "description": "Create a new ISMS scope definition.\n\nSupersedes any existing scope.", + "description": "Create a new ISMS scope definition. Supersedes any existing scope.", "operationId": "create_isms_scope_api_compliance_isms_scope_post", "parameters": [ { @@ -32905,7 +32880,7 @@ }, "/api/compliance/isms/scope/{scope_id}/approve": { "post": { - "description": "Approve the ISMS scope.\n\nThis is a MANDATORY step for ISO 27001 certification.\nMust be approved by top management.", + "description": "Approve the ISMS scope. Must be approved by top management.", "operationId": "approve_isms_scope_api_compliance_isms_scope__scope_id__approve_post", "parameters": [ { @@ -36325,7 +36300,6 @@ }, "/api/compliance/notfallplan/checklists": { "get": { - "description": "List checklist items, optionally filtered by scenario_id.", "operationId": "list_checklists_api_compliance_notfallplan_checklists_get", "parameters": [ { @@ -36388,7 +36362,6 @@ ] }, "post": { - "description": "Create a new checklist item.", "operationId": "create_checklist_api_compliance_notfallplan_checklists_post", "parameters": [ { @@ -36447,7 +36420,6 @@ }, "/api/compliance/notfallplan/checklists/{checklist_id}": { "delete": { - "description": "Delete a checklist item.", "operationId": "delete_checklist_api_compliance_notfallplan_checklists__checklist_id__delete", "parameters": [ { @@ -36503,7 +36475,6 @@ ] }, "put": { - "description": "Update a checklist item.", "operationId": "update_checklist_api_compliance_notfallplan_checklists__checklist_id__put", "parameters": [ { @@ -36571,7 +36542,6 @@ }, "/api/compliance/notfallplan/contacts": { "get": { - "description": "List all emergency contacts for a tenant.", "operationId": "list_contacts_api_compliance_notfallplan_contacts_get", "parameters": [ { @@ -36618,7 +36588,6 @@ ] }, "post": { - "description": "Create a new emergency contact.", "operationId": "create_contact_api_compliance_notfallplan_contacts_post", "parameters": [ { @@ -36677,7 +36646,6 @@ }, "/api/compliance/notfallplan/contacts/{contact_id}": { "delete": { - "description": "Delete an emergency contact.", "operationId": "delete_contact_api_compliance_notfallplan_contacts__contact_id__delete", "parameters": [ { @@ -36733,7 +36701,6 @@ ] }, "put": { - "description": "Update an existing emergency contact.", "operationId": "update_contact_api_compliance_notfallplan_contacts__contact_id__put", "parameters": [ { @@ -36801,7 +36768,6 @@ }, "/api/compliance/notfallplan/exercises": { "get": { - "description": "List all exercises for a tenant.", "operationId": "list_exercises_api_compliance_notfallplan_exercises_get", "parameters": [ { @@ -36848,7 +36814,6 @@ ] }, "post": { - "description": "Create a new exercise.", "operationId": "create_exercise_api_compliance_notfallplan_exercises_post", "parameters": [ { @@ -36907,7 +36872,6 @@ }, "/api/compliance/notfallplan/incidents": { "get": { - "description": "List all incidents for a tenant.", "operationId": "list_incidents_api_compliance_notfallplan_incidents_get", "parameters": [ { @@ -36986,7 +36950,6 @@ ] }, "post": { - "description": "Create a new incident.", "operationId": "create_incident_api_compliance_notfallplan_incidents_post", "parameters": [ { @@ -37010,7 +36973,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/compliance__api__notfallplan_routes__IncidentCreate" + "$ref": "#/components/schemas/compliance__schemas__notfallplan__IncidentCreate" } } }, @@ -37045,7 +37008,6 @@ }, "/api/compliance/notfallplan/incidents/{incident_id}": { "delete": { - "description": "Delete an incident.", "operationId": "delete_incident_api_compliance_notfallplan_incidents__incident_id__delete", "parameters": [ { @@ -37096,7 +37058,6 @@ ] }, "put": { - "description": "Update an incident (including status transitions).", "operationId": "update_incident_api_compliance_notfallplan_incidents__incident_id__put", "parameters": [ { @@ -37129,7 +37090,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/compliance__api__notfallplan_routes__IncidentUpdate" + "$ref": "#/components/schemas/compliance__schemas__notfallplan__IncidentUpdate" } } }, @@ -37164,7 +37125,6 @@ }, "/api/compliance/notfallplan/scenarios": { "get": { - "description": "List all scenarios for a tenant.", "operationId": "list_scenarios_api_compliance_notfallplan_scenarios_get", "parameters": [ { @@ -37211,7 +37171,6 @@ ] }, "post": { - "description": "Create a new scenario.", "operationId": "create_scenario_api_compliance_notfallplan_scenarios_post", "parameters": [ { @@ -37270,7 +37229,6 @@ }, "/api/compliance/notfallplan/scenarios/{scenario_id}": { "delete": { - "description": "Delete a scenario.", "operationId": "delete_scenario_api_compliance_notfallplan_scenarios__scenario_id__delete", "parameters": [ { @@ -37326,7 +37284,6 @@ ] }, "put": { - "description": "Update an existing scenario.", "operationId": "update_scenario_api_compliance_notfallplan_scenarios__scenario_id__put", "parameters": [ { @@ -37394,7 +37351,6 @@ }, "/api/compliance/notfallplan/stats": { "get": { - "description": "Return statistics for the Notfallplan module.", "operationId": "get_stats_api_compliance_notfallplan_stats_get", "parameters": [ { @@ -37443,7 +37399,6 @@ }, "/api/compliance/notfallplan/templates": { "get": { - "description": "List Melde-Templates for a tenant.", "operationId": "list_templates_api_compliance_notfallplan_templates_get", "parameters": [ { @@ -37506,7 +37461,6 @@ ] }, "post": { - "description": "Create a new Melde-Template.", "operationId": "create_template_api_compliance_notfallplan_templates_post", "parameters": [ { @@ -37530,7 +37484,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/compliance__api__notfallplan_routes__TemplateCreate" + "$ref": "#/components/schemas/compliance__schemas__notfallplan__TemplateCreate" } } }, @@ -37565,7 +37519,6 @@ }, "/api/compliance/notfallplan/templates/{template_id}": { "delete": { - "description": "Delete a Melde-Template.", "operationId": "delete_template_api_compliance_notfallplan_templates__template_id__delete", "parameters": [ { @@ -37616,7 +37569,6 @@ ] }, "put": { - "description": "Update a Melde-Template.", "operationId": "update_template_api_compliance_notfallplan_templates__template_id__put", "parameters": [ {