feat(sdk): Kunden-Dokumente + CRA-Meldewesen, Screening aus Frontend genommen
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
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>
This commit is contained in:
@@ -0,0 +1,142 @@
|
||||
"""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()
|
||||
Reference in New Issue
Block a user