Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
84 changes: 53 additions & 31 deletions backend/routers/reports.py
Original file line number Diff line number Diff line change
Expand Up @@ -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."""
Expand All @@ -31,7 +53,7 @@ def get_status_emoji(status: str) -> str:
"partial": "🟑",
"partially_implemented": "🟑",
"planned": "πŸ“",
"not_implemented": "πŸ›‘",
"not_implemented": "🚫",
"na": "βšͺ",
"not_started": "βšͺ",
}
Expand Down Expand Up @@ -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)}

---
Expand Down Expand Up @@ -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.*


"""

Expand All @@ -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 += """
Expand Down Expand Up @@ -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"'
},
)

Expand Down Expand Up @@ -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"},
Expand Down
15 changes: 15 additions & 0 deletions tests/test_palette_ux.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Loading