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)."""