Codex demo plan

Context

La pagina Demo (/demo, admin-only) ha backend completo e frontend funzionante (ShowcasePlayer con timeline playback, speed controls, fullscreen, keyboard shortcuts). Ma gli scenari sono fittizi e la UX è povera (8-11 eventi, nessuna animazione, nessun diagramma chain).

Obiettivo: Demo che un admin BeDefended presenta in screen-share a un potenziale cliente. Lo scenario principale viene da un pentest REALE su VulnHR (app HR interna, ~93 vulnerabilità). VulnHR viene esposto su internet via Cloudflare Tunnel perché il cliente possa vedere l'app live. Il pentest gira internamente su AX42-U.

Infrastruttura: Due server Hetzner attivi — CCX23 (dashboard pubblica) e AX42-U (runner + Docker). Cloudflare davanti.


FASE A — Deploy VulnHR su AX42-U + Cloudflare Tunnel

Questa fase è operativa (comandi da eseguire su AX42-U), non codice da implementare.

A1. Clonare e avviare VulnHR su AX42-U

# SSH su AX42-U ssh runner@

# Clonare VulnHR cd /opt/labs git clone https://github.com/sbbedefended/VulnHR.git vulnhr cd vulnhr

# Avviare i container docker compose up -d

# Attendere ~2 min che i servizi si stabilizzino, poi setup iniziale docker compose exec -T php mkdir -p bootstrap/cache storage/logs storage/framework/sessions storage/framework/views storage/framework/cache docker compose exec -T php chmod -R 777 bootstrap/cache storage docker compose exec -T php composer install --no-interaction --no-security-blocking docker compose exec -T php php artisan key:generate --force docker compose exec -T php php artisan migrate --force docker compose exec -T php php artisan db:seed --force docker compose exec -T php php artisan storage:link --force docker compose exec -T php chmod -R 777 storage bootstrap/cache

# Verificare che funziona curl -s -o /dev/null -w "%{http_code}" http://localhost:7331/login # Deve restituire 200

A2. Aggiungere hosts entry su AX42-U

echo "127.0.0.1 vulnhr.test" | sudo tee -a /etc/hosts

A3. Configurare Cloudflare Tunnel

Prerequisito: cloudflared installato su AX42-U, autenticato con cloudflared login.

# Creare il tunnel (una volta sola) cloudflared tunnel create demo-vulnhr

# Creare config cat > ~/.cloudflared/config-vulnhr.yml << 'EOF' tunnel: demo-vulnhr credentials-file: /root/.cloudflared/.json

ingress: - hostname: demo-hr.bedefended.com service: http://localhost:7331 - service: http_status:404 EOF

# Creare il DNS record (una volta sola) cloudflared tunnel route dns demo-vulnhr demo-hr.bedefended.com

# Avviare il tunnel cloudflared tunnel --config ~/.cloudflared/config-vulnhr.yml run demo-vulnhr

NOTA: sostituire demo-hr.bedefended.com col sottodominio reale. Il tunnel si accende/spegne con un comando — accenderlo solo prima della demo, spegnerlo dopo.

Per rendere persistente (opzionale): sudo cloudflared service install --config ~/.cloudflared/config-vulnhr.yml sudo systemctl enable cloudflared-demo-vulnhr sudo systemctl start cloudflared-demo-vulnhr

A4. Esporre anche n8n (porta 5678) — opzionale

Se vuoi mostrare anche il servizio n8n al cliente, aggiungi un secondo ingress rule nella config del tunnel:

ingress: - hostname: demo-hr.bedefended.com service: http://localhost:7331 - hostname: demo-hr-n8n.bedefended.com service: http://localhost:5678 - service: http_status:404

A5. Verifica

  • https://demo-hr.bedefended.com/login mostra la pagina login VulnHR
  • Login con a.rossi@meridian.local / Admin123! funziona
  • Il pentest girerà contro http://vulnhr.test:7331/ (interno su AX42-U)

FASE B — Eseguire il pentest reale su VulnHR

Dopo che VulnHR è up, lanciare il pentest dalla dashboard o CLI:

# Da AX42-U o dalla macchina con accesso al runner /pentest http://vulnhr.test:7331/ --fast

