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>
88 lines
3.7 KiB
Python
88 lines
3.7 KiB
Python
"""CRA Art. 14 reporting cascade — pure deadline + ENISA-export logic."""
|
|
import pytest
|
|
|
|
from compliance.services.cra_meldewesen import (
|
|
compute_deadlines, next_open_stage, build_enisa_report, report_completeness,
|
|
)
|
|
|
|
AWARE = "2026-09-15T08:00:00+00:00"
|
|
|
|
|
|
class TestDeadlines:
|
|
def test_due_times_are_24h_72h_14d_after_awareness(self):
|
|
d = compute_deadlines(AWARE, now_iso=AWARE)
|
|
by = {x["key"]: x for x in d}
|
|
assert by["early_warning"]["due_at"] == "2026-09-16T08:00:00+00:00"
|
|
assert by["notification"]["due_at"] == "2026-09-18T08:00:00+00:00"
|
|
assert by["final"]["due_at"] == "2026-09-29T08:00:00+00:00"
|
|
|
|
def test_all_pending_right_after_awareness(self):
|
|
d = compute_deadlines(AWARE, now_iso="2026-09-15T09:00:00+00:00")
|
|
assert all(x["status"] == "pending" for x in d)
|
|
|
|
def test_overdue_when_now_past_due_and_unsubmitted(self):
|
|
d = compute_deadlines(AWARE, now_iso="2026-09-17T00:00:00+00:00")
|
|
by = {x["key"]: x for x in d}
|
|
assert by["early_warning"]["status"] == "overdue" # 24h passed
|
|
assert by["notification"]["status"] == "pending" # 72h not yet
|
|
|
|
def test_due_soon_within_window(self):
|
|
# 4h before the 24h deadline → due_soon
|
|
d = compute_deadlines(AWARE, now_iso="2026-09-16T04:00:00+00:00")
|
|
by = {x["key"]: x for x in d}
|
|
assert by["early_warning"]["status"] == "due_soon"
|
|
|
|
def test_submitted_overrides_timing(self):
|
|
d = compute_deadlines(
|
|
AWARE, submissions={"early_warning": "2026-09-16T07:00:00+00:00"},
|
|
now_iso="2026-09-20T00:00:00+00:00")
|
|
by = {x["key"]: x for x in d}
|
|
assert by["early_warning"]["status"] == "submitted"
|
|
assert by["notification"]["status"] == "overdue"
|
|
|
|
def test_next_open_stage(self):
|
|
d = compute_deadlines(AWARE, submissions={"early_warning": AWARE}, now_iso=AWARE)
|
|
assert next_open_stage(d) == "notification"
|
|
d2 = compute_deadlines(
|
|
AWARE, submissions={k: AWARE for k in ("early_warning", "notification", "final")},
|
|
now_iso=AWARE)
|
|
assert next_open_stage(d2) is None
|
|
|
|
|
|
class TestEnisaReport:
|
|
def _incident(self):
|
|
return {
|
|
"manufacturer": "OWIS GmbH", "product_name": "PS 90+", "product_version": "2.1",
|
|
"kind": "exploited_vulnerability", "severity": "high", "aware_at": AWARE,
|
|
"summary": "Aktiv ausgenutzte Auth-Umgehung", "contact": "psirt@owis.eu",
|
|
"impact": "Fernsteuerung möglich", "root_cause": "fehlende Tokenprüfung",
|
|
"patch_available": True,
|
|
}
|
|
|
|
def test_early_warning_has_base_not_detail(self):
|
|
r = build_enisa_report(self._incident(), "early_warning")
|
|
assert r["manufacturer"] == "OWIS GmbH" and r["severity"] == "high"
|
|
assert "impact" not in r and "root_cause" not in r
|
|
assert r["submission_target"].startswith("ENISA")
|
|
|
|
def test_notification_adds_impact_not_rootcause(self):
|
|
r = build_enisa_report(self._incident(), "notification")
|
|
assert r["impact"] == "Fernsteuerung möglich"
|
|
assert "root_cause" not in r
|
|
|
|
def test_final_adds_root_cause_and_patch(self):
|
|
r = build_enisa_report(self._incident(), "final")
|
|
assert r["root_cause"] == "fehlende Tokenprüfung"
|
|
assert r["patch_available"] is True
|
|
|
|
def test_unknown_stage_raises(self):
|
|
with pytest.raises(ValueError):
|
|
build_enisa_report(self._incident(), "nonsense")
|
|
|
|
def test_completeness_flags_missing(self):
|
|
thin = {"manufacturer": "X", "aware_at": AWARE}
|
|
c = report_completeness(thin, "early_warning")
|
|
assert not c["complete"]
|
|
assert "product_name" in c["missing"]
|
|
assert "manufacturer" in c["filled"]
|