From 4d8e7c74f478bee1cb6d848f2ab4fc6957b839d0 Mon Sep 17 00:00:00 2001 From: DevEnv nis2-agile Date: Sat, 7 Mar 2026 14:23:28 +0100 Subject: [PATCH] =?UTF-8?q?[TEST]=20Test=20Runner=20NIS2=20Agile=20?= =?UTF-8?q?=E2=80=94=20pattern=20lg231?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Single-file PHP runner con token auth (Nis2Test2026), SSE streaming, dark terminal UI. Comandi: health, smoke, sim01-05, simulate, chain-verify, reset, all. Tab: Test / Simulazioni / Credenziali demo. Co-Authored-By: Claude Sonnet 4.6 --- test-runner.php | 687 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 687 insertions(+) create mode 100644 test-runner.php diff --git a/test-runner.php b/test-runner.php new file mode 100644 index 0000000..3d3577c --- /dev/null +++ b/test-runner.php @@ -0,0 +1,687 @@ +' + . '

401 — Token richiesto

Accedi con ?t=Nis2Test2026

'; + exit; +} + +// ── Router ──────────────────────────────────────────────────────────────────── + +$path = parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH); +$base = rtrim(dirname($_SERVER['SCRIPT_NAME']), '/'); +$slug = ltrim(substr($path, strlen($base)), '/'); + +if ($slug === 'events') { + serveEvents(); + exit; +} +if ($slug === 'status') { + serveStatus(); + exit; +} + +serveUI(); +exit; + +// ── Commands ────────────────────────────────────────────────────────────────── + +function getCommands(): array +{ + $php = PHP_BINARY ?: 'php'; + $root = PROJECT_ROOT; + $api = API_BASE; + + return [ + 'health' => [ + 'label' => 'Health Check', + 'bash' => "curl -sf {$api}/../api-status.php | python3 -m json.tool", + 'cwd' => $root, + 'timeout' => 15, + 'continue_on_fail' => false, + ], + 'smoke' => [ + 'label' => 'Smoke Tests (curl)', + 'bash' => implode(' && ', [ + "echo '=== Login demo ===' && curl -sf -X POST {$api}/auth/login -H 'Content-Type: application/json' -d '{\"email\":\"admin@datacore.demo\",\"password\":\"Demo2026!\"}' | python3 -m json.tool || echo '[SKIP] utente non ancora creato'", + "echo '=== /api/auth/me (no token) ===' && curl -sf {$api}/auth/me | python3 -m json.tool", + "echo '=== /api/dashboard/overview (no token → 401) ===' && curl -sf -w '\\nHTTP %{http_code}\\n' {$api}/dashboard/overview || true", + "echo '=== API status ===' && curl -sf {$api}/../api-status.php | python3 -m json.tool", + ]), + 'cwd' => $root, + 'timeout' => 30, + 'continue_on_fail' => true, + ], + 'sim01' => [ + 'label' => 'SIM-01 Onboarding + Assessment', + 'bash' => "NIS2_SIM=SIM01 {$php} {$root}/simulate-nis2.php", + 'cwd' => $root, + 'timeout' => 180, + 'continue_on_fail' => false, + ], + 'sim02' => [ + 'label' => 'SIM-02 Ransomware Art.23', + 'bash' => "NIS2_SIM=SIM02 {$php} {$root}/simulate-nis2.php", + 'cwd' => $root, + 'timeout' => 120, + 'continue_on_fail' => false, + ], + 'sim03' => [ + 'label' => 'SIM-03 Data Breach Supply Chain', + 'bash' => "NIS2_SIM=SIM03 {$php} {$root}/simulate-nis2.php", + 'cwd' => $root, + 'timeout' => 120, + 'continue_on_fail' => false, + ], + 'sim04' => [ + 'label' => 'SIM-04 Whistleblowing SCADA', + 'bash' => "NIS2_SIM=SIM04 {$php} {$root}/simulate-nis2.php", + 'cwd' => $root, + 'timeout' => 90, + 'continue_on_fail' => false, + ], + 'sim05' => [ + 'label' => 'SIM-05 Audit Chain Verify', + 'bash' => "NIS2_SIM=SIM05 {$php} {$root}/simulate-nis2.php", + 'cwd' => $root, + 'timeout' => 60, + 'continue_on_fail' => false, + ], + 'simulate' => [ + 'label' => 'Tutte le Simulazioni (SIM-01→05)', + 'bash' => "{$php} {$root}/simulate-nis2.php", + 'cwd' => $root, + 'timeout' => 600, + 'continue_on_fail' => false, + ], + 'chain-verify' => [ + 'label' => 'Verifica Hash Chain Audit', + 'bash' => implode(' && ', [ + "echo '=== Verifica chain org demo ==='", + "curl -sf -H 'Authorization: Bearer \$(cat /tmp/nis2_chain_token 2>/dev/null || echo \"\")' {$api}/audit/chain-verify | python3 -m json.tool || echo '[INFO] Autenticarsi prima con simulate'", + ]), + 'cwd' => $root, + 'timeout' => 30, + 'continue_on_fail' => true, + ], + 'reset' => [ + 'label' => 'Reset Dati Demo', + 'bash' => "mysql -u nis2_agile_user -p\$(grep DB_PASSWORD {$root}/.env | cut -d= -f2) nis2_agile_db < {$root}/docs/sql/reset-demo.sql", + 'cwd' => $root, + 'timeout' => 30, + 'continue_on_fail' => false, + ], + 'all' => [ + 'label' => 'Full Suite (health + simulate + chain)', + 'bash' => implode(' && ', [ + "curl -sf " . API_BASE . "/../api-status.php | python3 -m json.tool", + PHP_BINARY . " {$root}/simulate-nis2.php", + ]), + 'cwd' => $root, + 'timeout' => 660, + 'continue_on_fail' => false, + ], + ]; +} + +// ── SSE Events handler ──────────────────────────────────────────────────────── + +function serveEvents(): void +{ + $cmd = $_GET['cmd'] ?? ''; + $commands = getCommands(); + + if (!isset($commands[$cmd])) { + http_response_code(400); + echo "Comando sconosciuto: {$cmd}"; + return; + } + + $def = $commands[$cmd]; + + header('Content-Type: text/event-stream'); + header('Cache-Control: no-cache'); + header('X-Accel-Buffering: no'); + + if (ob_get_level()) ob_end_clean(); + + sseWrite('start', json_encode(['cmd' => $cmd, 'label' => $def['label'], 'ts' => date('c')])); + + $t0 = microtime(true); + runCommand($def, function (string $type, string $line) { + sseWrite($type, $line); + }); + + $elapsed = round(microtime(true) - $t0, 2); + sseWrite('done', json_encode(['cmd' => $cmd, 'elapsed' => $elapsed])); +} + +function runCommand(array $def, callable $emit): void +{ + $descriptors = [ + 0 => ['pipe', 'r'], + 1 => ['pipe', 'w'], + 2 => ['pipe', 'w'], + ]; + + $env = array_merge($_ENV, getenv() ?: [], ['NIS2_API_BASE' => API_BASE]); + + $proc = proc_open( + $def['bash'], + $descriptors, + $pipes, + $def['cwd'] ?? PROJECT_ROOT, + $env + ); + + if (!is_resource($proc)) { + $emit('stderr', '[ERRORE] Impossibile avviare il processo'); + return; + } + + fclose($pipes[0]); + stream_set_blocking($pipes[1], false); + stream_set_blocking($pipes[2], false); + + $timeout = $def['timeout'] ?? 120; + $deadline = microtime(true) + $timeout; + $buf1 = ''; + $buf2 = ''; + + while (true) { + if (microtime(true) > $deadline) { + $emit('stderr', "[TIMEOUT] Processo terminato dopo {$timeout}s"); + proc_terminate($proc, 9); + break; + } + + $r = [$pipes[1], $pipes[2]]; + $w = null; $e = null; + + $changed = @stream_select($r, $w, $e, 0, 50000); + + if ($changed === false) break; + + foreach ($r as $s) { + $chunk = fread($s, 4096); + if ($chunk === false || $chunk === '') continue; + + if ($s === $pipes[1]) { + $buf1 .= $chunk; + while (($nl = strpos($buf1, "\n")) !== false) { + $emit('stdout', rtrim(substr($buf1, 0, $nl))); + $buf1 = substr($buf1, $nl + 1); + } + } else { + $buf2 .= $chunk; + while (($nl = strpos($buf2, "\n")) !== false) { + $emit('stderr', rtrim(substr($buf2, 0, $nl))); + $buf2 = substr($buf2, $nl + 1); + } + } + } + + if (feof($pipes[1]) && feof($pipes[2])) break; + if (connection_aborted()) { proc_terminate($proc, 9); break; } + + // flush heartbeat + if (ob_get_level()) ob_flush(); + flush(); + } + + if ($buf1 !== '') $emit('stdout', rtrim($buf1)); + if ($buf2 !== '') $emit('stderr', rtrim($buf2)); + + fclose($pipes[1]); + fclose($pipes[2]); + proc_close($proc); + + if (ob_get_level()) ob_flush(); + flush(); +} + +function sseWrite(string $event, string $data): void +{ + // JSON-encode non-JSON data for transport safety + $out = "event: {$event}\ndata: " . $data . "\n\n"; + echo $out; + if (ob_get_level()) ob_flush(); + flush(); +} + +// ── Status JSON ─────────────────────────────────────────────────────────────── + +function serveStatus(): void +{ + header('Content-Type: application/json'); + + $api = API_BASE; + $up = false; + $info = []; + + $ch = curl_init("{$api}/../api-status.php"); + curl_setopt_array($ch, [ + CURLOPT_RETURNTRANSFER => true, + CURLOPT_TIMEOUT => 5, + CURLOPT_SSL_VERIFYPEER => false, + ]); + $resp = curl_exec($ch); + $code = curl_getinfo($ch, CURLINFO_HTTP_CODE); + curl_close($ch); + + if ($code === 200 && $resp) { + $up = true; + $info = json_decode($resp, true) ?: []; + } + + echo json_encode([ + 'api_up' => $up, + 'api_url' => $api, + 'api_info' => $info, + 'php_version'=> PHP_VERSION, + 'ts' => date('c'), + ], JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT); +} + +// ── UI ──────────────────────────────────────────────────────────────────────── + +function serveUI(): void +{ + $commands = getCommands(); + $cmdsJson = json_encode(array_map(fn($k, $v) => [ + 'id' => $k, + 'label' => $v['label'], + ], array_keys($commands), $commands), JSON_UNESCAPED_UNICODE); + + $token = ACCESS_TOKEN; + $api = API_BASE; + $url = "https://nis2.agile.software/test-runner.php?t={$token}"; + + $demoCredentials = [ + ['role' => 'Admin (DataCore IT)', 'email' => 'admin@datacore.demo', 'password' => 'Demo2026!', 'org' => 'DataCore S.r.l.'], + ['role' => 'Compliance (MedClinic)', 'email' => 'compliance@medclinic.demo', 'password' => 'Demo2026!', 'org' => 'MedClinic Italia S.p.A.'], + ['role' => 'CISO (EnerNet)', 'email' => 'ciso@enernet.demo', 'password' => 'Demo2026!', 'org' => 'EnerNet Distribuzione S.r.l.'], + ]; + $credsRows = ''; + foreach ($demoCredentials as $c) { + $credsRows .= "{$c['role']}{$c['email']}{$c['password']}{$c['org']}"; + } + + $simCards = ''; + $simDefs = [ + 'sim01' => ['SIM-01', 'Onboarding + Assessment', '3 aziende, gap analysis 80 domande, score iniziale', 'cyan'], + 'sim02' => ['SIM-02', 'Ransomware Art.23', 'Incidente critico, timeline 24h/72h, CSIRT notifica', 'orange'], + 'sim03' => ['SIM-03', 'Data Breach Supply Chain','Fornitore compromesso, Art.23 + GDPR Art.33 parallelo', 'red'], + 'sim04' => ['SIM-04', 'Whistleblowing SCADA', 'Segnalazione anonima, assegnazione, chiusura tracciata', 'purple'], + 'sim05' => ['SIM-05', 'Audit Chain Verify', 'Verifica integrità SHA-256, export certificato', 'green'], + ]; + foreach ($simDefs as $id => [$code, $name, $desc, $col]) { + $simCards .= << + {$code} +
+ {$name} + {$desc} +
+ + HTML; + } + + $testBtns = ''; + $testCmds = ['health', 'smoke', 'simulate', 'chain-verify', 'reset', 'all']; + foreach ($testCmds as $id) { + $label = $commands[$id]['label'] ?? $id; + $extra = in_array($id, ['reset', 'all']) ? ' btn-danger' : ''; + $testBtns .= "\n"; + } + + echo << + + + + +NIS2 Agile — Test Runner + + + + + + + + +
+
+
+ Pronto · seleziona un test o una simulazione + + +
+
+ NIS2 Agile Test Runner — pronto. + ──────────────────────────────────────────────────────── +
+
+ + + + +HTML; +}