Questo genera un engagement completo in engagements/vulnhr-* con findings reali, timeline reale, chain reali, evidence HTTP reali.

Attendere che il pentest termini (~30-60 min con --fast).


FASE C — Miglioramenti codice al sistema Demo (per Codex)

Questa è la parte da implementare. 7 file da modificare.

Task 1 — Schema backend: aggiungere campi evidence e mermaid

File: dashboard/backend/app/schemas.py

DemoFinding (riga 2167-2174). Aggiungere 3 campi DOPO cwe:

class DemoFinding(BaseModel): id: str title: str severity: str = "" cvss_score: float | None = None endpoint: str = "" description: str = "" cwe: str = "" poc_curl: str = "" # NUOVO request_snippet: str = "" # NUOVO response_snippet: str = "" # NUOVO

DemoChain (riga 2187-2191). Aggiungere mermaid_code DOPO findings:

class DemoChain(BaseModel): id: str title: str severity: str = "" findings: list[str] = Field(default_factory=list) mermaid_code: str = "" # NUOVO

Nessun'altra modifica backend. I campi sono opzionali, compatibili all'indietro.


Task 2 — TypeScript interfaces: allineare al backend

File: dashboard/frontend/src/api/demo.ts

DemoFinding (riga 5-13). Aggiungere dopo cwe:

export interface DemoFinding { id: string title: string severity: string cvss_score: number | null endpoint: string description: string cwe: string poc_curl?: string // NUOVO request_snippet?: string // NUOVO response_snippet?: string // NUOVO }

DemoChain (riga 23-28). Aggiungere dopo findings:

export interface DemoChain { id: string title: string severity: string findings: string[] mermaid_code?: string // NUOVO }

Aggiungere hook snapshot DOPO useDeleteDemoScenario (riga 111):

export interface DemoSnapshotRequest { engagement_name: string name: string description: string highlight_finding_ids: string[] }

export function useSnapshotEngagement() { const queryClient = useQueryClient() return useMutation({ mutationFn: async (payload: DemoSnapshotRequest): Promise => { const { data } = await apiClient.post('/demo/snapshot', payload) return data }, onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['demo', 'scenarios'] }) }, }) }


Task 3 — Animazioni Tailwind

File: dashboard/frontend/tailwind.config.ts

Dentro keyframes (dopo shimmer a riga ~152, PRIMA della } che chiude keyframes), aggiungere:

'critical-drop': { '0%': { transform: 'scale(1)', boxShadow: '0 0 0 rgba(255, 51, 51, 0)' }, '15%': { transform: 'scale(1.03)', boxShadow: '0 0 40px rgba(255, 51, 51, 0.6), 0 0 80px rgba(255, 51, 51, 0.3)' }, '100%': { transform: 'scale(1)', boxShadow: '0 0 15px rgba(255, 51, 51, 0.15)' }, }, 'slide-in-left': { 'from': { opacity: '0', transform: 'translateX(-20px)' }, 'to': { opacity: '1', transform: 'translateX(0)' }, }, 'narrator-glow': { '0%, 100%': { borderColor: 'rgba(34, 211, 238, 0.3)' }, '50%': { borderColor: 'rgba(34, 211, 238, 0.6)' }, },

Dentro animation (dopo shimmer a riga ~162, PRIMA della } che chiude animation), aggiungere:

'critical-drop': 'critical-drop 0.8s cubic-bezier(0.34, 1.56, 0.64, 1) forwards', 'slide-in-left': 'slide-in-left 0.4s ease-out forwards', 'narrator-glow': 'narrator-glow 3s ease-in-out infinite',


Task 4 — Arricchire i 3 scenari seed JSON

I seed servono come FALLBACK e per demo quando VulnHR non è disponibile. Lo scenario principale verrà dallo snapshot reale (Fase B), ma i seed vanno comunque migliorati.

File da modificare: - dashboard/data/demo-scenarios/quick-api-demo.json - dashboard/data/demo-scenarios/enterprise-webapp-demo.json - dashboard/data/demo-scenarios/burp-bidirectional-demo.json

Per ogni file, mantenere INTATTI tutti i campi top-level (id, name, description, target_display, target_url, tech_stack, total_duration_seconds, total_findings, severity_counts, phases, highlight_finding_ids, created_from, snapshot_date).

