From 4c206aa33250cdad2a8c351a3e82efcc3c94bc17 Mon Sep 17 00:00:00 2001 From: Benjamin Admin Date: Tue, 16 Jun 2026 07:05:33 +0200 Subject: [PATCH] =?UTF-8?q?feat(cra):=20scanner-repo=E2=86=92IACE-Projekt-?= =?UTF-8?q?Mapping=20persistieren=20(Pull-Flow)=20[migration-approved]?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Ersetzt die ephemere Dropdown-Auswahl durch DB-Persistenz pro IACE-Projekt: - Migration 156: compliance_cra_scanner_repo_map (tenant_id, iace_project_id PK, scanner_repo_id). Additiv + idempotent. - GET/PUT /v1/cra/scanner-repo-map/{iace_project_id} (Upsert/Clear). - useCRA lädt das gespeicherte Repo beim Laden + persistiert bei Auswahl. Co-Authored-By: Claude Opus 4.7 --- .../sdk/iace/[projectId]/cra/_hooks/useCRA.ts | 22 ++++++++- .../compliance/api/cra_link_routes.py | 46 +++++++++++++++++++ .../migrations/156_cra_scanner_repo_map.sql | 12 +++++ 3 files changed, 79 insertions(+), 1 deletion(-) create mode 100644 backend-compliance/migrations/156_cra_scanner_repo_map.sql diff --git a/admin-compliance/app/sdk/iace/[projectId]/cra/_hooks/useCRA.ts b/admin-compliance/app/sdk/iace/[projectId]/cra/_hooks/useCRA.ts index 6081f6b0..2baeac6c 100644 --- a/admin-compliance/app/sdk/iace/[projectId]/cra/_hooks/useCRA.ts +++ b/admin-compliance/app/sdk/iace/[projectId]/cra/_hooks/useCRA.ts @@ -89,7 +89,7 @@ function merge(live: any): CRADemo { export function useCRA(projectId?: string) { const [data, setData] = useState(null) const [live, setLive] = useState(false) - const [scannerRepo, setScannerRepo] = useState('') // pull-flow: chosen scanner repo_id + const [scannerRepo, setScannerRepoState] = useState('') // pull-flow: chosen scanner repo_id const [weights, setWeights] = useState({}) const [snapshots, setSnapshots] = useState([]) @@ -171,5 +171,25 @@ export function useCRA(projectId?: string) { return r.ok ? r.json() : null }, []) + // Pull-flow mapping: load the persisted scanner repo for this IACE project, + // and persist it on change (replaces the old ephemeral dropdown state). + useEffect(() => { + if (!projectId) return + fetch(`/api/v1/cra/scanner-repo-map/${projectId}`) + .then((r) => (r.ok ? r.json() : null)) + .then((j) => { if (j?.scanner_repo_id) setScannerRepoState(j.scanner_repo_id) }) + .catch(() => {}) + }, [projectId]) + + const setScannerRepo = useCallback((v: string) => { + setScannerRepoState(v) + if (projectId) { + fetch(`/api/v1/cra/scanner-repo-map/${projectId}`, { + method: 'PUT', headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ scanner_repo_id: v }), + }).catch(() => {}) + } + }, [projectId]) + return { data, live, weights, setWeights, snapshots, saveSnapshot, viewSnapshot, scannerRepo, setScannerRepo } } diff --git a/backend-compliance/compliance/api/cra_link_routes.py b/backend-compliance/compliance/api/cra_link_routes.py index f131731f..daac1802 100644 --- a/backend-compliance/compliance/api/cra_link_routes.py +++ b/backend-compliance/compliance/api/cra_link_routes.py @@ -68,3 +68,49 @@ async def by_iace(iace_project_id: str, tenant_id: str = Depends(get_tenant_id)) } finally: db.close() + + +class ScannerRepoMap(BaseModel): + scanner_repo_id: str + + +@router.get("/scanner-repo-map/{iace_project_id}") +async def get_scanner_repo_map(iace_project_id: str, tenant_id: str = Depends(get_tenant_id)): + """The scanner repo persisted for this IACE project (pull-flow), or empty.""" + db = SessionLocal() + try: + row = db.execute(text(""" + SELECT scanner_repo_id FROM compliance_cra_scanner_repo_map + WHERE tenant_id = :tid AND iace_project_id = CAST(:p AS uuid) + """), {"tid": tenant_id, "p": iace_project_id}).fetchone() + return {"scanner_repo_id": row[0] if row else ""} + finally: + db.close() + + +@router.put("/scanner-repo-map/{iace_project_id}") +async def put_scanner_repo_map(iace_project_id: str, body: ScannerRepoMap, + tenant_id: str = Depends(get_tenant_id)): + """Upsert (or clear, on empty) the scanner repo for this IACE project.""" + db = SessionLocal() + try: + if body.scanner_repo_id: + db.execute(text(""" + INSERT INTO compliance_cra_scanner_repo_map + (tenant_id, iace_project_id, scanner_repo_id, updated_at) + VALUES (:tid, CAST(:p AS uuid), :r, NOW()) + ON CONFLICT (tenant_id, iace_project_id) + DO UPDATE SET scanner_repo_id = EXCLUDED.scanner_repo_id, updated_at = NOW() + """), {"tid": tenant_id, "p": iace_project_id, "r": body.scanner_repo_id}) + else: + db.execute(text(""" + DELETE FROM compliance_cra_scanner_repo_map + WHERE tenant_id = :tid AND iace_project_id = CAST(:p AS uuid) + """), {"tid": tenant_id, "p": iace_project_id}) + db.commit() + return {"iace_project_id": iace_project_id, "scanner_repo_id": body.scanner_repo_id} + except Exception: + db.rollback() + raise + finally: + db.close() diff --git a/backend-compliance/migrations/156_cra_scanner_repo_map.sql b/backend-compliance/migrations/156_cra_scanner_repo_map.sql new file mode 100644 index 00000000..d2d730e6 --- /dev/null +++ b/backend-compliance/migrations/156_cra_scanner_repo_map.sql @@ -0,0 +1,12 @@ +-- Migration 156: persist the scanner repo chosen per IACE project for the +-- CRA/Cyber pull-flow (replaces the ephemeral UI dropdown selection). Keyed by +-- IACE project id so it works for any project, independent of a linked CRA +-- project. Additive + idempotent. + +CREATE TABLE IF NOT EXISTS compliance_cra_scanner_repo_map ( + tenant_id UUID NOT NULL, + iace_project_id UUID NOT NULL, + scanner_repo_id TEXT NOT NULL, + updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + PRIMARY KEY (tenant_id, iace_project_id) +);