8f21650d74
CI / detect-changes (push) Successful in 16s
CI / branch-name (push) Has been skipped
CI / guardrail-integrity (push) Has been skipped
CI / secret-scan (push) Has been skipped
CI / dep-audit (push) Has been skipped
CI / sbom-scan (push) Has been skipped
CI / build-sha-integrity (push) Successful in 15s
CI / validate-canonical-controls (push) Successful in 13s
CI / loc-budget (push) Successful in 25s
CI / go-lint (push) Has been skipped
CI / python-lint (push) Has been skipped
CI / nodejs-lint (push) Has been skipped
CI / nodejs-build (push) Successful in 3m9s
CI / test-go (push) Has been skipped
CI / iace-gt-coverage (push) Has been skipped
CI / test-python-backend (push) Successful in 31s
CI / test-python-document-crawler (push) Has been skipped
CI / test-python-dsms-gateway (push) Has been skipped
- /sdk/dokumente: Kundensicht nur auf veroeffentlichte Rechtsdokumente (Ansehen + Download); Proxy mit Allow-List nur /public — Templates/Drafts/ Generator bleiben unerreichbar. - /sdk/cra-meldewesen: CRA Art. 14 Meldewesen (24h/72h/14d-Kaskade) mit Fristen-Tracking + ENISA-SRP-Export-Entwurf (kein Live-API). Backend: cra_meldewesen (pure, getestet) + cra_incident_store (schema-neutral ueber compliance_cra_documents) + /api/v1/cra/incidents (additiv, contract-safe). - Screening (Self-Scan) aus dem Frontend genommen: Flow-Stepper-Eintrag ausgeblendet (visibleWhen), Dashboard-Kachel + Import-Button entfernt. Repo-Scanning laeuft extern im Compliance-Scanner; Backend-Router bleibt vorerst gemountet (Contract-Stabilitaet). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
143 lines
5.7 KiB
Python
143 lines
5.7 KiB
Python
"""Persist CRA Art. 14 incident reports — schema-neutral (frozen DB).
|
|
|
|
Reuses the existing compliance_cra_documents table (doc_type='doc_incident_report'):
|
|
one row per incident, the full incident + per-stage submissions live in
|
|
generation_context (jsonb). No supersede chaining (unlike the assessment
|
|
snapshot store) — each incident is an independent, evolving record.
|
|
"""
|
|
import json
|
|
from typing import Optional
|
|
|
|
from sqlalchemy import text
|
|
|
|
from database import SessionLocal
|
|
|
|
DOC_TYPE = "doc_incident_report"
|
|
|
|
|
|
def _summary_md(inc: dict) -> str:
|
|
return "\n".join([
|
|
"# CRA-Meldung: {}".format(inc.get("summary", "(ohne Titel)")),
|
|
"",
|
|
"- Produkt: {} {}".format(inc.get("product_name", ""), inc.get("product_version", "")),
|
|
"- Art: {} · Schwere: {}".format(inc.get("kind", ""), inc.get("severity", "")),
|
|
"- Bekannt seit: {}".format(inc.get("aware_at", "")),
|
|
])
|
|
|
|
|
|
def create_incident(cra_project_id: str, tenant_id: str, incident: dict) -> dict:
|
|
inc = dict(incident)
|
|
inc.setdefault("submissions", {}) # stage_key -> {submitted_at, report}
|
|
db = SessionLocal()
|
|
try:
|
|
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, 1,
|
|
CAST('{}' AS jsonb), CAST(:ctx AS jsonb), 'open')
|
|
RETURNING id, generated_at
|
|
"""), {"pid": cra_project_id, "tid": tenant_id, "dt": DOC_TYPE,
|
|
"title": inc.get("summary", "CRA-Meldung")[:200],
|
|
"md": _summary_md(inc), "ctx": json.dumps(inc)}).fetchone()
|
|
db.commit()
|
|
return {"id": str(row.id), "created_at": row.generated_at.isoformat(),
|
|
"status": "open", **inc}
|
|
except Exception:
|
|
db.rollback()
|
|
raise
|
|
finally:
|
|
db.close()
|
|
|
|
|
|
def list_incidents(cra_project_id: str, tenant_id: str) -> list:
|
|
db = SessionLocal()
|
|
try:
|
|
rows = db.execute(text("""
|
|
SELECT id, status, generated_at, generation_context
|
|
FROM compliance_cra_documents
|
|
WHERE cra_project_id = CAST(:pid AS uuid) AND tenant_id = :tid AND doc_type = :dt
|
|
ORDER BY generated_at DESC
|
|
"""), {"pid": cra_project_id, "tid": tenant_id, "dt": DOC_TYPE}).fetchall()
|
|
out = []
|
|
for r in rows:
|
|
ctx = r.generation_context if isinstance(r.generation_context, dict) else {}
|
|
out.append({"id": str(r.id), "status": r.status,
|
|
"created_at": r.generated_at.isoformat(), **ctx})
|
|
return out
|
|
finally:
|
|
db.close()
|
|
|
|
|
|
def get_incident(incident_id: str, tenant_id: str) -> Optional[dict]:
|
|
db = SessionLocal()
|
|
try:
|
|
r = db.execute(text("""
|
|
SELECT id, status, generated_at, generation_context
|
|
FROM compliance_cra_documents
|
|
WHERE id = CAST(:iid AS uuid) AND tenant_id = :tid AND doc_type = :dt
|
|
"""), {"iid": incident_id, "tid": tenant_id, "dt": DOC_TYPE}).fetchone()
|
|
if not r:
|
|
return None
|
|
ctx = r.generation_context if isinstance(r.generation_context, dict) else {}
|
|
return {"id": str(r.id), "status": r.status,
|
|
"created_at": r.generated_at.isoformat(), **ctx}
|
|
finally:
|
|
db.close()
|
|
|
|
|
|
def _save_ctx(db, incident_id: str, tenant_id: str, ctx: dict, status: str) -> None:
|
|
db.execute(text("""
|
|
UPDATE compliance_cra_documents
|
|
SET generation_context = CAST(:ctx AS jsonb), status = :st, updated_at = NOW()
|
|
WHERE id = CAST(:iid AS uuid) AND tenant_id = :tid AND doc_type = :dt
|
|
"""), {"ctx": json.dumps(ctx), "st": status, "iid": incident_id,
|
|
"tid": tenant_id, "dt": DOC_TYPE})
|
|
|
|
|
|
def update_incident(incident_id: str, tenant_id: str, patch: dict) -> Optional[dict]:
|
|
db = SessionLocal()
|
|
try:
|
|
r = db.execute(text("""
|
|
SELECT status, generation_context FROM compliance_cra_documents
|
|
WHERE id = CAST(:iid AS uuid) AND tenant_id = :tid AND doc_type = :dt
|
|
"""), {"iid": incident_id, "tid": tenant_id, "dt": DOC_TYPE}).fetchone()
|
|
if not r:
|
|
return None
|
|
ctx = dict(r.generation_context if isinstance(r.generation_context, dict) else {})
|
|
ctx.update({k: v for k, v in patch.items() if k != "submissions"})
|
|
_save_ctx(db, incident_id, tenant_id, ctx, r.status)
|
|
db.commit()
|
|
return {"id": incident_id, "status": r.status, **ctx}
|
|
except Exception:
|
|
db.rollback()
|
|
raise
|
|
finally:
|
|
db.close()
|
|
|
|
|
|
def record_submission(incident_id: str, tenant_id: str, stage: str,
|
|
submitted_at: str, report: dict) -> Optional[dict]:
|
|
"""Mark a cascade stage as submitted (stores the ENISA export draft + ts)."""
|
|
db = SessionLocal()
|
|
try:
|
|
r = db.execute(text("""
|
|
SELECT generation_context FROM compliance_cra_documents
|
|
WHERE id = CAST(:iid AS uuid) AND tenant_id = :tid AND doc_type = :dt
|
|
"""), {"iid": incident_id, "tid": tenant_id, "dt": DOC_TYPE}).fetchone()
|
|
if not r:
|
|
return None
|
|
ctx = dict(r.generation_context if isinstance(r.generation_context, dict) else {})
|
|
subs = dict(ctx.get("submissions") or {})
|
|
subs[stage] = {"submitted_at": submitted_at, "report": report}
|
|
ctx["submissions"] = subs
|
|
status = "closed" if stage == "final" else "reporting"
|
|
_save_ctx(db, incident_id, tenant_id, ctx, status)
|
|
db.commit()
|
|
return {"id": incident_id, "status": status, **ctx}
|
|
except Exception:
|
|
db.rollback()
|
|
raise
|
|
finally:
|
|
db.close()
|