Modifiche richieste per ogni scenario:

4a. Espandere timeline da ~10 a 45-55 eventi

Distribuire uniformemente su total_duration_seconds * 1000 ms. Usare 5 event_type: - PHASE — transizioni di fase (5 per scenario: discover, scan, test, verify, report) - FINDING — scoperta vulnerability. Il campo details DEVE contenere il finding ID (es. "FINDING-001 confirmed via UNION SELECT") - INFO — eventi informativi (tool output, progress, endpoint count) - WARN — segnali di allarme (WAF detected, rate limit, anomalia) - NARRATOR — NUOVO. Narrazione per il presenter. Tono descrittivo per il cliente, NON jargon tecnico. 6-8 per scenario. Esempi: - "L'AI ha analizzato 23 endpoint e identificato 3 pattern sospetti nei meccanismi di autenticazione." - "Qui l'intelligenza artificiale sta correlando automaticamente due vulnerabilità separate per costruire una catena di attacco completa." - "Il sistema ha verificato che questa vulnerabilità permette a un utente standard di accedere ai dati salariali di tutti i dipendenti."

Tipi di eventi intermedi da inserire (mix di tutti): - Dispatch agenti: "Dispatching 3 parallel agents for injection testing wave" - Tool invocations: "Running nuclei with 847 templates against 23 endpoints" - Progress: "18/23 endpoints scanned, 3 high-confidence signals promoted" - AI decisions: "[AI-DECISION] Choosing UNION-based over blind SQLi based on error reflection in response" - Discovery: "Found hidden /api/v1/admin/debug endpoint in JavaScript bundle" - Verification steps: "Replaying SQLi with different payload to confirm — not a false positive"

4b. Aggiungere evidence ai findings

Per ogni finding, aggiungere poc_curl, request_snippet, response_snippet con dati verosimili. Esempio per un SQLi:

"poc_curl": "curl -s 'https://api.demo.local/api/v1/users?search=%27%20UNION%20SELECT%201,username,password,4,5%20FROM%20users--' -H 'Authorization: Bearer eyJhbG...'", "request_snippet": "GET /api/v1/users?search=' UNION SELECT 1,username,password,4,5 FROM users-- HTTP/1.1\nHost: api.demo.local\nAuthorization: Bearer eyJhbG...", "response_snippet": "HTTP/1.1 200 OK\nContent-Type: application/json\n\n{\"users\":[{\"id\":1,\"name\":\"admin\",\"email\":\"admin@demo.local\"}]}"

4c. Aggiungere mermaid_code alle chains

Per ogni chain, aggiungere il diagramma Mermaid. Usare graph LR con nodi colorati per severity: - Critical: fill:#dc2626,stroke:#991b1b,color:#fff - High: fill:#ea580c,stroke:#c2410c,color:#fff - Medium: fill:#eab308,stroke:#a16207,color:#fff

Esempio: "mermaid_code": "graph LR\n A[\"FINDING-001\nSQL Injection\"] -->|extracts credentials| B[\"FINDING-002\nWeak JWT Secret\"]\n B -->|forges admin token| C[\"FINDING-003\nIDOR on Profiles\"]\n style A fill:#dc2626,stroke:#991b1b,color:#fff\n style B fill:#dc2626,stroke:#991b1b,color:#fff\n style C fill:#ea580c,stroke:#c2410c,color:#fff"


Task 5 — Riscrivere DemoPage.tsx

File: dashboard/frontend/src/pages/DemoPage.tsx

Questo è il task più grande. Il file ha 708 righe. Le modifiche riguardano il componente ShowcasePlayer (righe 141-489) e il componente DemoPage (righe 491-707). ScenarioCard (righe 64-139) resta invariato. Le helper functions formatMs, severityColor, findTriggeredFinding (righe 37-62) restano invariate.

5.0 — Import aggiuntivi

Aggiungere in cima al file (riga 1-27):

import { PieChart, Pie, Cell, ResponsiveContainer } from 'recharts' import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter' import { oneDark } from 'react-syntax-highlighter/dist/esm/styles/prism' import { MermaidRenderer } from '../components/chains/MermaidRenderer' import { useEngagements } from '../api/engagements' import { BaseModal } from '../components/base'

