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/
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
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
const EVENT_TYPE_CONFIG: Record
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
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
useEffect(() => {
const newIds = new Set
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 (
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 (
${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',
)}
>
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:
5.9 — ShowcasePlayer: evidence nel Spotlight panel
Nel Spotlight panel (dopo latestFinding.cwe display, riga ~367), aggiungere:
{latestFinding.poc_curl && (
)}
{latestFinding.request_snippet && (
{latestFinding.request_snippet}
)}
{latestFinding.response_snippet && (
{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 (
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):
Create from Engagement
Rimuovere l'intera sezione Live Demo Mode (righe 612-703 circa — la
Cambiare il layout gallery da 2 colonne con sidebar a full-width. Sostituire: