diff --git a/admin-compliance/app/sdk/compliance-hub/page.tsx b/admin-compliance/app/sdk/compliance-hub/page.tsx index bdc5174..c089bd8 100644 --- a/admin-compliance/app/sdk/compliance-hub/page.tsx +++ b/admin-compliance/app/sdk/compliance-hub/page.tsx @@ -365,7 +365,7 @@ export default function ComplianceHubPage() {
{mappings?.total || 474}
+{mappings?.total || 0}
Mappings gesamt
- Automatisch generierte Verknuepfungen zwischen {dashboard?.total_controls || 44} Controls - und {dashboard?.total_requirements || 558} Anforderungen aus {dashboard?.total_regulations || 19} Verordnungen. + Automatisch generierte Verknuepfungen zwischen {dashboard?.total_controls || 0} Controls + und {dashboard?.total_requirements || 0} Anforderungen aus {dashboard?.total_regulations || 0} Verordnungen.
diff --git a/backend-compliance/compliance/api/dashboard_routes.py b/backend-compliance/compliance/api/dashboard_routes.py index f95c395..182876d 100644 --- a/backend-compliance/compliance/api/dashboard_routes.py +++ b/backend-compliance/compliance/api/dashboard_routes.py @@ -164,16 +164,14 @@ async def get_executive_dashboard(db: Session = Depends(get_db)): else: traffic_light = "red" - # Generate trend data (last 12 months - simulated for now) + # Trend data — only show current score, no simulated history trend_data = [] - now = datetime.utcnow() - for i in range(11, -1, -1): - month_date = now - timedelta(days=i * 30) - trend_score = max(0, min(100, score - (11 - i) * 2 + (5 if i > 6 else 0))) + if total > 0: + now = datetime.utcnow() trend_data.append(TrendDataPoint( - date=month_date.strftime("%Y-%m-%d"), - score=round(trend_score, 1), - label=month_abbr[month_date.month][:3], + date=now.strftime("%Y-%m-%d"), + score=round(score, 1), + label=month_abbr[now.month][:3], )) # Get top 5 risks (sorted by severity) @@ -304,21 +302,16 @@ async def get_compliance_trend( current_score = ((passing + partial * 0.5) / total) * 100 if total > 0 else 0 - # Generate simulated historical data + # Trend data — only current score, no simulated history trend_data = [] - now = datetime.utcnow() - - for i in range(months - 1, -1, -1): - month_date = now - timedelta(days=i * 30) - variation = ((i * 7) % 5) - 2 - trend_score = max(0, min(100, current_score - (months - 1 - i) * 1.5 + variation)) - + if total > 0: + now = datetime.utcnow() trend_data.append({ - "date": month_date.strftime("%Y-%m-%d"), - "score": round(trend_score, 1), - "label": f"{month_abbr[month_date.month]} {month_date.year % 100}", - "month": month_date.month, - "year": month_date.year, + "date": now.strftime("%Y-%m-%d"), + "score": round(current_score, 1), + "label": f"{month_abbr[now.month]} {now.year % 100}", + "month": now.month, + "year": now.year, }) return { diff --git a/backend-compliance/compliance/api/isms_routes.py b/backend-compliance/compliance/api/isms_routes.py index 96825e5..31c2b43 100644 --- a/backend-compliance/compliance/api/isms_routes.py +++ b/backend-compliance/compliance/api/isms_routes.py @@ -1399,8 +1399,14 @@ async def run_readiness_check( 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="pass", # Support - typically ok - chapter_8_status="pass", # Operation - checked via controls + 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], @@ -1517,7 +1523,7 @@ async def get_iso27001_overview(db: Session = Depends(get_db)): soa_total = db.query(StatementOfApplicabilityDB).count() soa_approved = db.query(StatementOfApplicabilityDB).filter( - StatementOfApplicabilityDB.approved_at is not None + StatementOfApplicabilityDB.approved_at.isnot(None) ).count() soa_all_approved = soa_total > 0 and soa_approved == soa_total @@ -1555,32 +1561,58 @@ async def get_iso27001_overview(db: Session = Depends(get_db)): SecurityObjectiveDB.status == "achieved" ).count() - # Calculate readiness - 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, - policies_total > 0 and policies_approved >= policies_total * 0.8, - objectives_total > 0 - ] - certification_readiness = sum(readiness_factors) / len(readiness_factors) * 100 + # Calculate readiness — empty DB must yield 0% + # Each factor requires positive evidence (not just absence of problems) + 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), # Only counts if there's actual data + policies_total > 0 and policies_approved >= policies_total * 0.8, + objectives_total > 0 + ] + certification_readiness = sum(readiness_factors) / len(readiness_factors) * 100 # Overall status - if open_majors > 0: + 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" - # Build chapter status list + # Build chapter status list — empty DB must show 0% / "not_started" + 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" + + # Chapter 9: count sub-components for percentage + ch9_parts = sum([last_mgmt_review is not None, last_internal_audit is not None]) + ch9_pct = (ch9_parts / 2) * 100 + + # Chapter 10: only show 100% if there's actual CAPA activity, not just empty + 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="compliant" if scope_approved else "non_compliant", + 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 [], @@ -1589,37 +1621,40 @@ async def get_iso27001_overview(db: Session = Depends(get_db)): ISO27001ChapterStatus( chapter="5", title="Führung", - status="compliant" if policies_approved > 0 else "non_compliant", - completion_percentage=(policies_approved / max(policies_total, 1)) * 100, + 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))], + 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="compliant" if objectives_total > 0 else "partial", - completion_percentage=75.0 if objectives_total > 0 else 25.0, + 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"], + key_documents=["Risk Register", "Security Objectives"] if objectives_total > 0 else [], last_reviewed=None ), ISO27001ChapterStatus( chapter="9", title="Bewertung der Leistung", - status="compliant" if (last_mgmt_review and last_internal_audit) else "non_compliant", - completion_percentage=100.0 if (last_mgmt_review and last_internal_audit) else 50.0, + 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", "Management Review Minutes"], + 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="compliant" if open_majors == 0 else "non_compliant", - completion_percentage=100.0 if open_majors == 0 else 50.0, + status=_chapter_status(ch10_has_data, open_majors > 0), + completion_percentage=ch10_pct, open_findings=open_majors, - key_documents=["CAPA Register"], + key_documents=["CAPA Register"] if ch10_has_data else [], last_reviewed=None ) ] diff --git a/backend-compliance/compliance/api/schemas.py b/backend-compliance/compliance/api/schemas.py index 19854f5..69b9ec2 100644 --- a/backend-compliance/compliance/api/schemas.py +++ b/backend-compliance/compliance/api/schemas.py @@ -1677,7 +1677,7 @@ class ISO27001ChapterStatus(BaseModel): class ISO27001OverviewResponse(BaseModel): """Complete ISO 27001 status overview.""" - overall_status: str # "ready", "at_risk", "not_ready" + overall_status: str # "ready", "at_risk", "not_ready", "not_started" certification_readiness: float # 0-100 chapters: List[ISO27001ChapterStatus] scope_approved: bool diff --git a/backend-compliance/tests/test_isms_routes.py b/backend-compliance/tests/test_isms_routes.py index da2a7a4..8eb9364 100644 --- a/backend-compliance/tests/test_isms_routes.py +++ b/backend-compliance/tests/test_isms_routes.py @@ -821,14 +821,18 @@ class TestOverviewDashboard: """Tests for the ISO 27001 overview endpoint.""" def test_overview_empty_isms(self): - """GET /isms/overview on empty DB should return not_ready.""" + """GET /isms/overview on empty DB should return not_started with 0% readiness.""" r = client.get("/isms/overview") assert r.status_code == 200 body = r.json() - assert body["overall_status"] in ("not_ready", "at_risk") + assert body["overall_status"] == "not_started" + assert body["certification_readiness"] == 0.0 assert body["scope_approved"] is False assert body["open_major_findings"] == 0 assert body["policies_count"] == 0 + # All chapters should show 0% on empty DB + for ch in body["chapters"]: + assert ch["completion_percentage"] == 0.0, f"Chapter {ch['chapter']} should be 0% on empty DB" def test_overview_with_data(self): """GET /isms/overview should reflect created data."""