E aggiungere useSnapshotEngagement e DemoSnapshotRequest all'import da ../api/demo.

Rimuovere FlaskConical dall'import lucide (riga 6) — non serve più.

5.1 — Rimuovere costanti Live Demo

Eliminare le righe 31-35: const LIVE_DEMO_LABS = [ { id: 'portswigger', label: 'PortSwigger Labs' }, { id: 'juice-shop', label: 'OWASP Juice Shop' }, { id: 'dvwa', label: 'DVWA' }, ] as const

5.2 — Aggiungere costanti severity per donut chart

Dopo BURP_DEMO_ID (riga 30), aggiungere:

const SEVERITY_COLORS: Record = { Critical: '#dc2626', High: '#ea580c', Medium: '#eab308', Low: '#22c55e', Info: '#64748b', }

const EVENT_TYPE_CONFIG: Record = { PHASE: { icon: '◆', borderClass: 'border-accent-500/40', bgClass: 'bg-accent-500/10' }, FINDING: { icon: '⚠', borderClass: 'border-red-500/40', bgClass: 'bg-red-500/10' }, INFO: { icon: '●', borderClass: 'border-slate-500/30', bgClass: 'bg-slate-800/60' }, WARN: { icon: '△', borderClass: 'border-yellow-500/40', bgClass: 'bg-yellow-500/10' }, NARRATOR: { icon: '💬', borderClass: 'border-cyan-400/40', bgClass: 'bg-gradient-to-r from-cyan-500/10 to-purple-500/10' }, }

5.3 — Aggiungere useAnimatedCounter hook

Dopo le costanti, prima di ScenarioCard:

function useAnimatedCounter(target: number, duration = 800): number { const [value, setValue] = useState(0) const prevTarget = useRef(0) useEffect(() => { if (target === prevTarget.current) return prevTarget.current = target const start = performance.now() function tick(now: number) { const elapsed = now - start const progress = Math.min(elapsed / duration, 1) const eased = 1 - Math.pow(1 - progress, 3) setValue(Math.round(target * eased)) if (progress < 1) requestAnimationFrame(tick) } requestAnimationFrame(tick) }, [target, duration]) return value }

5.4 — ShowcasePlayer: aggiungere auto-scroll

Dentro ShowcasePlayer, dopo la dichiarazione di latestFinding (riga 221), aggiungere:

const timelineEndRef = useRef(null) const prevVisibleCount = useRef(0)

useEffect(() => { if (visibleEvents.length > prevVisibleCount.current) { prevVisibleCount.current = visibleEvents.length timelineEndRef.current?.scrollIntoView({ behavior: 'smooth', block: 'nearest' }) } }, [visibleEvents.length])

E piazzare

subito DOPO il .map() degli eventi timeline (dopo riga 351, dentro il div space-y-3 overflow-y-auto).

5.5 — ShowcasePlayer: aggiungere tracking Critical glow

Dopo il blocco auto-scroll, aggiungere:

const prevFindingIds = useRef(new Set()) const [freshCriticalIds, setFreshCriticalIds] = useState(new Set())

useEffect(() => { const newIds = new Set() for (const f of visibleFindings) { if (!prevFindingIds.current.has(f.id) && f.severity === 'Critical') { newIds.add(f.id) } } prevFindingIds.current = new Set(visibleFindings.map(f => f.id)) if (newIds.size > 0) { setFreshCriticalIds(newIds) const timer = setTimeout(() => setFreshCriticalIds(new Set()), 1500) return () => clearTimeout(timer) } }, [visibleFindings])

5.6 — ShowcasePlayer: aggiungere donut counter

Dopo freshCriticalIds, aggiungere:

const donutData = Object.entries(severityCounts) .filter(([, value]) => value > 0) .map(([name, value]) => ({ name, value })) const animatedTotal = useAnimatedCounter(visibleFindings.length)

5.7 — ShowcasePlayer: riscrivere rendering eventi timeline

Il blocco di rendering di ogni evento (righe 320-351) va sostituito. Attualmente:

