"""Persist CRA risk-assessment snapshots — the CRA Art. 13 'running system'. Reuses the existing compliance_cra_documents table (NO new schema, frozen DB): a snapshot is a document of doc_type='doc_risk_assessment', versioned, with the full assessment stored in generation_context. Each new snapshot supersedes the prior one, giving an auditable history over the product lifecycle. """ import json from sqlalchemy import text from database import SessionLocal DOC_TYPE = "doc_risk_assessment" def assessment_markdown(assessment: dict) -> str: by = assessment.get("by_risk", {}) lines = [ "# CRA-Cyber-Risikobeurteilung", "", "- Befunde gesamt: {}".format(assessment.get("findings_total", 0)), "- Abdeckung: {} %".format(assessment.get("coverage_pct", 0)), "- Risiko: kritisch {} · hoch {} · mittel {} · niedrig {}".format( by.get("CRITICAL", 0), by.get("HIGH", 0), by.get("MEDIUM", 0), by.get("LOW", 0)), "- CRA-Anforderungen betroffen: {} / 40".format(len(assessment.get("requirements_touched", []))), "", "## Priorisierte Befunde", ] for m in assessment.get("mapped", []): lines.append("- [{}] {} → {} — {}".format( m.get("priority_tier", ""), m.get("finding_id", ""), m.get("primary_requirement", ""), m.get("priority_reason", ""))) if assessment.get("cross_links"): lines += ["", "## Cyber trifft Safety"] for cl in assessment["cross_links"]: lines.append("- {} ← {}".format(cl.get("safety_ref", ""), ", ".join(cl.get("cyber_finding_ids", [])))) return "\n".join(lines) def save_snapshot(cra_project_id: str, tenant_id: str, assessment: dict, title: str = "CRA-Cyber-Risikobeurteilung") -> dict: db = SessionLocal() try: db.execute(text(""" UPDATE compliance_cra_documents SET status='superseded', superseded_at=NOW() WHERE cra_project_id = CAST(:pid AS uuid) AND tenant_id = :tid AND doc_type = :dt AND status != 'superseded' """), {"pid": cra_project_id, "tid": tenant_id, "dt": DOC_TYPE}) ver = db.execute(text(""" SELECT COALESCE(MAX(version), 0) + 1 FROM compliance_cra_documents WHERE cra_project_id = CAST(:pid AS uuid) AND tenant_id = :tid AND doc_type = :dt """), {"pid": cra_project_id, "tid": tenant_id, "dt": DOC_TYPE}).scalar() coverage = {"covered": assessment.get("requirements_touched", []), "total": 40} row = db.execute(text(""" INSERT INTO compliance_cra_documents (cra_project_id, tenant_id, doc_type, title, content_md, version, requirements_coverage, generation_context, status) VALUES (CAST(:pid AS uuid), :tid, :dt, :title, :md, :ver, CAST(:cov AS jsonb), CAST(:ctx AS jsonb), 'draft') RETURNING id, version, generated_at """), {"pid": cra_project_id, "tid": tenant_id, "dt": DOC_TYPE, "title": title, "md": assessment_markdown(assessment), "ver": ver, "cov": json.dumps(coverage), "ctx": json.dumps(assessment)}).fetchone() db.commit() return {"id": str(row.id), "version": row.version, "generated_at": row.generated_at.isoformat()} except Exception: db.rollback() raise finally: db.close() def list_snapshots(cra_project_id: str, tenant_id: str) -> list: db = SessionLocal() try: rows = db.execute(text(""" SELECT id, version, status, generated_at, requirements_coverage FROM compliance_cra_documents WHERE cra_project_id = CAST(:pid AS uuid) AND tenant_id = :tid AND doc_type = :dt ORDER BY version DESC """), {"pid": cra_project_id, "tid": tenant_id, "dt": DOC_TYPE}).fetchall() return [{"id": str(r.id), "version": r.version, "status": r.status, "generated_at": r.generated_at.isoformat(), "coverage": r.requirements_coverage} for r in rows] finally: db.close() def get_snapshot(snapshot_id: str, tenant_id: str): db = SessionLocal() try: r = db.execute(text(""" SELECT id, version, status, generated_at, content_md, generation_context FROM compliance_cra_documents WHERE id = CAST(:sid AS uuid) AND tenant_id = :tid AND doc_type = :dt """), {"sid": snapshot_id, "tid": tenant_id, "dt": DOC_TYPE}).fetchone() if not r: return None return {"id": str(r.id), "version": r.version, "status": r.status, "generated_at": r.generated_at.isoformat(), "content_md": r.content_md, "assessment": r.generation_context} finally: db.close()