diff --git a/backend/routers/reports.py b/backend/routers/reports.py index 5f2660b..f48f9b3 100644 --- a/backend/routers/reports.py +++ b/backend/routers/reports.py @@ -8,21 +8,43 @@ import csv import io -import json from datetime import UTC, date, datetime -from typing import Any, Dict, List +from typing import List from fastapi import APIRouter, Depends from fastapi.responses import PlainTextResponse from sqlalchemy import func, select from sqlalchemy.ext.asyncio import AsyncSession -from backend.db.database import (AssessmentRecord, ControlRecord, - EvidenceRecord, get_db, +from backend.db.database import (AssessmentRecord, ControlRecord, get_db, get_latest_assessments) router = APIRouter() +# Zero Trust Pillar to CMMC Domain mapping +ZT_PILLAR_DOMAINS = { + "User": ["AC", "IA", "PS"], + "Device": ["CM", "MA", "PE"], + "Network": ["SC", "AC"], + "Application": ["CM", "CA", "SI"], + "Data": ["MP", "SC", "AU"], + "Visibility & Analytics": ["AU", "IR", "RA"], + "Automation & Orchestration": ["IR", "SI", "CA"], +} + + +def get_maturity_pct(assessments: List[AssessmentRecord]) -> float: + """Calculate maturity percentage with 0.5 weight for partial implementations.""" + if not assessments: + return 0.0 + implemented = sum(1 for a in assessments if a.status == "implemented") + partial = sum( + 1 + for a in assessments + if a.status in ["partial", "partially_implemented", "planned"] + ) + return ((implemented + (partial * 0.5)) / len(assessments)) * 100 + def get_status_emoji(status: str) -> str: """Map implementation status to a visual emoji for better scannability.""" @@ -31,7 +53,7 @@ def get_status_emoji(status: str) -> str: "partial": "🟡", "partially_implemented": "🟡", "planned": "📝", - "not_implemented": "🛑", + "not_implemented": "🚫", "na": "⚪", "not_started": "⚪", } @@ -94,21 +116,23 @@ async def generate_ssp( ) sprs_estimate = max(-203, round(sprs_estimate, 0)) - total_controls_count = len(controls) - compliance_pct = ( - (status_counts["implemented"] / total_controls_count * 100) - if total_controls_count > 0 - else 0 - ) - progress_bar = get_progress_bar(compliance_pct) + # Calculate ZT Pillar progress + zt_alignment_rows = "" + for pillar, domains in ZT_PILLAR_DOMAINS.items(): + pillar_assessments = [ + a for a in assessments + if any(a.control_id.startswith(d) for d in domains) + ] + maturity = get_maturity_pct(pillar_assessments) + zt_alignment_rows += f"| {pillar} | {', '.join(domains)} | {get_progress_bar(maturity)} |\n" ssp = f"""# System Security Plan (SSP) ## {system_name} -**Classification:** {classification} +**Classification:** {classification} **Generated:** {datetime.now(UTC).strftime('%Y-%m-%d %H:%M UTC')} -**Framework:** CMMC 2.0 Level 2 / NIST SP 800-171 Rev 2 -**SPRS Score Estimate:** {sprs_estimate} +**Framework:** CMMC 2.0 Level 2 / NIST SP 800-171 Rev 2 +**SPRS Score Estimate:** {sprs_estimate} **Overall Compliance:** {get_progress_bar(implemented_pct)} --- @@ -136,23 +160,22 @@ async def generate_ssp( | Not Implemented | {get_status_emoji('not_implemented')} {status_counts['not_implemented']} | | N/A | {get_status_emoji('na')} {status_counts['na']} | +[↑ Back to Top](#system-security-plan-ssp) + ## 2. Control Implementation Summary ### Zero Trust Pillar Alignment | ZT Pillar | CMMC Domains | Status | |-----------|--------------|--------| -| User | AC, IA, PS | See assessment | -| Device | CM, MA, PE | See assessment | -| Network | SC, AC | See assessment | -| Application | CM, CA, SI | See assessment | -| Data | MP, SC, AU | See assessment | -| Visibility & Analytics | AU, IR, RA | See assessment | -| Automation & Orchestration | IR, SI, CA | See assessment | +{zt_alignment_rows} + +[↑ Back to Top](#system-security-plan-ssp) ## 3. Assessment Findings -*Note: Only the first 20 assessment findings are displayed in this summary.* +*Showing {min(20, len(assessments))} of {len(assessments)} findings.* + """ @@ -171,6 +194,11 @@ async def generate_ssp( - **Notes:** {a.notes or 'None'} - **Evidence IDs:** {', '.join(a.evidence_ids or []) or 'None'} +""" + + ssp += """ +[↑ Back to Top](#system-security-plan-ssp) + """ ssp += """ @@ -249,7 +277,7 @@ async def generate_poam( content=csv_content, media_type="text/csv", headers={ - "Content-Disposition": f'attachment; filename="poam_{system_name.replace(" ","_")}.csv"' + "Content-Disposition": f'attachment; filename="poam_{system_name.replace(" ", "_")}.csv"' }, ) @@ -300,13 +328,7 @@ async def get_dashboard( round(implemented / total_controls * 100, 1) if total_controls else 0 ), "zt_pillars": [ - {"pillar": "User", "domains": ["AC", "IA", "PS"]}, - {"pillar": "Device", "domains": ["CM", "MA", "PE"]}, - {"pillar": "Network", "domains": ["SC", "AC"]}, - {"pillar": "Application", "domains": ["CM", "CA", "SI"]}, - {"pillar": "Data", "domains": ["MP", "SC", "AU"]}, - {"pillar": "Visibility & Analytics", "domains": ["AU", "IR", "RA"]}, - {"pillar": "Automation & Orchestration", "domains": ["IR", "SI", "CA"]}, + {"pillar": p, "domains": d} for p, d in ZT_PILLAR_DOMAINS.items() ], "agents": [ {"name": "orchestrator", "endpoint": "/api/orchestrator"}, diff --git a/tests/test_palette_ux.py b/tests/test_palette_ux.py index 04bed3a..c4284a2 100644 --- a/tests/test_palette_ux.py +++ b/tests/test_palette_ux.py @@ -73,3 +73,18 @@ async def test_ssp_ux_elements(): assert "⭐⭐⭐⭐⭐" in content # 0.5 confidence should have 3 stars: ⭐⭐⭐☆☆ (based on int(0.5 * 5 + 0.5) = 3) assert "⭐⭐⭐☆☆" in content + + # Check for navigation links + assert "[↑ Back to Top](#system-security-plan-ssp)" in content + + # Check for summary findings line + assert "Showing 2 of 2 findings." in content + + # Check for ZT pillars in the alignment table + assert "User" in content + assert "Device" in content + assert "Network" in content + assert "Application" in content + assert "Data" in content + assert "Visibility & Analytics" in content + assert "Automation & Orchestration" in content