{scenario.timeline.map(event => { const active = event.relative_time_ms <= positionMs const finding = event.event_type === 'FINDING' ? findTriggeredFinding(scenario, event.details) : null return (

${event.relative_time_ms}-${event.action}} className={cn( 'rounded border px-4 py-3 transition-all', active ? 'border-accent-500/30 bg-slate-900/90 text-white' : 'border-white/10 bg-slate-950/40 text-slate-500' )} > ...
) })}

Sostituire con:

{scenario.timeline.map(event => { const active = event.relative_time_ms <= positionMs const finding = event.event_type === 'FINDING' ? findTriggeredFinding(scenario, event.details) : null const config = EVENT_TYPE_CONFIG[event.event_type] || EVENT_TYPE_CONFIG.INFO const isNarrator = event.event_type === 'NARRATOR' const isCriticalFinding = finding && freshCriticalIds.has(finding.id)

return (

${event.relative_time_ms}-${event.action}} className={cn( 'rounded border px-4 py-3 transition-all', active ? ${config.borderClass} ${config.bgClass} text-white : 'border-white/10 bg-slate-950/40 text-slate-500', active && 'animate-slide-in-left', isNarrator && active && 'animate-narrator-glow', isCriticalFinding && 'animate-critical-drop', )} >
{formatMs(event.relative_time_ms)} {config.icon} {event.event_type} {!isNarrator && event.skill && ( {event.skill} )}
{isNarrator ? (
{event.details}
) : ( <>
{event.action}
{event.details}
</> )} {finding && (
{finding.id} · {finding.severity}
)}
) })}

5.8 — ShowcasePlayer: sostituire severity counters con donut chart

Sostituire il blocco severity grid (righe 403-418, la BaseCard con grid grid-cols-2 gap-3 xl:grid-cols-3) con:


0 ? donutData : [{ name: 'Empty', value: 1 }]} cx="50%" cy="50%" innerRadius="60%" outerRadius="85%" dataKey="value" stroke="none" isAnimationActive={true} animationDuration={600} > {donutData.map((entry) => ( ))} {donutData.length === 0 && }
{animatedTotal}
findings
{(['Critical', 'High', 'Medium', 'Low'] as const).map(sev => (
{severityCounts[sev]} {sev}
))}

5.9 — ShowcasePlayer: evidence nel Spotlight panel

Nel Spotlight panel (dopo latestFinding.cwe display, riga ~367), aggiungere:

{latestFinding.poc_curl && (

PoC
{latestFinding.poc_curl}

)} {latestFinding.request_snippet && (

Request
{latestFinding.request_snippet}

)} {latestFinding.response_snippet && (

Response
{latestFinding.response_snippet}

)}

5.10 — ShowcasePlayer: chains con Mermaid

Sostituire il rendering delle chains (righe 458-482). Il blocco scenario.chains.map(chain => ...) diventa:

{scenario.chains.map(chain => { const allVisible = chain.findings.every(id => visibleFindingIds.has(id)) return (

{chain.title}
{chain.severity}
{chain.findings.map(findingId => ( {findingId} ))}
{chain.mermaid_code && allVisible && (
)}
) })}

5.11 — DemoPage: rimuovere Live Demo Mode, aggiungere snapshot wizard

Nel componente DemoPage (riga 491+):

Rimuovere state e hook non più necessari: - selectedLabId (riga 495) - useDemoPreflight (riga 497)

Aggiungere nuovi state e hook: const [snapshotOpen, setSnapshotOpen] = useState(false) const { data: engagements } = useEngagements() const snapshot = useSnapshotEngagement()

Aggiungere bottone "Create from Engagement" nell'header della pagina, accanto al testo descrittivo (riga ~535):

setSnapshotOpen(true)}

Create from Engagement

Rimuovere l'intera sezione Live Demo Mode (righe 612-703 circa — la

con FlaskConical, i LIVE_DEMO_LABS buttons, preflight checks, operator notes).

Cambiare il layout gallery da 2 colonne con sidebar a full-width. Sostituire:

