feat(cra): Flow-2 UI — Scanner-Repo wählen → echtes Assessment
- 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 <noreply@anthropic.com>
This commit is contained in:
@@ -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<RepoOpt[]>([])
|
||||
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 (
|
||||
<div className="rounded-lg border border-gray-200 dark:border-gray-700 bg-white dark:bg-gray-800 px-4 py-3 text-sm text-gray-500">
|
||||
Kein Repo-Scanner verbunden — es wird das Demo-Szenario gezeigt.
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="rounded-lg border border-purple-200 dark:border-purple-800 bg-purple-50/50 dark:bg-purple-900/20 px-4 py-3 flex flex-wrap items-center gap-3">
|
||||
<span className="text-sm font-medium text-gray-700 dark:text-gray-200">Repo-Scanner:</span>
|
||||
<select
|
||||
value={value}
|
||||
onChange={(e) => onChange(e.target.value)}
|
||||
className="text-sm rounded border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-700 px-2 py-1.5"
|
||||
>
|
||||
<option value="">Demo-Szenario (kein echtes Repo)</option>
|
||||
{repos.map((r) => (
|
||||
<option key={r.repo_id} value={r.repo_id}>{r.repo_id} ({r.count} Befunde)</option>
|
||||
))}
|
||||
</select>
|
||||
{value && <span className="text-sm text-purple-700 dark:text-purple-300">Echte Scanner-Befunde werden bewertet.</span>}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -89,6 +89,7 @@ function merge(live: any): CRADemo {
|
||||
export function useCRA(projectId?: string) {
|
||||
const [data, setData] = useState<CRADemo | null>(null)
|
||||
const [live, setLive] = useState(false)
|
||||
const [scannerRepo, setScannerRepo] = useState('') // pull-flow: chosen scanner repo_id
|
||||
const [weights, setWeights] = useState<Weights>({})
|
||||
const [snapshots, setSnapshots] = useState<SnapshotMeta[]>([])
|
||||
|
||||
@@ -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 }
|
||||
}
|
||||
|
||||
@@ -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 <p className="text-sm text-gray-500">CRA-Risikobeurteilung wird geladen …</p>
|
||||
}
|
||||
@@ -28,6 +29,7 @@ export default function CRAPage() {
|
||||
Backend nicht erreichbar — statisches Szenario angezeigt.
|
||||
</p>
|
||||
)}
|
||||
<ScannerRepoPicker value={scannerRepo} onChange={setScannerRepo} />
|
||||
<WeightsControl weights={weights} onChange={setWeights} />
|
||||
<CRACyberView data={data} />
|
||||
<SnapshotPanel snapshots={snapshots} onSave={saveSnapshot} onView={viewSnapshot} />
|
||||
|
||||
@@ -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)."""
|
||||
|
||||
Reference in New Issue
Block a user