fix: ensure JSONB array fields are always arrays in control API

Backend: _ensure_list() converts null/string/malformed JSONB to []
for requirements, test_procedure, evidence, open_anchors, tags.

Frontend: defensive Array.isArray() check on ControlDetail.tsx.

Fixes: TypeError: A.requirements.map is not a function

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Benjamin Admin
2026-05-02 21:18:10 +02:00
parent db697924ed
commit fe6764df9a
2 changed files with 24 additions and 8 deletions
@@ -212,14 +212,14 @@ export function ControlDetail({
</section>
) : null}
{ctrl.requirements.length > 0 && (
{Array.isArray(ctrl.requirements) && ctrl.requirements.length > 0 && (
<section>
<h3 className="text-sm font-semibold text-gray-900 mb-2">Anforderungen</h3>
<ol className="list-decimal list-inside space-y-1">{ctrl.requirements.map((r, i) => <li key={i} className="text-sm text-gray-700">{r}</li>)}</ol>
</section>
)}
{ctrl.test_procedure.length > 0 && (
{Array.isArray(ctrl.test_procedure) && ctrl.test_procedure.length > 0 && (
<section>
<h3 className="text-sm font-semibold text-gray-900 mb-2">Pruefverfahren</h3>
<ol className="list-decimal list-inside space-y-1">{ctrl.test_procedure.map((s, i) => <li key={i} className="text-sm text-gray-700">{s}</li>)}</ol>
@@ -40,6 +40,22 @@ _CONTROL_COLUMNS = """
"""
def _ensure_list(val: Any) -> list:
"""Ensure a JSONB value is always a Python list."""
if isinstance(val, list):
return val
if val is None:
return []
if isinstance(val, str):
try:
import json
parsed = json.loads(val)
return parsed if isinstance(parsed, list) else []
except (json.JSONDecodeError, TypeError):
return []
return []
def _control_row(r: Any) -> dict[str, Any]:
"""Serialize a canonical_controls SELECT row to a response dict."""
return {
@@ -49,19 +65,19 @@ def _control_row(r: Any) -> dict[str, Any]:
"title": r.title,
"objective": r.objective,
"rationale": r.rationale,
"scope": r.scope,
"requirements": r.requirements,
"test_procedure": r.test_procedure,
"evidence": r.evidence,
"scope": r.scope if isinstance(r.scope, dict) else {},
"requirements": _ensure_list(r.requirements),
"test_procedure": _ensure_list(r.test_procedure),
"evidence": _ensure_list(r.evidence),
"severity": r.severity,
"risk_score": float(r.risk_score) if r.risk_score is not None else None,
"implementation_effort": r.implementation_effort,
"evidence_confidence": (
float(r.evidence_confidence) if r.evidence_confidence is not None else None
),
"open_anchors": r.open_anchors,
"open_anchors": _ensure_list(r.open_anchors),
"release_state": r.release_state,
"tags": r.tags or [],
"tags": _ensure_list(r.tags),
"created_at": r.created_at.isoformat() if r.created_at else None,
"updated_at": r.updated_at.isoformat() if r.updated_at else None,
}