con:
E la grid delle card:
con:
Rimuovere il di chiusura della sezione Live Demo Mode e il
del grid wrapper esterno. Aggiungere modal snapshot alla fine del JSX di DemoPage, prima dell'ultimo
: {snapshotOpen && ( setSnapshotOpen(false)}>
{ e.preventDefault() const fd = new FormData(e.currentTarget) snapshot.mutate({ engagement_name: fd.get('engagement') as string, name: fd.get('name') as string, description: fd.get('description') as string, highlight_finding_ids: [], }, { onSuccess: () => setSnapshotOpen(false), }) }}>
Create Snapshot
)} --- Riepilogo file da modificare ┌─────┬────────────────────────────────────────────────────────────┬─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ │ # │ File │ Cosa │ ├─────┼────────────────────────────────────────────────────────────┼─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┤ │ 1 │ dashboard/backend/app/schemas.py │ +3 campi DemoFinding, +1 campo DemoChain │ ├─────┼────────────────────────────────────────────────────────────┼─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┤ │ 2 │ dashboard/frontend/src/api/demo.ts │ +3 campi DemoFinding, +1 campo DemoChain, +DemoSnapshotRequest, +useSnapshotEngagement │ ├─────┼────────────────────────────────────────────────────────────┼─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┤ │ 3 │ dashboard/frontend/tailwind.config.ts │ +3 keyframes (critical-drop, slide-in-left, narrator-glow), +3 animation │ ├─────┼────────────────────────────────────────────────────────────┼─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┤ │ 4 │ dashboard/data/demo-scenarios/quick-api-demo.json │ Espandere timeline a ~50 eventi con NARRATOR, +evidence findings, +mermaid chain │ ├─────┼────────────────────────────────────────────────────────────┼─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┤ │ 5 │ dashboard/data/demo-scenarios/enterprise-webapp-demo.json │ Espandere timeline a ~50 eventi con NARRATOR, +evidence findings, +mermaid chain │ ├─────┼────────────────────────────────────────────────────────────┼─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┤ │ 6 │ dashboard/data/demo-scenarios/burp-bidirectional-demo.json │ Espandere timeline a ~45 eventi con NARRATOR, +evidence findings, +mermaid chain │ ├─────┼────────────────────────────────────────────────────────────┼─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┤ │ 7 │ dashboard/frontend/src/pages/DemoPage.tsx │ Auto-scroll, event styling con EVENT_TYPE_CONFIG, critical glow, donut chart recharts, evidence panel con SyntaxHighlighter, Mermaid chains, rimuovere Live Demo Mode, │ │ │ │ gallery 3-col, snapshot wizard modal │ └─────┴────────────────────────────────────────────────────────────┴─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ --- FASE D — Creare lo scenario demo reale da VulnHR Dopo che il pentest (Fase B) è completato e il codice (Fase C) è deployato: 1. Aprire dashboard → Demo → "Create from Engagement" 2. Selezionare l'engagement vulnhr dal dropdown 3. Nome: "Enterprise HR Portal Assessment" 4. Descrizione: "Real penetration test of a multi-role HR management system with LDAP, REST API, and workflow automation. 93+ documented vulnerabilities across all OWASP Top 10 categories." 5. Click "Create Snapshot" Lo snapshot cattura automaticamente findings reali, timeline reale, e metadata dal context.json. Post-snapshot manuale (opzionale per massimo impatto): aprire il JSON generato in dashboard/data/demo-scenarios/snapshot-*.json e: - Aggiungere 6-8 eventi NARRATOR nei punti chiave della timeline - Aggiungere mermaid_code alle chain - Aggiungere poc_curl ai finding più impattanti --- Verifica finale 1. cd dashboard/frontend && npx tsc --noEmit — zero errori TypeScript 2. Dashboard up → navigare a /demo come admin 3. Scenari seed visibili nella gallery a 3 colonne 4. Aprire uno scenario → Play: - Timeline scrolla automaticamente - Eventi NARRATOR in italic con sfondo gradient cyan/purple - Critical finding brilla rosso per ~1s - Donut chart si anima in tempo reale - PoC curl syntax-highlighted nel Spotlight - Mermaid diagram appare quando tutti i finding della chain sono visibili 5. Bottone "Create from Engagement" → form si apre, crea snapshot 6. Sezione Live Demo Mode NON deve apparire 7. https://demo-hr.bedefended.com/login accessibile (VulnHR su internet)