From 90def4d8575de2ffcaba6715d37e8dfc5dd1fa7e Mon Sep 17 00:00:00 2001
From: Benjamin Admin
Date: Tue, 16 Jun 2026 05:49:15 +0200
Subject: [PATCH] =?UTF-8?q?feat(cra):=20Flow-2=20UI=20=E2=80=94=20Scanner-?=
=?UTF-8?q?Repo=20w=C3=A4hlen=20=E2=86=92=20echtes=20Assessment?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
- GET /v1/cra/scanner-repos: distinct repo_ids (+counts) vom Scanner-MCP für den Picker.
- useCRA: scannerRepo-State; bei Auswahl POST /assess-from-scanner (echte Findings),
sonst by-iace/Demo wie bisher.
- ScannerRepoPicker im CRA/Cyber-Tab; leere Auswahl = Demo, Repo gewählt = echte Befunde.
Mapping repo_id↔Projekt aktuell UI-seitig (ephemeral); DB-Persistenz pro Projekt folgt.
Co-Authored-By: Claude Opus 4.7
---
.../cra/_components/ScannerRepoPicker.tsx | 46 +++++++++++++++++++
.../sdk/iace/[projectId]/cra/_hooks/useCRA.ts | 19 +++++++-
.../app/sdk/iace/[projectId]/cra/page.tsx | 4 +-
.../compliance/api/cra_assess_routes.py | 17 +++++++
4 files changed, 83 insertions(+), 3 deletions(-)
create mode 100644 admin-compliance/app/sdk/iace/[projectId]/cra/_components/ScannerRepoPicker.tsx
diff --git a/admin-compliance/app/sdk/iace/[projectId]/cra/_components/ScannerRepoPicker.tsx b/admin-compliance/app/sdk/iace/[projectId]/cra/_components/ScannerRepoPicker.tsx
new file mode 100644
index 00000000..f7e4bdb9
--- /dev/null
+++ b/admin-compliance/app/sdk/iace/[projectId]/cra/_components/ScannerRepoPicker.tsx
@@ -0,0 +1,46 @@
+'use client'
+
+import { useEffect, useState } from 'react'
+
+interface RepoOpt { repo_id: string; count: number }
+
+// Pull-flow control: pick a scanner repo → useCRA assesses its real findings.
+// Empty selection keeps the demo/linked assessment.
+export function ScannerRepoPicker({ value, onChange }: { value: string; onChange: (v: string) => void }) {
+ const [repos, setRepos] = useState([])
+ const [loaded, setLoaded] = useState(false)
+
+ useEffect(() => {
+ let cancelled = false
+ fetch('/api/v1/cra/scanner-repos')
+ .then((r) => (r.ok ? r.json() : { repos: [] }))
+ .then((j) => { if (!cancelled) { setRepos(j.repos || []); setLoaded(true) } })
+ .catch(() => { if (!cancelled) setLoaded(true) })
+ return () => { cancelled = true }
+ }, [])
+
+ if (loaded && repos.length === 0) {
+ return (
+
+ Kein Repo-Scanner verbunden — es wird das Demo-Szenario gezeigt.
+
+ )
+ }
+
+ return (
+
+ Repo-Scanner:
+
+ {value && Echte Scanner-Befunde werden bewertet.}
+
+ )
+}
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 b59ae334..6081f6b0 100644
--- a/admin-compliance/app/sdk/iace/[projectId]/cra/_hooks/useCRA.ts
+++ b/admin-compliance/app/sdk/iace/[projectId]/cra/_hooks/useCRA.ts
@@ -89,6 +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 [weights, setWeights] = useState({})
const [snapshots, setSnapshots] = useState([])
@@ -103,6 +104,20 @@ export function useCRA(projectId?: string) {
useEffect(() => {
let cancelled = false
;(async () => {
+ // 0. Pull-flow: a scanner repo is chosen → assess its real findings.
+ if (scannerRepo) {
+ try {
+ const r = await fetch('/api/v1/cra/assess-from-scanner', {
+ method: 'POST', headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({ repo_id: scannerRepo, weights, safety_functions: SAFETY_FUNCTIONS }),
+ })
+ if (r.ok) {
+ const j = await r.json()
+ if (!cancelled) { setData(merge(j)); setLive(true) }
+ return
+ }
+ } catch { /* fall through */ }
+ }
// 1. Is a CRA project LINKED to this IACE project? Then show its real
// assessment (latest snapshot) instead of the demo scenario.
if (projectId) {
@@ -131,7 +146,7 @@ export function useCRA(projectId?: string) {
}
})()
return () => { cancelled = true }
- }, [buildPayload, projectId])
+ }, [buildPayload, projectId, scannerRepo, weights])
const refreshSnapshots = useCallback(() => {
if (!projectId) return
@@ -156,5 +171,5 @@ export function useCRA(projectId?: string) {
return r.ok ? r.json() : null
}, [])
- return { data, live, weights, setWeights, snapshots, saveSnapshot, viewSnapshot }
+ return { data, live, weights, setWeights, snapshots, saveSnapshot, viewSnapshot, scannerRepo, setScannerRepo }
}
diff --git a/admin-compliance/app/sdk/iace/[projectId]/cra/page.tsx b/admin-compliance/app/sdk/iace/[projectId]/cra/page.tsx
index 456e8822..f84172aa 100644
--- a/admin-compliance/app/sdk/iace/[projectId]/cra/page.tsx
+++ b/admin-compliance/app/sdk/iace/[projectId]/cra/page.tsx
@@ -5,11 +5,12 @@ import { useCRA } from './_hooks/useCRA'
import { CRACyberView } from './_components/CRACyberView'
import { WeightsControl } from './_components/WeightsControl'
import { SnapshotPanel } from './_components/SnapshotPanel'
+import { ScannerRepoPicker } from './_components/ScannerRepoPicker'
export default function CRAPage() {
const params = useParams()
const projectId = params?.projectId as string | undefined
- const { data, live, weights, setWeights, snapshots, saveSnapshot, viewSnapshot } = useCRA(projectId)
+ const { data, live, weights, setWeights, snapshots, saveSnapshot, viewSnapshot, scannerRepo, setScannerRepo } = useCRA(projectId)
if (!data) {
return CRA-Risikobeurteilung wird geladen …
}
@@ -28,6 +29,7 @@ export default function CRAPage() {
Backend nicht erreichbar — statisches Szenario angezeigt.
)}
+
diff --git a/backend-compliance/compliance/api/cra_assess_routes.py b/backend-compliance/compliance/api/cra_assess_routes.py
index 45922c06..dcefaf8f 100644
--- a/backend-compliance/compliance/api/cra_assess_routes.py
+++ b/backend-compliance/compliance/api/cra_assess_routes.py
@@ -134,6 +134,23 @@ async def assess_from_scanner(body: ScannerPullRequest):
return result
+@router.get("/scanner-repos")
+async def scanner_repos():
+ """Distinct repo_ids the scanner has findings for, so the UI can pick which
+ repo to assess. Best-effort (one findings page); empty if no scanner config."""
+ findings = await fetch_findings(limit=200)
+ counts: Dict[str, int] = {}
+ for f in findings:
+ rid = f.get("repo_id")
+ if rid:
+ counts[rid] = counts.get(rid, 0) + 1
+ repos = sorted(
+ ({"repo_id": k, "count": v} for k, v in counts.items()),
+ key=lambda r: -r["count"],
+ )
+ return {"repos": repos, "sampled": len(findings) >= 200}
+
+
@router.post("/projects/{project_id}/assess-snapshot")
async def assess_snapshot(project_id: str, body: AssessRequest, tenant_id: str = Depends(get_tenant_id)):
"""Run the assessment and persist it as a versioned snapshot (running system)."""