Compare commits
7 Commits
a0fc0bd042
...
0a3f2d15e2
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0a3f2d15e2 | ||
|
|
459d2bc8cd | ||
|
|
527446f4e3 | ||
|
|
1876798836 | ||
|
|
e8b74a7cb7 | ||
|
|
ca8f077a7a | ||
|
|
fc4fbda732 |
789
public/docs/testing-simulazione.html
Normal file
789
public/docs/testing-simulazione.html
Normal file
@ -0,0 +1,789 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="it">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>NIS2 Agile — Sistema di Testing & Simulazione</title>
|
||||
<style>
|
||||
:root {
|
||||
--primary: #06b6d4;
|
||||
--green: #22c55e;
|
||||
--red: #ef4444;
|
||||
--yellow: #f59e0b;
|
||||
--purple: #8b5cf6;
|
||||
--orange: #f97316;
|
||||
--dark: #0f172a;
|
||||
--card: #1e293b;
|
||||
--border: #334155;
|
||||
--text: #e2e8f0;
|
||||
--muted: #94a3b8;
|
||||
}
|
||||
* { box-sizing: border-box; margin: 0; padding: 0; }
|
||||
body {
|
||||
background: var(--dark);
|
||||
color: var(--text);
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', system-ui, sans-serif;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
/* ── Header ── */
|
||||
.header {
|
||||
background: linear-gradient(135deg, #0c4a6e 0%, #0e7490 100%);
|
||||
padding: 48px;
|
||||
border-bottom: 1px solid #0e7490;
|
||||
}
|
||||
.header-inner { max-width: 1100px; margin: 0 auto; }
|
||||
.header h1 { font-size: 2rem; font-weight: 800; color: #fff; margin-bottom: 8px; }
|
||||
.header p { color: #7dd3fc; font-size: 1rem; max-width: 640px; }
|
||||
.badge-row { margin-top: 20px; display: flex; flex-wrap: wrap; gap: 8px; }
|
||||
.badge {
|
||||
display: inline-block; padding: 4px 14px; border-radius: 20px;
|
||||
font-size: 0.75rem; font-weight: 700;
|
||||
}
|
||||
.badge-cyan { background: rgba(6,182,212,.2); color: #67e8f9; border: 1px solid rgba(6,182,212,.3); }
|
||||
.badge-green { background: rgba(34,197,94,.2); color: #86efac; border: 1px solid rgba(34,197,94,.3); }
|
||||
.badge-purple { background: rgba(139,92,246,.2); color: #c4b5fd; border: 1px solid rgba(139,92,246,.3); }
|
||||
|
||||
/* ── Nav ── */
|
||||
.nav {
|
||||
background: #1e293b;
|
||||
border-bottom: 1px solid var(--border);
|
||||
position: sticky; top: 0; z-index: 100;
|
||||
}
|
||||
.nav-inner {
|
||||
max-width: 1100px; margin: 0 auto;
|
||||
display: flex; gap: 0; overflow-x: auto;
|
||||
}
|
||||
.nav a {
|
||||
padding: 14px 20px; font-size: 0.875rem; font-weight: 600;
|
||||
color: var(--muted); text-decoration: none; white-space: nowrap;
|
||||
border-bottom: 2px solid transparent; transition: all .2s;
|
||||
}
|
||||
.nav a:hover { color: var(--text); background: rgba(255,255,255,.03); }
|
||||
.nav a.active { color: var(--primary); border-bottom-color: var(--primary); }
|
||||
|
||||
/* ── Layout ── */
|
||||
.main { max-width: 1100px; margin: 0 auto; padding: 48px 24px; }
|
||||
section { margin-bottom: 64px; }
|
||||
h2 {
|
||||
font-size: 1.375rem; font-weight: 800; color: #fff;
|
||||
margin-bottom: 24px; padding-bottom: 12px;
|
||||
border-bottom: 1px solid var(--border);
|
||||
display: flex; align-items: center; gap: 10px;
|
||||
}
|
||||
h2 .icon { font-size: 1.25rem; }
|
||||
h3 { font-size: 1rem; font-weight: 700; color: #cbd5e1; margin-bottom: 12px; margin-top: 28px; }
|
||||
p { color: #94a3b8; margin-bottom: 12px; }
|
||||
|
||||
/* ── Cards ── */
|
||||
.card {
|
||||
background: var(--card); border: 1px solid var(--border);
|
||||
border-radius: 12px; padding: 24px; margin-bottom: 16px;
|
||||
}
|
||||
.card-title { font-size: 0.9375rem; font-weight: 700; color: #fff; margin-bottom: 6px; }
|
||||
.card p { margin-bottom: 0; }
|
||||
|
||||
/* ── Grid ── */
|
||||
.grid-2 { display: grid; grid-template-columns: 1fr 1fr; gap: 16px; }
|
||||
.grid-3 { display: grid; grid-template-columns: repeat(3, 1fr); gap: 16px; }
|
||||
@media (max-width: 768px) {
|
||||
.grid-2, .grid-3 { grid-template-columns: 1fr; }
|
||||
.header { padding: 32px 24px; }
|
||||
.header h1 { font-size: 1.5rem; }
|
||||
}
|
||||
|
||||
/* ── Level badges ── */
|
||||
.level-badge {
|
||||
display: inline-block; padding: 2px 10px; border-radius: 6px;
|
||||
font-size: 0.7rem; font-weight: 800; font-family: monospace;
|
||||
margin-right: 8px; vertical-align: middle;
|
||||
}
|
||||
.lv-l1 { background: rgba(6,182,212,.15); color: #67e8f9; border: 1px solid rgba(6,182,212,.3); }
|
||||
.lv-l2 { background: rgba(139,92,246,.15); color: #c4b5fd; border: 1px solid rgba(139,92,246,.3); }
|
||||
.lv-l3 { background: rgba(34,197,94,.15); color: #86efac; border: 1px solid rgba(34,197,94,.3); }
|
||||
.lv-l4 { background: rgba(249,115,22,.15); color: #fdba74; border: 1px solid rgba(249,115,22,.3); }
|
||||
.lv-l5 { background: rgba(245,158,11,.15); color: #fcd34d; border: 1px solid rgba(245,158,11,.3); }
|
||||
.lv-l6 { background: rgba(236,72,153,.15); color: #f9a8d4; border: 1px solid rgba(236,72,153,.3); }
|
||||
.lv-sim { background: rgba(239,68,68,.15); color: #fca5a5; border: 1px solid rgba(239,68,68,.3); }
|
||||
.lv-infra { background: rgba(148,163,184,.15); color: #cbd5e1; border: 1px solid rgba(148,163,184,.3); }
|
||||
|
||||
/* ── Test table ── */
|
||||
.test-table { width: 100%; border-collapse: collapse; font-size: 0.875rem; }
|
||||
.test-table th {
|
||||
text-align: left; padding: 10px 14px; font-size: 0.75rem; font-weight: 700;
|
||||
color: var(--muted); text-transform: uppercase; letter-spacing: .05em;
|
||||
border-bottom: 1px solid var(--border); background: rgba(255,255,255,.02);
|
||||
}
|
||||
.test-table td { padding: 10px 14px; border-bottom: 1px solid rgba(51,65,85,.5); color: #cbd5e1; }
|
||||
.test-table tr:last-child td { border-bottom: none; }
|
||||
.test-table tr:hover td { background: rgba(255,255,255,.02); }
|
||||
|
||||
/* ── Scenario cards ── */
|
||||
.sim-card {
|
||||
background: var(--card); border: 1px solid var(--border);
|
||||
border-radius: 10px; padding: 20px; position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
.sim-card::before {
|
||||
content: ''; position: absolute; top: 0; left: 0; right: 0; height: 3px;
|
||||
}
|
||||
.sim-card.cyan::before { background: linear-gradient(90deg, #06b6d4, #0891b2); }
|
||||
.sim-card.orange::before { background: linear-gradient(90deg, #f97316, #ea580c); }
|
||||
.sim-card.red::before { background: linear-gradient(90deg, #ef4444, #dc2626); }
|
||||
.sim-card.purple::before { background: linear-gradient(90deg, #8b5cf6, #7c3aed); }
|
||||
.sim-card.green::before { background: linear-gradient(90deg, #22c55e, #16a34a); }
|
||||
.sim-card.yellow::before { background: linear-gradient(90deg, #f59e0b, #d97706); }
|
||||
.sim-num { font-size: 0.7rem; font-weight: 800; color: var(--muted); margin-bottom: 6px; font-family: monospace; }
|
||||
.sim-title { font-size: 0.9375rem; font-weight: 700; color: #fff; margin-bottom: 6px; }
|
||||
.sim-desc { font-size: 0.8125rem; color: var(--muted); line-height: 1.5; }
|
||||
.sim-steps { margin-top: 12px; padding-top: 12px; border-top: 1px solid var(--border); }
|
||||
.sim-step { font-size: 0.75rem; color: #64748b; display: flex; gap: 8px; margin-bottom: 4px; }
|
||||
.sim-step span { color: #94a3b8; }
|
||||
|
||||
/* ── Company pills ── */
|
||||
.company-pill {
|
||||
display: inline-flex; align-items: center; gap: 6px;
|
||||
padding: 4px 12px; border-radius: 20px; font-size: 0.75rem; font-weight: 600;
|
||||
border: 1px solid var(--border); background: rgba(255,255,255,.04);
|
||||
margin: 3px;
|
||||
}
|
||||
.dot { width: 8px; height: 8px; border-radius: 50%; flex-shrink: 0; }
|
||||
|
||||
/* ── Code block ── */
|
||||
.code-block {
|
||||
background: #0f172a; border: 1px solid var(--border);
|
||||
border-radius: 8px; padding: 16px 20px; font-family: 'Fira Code', 'Cascadia Code', monospace;
|
||||
font-size: 0.8125rem; color: #93c5fd; overflow-x: auto;
|
||||
margin: 12px 0;
|
||||
}
|
||||
.code-block .comment { color: #475569; }
|
||||
.code-block .keyword { color: #67e8f9; }
|
||||
.code-block .string { color: #86efac; }
|
||||
.code-block .url { color: #fcd34d; }
|
||||
|
||||
/* ── Alert ── */
|
||||
.alert {
|
||||
padding: 14px 18px; border-radius: 8px; font-size: 0.875rem;
|
||||
display: flex; gap: 12px; align-items: flex-start; margin-bottom: 16px;
|
||||
}
|
||||
.alert-info { background: rgba(6,182,212,.08); border: 1px solid rgba(6,182,212,.2); color: #7dd3fc; }
|
||||
.alert-warning { background: rgba(245,158,11,.08); border: 1px solid rgba(245,158,11,.2); color: #fcd34d; }
|
||||
.alert-success { background: rgba(34,197,94,.08); border: 1px solid rgba(34,197,94,.2); color: #86efac; }
|
||||
|
||||
/* ── URL button ── */
|
||||
.url-btn {
|
||||
display: inline-flex; align-items: center; gap: 8px;
|
||||
background: rgba(6,182,212,.1); border: 1px solid rgba(6,182,212,.3);
|
||||
color: #67e8f9; padding: 10px 18px; border-radius: 8px;
|
||||
text-decoration: none; font-size: 0.875rem; font-weight: 600;
|
||||
transition: all .2s; margin-right: 8px; margin-bottom: 8px;
|
||||
}
|
||||
.url-btn:hover { background: rgba(6,182,212,.2); border-color: #06b6d4; }
|
||||
.url-btn-red {
|
||||
background: rgba(239,68,68,.1); border-color: rgba(239,68,68,.3); color: #fca5a5;
|
||||
}
|
||||
.url-btn-red:hover { background: rgba(239,68,68,.2); }
|
||||
|
||||
/* ── Stats strip ── */
|
||||
.stats-strip { display: flex; gap: 16px; flex-wrap: wrap; margin-bottom: 24px; }
|
||||
.stat-box {
|
||||
background: var(--card); border: 1px solid var(--border);
|
||||
border-radius: 10px; padding: 16px 24px; text-align: center; flex: 1; min-width: 100px;
|
||||
}
|
||||
.stat-num { font-size: 2rem; font-weight: 800; }
|
||||
.stat-lbl { font-size: 0.75rem; color: var(--muted); margin-top: 2px; }
|
||||
|
||||
/* ── Worker box ── */
|
||||
.worker-flow {
|
||||
display: flex; align-items: center; flex-wrap: wrap; gap: 0;
|
||||
background: #0f172a; border: 1px solid var(--border);
|
||||
border-radius: 10px; overflow: hidden; margin: 16px 0;
|
||||
}
|
||||
.wf-step {
|
||||
padding: 14px 18px; font-size: 0.8125rem; flex: 1; min-width: 120px;
|
||||
text-align: center; border-right: 1px solid var(--border);
|
||||
}
|
||||
.wf-step:last-child { border-right: none; }
|
||||
.wf-step .wf-icon { font-size: 1.25rem; display: block; margin-bottom: 4px; }
|
||||
.wf-step .wf-label { color: #cbd5e1; font-weight: 600; }
|
||||
.wf-step .wf-sub { color: var(--muted); font-size: 0.725rem; margin-top: 2px; }
|
||||
|
||||
/* ── Credentials box ── */
|
||||
.creds-box {
|
||||
background: #0f172a; border: 1px solid var(--border);
|
||||
border-radius: 8px; overflow: hidden;
|
||||
}
|
||||
.creds-row {
|
||||
display: flex; gap: 0; border-bottom: 1px solid rgba(51,65,85,.5);
|
||||
font-size: 0.8125rem;
|
||||
}
|
||||
.creds-row:last-child { border-bottom: none; }
|
||||
.creds-key {
|
||||
padding: 10px 16px; width: 200px; flex-shrink: 0;
|
||||
color: var(--muted); font-weight: 600; border-right: 1px solid rgba(51,65,85,.5);
|
||||
font-family: monospace; font-size: 0.75rem;
|
||||
}
|
||||
.creds-val {
|
||||
padding: 10px 16px; color: #93c5fd; font-family: monospace; font-size: 0.75rem;
|
||||
}
|
||||
|
||||
/* ── Footer ── */
|
||||
footer {
|
||||
background: #1e293b; border-top: 1px solid var(--border);
|
||||
padding: 32px 48px; text-align: center; color: var(--muted); font-size: 0.875rem;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<!-- ── Header ── -->
|
||||
<div class="header">
|
||||
<div class="header-inner">
|
||||
<div style="font-size:.8rem;color:#7dd3fc;margin-bottom:8px;font-weight:600;">
|
||||
NIS2 Agile — Documentazione Tecnica
|
||||
</div>
|
||||
<h1>Sistema di Testing & Simulazione</h1>
|
||||
<p>Due strumenti integrati per verificare la correttezza della piattaforma e dimostrare scenari reali NIS2 in ambienti demo.</p>
|
||||
<div class="badge-row">
|
||||
<span class="badge badge-cyan">Test Runner v2 — 36+ test</span>
|
||||
<span class="badge badge-green">Simulazione — 6 scenari</span>
|
||||
<span class="badge badge-purple">3 aziende demo</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ── Nav ── -->
|
||||
<nav class="nav">
|
||||
<div class="nav-inner">
|
||||
<a href="#panoramica" class="active">Panoramica</a>
|
||||
<a href="#test-runner">Test Runner</a>
|
||||
<a href="#simulazione">Simulazione</a>
|
||||
<a href="#aziende-demo">Aziende Demo</a>
|
||||
<a href="#worker">Worker Feedback</a>
|
||||
<a href="#accesso">Accesso & URL</a>
|
||||
<a href="#credenziali">Credenziali</a>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<div class="main">
|
||||
|
||||
<!-- ── Panoramica ── -->
|
||||
<section id="panoramica">
|
||||
<h2><span class="icon">🧭</span> Panoramica</h2>
|
||||
|
||||
<p>NIS2 Agile dispone di due strumenti distinti ma complementari per la verifica della qualità e la dimostrazione delle funzionalità:</p>
|
||||
|
||||
<div class="grid-2">
|
||||
<div class="card" style="border-color:rgba(6,182,212,.3);">
|
||||
<div class="card-title" style="color:#67e8f9;">🧪 Test Runner</div>
|
||||
<p>Suite di test automatizzati a 6 livelli che verifica ogni endpoint API con asserzioni precise. Accesso protetto da token, output streaming in tempo reale, reset dati integrato.</p>
|
||||
<div style="margin-top:12px;">
|
||||
<a href="https://nis2.agile.software/test-runner.php?t=Nis2Test2026" target="_blank" class="url-btn">Apri Test Runner →</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card" style="border-color:rgba(34,197,94,.3);">
|
||||
<div class="card-title" style="color:#86efac;">🎬 Simulazione Demo</div>
|
||||
<p>Costruisce dati realistici attraverso le API reali (nessun INSERT SQL diretto) simulando 3 aziende con settori e complessità diversi su 6 scenari NIS2 reali.</p>
|
||||
<div style="margin-top:12px;">
|
||||
<a href="https://nis2.agile.software/simulate.html" target="_blank" class="url-btn">Apri Simulatore →</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="alert alert-info">
|
||||
<span>ℹ️</span>
|
||||
<div>I due sistemi sono progettati per lavorare in sequenza: prima si esegue la <strong>simulazione</strong> per creare i dati demo, poi il <strong>test runner</strong> per verificare che tutto funzioni correttamente. Il pulsante "Reset + Simula + Testa Tutto" li esegue entrambi in automatico.</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- ── Test Runner ── -->
|
||||
<section id="test-runner">
|
||||
<h2><span class="icon">🧪</span> Test Runner</h2>
|
||||
|
||||
<p>Il Test Runner è un'applicazione PHP (<code>public/test-runner.php</code>) che espone una UI web dark-theme con streaming SSE dei risultati in tempo reale. I test sono organizzati in <strong>6 livelli funzionali</strong> più un livello infrastrutturale.</p>
|
||||
|
||||
<div class="stats-strip">
|
||||
<div class="stat-box"><div class="stat-num" style="color:#67e8f9;">36+</div><div class="stat-lbl">Test totali</div></div>
|
||||
<div class="stat-box"><div class="stat-num" style="color:#86efac;">6</div><div class="stat-lbl">Livelli</div></div>
|
||||
<div class="stat-box"><div class="stat-num" style="color:#c4b5fd;">SSE</div><div class="stat-lbl">Output streaming</div></div>
|
||||
<div class="stat-box"><div class="stat-num" style="color:#fcd34d;">JWT</div><div class="stat-lbl">Auth token-based</div></div>
|
||||
</div>
|
||||
|
||||
<h3>Livelli di Test</h3>
|
||||
<div class="card" style="padding:0; overflow:hidden;">
|
||||
<table class="test-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Livello</th>
|
||||
<th>Nome</th>
|
||||
<th>Cosa verifica</th>
|
||||
<th>Dipendenze</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td><span class="level-badge lv-infra">INFRA</span></td>
|
||||
<td>Health & Smoke</td>
|
||||
<td>API status, endpoint rapidi, connettività DB</td>
|
||||
<td>—</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><span class="level-badge lv-l1">L1</span></td>
|
||||
<td>Auth & JWT</td>
|
||||
<td>Login, register, JWT access/refresh, change-password, rate limiting</td>
|
||||
<td>—</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><span class="level-badge lv-l2">L2</span></td>
|
||||
<td>Multi-Tenant Isolation</td>
|
||||
<td>Isolamento dati tra organizzazioni diverse, cross-org protection</td>
|
||||
<td>SIM-01</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><span class="level-badge lv-l3">L3</span></td>
|
||||
<td>Compliance Core</td>
|
||||
<td>Assessment, rischi, incidenti, policy, supply chain, asset, training, NCR/CAPA</td>
|
||||
<td>SIM-01</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><span class="level-badge lv-l4">L4</span></td>
|
||||
<td>B2B & Services API</td>
|
||||
<td>X-API-Key auth, scopes (read:all, admin:licenses), invite lifecycle, webhook delivery</td>
|
||||
<td>SIM-01, SIM-06</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><span class="level-badge lv-l5">L5</span></td>
|
||||
<td>Export & Reports</td>
|
||||
<td>CSV export (rischi/incidenti/asset/controlli), report HTML, audit executive</td>
|
||||
<td>SIM-01</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><span class="level-badge lv-l6">L6</span></td>
|
||||
<td>AI Features</td>
|
||||
<td>Cross-org portfolio analysis, AI classify incident, AI suggest risk, normative feed</td>
|
||||
<td>SIM-01, consultant user</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><span class="level-badge lv-sim">SIM</span></td>
|
||||
<td>Simulazioni</td>
|
||||
<td>Esecuzione completa SIM-01→06 con output dettagliato</td>
|
||||
<td>Demo users nel DB</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<h3>Accesso e autenticazione</h3>
|
||||
<p>Il Test Runner è protetto da un token di sessione configurato come costante PHP:</p>
|
||||
<div class="code-block">
|
||||
<span class="comment">// Primo accesso — passa il token come query string</span>
|
||||
<span class="url">https://nis2.agile.software/test-runner.php?t=Nis2Test2026</span>
|
||||
|
||||
<span class="comment">// Sessioni successive — il token è memorizzato in $_SESSION</span>
|
||||
<span class="url">https://nis2.agile.software/test-runner.php</span>
|
||||
</div>
|
||||
|
||||
<h3>Output streaming (SSE)</h3>
|
||||
<p>I test vengono eseguiti come comandi bash su server. I risultati vengono trasmessi in tempo reale tramite <strong>Server-Sent Events</strong> al browser. Ogni test mostra:</p>
|
||||
<div class="grid-2">
|
||||
<div class="card"><div class="card-title" style="color:#86efac;">✓ PASS</div><p>Il comando ha restituito exit code 0 e l'output atteso.</p></div>
|
||||
<div class="card"><div class="card-title" style="color:#fca5a5;">✗ FAIL</div><p>Il comando ha restituito errore o output non corrispondente.</p></div>
|
||||
<div class="card"><div class="card-title" style="color:#fcd34d;">⚠ SKIP</div><p>Dipendenza mancante (utente demo non creato, SIM non eseguita).</p></div>
|
||||
<div class="card"><div class="card-title" style="color:#f97316;">⚡ WARN</div><p>Il test è passato ma con avvertimenti non bloccanti.</p></div>
|
||||
</div>
|
||||
|
||||
<h3>Comandi speciali</h3>
|
||||
<div class="card">
|
||||
<table class="test-table">
|
||||
<thead><tr><th>Comando</th><th>Funzione</th></tr></thead>
|
||||
<tbody>
|
||||
<tr><td><span class="level-badge lv-infra">Verifica Hash Chain</span></td><td>Esegue <code>simulate-nis2.php --sim=SIM05</code> per verificare integrità SHA-256 del audit trail</td></tr>
|
||||
<tr><td><span class="level-badge lv-infra">Reset Dati Demo</span></td><td>Esegue <code>reset-demo.sql</code> — cancella org/utenti con id>4 e email <code>%.demo%</code></td></tr>
|
||||
<tr><td><span class="level-badge lv-infra">Full Suite L1→L6</span></td><td>Esegue tutti i 6 livelli in sequenza, con output cumulativo</td></tr>
|
||||
<tr><td><span class="level-badge lv-infra">Reset + Simula + Testa</span></td><td>Pipeline completa: reset → SIM-01→06 → Full Suite L1→L6</td></tr>
|
||||
<tr><td><span class="level-badge lv-infra">DB Stats</span></td><td>Conta record per tabella via endpoint JSON</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- ── Simulazione ── -->
|
||||
<section id="simulazione">
|
||||
<h2><span class="icon">🎬</span> Sistema di Simulazione</h2>
|
||||
|
||||
<p>Il simulatore (<code>simulate-nis2.php</code>) costruisce dati demo realistici <em>esclusivamente attraverso le API REST</em>, come farebbe un utente reale. Nessun INSERT SQL diretto: ogni operazione testa sia la logica di business che la persistenza dati.</p>
|
||||
|
||||
<div class="alert alert-warning">
|
||||
<span>⚠️</span>
|
||||
<div><strong>Idempotenza:</strong> se un'organizzazione demo esiste già (stessa P.IVA), lo step viene saltato silenziosamente (<code>SKIP idempotent</code>) senza errore. Per ripartire da zero usare prima il <strong>Reset Dati Demo</strong>.</div>
|
||||
</div>
|
||||
|
||||
<h3>Utilizzo</h3>
|
||||
<div class="code-block">
|
||||
<span class="comment"># Via browser (SSE streaming con UI dark-theme)</span>
|
||||
<span class="url">https://nis2.agile.software/simulate.html</span>
|
||||
|
||||
<span class="comment"># Via CLI sul server</span>
|
||||
<span class="keyword">php</span> simulate-nis2.php <span class="comment"># tutti gli scenari</span>
|
||||
<span class="keyword">php</span> simulate-nis2.php --sim=SIM02 <span class="comment"># solo SIM-02</span>
|
||||
<span class="keyword">php</span> simulate-nis2.php --sim=SIM06 <span class="comment"># solo B2B (indipendente)</span>
|
||||
|
||||
<span class="comment"># Variabili d'ambiente opzionali</span>
|
||||
<span class="keyword">NIS2_SIM</span>=SIM06 <span class="keyword">php</span> simulate-nis2.php <span class="comment"># alias env per --sim</span>
|
||||
<span class="keyword">NIS2_API_BASE</span>=https://... <span class="keyword">php</span> simulate-nis2.php <span class="comment"># override URL API</span>
|
||||
<span class="keyword">NIS2_DEMO_EMAIL</span>=test@... <span class="keyword">php</span> simulate-nis2.php <span class="comment"># redirect email demo</span>
|
||||
</div>
|
||||
|
||||
<h3>I 6 Scenari</h3>
|
||||
<div class="grid-2">
|
||||
|
||||
<div class="sim-card cyan">
|
||||
<div class="sim-num">SIM-01</div>
|
||||
<div class="sim-title">Onboarding + Gap Assessment</div>
|
||||
<div class="sim-desc">Registra 3 aziende, completa l'onboarding multi-step, esegue il gap assessment completo con tutte le 80 domande Art.21 NIS2 per ogni organizzazione.</div>
|
||||
<div class="sim-steps">
|
||||
<div class="sim-step">→ <span>Registrazione + email welcome</span></div>
|
||||
<div class="sim-step">→ <span>Setup org (settore, dipendenti, entity type)</span></div>
|
||||
<div class="sim-step">→ <span>80 domande × 3 aziende (categorizzate per Art.21)</span></div>
|
||||
<div class="sim-step">→ <span>Completamento assessment + score compliance</span></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="sim-card orange">
|
||||
<div class="sim-num">SIM-02</div>
|
||||
<div class="sim-title">Incidente Ransomware Art.23</div>
|
||||
<div class="sim-desc">Simula un attacco ransomware critico su DataCore S.r.l. con attivazione della timeline obbligatoria NIS2 (early warning 24h, notification 72h, final report 30d).</div>
|
||||
<div class="sim-steps">
|
||||
<div class="sim-step">→ <span>Creazione incidente severity=critical</span></div>
|
||||
<div class="sim-step">→ <span>AI classify: categoria, suggerimenti, severity</span></div>
|
||||
<div class="sim-step">→ <span>Early warning CSIRT (24h)</span></div>
|
||||
<div class="sim-step">→ <span>Notification formale (72h) + final report (30d)</span></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="sim-card red">
|
||||
<div class="sim-num">SIM-03</div>
|
||||
<div class="sim-title">Data Breach Supply Chain</div>
|
||||
<div class="sim-desc">Fornitore IT di MedClinic compromesso: aggiunta fornitore critico, assessment sicurezza, attivazione incidente Art.23 parallelo con gestione supply chain.</div>
|
||||
<div class="sim-steps">
|
||||
<div class="sim-step">→ <span>Aggiunta fornitore risk=critical</span></div>
|
||||
<div class="sim-step">→ <span>Security assessment fornitore</span></div>
|
||||
<div class="sim-step">→ <span>Data breach incident con AI classification</span></div>
|
||||
<div class="sim-step">→ <span>Risk register aggiornato automaticamente</span></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="sim-card purple">
|
||||
<div class="sim-num">SIM-04</div>
|
||||
<div class="sim-title">Whistleblowing Anonimo SCADA</div>
|
||||
<div class="sim-desc">Segnalazione anonima Art.32 NIS2 per accesso non autorizzato ai sistemi SCADA di EnerNet. Assegnazione investigatore, tracciamento timeline, chiusura con esito.</div>
|
||||
<div class="sim-steps">
|
||||
<div class="sim-step">→ <span>Submit anonimo (no auth, solo token tracking)</span></div>
|
||||
<div class="sim-step">→ <span>Assegnazione a compliance manager</span></div>
|
||||
<div class="sim-step">→ <span>Timeline investigazione (3 eventi)</span></div>
|
||||
<div class="sim-step">→ <span>Chiusura con esito e verifica tracking anonimo</span></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="sim-card green">
|
||||
<div class="sim-num">SIM-05</div>
|
||||
<div class="sim-title">Audit Trail Hash Chain</div>
|
||||
<div class="sim-desc">Verifica l'integrità della catena SHA-256 dell'audit trail per tutte e 3 le organizzazioni. Rileva eventuali record manomessi e genera export certificato.</div>
|
||||
<div class="sim-steps">
|
||||
<div class="sim-step">→ <span>Fetch audit log con prev_hash + entry_hash</span></div>
|
||||
<div class="sim-step">→ <span>Verifica catena SHA-256 record per record</span></div>
|
||||
<div class="sim-step">→ <span>Report integrità: verified/tampered/gaps</span></div>
|
||||
<div class="sim-step">→ <span>Export certificato JSON (con hash contenuto)</span></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="sim-card yellow">
|
||||
<div class="sim-num">SIM-06</div>
|
||||
<div class="sim-title">B2B License Provisioning</div>
|
||||
<div class="sim-desc">Provisioning automatico di una nuova organizzazione tramite invite token B2B. SSO federato, token exchange, creazione org + utente + API key in un'unica chiamata.</div>
|
||||
<div class="sim-steps">
|
||||
<div class="sim-step">→ <span>Creazione invite token (admin:licenses)</span></div>
|
||||
<div class="sim-step">→ <span>POST /api/services/provision con X-Provision-Secret</span></div>
|
||||
<div class="sim-step">→ <span>Org + user + api_key creati atomicamente</span></div>
|
||||
<div class="sim-step">→ <span>Verifica scadenza licenza + SSO JWT exchange</span></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h3>Architettura del simulatore</h3>
|
||||
<p>Il simulatore mantiene uno <strong>stato globale</strong> <code>$S</code> durante l'esecuzione per tracciare JWT, org IDs e statistiche:</p>
|
||||
<div class="code-block">
|
||||
<span class="comment">// Stato globale simulazione (in-memory per sessione)</span>
|
||||
$S = [
|
||||
<span class="string">'jwt'</span> => [], <span class="comment">// ['email' => token]</span>
|
||||
<span class="string">'orgs'</span> => [], <span class="comment">// ['slug' => ['id', 'name', 'jwt']]</span>
|
||||
<span class="string">'users'</span> => [], <span class="comment">// ['email' => ['id', 'jwt']]</span>
|
||||
<span class="string">'stats'</span> => [<span class="string">'pass'</span> => 0, <span class="string">'skip'</span> => 0, <span class="string">'fail'</span> => 0, <span class="string">'warn'</span> => 0],
|
||||
];
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- ── Aziende Demo ── -->
|
||||
<section id="aziende-demo">
|
||||
<h2><span class="icon">🏢</span> Aziende Demo</h2>
|
||||
|
||||
<p>Le simulazioni creano 3 organizzazioni demo con caratteristiche diversificate per coprire i principali settori NIS2 e i diversi livelli di obblighi normativi:</p>
|
||||
|
||||
<div class="grid-3">
|
||||
<div class="card" style="border-color:rgba(6,182,212,.3);">
|
||||
<div style="display:flex;align-items:center;gap:10px;margin-bottom:12px;">
|
||||
<div class="dot" style="background:#06b6d4;width:12px;height:12px;"></div>
|
||||
<div class="card-title" style="color:#67e8f9;">DataCore S.r.l.</div>
|
||||
</div>
|
||||
<div style="font-size:0.8rem;color:#94a3b8;line-height:1.8;">
|
||||
<div><strong style="color:#cbd5e1;">Settore:</strong> IT / Cloud & Data Center</div>
|
||||
<div><strong style="color:#cbd5e1;">Tipo:</strong> Essential Entity</div>
|
||||
<div><strong style="color:#cbd5e1;">Dipendenti:</strong> 320</div>
|
||||
<div><strong style="color:#cbd5e1;">Sede:</strong> Milano</div>
|
||||
<div><strong style="color:#cbd5e1;">Scenari:</strong> SIM-01, SIM-02 (Ransomware)</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card" style="border-color:rgba(139,92,246,.3);">
|
||||
<div style="display:flex;align-items:center;gap:10px;margin-bottom:12px;">
|
||||
<div class="dot" style="background:#8b5cf6;width:12px;height:12px;"></div>
|
||||
<div class="card-title" style="color:#c4b5fd;">MedClinic Italia S.p.A.</div>
|
||||
</div>
|
||||
<div style="font-size:0.8rem;color:#94a3b8;line-height:1.8;">
|
||||
<div><strong style="color:#cbd5e1;">Settore:</strong> Sanità</div>
|
||||
<div><strong style="color:#cbd5e1;">Tipo:</strong> Important Entity</div>
|
||||
<div><strong style="color:#cbd5e1;">Dipendenti:</strong> 750</div>
|
||||
<div><strong style="color:#cbd5e1;">Sede:</strong> Roma</div>
|
||||
<div><strong style="color:#cbd5e1;">Scenari:</strong> SIM-01, SIM-03 (Supply Chain)</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card" style="border-color:rgba(34,197,94,.3);">
|
||||
<div style="display:flex;align-items:center;gap:10px;margin-bottom:12px;">
|
||||
<div class="dot" style="background:#22c55e;width:12px;height:12px;"></div>
|
||||
<div class="card-title" style="color:#86efac;">EnerNet Distribuzione S.r.l.</div>
|
||||
</div>
|
||||
<div style="font-size:0.8rem;color:#94a3b8;line-height:1.8;">
|
||||
<div><strong style="color:#cbd5e1;">Settore:</strong> Energia</div>
|
||||
<div><strong style="color:#cbd5e1;">Tipo:</strong> Essential / Critical</div>
|
||||
<div><strong style="color:#cbd5e1;">Dipendenti:</strong> 1.800</div>
|
||||
<div><strong style="color:#cbd5e1;">Sede:</strong> Torino</div>
|
||||
<div><strong style="color:#cbd5e1;">Scenari:</strong> SIM-01, SIM-04 (SCADA)</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="alert alert-success">
|
||||
<span>✅</span>
|
||||
<div>Tutti gli utenti demo usano la password <strong>NIS2Demo2026!</strong>. Le email vengono reindirizzate all'indirizzo configurato in <code>NIS2_DEMO_EMAIL</code> (default: <code>demo@nis2agile.it</code>).</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- ── Worker Feedback ── -->
|
||||
<section id="worker">
|
||||
<h2><span class="icon">🤖</span> Worker Feedback AI</h2>
|
||||
|
||||
<p>Oltre ai test e alle simulazioni, la piattaforma include un <strong>worker autonomo</strong> per la risoluzione automatica delle segnalazioni utente tramite Claude AI.</p>
|
||||
|
||||
<div class="worker-flow">
|
||||
<div class="wf-step">
|
||||
<span class="wf-icon">🕐</span>
|
||||
<div class="wf-label">Ogni 30 min</div>
|
||||
<div class="wf-sub">cron root</div>
|
||||
</div>
|
||||
<div class="wf-step">
|
||||
<span class="wf-icon">🔍</span>
|
||||
<div class="wf-label">Fetch ticket</div>
|
||||
<div class="wf-sub">status=in_lavorazione</div>
|
||||
</div>
|
||||
<div class="wf-step">
|
||||
<span class="wf-icon">🤖</span>
|
||||
<div class="wf-label">Claude Code CLI</div>
|
||||
<div class="wf-sub">docker exec nis2-agile-devenv</div>
|
||||
</div>
|
||||
<div class="wf-step">
|
||||
<span class="wf-icon">✅</span>
|
||||
<div class="wf-label">POST /resolve</div>
|
||||
<div class="wf-sub">con password gate</div>
|
||||
</div>
|
||||
<div class="wf-step">
|
||||
<span class="wf-icon">📧</span>
|
||||
<div class="wf-label">Email broadcast</div>
|
||||
<div class="wf-sub">tutti i membri org</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h3>Configurazione</h3>
|
||||
<div class="creds-box">
|
||||
<div class="creds-row">
|
||||
<div class="creds-key">Script</div>
|
||||
<div class="creds-val">scripts/feedback-worker.php</div>
|
||||
</div>
|
||||
<div class="creds-row">
|
||||
<div class="creds-key">Crontab</div>
|
||||
<div class="creds-val">*/30 * * * * /usr/bin/php8.4 /var/www/nis2-agile/scripts/feedback-worker.php</div>
|
||||
</div>
|
||||
<div class="creds-row">
|
||||
<div class="creds-key">Log</div>
|
||||
<div class="creds-val">/var/log/nis2/feedback-worker.log</div>
|
||||
</div>
|
||||
<div class="creds-row">
|
||||
<div class="creds-key">Account worker</div>
|
||||
<div class="creds-val">worker@nis2.agile.software (super_admin, dedicato)</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h3>Test del worker</h3>
|
||||
<div class="code-block">
|
||||
<span class="comment"># Run manuale (Hetzner SSH)</span>
|
||||
<span class="keyword">php8.4</span> /var/www/nis2-agile/scripts/feedback-worker.php
|
||||
|
||||
<span class="comment"># Verifica log</span>
|
||||
<span class="keyword">tail</span> -f /var/log/nis2/feedback-worker.log
|
||||
|
||||
<span class="comment"># Output atteso (nessun ticket)</span>
|
||||
[2026-03-10 09:16:27] === Feedback Worker avviato ===
|
||||
[2026-03-10 09:16:27] Nessun ticket in lavorazione. Worker terminato.
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- ── Accesso ── -->
|
||||
<section id="accesso">
|
||||
<h2><span class="icon">🔗</span> URL di Accesso</h2>
|
||||
|
||||
<div class="grid-2">
|
||||
<div>
|
||||
<h3>Produzione</h3>
|
||||
<a href="https://nis2.agile.software/simulate.html" target="_blank" class="url-btn">
|
||||
🎬 Simulatore Demo
|
||||
</a>
|
||||
<a href="https://nis2.agile.software/test-runner.php?t=Nis2Test2026" target="_blank" class="url-btn">
|
||||
🧪 Test Runner
|
||||
</a>
|
||||
<a href="https://nis2.agile.software/api-status.php" target="_blank" class="url-btn">
|
||||
❤️ Health Check
|
||||
</a>
|
||||
</div>
|
||||
<div>
|
||||
<h3>DevEnv (container locale)</h3>
|
||||
<a href="https://certisource.it/dev-nis2-api/" target="_blank" class="url-btn">
|
||||
🔧 Dev API
|
||||
</a>
|
||||
<a href="https://certisource.it/dev-nis2-ide/" target="_blank" class="url-btn">
|
||||
💻 Dev IDE
|
||||
</a>
|
||||
<a href="https://certisource.it/dev-nis2-browser/" target="_blank" class="url-btn">
|
||||
🌐 Dev Browser
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h3>Reset dati demo</h3>
|
||||
<div class="alert alert-warning">
|
||||
<span>⚠️</span>
|
||||
<div>Il reset cancella tutte le organizzazioni e gli utenti con <code>id > 4</code> e email che contengono <code>.demo</code>. Le 4 org base e i super admin vengono preservati.</div>
|
||||
</div>
|
||||
<div class="code-block">
|
||||
<span class="comment"># Via Test Runner UI → tasto "Reset Dati Demo"</span>
|
||||
<span class="comment"># Via SSH (Hetzner)</span>
|
||||
<span class="keyword">mysql</span> -u nis2_user -p nis2_agile_db < /var/www/nis2-agile/docs/sql/reset-demo.sql
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- ── Credenziali ── -->
|
||||
<section id="credenziali">
|
||||
<h2><span class="icon">🔑</span> Credenziali</h2>
|
||||
|
||||
<h3>Test Runner</h3>
|
||||
<div class="creds-box">
|
||||
<div class="creds-row">
|
||||
<div class="creds-key">Token accesso</div>
|
||||
<div class="creds-val">Nis2Test2026</div>
|
||||
</div>
|
||||
<div class="creds-row">
|
||||
<div class="creds-key">URL</div>
|
||||
<div class="creds-val">https://nis2.agile.software/test-runner.php?t=Nis2Test2026</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h3>Utenti demo (creati da SIM-01)</h3>
|
||||
<div class="creds-box">
|
||||
<div class="creds-row">
|
||||
<div class="creds-key">Password comune</div>
|
||||
<div class="creds-val">NIS2Demo2026!</div>
|
||||
</div>
|
||||
<div class="creds-row">
|
||||
<div class="creds-key">DataCore admin</div>
|
||||
<div class="creds-val">admin@datacore-srl.demo</div>
|
||||
</div>
|
||||
<div class="creds-row">
|
||||
<div class="creds-key">MedClinic admin</div>
|
||||
<div class="creds-val">admin@medclinic.demo</div>
|
||||
</div>
|
||||
<div class="creds-row">
|
||||
<div class="creds-key">EnerNet admin</div>
|
||||
<div class="creds-val">admin@enernet.demo</div>
|
||||
</div>
|
||||
<div class="creds-row">
|
||||
<div class="creds-key">Consultant</div>
|
||||
<div class="creds-val">consultant@nis2agile.demo</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h3>API Key demo (DataCore — read:all)</h3>
|
||||
<div class="creds-box">
|
||||
<div class="creds-row">
|
||||
<div class="creds-key">Chiave</div>
|
||||
<div class="creds-val">nis2_152c1d87f8e6613d18a0510fd907c082</div>
|
||||
</div>
|
||||
<div class="creds-row">
|
||||
<div class="creds-key">Scope</div>
|
||||
<div class="creds-val">read:all</div>
|
||||
</div>
|
||||
<div class="creds-row">
|
||||
<div class="creds-key">Header</div>
|
||||
<div class="creds-val">X-API-Key: nis2_152c1d87f8e6613d18a0510fd907c082</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h3>Sistema Feedback</h3>
|
||||
<div class="creds-box">
|
||||
<div class="creds-row">
|
||||
<div class="creds-key">Password resolve</div>
|
||||
<div class="creds-val">Nis2Feedback2026!</div>
|
||||
</div>
|
||||
<div class="creds-row">
|
||||
<div class="creds-key">Worker account</div>
|
||||
<div class="creds-val">worker@nis2.agile.software</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="alert alert-warning" style="margin-top:24px;">
|
||||
<span>🔒</span>
|
||||
<div>Queste credenziali sono per uso esclusivo in ambiente di test/demo. Non usarle in produzione reale. La pagina non è indicizzata dai motori di ricerca.</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
</div>
|
||||
|
||||
<footer>
|
||||
NIS2 Agile — Documentazione Testing & Simulazione · Aggiornata 2026-03-10 ·
|
||||
<a href="https://nis2.agile.software/" style="color:var(--primary);">nis2.agile.software</a>
|
||||
</footer>
|
||||
|
||||
<script>
|
||||
// Highlight nav link on scroll
|
||||
const sections = document.querySelectorAll('section[id]');
|
||||
const navLinks = document.querySelectorAll('.nav a');
|
||||
const observer = new IntersectionObserver(entries => {
|
||||
entries.forEach(entry => {
|
||||
if (entry.isIntersecting) {
|
||||
navLinks.forEach(a => a.classList.remove('active'));
|
||||
const active = document.querySelector(`.nav a[href="#${entry.target.id}"]`);
|
||||
if (active) active.classList.add('active');
|
||||
}
|
||||
});
|
||||
}, { threshold: 0.3 });
|
||||
sections.forEach(s => observer.observe(s));
|
||||
</script>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
27
public/simulate-nis2.php
Normal file
27
public/simulate-nis2.php
Normal file
@ -0,0 +1,27 @@
|
||||
<?php
|
||||
/**
|
||||
* NIS2 Agile — Wrapper pubblico per simulate-nis2.php
|
||||
*
|
||||
* DocumentRoot è public/ → il file principale è fuori dalla web root.
|
||||
* Questo wrapper converte il query param ?sim= nell'env var NIS2_SIM
|
||||
* e include il simulatore reale dalla root del progetto.
|
||||
*
|
||||
* URL: https://nis2.agile.software/simulate-nis2.php?sim=all
|
||||
*/
|
||||
|
||||
// La simulazione completa richiede 8-12 minuti: rimuovi il limite di esecuzione PHP
|
||||
set_time_limit(0);
|
||||
ignore_user_abort(true);
|
||||
ini_set('memory_limit', '256M');
|
||||
|
||||
// Mappa ?sim=sim06 → NIS2_SIM=SIM06 (unico filtro attivo nel simulatore)
|
||||
$simParam = strtolower(trim($_GET['sim'] ?? 'all'));
|
||||
if ($simParam === 'sim06') {
|
||||
putenv('NIS2_SIM=SIM06');
|
||||
}
|
||||
// sim01-sim05 e 'all': SIM_FILTER rimane null → esegue tutti gli scenari
|
||||
|
||||
// Includi il simulatore dalla root del progetto.
|
||||
// __DIR__ nel file incluso punta alla directory di quel file (/var/www/nis2-agile/)
|
||||
// quindi readEnvValue() trova correttamente .env nella root.
|
||||
require __DIR__ . '/../simulate-nis2.php';
|
||||
@ -13,28 +13,29 @@
|
||||
}
|
||||
body { background: var(--dark); color: #e2e8f0; font-family: -apple-system,BlinkMacSystemFont,'Segoe UI',system-ui,sans-serif; min-height: 100vh; }
|
||||
|
||||
.header { background: linear-gradient(135deg,#0c4a6e,#0e7490); padding: 32px 48px; border-bottom: 1px solid #0e7490; }
|
||||
.header { background: linear-gradient(135deg,#0c4a6e,#0e7490); padding: 28px 48px; border-bottom: 1px solid #0e7490; }
|
||||
.header h1 { font-size: 1.5rem; font-weight: 800; color: #fff; margin-bottom: 4px; }
|
||||
.header p { color: #7dd3fc; font-size: 0.875rem; }
|
||||
.badge { display: inline-block; padding: 2px 10px; border-radius: 20px; font-size: 0.7rem; font-weight: 700; margin-right: 8px; }
|
||||
.badge-cyan { background: rgba(6,182,212,.2); color: #67e8f9; border: 1px solid rgba(6,182,212,.3); }
|
||||
.badge-green { background: rgba(34,197,94,.2); color: #86efac; border: 1px solid rgba(34,197,94,.3); }
|
||||
.badge-yellow { background: rgba(245,158,11,.2); color: #fcd34d; border: 1px solid rgba(245,158,11,.3); }
|
||||
|
||||
.container { max-width: 1100px; margin: 0 auto; padding: 32px 24px; display: grid; grid-template-columns: 320px 1fr; gap: 24px; }
|
||||
.container { max-width: 1100px; margin: 0 auto; padding: 28px 24px; display: grid; grid-template-columns: 330px 1fr; gap: 24px; }
|
||||
|
||||
/* Left panel */
|
||||
.panel { background: var(--card); border: 1px solid var(--border); border-radius: 12px; padding: 20px; }
|
||||
.panel h3 { font-size: 0.875rem; font-weight: 700; color: #94a3b8; text-transform: uppercase; letter-spacing: .05em; margin-bottom: 16px; }
|
||||
.panel { background: var(--card); border: 1px solid var(--border); border-radius: 12px; padding: 18px; }
|
||||
.panel h3 { font-size: 0.8rem; font-weight: 700; color: #94a3b8; text-transform: uppercase; letter-spacing: .05em; margin-bottom: 14px; }
|
||||
|
||||
.sim-card { background: #0f172a; border: 1px solid var(--border); border-radius: 8px; padding: 14px; margin-bottom: 10px; cursor: pointer; transition: border-color .2s; }
|
||||
.sim-card { background: #0f172a; border: 1px solid var(--border); border-radius: 8px; padding: 12px; margin-bottom: 8px; cursor: pointer; transition: border-color .2s; }
|
||||
.sim-card:hover { border-color: var(--primary); }
|
||||
.sim-card.active { border-color: var(--primary); background: #0c2230; }
|
||||
.sim-card h4 { font-size: 0.8125rem; font-weight: 700; color: #e2e8f0; margin-bottom: 4px; }
|
||||
.sim-card p { font-size: 0.75rem; color: #64748b; line-height: 1.5; }
|
||||
.sim-card .sim-badge { font-size: 0.65rem; padding: 1px 7px; border-radius: 10px; font-weight: 700; margin-bottom: 6px; display: inline-block; }
|
||||
.sim-card h4 { font-size: 0.8125rem; font-weight: 700; color: #e2e8f0; margin-bottom: 3px; }
|
||||
.sim-card p { font-size: 0.7375rem; color: #64748b; line-height: 1.5; }
|
||||
.sim-card .sim-badge { font-size: 0.65rem; padding: 1px 7px; border-radius: 10px; font-weight: 700; margin-bottom: 5px; display: inline-block; }
|
||||
|
||||
.company-list { margin-bottom: 20px; }
|
||||
.company-item { display: flex; align-items: center; gap: 10px; padding: 10px 12px; border-radius: 8px; background: #0f172a; border: 1px solid var(--border); margin-bottom: 8px; }
|
||||
.company-list { margin-bottom: 16px; }
|
||||
.company-item { display: flex; align-items: center; gap: 10px; padding: 9px 12px; border-radius: 8px; background: #0f172a; border: 1px solid var(--border); margin-bottom: 6px; }
|
||||
.company-dot { width: 8px; height: 8px; border-radius: 50%; flex-shrink: 0; }
|
||||
.dot-cyan { background: #06b6d4; }
|
||||
.dot-purple { background: #8b5cf6; }
|
||||
@ -43,8 +44,8 @@
|
||||
.company-item small { font-size: 0.7rem; color: #64748b; display: block; }
|
||||
|
||||
/* Controls */
|
||||
.controls { display: flex; flex-direction: column; gap: 10px; margin-bottom: 16px; }
|
||||
.btn { padding: 10px 20px; border: none; border-radius: 8px; font-size: 0.875rem; font-weight: 700; cursor: pointer; transition: all .2s; }
|
||||
.controls { display: flex; flex-direction: column; gap: 8px; margin-bottom: 12px; }
|
||||
.btn { padding: 10px 20px; border: none; border-radius: 8px; font-size: 0.875rem; font-weight: 700; cursor: pointer; transition: all .2s; display: flex; align-items: center; justify-content: center; gap: 8px; }
|
||||
.btn-primary { background: var(--primary); color: #0f172a; }
|
||||
.btn-primary:hover:not(:disabled) { background: #0891b2; }
|
||||
.btn-primary:disabled { opacity: .5; cursor: not-allowed; }
|
||||
@ -53,7 +54,16 @@
|
||||
.btn-gray { background: #1e293b; color: #94a3b8; border: 1px solid var(--border); }
|
||||
.btn-gray:hover { background: #334155; }
|
||||
|
||||
.status-bar { padding: 10px 12px; border-radius: 8px; font-size: 0.8rem; background: #0f172a; border: 1px solid var(--border); }
|
||||
/* Spinner */
|
||||
.spinner { display: none; width: 13px; height: 13px; border: 2px solid rgba(15,23,42,.3); border-top-color: #0f172a; border-radius: 50%; animation: spin .7s linear infinite; flex-shrink: 0; }
|
||||
.spinner.active { display: inline-block; }
|
||||
@keyframes spin { to { transform: rotate(360deg); } }
|
||||
|
||||
/* Auto-reset note */
|
||||
.auto-reset-note { font-size: 0.7rem; color: #475569; text-align: center; padding: 6px; border: 1px dashed #334155; border-radius: 6px; background: rgba(6,182,212,.03); }
|
||||
.auto-reset-note span { color: #67e8f9; }
|
||||
|
||||
.status-bar { padding: 9px 12px; border-radius: 8px; font-size: 0.8rem; background: #0f172a; border: 1px solid var(--border); }
|
||||
.status-dot { width: 8px; height: 8px; border-radius: 50%; display: inline-block; margin-right: 6px; }
|
||||
.dot-idle { background: #475569; }
|
||||
.dot-running { background: #22c55e; animation: pulse 1s infinite; }
|
||||
@ -61,59 +71,88 @@
|
||||
.dot-error { background: #ef4444; }
|
||||
@keyframes pulse { 0%,100%{opacity:1} 50%{opacity:.4} }
|
||||
|
||||
.stats-grid { display: grid; grid-template-columns: repeat(4,1fr); gap: 8px; margin-top: 12px; }
|
||||
.stat-box { background: #0f172a; border: 1px solid var(--border); border-radius: 6px; padding: 10px; text-align: center; }
|
||||
.stat-box .num { font-size: 1.25rem; font-weight: 800; }
|
||||
.stat-box .lbl { font-size: 0.65rem; color: #64748b; text-transform: uppercase; margin-top: 2px; }
|
||||
.stats-grid { display: grid; grid-template-columns: repeat(4,1fr); gap: 6px; margin-top: 10px; }
|
||||
.stat-box { background: #0f172a; border: 1px solid var(--border); border-radius: 6px; padding: 8px; text-align: center; }
|
||||
.stat-box .num { font-size: 1.2rem; font-weight: 800; }
|
||||
.stat-box .lbl { font-size: 0.6rem; color: #64748b; text-transform: uppercase; margin-top: 2px; }
|
||||
.num-pass { color: var(--green); }
|
||||
.num-skip { color: #94a3b8; }
|
||||
.num-warn { color: var(--yellow); }
|
||||
.num-fail { color: var(--red); }
|
||||
|
||||
/* Run History */
|
||||
.history-section { margin-top: 14px; }
|
||||
.history-title { font-size: 0.75rem; font-weight: 700; color: #475569; text-transform: uppercase; letter-spacing: .05em; margin-bottom: 8px; }
|
||||
.history-item { display: flex; align-items: center; gap: 8px; padding: 7px 10px; border-radius: 6px; background: #0f172a; border: 1px solid var(--border); margin-bottom: 5px; cursor: pointer; transition: border-color .15s; font-size: 0.73rem; }
|
||||
.history-item:hover { border-color: #475569; }
|
||||
.hi-dot { width: 7px; height: 7px; border-radius: 50%; flex-shrink: 0; }
|
||||
.hi-dot.ok { background: #22c55e; }
|
||||
.hi-dot.fail { background: #ef4444; }
|
||||
.hi-label { color: #94a3b8; flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||||
.hi-time { color: #475569; font-size: 0.65rem; white-space: nowrap; }
|
||||
.hi-elapsed { color: #67e8f9; font-size: 0.65rem; margin-left: 4px; white-space: nowrap; }
|
||||
.history-empty { font-size: 0.73rem; color: #475569; text-align: center; padding: 8px; }
|
||||
|
||||
/* Right panel — Console */
|
||||
.console-panel { display: flex; flex-direction: column; gap: 0; }
|
||||
.console-header { background: #1e293b; border: 1px solid var(--border); border-radius: 12px 12px 0 0; padding: 12px 16px; display: flex; align-items: center; justify-content: space-between; }
|
||||
.console-header { background: #1e293b; border: 1px solid var(--border); border-radius: 12px 12px 0 0; padding: 11px 16px; display: flex; align-items: center; justify-content: space-between; }
|
||||
.console-header span { font-size: 0.8rem; color: #94a3b8; }
|
||||
.console-dots { display: flex; gap: 6px; }
|
||||
.console-dot { width: 10px; height: 10px; border-radius: 50%; }
|
||||
.console-dot-r { background: #ef4444; }
|
||||
.console-dot-y { background: #f59e0b; }
|
||||
.console-dot-g { background: #22c55e; }
|
||||
#console { background: #0a0f1a; border: 1px solid var(--border); border-top: none; border-radius: 0 0 12px 12px; padding: 16px; height: 520px; overflow-y: auto; font-family: 'Cascadia Code','Consolas',monospace; font-size: 0.78rem; line-height: 1.7; }
|
||||
.log-phase { color: #38bdf8; font-weight: 700; border-top: 1px solid #1e3a5f; padding-top: 8px; margin-top: 4px; }
|
||||
|
||||
#console {
|
||||
background: #0a0f1a;
|
||||
border: 1px solid var(--border); border-top: none; border-radius: 0 0 12px 12px;
|
||||
padding: 16px; height: 560px; overflow-y: auto;
|
||||
font-family: 'Cascadia Code','Consolas','Fira Code',monospace;
|
||||
font-size: 0.775rem; line-height: 1.75;
|
||||
}
|
||||
/* log line colors */
|
||||
.log-phase { color: #38bdf8; font-weight: 700; border-top: 1px solid #1e3a5f; padding-top: 8px; margin-top: 6px; }
|
||||
.log-ok { color: #86efac; }
|
||||
.log-skip { color: #64748b; }
|
||||
.log-warn { color: #fcd34d; }
|
||||
.log-error { color: #fca5a5; }
|
||||
.log-email { color: #67e8f9; }
|
||||
.log-info { color: #94a3b8; }
|
||||
.log-done { color: #a78bfa; font-weight: 700; border-top: 1px solid #3730a3; padding-top: 8px; margin-top: 4px; }
|
||||
.log-done { color: #a78bfa; font-weight: 700; border-top: 1px solid #3730a3; padding-top: 8px; margin-top: 6px; }
|
||||
|
||||
/* Phase step banner */
|
||||
.log-phase-banner {
|
||||
background: rgba(56,189,248,.07);
|
||||
border-left: 3px solid #38bdf8;
|
||||
padding: 4px 10px; margin: 6px 0 4px;
|
||||
color: #7dd3fc; font-weight: 700;
|
||||
}
|
||||
|
||||
/* Progress bar */
|
||||
.progress-wrap { height: 4px; background: #1e293b; border-radius: 2px; overflow: hidden; margin-bottom: 16px; }
|
||||
.progress-bar { height: 100%; background: var(--primary); width: 0%; transition: width .3s; }
|
||||
.progress-wrap { height: 4px; background: #1e293b; border-radius: 2px; overflow: hidden; margin-bottom: 14px; }
|
||||
.progress-bar { height: 100%; background: var(--primary); width: 0%; transition: width .4s; }
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.container { grid-template-columns: 1fr; }
|
||||
}
|
||||
@media (max-width: 768px) { .container { grid-template-columns: 1fr; } }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<div class="header">
|
||||
<div style="margin-bottom:10px;">
|
||||
<div style="margin-bottom:8px;">
|
||||
<span class="badge badge-cyan">NIS2 Agile</span>
|
||||
<span class="badge badge-green">Demo Simulator v1.0</span>
|
||||
<span class="badge badge-green">Demo Simulator v2.0</span>
|
||||
<span class="badge badge-yellow">Auto-Reset</span>
|
||||
</div>
|
||||
<h1>Simulazione Demo Realistica</h1>
|
||||
<p>Genera dati demo NIS2 realistici tramite API reali — 3 aziende, 5 scenari, audit trail certificato SHA-256</p>
|
||||
<p>3 aziende · 6 scenari · reset automatico · audit trail SHA-256 · dati creati via API reali</p>
|
||||
</div>
|
||||
|
||||
<div class="container">
|
||||
|
||||
<!-- Left Panel -->
|
||||
<div style="display:flex;flex-direction:column;gap:16px;">
|
||||
<div style="display:flex;flex-direction:column;gap:14px;">
|
||||
|
||||
<!-- Aziende -->
|
||||
<div class="panel">
|
||||
<h3>3 Aziende Demo</h3>
|
||||
<div class="company-list">
|
||||
@ -121,14 +160,14 @@
|
||||
<div class="company-dot dot-cyan"></div>
|
||||
<div>
|
||||
<span>DataCore S.r.l.</span>
|
||||
<small>IT/Cloud · Essential Entity · 320 dip · Milano</small>
|
||||
<small>IT/Cloud · Essential · 320 dip · Milano</small>
|
||||
</div>
|
||||
</div>
|
||||
<div class="company-item">
|
||||
<div class="company-dot dot-purple"></div>
|
||||
<div>
|
||||
<span>MedClinic Italia S.p.A.</span>
|
||||
<small>Sanità · Important Entity · 750 dip · Roma</small>
|
||||
<small>Sanità · Important · 750 dip · Roma</small>
|
||||
</div>
|
||||
</div>
|
||||
<div class="company-item">
|
||||
@ -141,50 +180,60 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Scenari -->
|
||||
<div class="panel">
|
||||
<h3>5 Scenari Reali</h3>
|
||||
<h3>6 Scenari Reali</h3>
|
||||
|
||||
<div class="sim-card active" onclick="selectSim(this, 'all')">
|
||||
<div class="sim-badge" style="background:rgba(6,182,212,.15);color:#67e8f9;border:1px solid rgba(6,182,212,.3);">COMPLETA</div>
|
||||
<h4>Tutti gli scenari</h4>
|
||||
<p>Esegue tutti e 5 gli scenari in sequenza: onboarding, incidenti, data breach, whistleblowing, audit chain.</p>
|
||||
<p>SIM-01→06 in sequenza: onboarding, incidenti, data breach, whistleblowing, audit chain, B2B.</p>
|
||||
</div>
|
||||
<div class="sim-card" onclick="selectSim(this, 'sim01')">
|
||||
<div class="sim-badge" style="background:rgba(34,197,94,.1);color:#86efac;border:1px solid rgba(34,197,94,.3);">SIM-01</div>
|
||||
<h4>Onboarding + Gap Assessment</h4>
|
||||
<p>Registra le 3 aziende, classifica NIS2 (Essential/Important), esegue assessment 80 domande Art.21.</p>
|
||||
<p>Registra 3 aziende, classifica NIS2, esegue assessment 80 domande Art.21.</p>
|
||||
</div>
|
||||
<div class="sim-card" onclick="selectSim(this, 'sim02')">
|
||||
<div class="sim-badge" style="background:rgba(239,68,68,.1);color:#fca5a5;border:1px solid rgba(239,68,68,.3);">SIM-02</div>
|
||||
<h4>Ransomware Art.23 DataCore</h4>
|
||||
<p>Attacco ransomware su infrastruttura cloud. Timeline 24h/72h/30d con notifiche ACN e CSIRT.</p>
|
||||
<p>Attacco critico su cloud. Timeline 24h/72h/30d con notifiche CSIRT.</p>
|
||||
</div>
|
||||
<div class="sim-card" onclick="selectSim(this, 'sim03')">
|
||||
<div class="sim-badge" style="background:rgba(139,92,246,.1);color:#ddd6fe;border:1px solid rgba(139,92,246,.3);">SIM-03</div>
|
||||
<h4>Data Breach Supply Chain</h4>
|
||||
<p>Fornitore LIS compromesso, esfiltrazione 2.340 pazienti. GDPR Art.33 + NIS2 Art.23 in parallelo.</p>
|
||||
<p>Fornitore LIS compromesso. GDPR Art.33 + NIS2 Art.23 in parallelo.</p>
|
||||
</div>
|
||||
<div class="sim-card" onclick="selectSim(this, 'sim04')">
|
||||
<div class="sim-badge" style="background:rgba(245,158,11,.1);color:#fcd34d;border:1px solid rgba(245,158,11,.3);">SIM-04</div>
|
||||
<h4>Whistleblowing SCADA EnerNet</h4>
|
||||
<p>Segnalazione anonima accesso non autorizzato SCADA. Token tracking, investigazione, chiusura.</p>
|
||||
<p>Segnalazione anonima accesso non autorizzato SCADA. Token tracking.</p>
|
||||
</div>
|
||||
<div class="sim-card" onclick="selectSim(this, 'sim05')">
|
||||
<div class="sim-badge" style="background:rgba(6,182,212,.1);color:#67e8f9;border:1px solid rgba(6,182,212,.3);">SIM-05</div>
|
||||
<h4>Audit Trail Hash Chain</h4>
|
||||
<p>Verifica integrità SHA-256 hash chain per le 3 org. Export certificato NIS2 + ISO 27001.</p>
|
||||
<p>Verifica integrità SHA-256 hash chain per le 3 org. Export certificato.</p>
|
||||
</div>
|
||||
<div class="sim-card" onclick="selectSim(this, 'sim06')">
|
||||
<div class="sim-badge" style="background:rgba(245,158,11,.12);color:#fbbf24;border:1px solid rgba(245,158,11,.3);">SIM-06</div>
|
||||
<h4>B2B License Provisioning</h4>
|
||||
<p>Invite token → org + user + API key creati atomicamente. SSO JWT.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Controlli -->
|
||||
<div class="panel">
|
||||
<h3>Controlli</h3>
|
||||
<div class="controls">
|
||||
<button id="btnRun" class="btn btn-primary" onclick="runSimulation()">▶ Avvia Simulazione</button>
|
||||
<button id="btnRun" class="btn btn-primary" onclick="runSimulation()">
|
||||
<span class="spinner" id="spinner"></span>
|
||||
<span id="btnRunLabel">▶ Avvia Simulazione</span>
|
||||
</button>
|
||||
<button id="btnStop" class="btn btn-danger" onclick="stopSimulation()" disabled>■ Interrompi</button>
|
||||
<button class="btn btn-gray" onclick="clearConsole()">⌫ Pulisci Console</button>
|
||||
<button class="btn btn-gray" onclick="resetDemo()">↺ Reset Dati Demo</button>
|
||||
</div>
|
||||
<div class="progress-wrap"><div class="progress-bar" id="progressBar"></div></div>
|
||||
<div class="auto-reset-note">↺ <span>Reset automatico</span> dati demo precedenti all'avvio</div>
|
||||
<div class="progress-wrap" style="margin-top:12px;"><div class="progress-bar" id="progressBar"></div></div>
|
||||
<div class="status-bar">
|
||||
<span class="status-dot dot-idle" id="statusDot"></span>
|
||||
<span id="statusText">In attesa</span>
|
||||
@ -195,6 +244,12 @@
|
||||
<div class="stat-box"><div class="num num-warn" id="sWarn">0</div><div class="lbl">Warn</div></div>
|
||||
<div class="stat-box"><div class="num num-fail" id="sFail">0</div><div class="lbl">Fail</div></div>
|
||||
</div>
|
||||
|
||||
<!-- Run History -->
|
||||
<div class="history-section">
|
||||
<div class="history-title">Ultimi run</div>
|
||||
<div id="historyList"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
@ -207,55 +262,91 @@
|
||||
<div class="console-dot console-dot-y"></div>
|
||||
<div class="console-dot console-dot-g"></div>
|
||||
</div>
|
||||
<span id="consoleTitle">NIS2 Agile Simulator — Console</span>
|
||||
<span id="elapsedTime"></span>
|
||||
<span id="consoleTitle">NIS2 Agile Simulator v2.0 — Console</span>
|
||||
<span id="elapsedTime" style="font-variant-numeric:tabular-nums;"></span>
|
||||
</div>
|
||||
<div id="console">
|
||||
<div class="log-info">NIS2 Agile Demo Simulator v1.0 — Pronto</div>
|
||||
<div class="log-info">NIS2 Agile Demo Simulator v2.0 — Pronto</div>
|
||||
<div class="log-info">Seleziona uno scenario e premi "Avvia Simulazione".</div>
|
||||
<div class="log-info" style="margin-top:8px;">Scenari disponibili:</div>
|
||||
<div class="log-info" style="margin-top:8px;color:#475569;">Scenari disponibili:</div>
|
||||
<div class="log-ok"> SIM-01 Onboarding + Gap Assessment 80 domande</div>
|
||||
<div class="log-ok"> SIM-02 Incidente Ransomware Art.23 (24h/72h/30d)</div>
|
||||
<div class="log-ok"> SIM-03 Data Breach Supply Chain + GDPR</div>
|
||||
<div class="log-ok"> SIM-04 Whistleblowing anonimo SCADA</div>
|
||||
<div class="log-ok"> SIM-05 Audit Trail Hash Chain Verification</div>
|
||||
<div class="log-info" style="margin-top:8px;">Tutti i dati vengono creati tramite API reali (nessun INSERT SQL diretto).</div>
|
||||
<div class="log-ok"> SIM-06 B2B License Provisioning (invite token)</div>
|
||||
<div class="log-info" style="margin-top:8px;color:#38bdf8;">↺ Ogni run pulisce automaticamente i dati demo precedenti.</div>
|
||||
<div class="log-info" style="color:#475569;">Tutti i dati vengono creati tramite API reali (nessun INSERT SQL diretto).</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<script>
|
||||
let evtSource = null;
|
||||
const HISTORY_KEY = 'nis2_sim_history';
|
||||
let evtSource = null;
|
||||
let selectedSim = 'all';
|
||||
let startTime = null;
|
||||
let startTime = null;
|
||||
let timerInterval = null;
|
||||
|
||||
const SIM_LABELS = {
|
||||
all: 'Tutti (SIM-01→06)',
|
||||
sim01: 'SIM-01 Onboarding',
|
||||
sim02: 'SIM-02 Ransomware',
|
||||
sim03: 'SIM-03 Data Breach',
|
||||
sim04: 'SIM-04 Whistleblowing',
|
||||
sim05: 'SIM-05 Audit Chain',
|
||||
sim06: 'SIM-06 B2B License',
|
||||
};
|
||||
|
||||
// ── Selezione scenario ────────────────────────────────────────────────────
|
||||
|
||||
function selectSim(el, sim) {
|
||||
document.querySelectorAll('.sim-card').forEach(c => c.classList.remove('active'));
|
||||
el.classList.add('active');
|
||||
selectedSim = sim;
|
||||
}
|
||||
|
||||
// ── Stato UI ─────────────────────────────────────────────────────────────
|
||||
|
||||
function setStatus(text, dotClass) {
|
||||
document.getElementById('statusText').textContent = text;
|
||||
const dot = document.getElementById('statusDot');
|
||||
dot.className = 'status-dot ' + dotClass;
|
||||
document.getElementById('statusDot').className = 'status-dot ' + dotClass;
|
||||
}
|
||||
|
||||
function setRunning(running) {
|
||||
const runBtn = document.getElementById('btnRun');
|
||||
const stopBtn = document.getElementById('btnStop');
|
||||
const spinner = document.getElementById('spinner');
|
||||
const label = document.getElementById('btnRunLabel');
|
||||
runBtn.disabled = running;
|
||||
stopBtn.disabled = !running;
|
||||
spinner.classList.toggle('active', running);
|
||||
label.textContent = running ? 'Simulazione in corso...' : '▶ Avvia Simulazione';
|
||||
}
|
||||
|
||||
// ── Console ───────────────────────────────────────────────────────────────
|
||||
|
||||
function appendLog(msg, type) {
|
||||
const console = document.getElementById('console');
|
||||
const con = document.getElementById('console');
|
||||
const line = document.createElement('div');
|
||||
line.className = 'log-' + (type || 'info');
|
||||
|
||||
if (type === 'phase') {
|
||||
line.className = 'log-phase-banner';
|
||||
} else {
|
||||
line.className = 'log-' + (type || 'info');
|
||||
}
|
||||
line.textContent = msg;
|
||||
console.appendChild(line);
|
||||
console.scrollTop = console.scrollHeight;
|
||||
con.appendChild(line);
|
||||
con.scrollTop = con.scrollHeight;
|
||||
}
|
||||
|
||||
function clearConsole() {
|
||||
document.getElementById('console').innerHTML = '<div class="log-info">Console pulita.</div>';
|
||||
}
|
||||
|
||||
// ── Progress ──────────────────────────────────────────────────────────────
|
||||
|
||||
function updateProgress(pct) {
|
||||
document.getElementById('progressBar').style.width = pct + '%';
|
||||
}
|
||||
@ -269,106 +360,178 @@ function updateStats(stats) {
|
||||
document.getElementById('sFail').textContent = stats.fail || 0;
|
||||
}
|
||||
|
||||
// ── Timer ─────────────────────────────────────────────────────────────────
|
||||
|
||||
function startTimer() {
|
||||
startTime = Date.now();
|
||||
timerInterval = setInterval(() => {
|
||||
const elapsed = ((Date.now() - startTime) / 1000).toFixed(1);
|
||||
document.getElementById('elapsedTime').textContent = elapsed + 's';
|
||||
const s = ((Date.now() - startTime) / 1000).toFixed(1);
|
||||
document.getElementById('elapsedTime').textContent = s + 's';
|
||||
}, 500);
|
||||
}
|
||||
|
||||
function stopTimer() {
|
||||
if (timerInterval) clearInterval(timerInterval);
|
||||
if (timerInterval) { clearInterval(timerInterval); timerInterval = null; }
|
||||
const s = startTime ? ((Date.now() - startTime) / 1000).toFixed(1) : null;
|
||||
return s;
|
||||
}
|
||||
|
||||
// ── Run History (localStorage) ────────────────────────────────────────────
|
||||
|
||||
function getHistory() {
|
||||
try { return JSON.parse(localStorage.getItem(HISTORY_KEY) || '[]'); }
|
||||
catch(e) { return []; }
|
||||
}
|
||||
|
||||
function addHistory(sim, stats, elapsed) {
|
||||
const ok = (stats.fail || 0) === 0;
|
||||
const h = getHistory();
|
||||
h.unshift({
|
||||
sim,
|
||||
label: SIM_LABELS[sim] || sim,
|
||||
ok,
|
||||
pass: stats.pass || 0,
|
||||
fail: stats.fail || 0,
|
||||
skip: stats.skip || 0,
|
||||
elapsed: parseFloat(elapsed) || 0,
|
||||
ts: new Date().toLocaleString('it-IT', { day:'2-digit', month:'2-digit', hour:'2-digit', minute:'2-digit' }),
|
||||
});
|
||||
localStorage.setItem(HISTORY_KEY, JSON.stringify(h.slice(0, 5)));
|
||||
renderHistory();
|
||||
}
|
||||
|
||||
function renderHistory() {
|
||||
const list = document.getElementById('historyList');
|
||||
const h = getHistory();
|
||||
if (!h.length) {
|
||||
list.innerHTML = '<div class="history-empty">Nessun run ancora</div>';
|
||||
return;
|
||||
}
|
||||
list.innerHTML = h.map(e => `
|
||||
<div class="history-item" onclick="replayRun('${e.sim}')" title="Riesegui ${e.label}">
|
||||
<span class="hi-dot ${e.ok ? 'ok' : 'fail'}"></span>
|
||||
<span class="hi-label">${e.label}</span>
|
||||
<span class="hi-time">${e.ts}</span>
|
||||
<span class="hi-elapsed">${e.elapsed}s</span>
|
||||
</div>
|
||||
`).join('');
|
||||
}
|
||||
|
||||
function replayRun(sim) {
|
||||
// Seleziona lo scenario e avvia
|
||||
document.querySelectorAll('.sim-card').forEach(c => {
|
||||
const badge = c.querySelector('.sim-badge');
|
||||
const isSim06 = badge && badge.textContent.trim() === 'SIM-06';
|
||||
const isAll = c.querySelector('h4') && c.querySelector('h4').textContent.includes('Tutti');
|
||||
if (
|
||||
(sim === 'all' && isAll) ||
|
||||
(sim === 'sim06' && isSim06) ||
|
||||
(sim !== 'all' && sim !== 'sim06' && badge && badge.textContent.trim().toLowerCase().replace('-','') === sim.replace('sim','sim-'))
|
||||
) {
|
||||
selectSim(c, sim);
|
||||
}
|
||||
});
|
||||
selectedSim = sim;
|
||||
runSimulation();
|
||||
}
|
||||
|
||||
// ── Avvio simulazione ─────────────────────────────────────────────────────
|
||||
|
||||
function runSimulation() {
|
||||
// Confirm dialog (pattern lg231)
|
||||
const labels = {
|
||||
all: 'Tutti gli scenari (SIM-01→06)',
|
||||
sim01: 'SIM-01 — Onboarding + Gap Assessment',
|
||||
sim02: 'SIM-02 — Ransomware Art.23',
|
||||
sim03: 'SIM-03 — Data Breach Supply Chain',
|
||||
sim04: 'SIM-04 — Whistleblowing SCADA',
|
||||
sim05: 'SIM-05 — Audit Trail Hash Chain',
|
||||
sim06: 'SIM-06 — B2B License Provisioning',
|
||||
};
|
||||
const scenarioName = labels[selectedSim] || selectedSim;
|
||||
const resetNote = selectedSim !== 'sim06'
|
||||
? '\n↺ I dati demo precedenti verranno ripuliti automaticamente.'
|
||||
: '\n(SIM-06 è indipendente — nessun reset automatico)';
|
||||
|
||||
if (!confirm(
|
||||
'Avvia Simulazione NIS2 Agile?\n\n' +
|
||||
'▸ ' + scenarioName + '\n\n' +
|
||||
'• DataCore S.r.l. (IT/Cloud - Essential)\n' +
|
||||
'• MedClinic Italia S.p.A. (Sanità - Important)\n' +
|
||||
'• EnerNet Distribuzione S.r.l. (Energia - Critical)\n' +
|
||||
resetNote + '\n' +
|
||||
'\nDurata stimata: ~8-12 minuti'
|
||||
)) return;
|
||||
|
||||
if (evtSource) { evtSource.close(); evtSource = null; }
|
||||
|
||||
const runBtn = document.getElementById('btnRun');
|
||||
const stopBtn = document.getElementById('btnStop');
|
||||
runBtn.disabled = true;
|
||||
stopBtn.disabled = false;
|
||||
|
||||
setRunning(true);
|
||||
clearConsole();
|
||||
setStatus('Simulazione in corso...', 'dot-running');
|
||||
updateProgress(5);
|
||||
startTimer();
|
||||
document.getElementById('statsGrid').style.display = 'none';
|
||||
document.getElementById('consoleTitle').textContent = 'NIS2 Agile Simulator v2.0 — ' + (SIM_LABELS[selectedSim] || selectedSim);
|
||||
|
||||
const url = `../simulate-nis2.php?sim=${selectedSim}&t=${Date.now()}`;
|
||||
evtSource = new EventSource(url);
|
||||
const url = `simulate-nis2.php?sim=${selectedSim}&t=${Date.now()}`;
|
||||
evtSource = new EventSource(url);
|
||||
|
||||
const phaseKeywords = ['FASE', '══'];
|
||||
let phaseCount = 0;
|
||||
|
||||
evtSource.onmessage = (e) => {
|
||||
const data = JSON.parse(e.data);
|
||||
|
||||
if (data.t === 'done') {
|
||||
evtSource.close();
|
||||
runBtn.disabled = false;
|
||||
stopBtn.disabled = true;
|
||||
setStatus('Completata', 'dot-done');
|
||||
stopTimer();
|
||||
evtSource.close(); evtSource = null;
|
||||
const elapsed = stopTimer();
|
||||
setRunning(false);
|
||||
setStatus('Completata (' + elapsed + 's)', 'dot-done');
|
||||
updateProgress(100);
|
||||
updateStats(data.stats);
|
||||
appendLog('', 'info');
|
||||
appendLog('Simulazione completata.', 'done');
|
||||
document.getElementById('consoleTitle').textContent = 'NIS2 Agile Simulator — Completata';
|
||||
appendLog('Simulazione completata in ' + elapsed + 's', 'done');
|
||||
document.getElementById('consoleTitle').textContent = 'NIS2 Agile Simulator v2.0 — Completata ✓';
|
||||
if (data.stats) addHistory(selectedSim, data.stats, elapsed);
|
||||
return;
|
||||
}
|
||||
|
||||
const type = data.t || 'info';
|
||||
const msg = data.m || '';
|
||||
|
||||
const msg = data.m || '';
|
||||
appendLog(msg, type);
|
||||
|
||||
// Stima progress
|
||||
if (type === 'phase' || msg.includes('FASE')) {
|
||||
if (type === 'phase') {
|
||||
phaseCount++;
|
||||
updateProgress(Math.min(5 + phaseCount * 8, 95));
|
||||
updateProgress(Math.min(5 + phaseCount * 7, 93));
|
||||
}
|
||||
};
|
||||
|
||||
evtSource.onerror = () => {
|
||||
evtSource.close();
|
||||
runBtn.disabled = false;
|
||||
stopBtn.disabled = true;
|
||||
evtSource.close(); evtSource = null;
|
||||
const elapsed = stopTimer();
|
||||
setRunning(false);
|
||||
setStatus('Errore connessione', 'dot-error');
|
||||
stopTimer();
|
||||
appendLog('Errore SSE — connessione interrotta.', 'error');
|
||||
if (elapsed) {
|
||||
addHistory(selectedSim, { pass: 0, fail: 1, skip: 0, warn: 0 }, elapsed);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
function stopSimulation() {
|
||||
if (evtSource) { evtSource.close(); evtSource = null; }
|
||||
document.getElementById('btnRun').disabled = false;
|
||||
document.getElementById('btnStop').disabled = true;
|
||||
const elapsed = stopTimer();
|
||||
setRunning(false);
|
||||
setStatus('Interrotta', 'dot-error');
|
||||
stopTimer();
|
||||
appendLog('Simulazione interrotta manualmente.', 'warn');
|
||||
if (elapsed) {
|
||||
addHistory(selectedSim, { pass: 0, fail: 0, skip: 1, warn: 1 }, elapsed);
|
||||
}
|
||||
}
|
||||
|
||||
function resetDemo() {
|
||||
if (!confirm('Reset dati demo? Verranno eliminati tutti i dati con organization_id > 4.')) return;
|
||||
fetch('../api/admin/reset-demo', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': 'Bearer ' + (localStorage.getItem('nis2_access_token') || ''),
|
||||
},
|
||||
})
|
||||
.then(r => r.json())
|
||||
.then(d => {
|
||||
if (d.success) {
|
||||
appendLog('Reset demo completato.', 'ok');
|
||||
} else {
|
||||
appendLog('Reset demo fallito: ' + (d.error || 'errore'), 'error');
|
||||
appendLog('Tip: eseguire manualmente docs/sql/reset-demo.sql su Hetzner.', 'warn');
|
||||
}
|
||||
})
|
||||
.catch(() => appendLog('Reset demo: errore di rete — eseguire manualmente su server.', 'warn'));
|
||||
}
|
||||
// ── Init ──────────────────────────────────────────────────────────────────
|
||||
|
||||
renderHistory();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@ -14,7 +14,8 @@
|
||||
* 5. Logga risultati in FEEDBACK_WORKER_LOG
|
||||
*
|
||||
* Crontab (root):
|
||||
* */30 * * * * root /usr/bin/php8.4 /var/www/nis2-agile/scripts/feedback-worker.php
|
||||
* * /30 * * * * root /usr/bin/php8.4 /var/www/nis2-agile/scripts/feedback-worker.php
|
||||
* (rimuovi lo spazio dopo * per avere: asterisco-slash-30)
|
||||
*
|
||||
* Variabili .env necessarie:
|
||||
* FEEDBACK_RESOLVE_PASSWORD=... (password per POST /api/feedback/{id}/resolve)
|
||||
|
||||
@ -85,6 +85,7 @@ $S = [
|
||||
|
||||
function simLog(string $msg, string $type = 'info'): void
|
||||
{
|
||||
static $lastHeartbeat = 0;
|
||||
$ts = date('H:i:s');
|
||||
if (IS_CLI) {
|
||||
$prefix = [
|
||||
@ -99,6 +100,12 @@ function simLog(string $msg, string $type = 'info'): void
|
||||
echo "[$ts] {$prefix} {$msg}\n";
|
||||
flush();
|
||||
} else {
|
||||
// Heartbeat SSE ogni 25s — mantiene viva la connessione attraverso proxy (pattern lg231)
|
||||
$now = time();
|
||||
if ($now - $lastHeartbeat >= 25) {
|
||||
echo ": heartbeat " . $ts . "\n\n";
|
||||
$lastHeartbeat = $now;
|
||||
}
|
||||
echo 'data: ' . json_encode(['t' => $type, 'm' => "[$ts] $msg"]) . "\n\n";
|
||||
flush();
|
||||
}
|
||||
@ -253,6 +260,19 @@ function ensureUser(string $firstName, string $lastName, string $email, string $
|
||||
return $jwt;
|
||||
}
|
||||
|
||||
// Ultimo tentativo: se register fallisce per "email già registrata" (utente inserito da
|
||||
// dbSeedUser ma login aveva fallito per motivo transitorio), riprova il login.
|
||||
$regMsg = strtolower($regRes['message'] ?? '');
|
||||
if (str_contains($regMsg, 'registrat') || str_contains($regMsg, 'esiste') || $regRes['_http'] === 409) {
|
||||
$retryLogin = api('POST', '/auth/login', ['email' => $email, 'password' => $password]);
|
||||
if (!empty($retryLogin['data']['access_token'])) {
|
||||
$jwt = $retryLogin['data']['access_token'];
|
||||
$S['users'][$email] = ['jwt' => $jwt, 'id' => $retryLogin['data']['user']['id'] ?? null];
|
||||
ok("Login (retry): $email");
|
||||
return $jwt;
|
||||
}
|
||||
}
|
||||
|
||||
fail("Registrazione fallita per $email: " . ($regRes['message'] ?? 'errore'));
|
||||
return null;
|
||||
}
|
||||
@ -606,6 +626,104 @@ function getAssessmentResponses(string $sector): array
|
||||
return array_merge($base, $overrides);
|
||||
}
|
||||
|
||||
// ────────────────────────────────────────────────────────────────────────────
|
||||
// Auto-Reset Demo Data — eseguito prima di ogni run SIM-01→05
|
||||
// Usa PDO con nis2_user (NO SUPER privilege → non tocca audit_logs/trigger)
|
||||
// ────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
function autoResetDemo(): void
|
||||
{
|
||||
$host = readEnvValue('DB_HOST', 'localhost');
|
||||
$port = readEnvValue('DB_PORT', '3306');
|
||||
$name = readEnvValue('DB_NAME', 'nis2_agile_db');
|
||||
$user = readEnvValue('DB_USER', 'nis2_user');
|
||||
$pass = readEnvValue('DB_PASS', '');
|
||||
|
||||
try {
|
||||
$pdo = new PDO(
|
||||
"mysql:host={$host};port={$port};dbname={$name};charset=utf8mb4",
|
||||
$user,
|
||||
$pass,
|
||||
[PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION, PDO::ATTR_TIMEOUT => 10]
|
||||
);
|
||||
} catch (\Throwable $e) {
|
||||
simLog('Auto-reset: connessione DB fallita — ' . $e->getMessage(), 'warn');
|
||||
return;
|
||||
}
|
||||
|
||||
$pdo->exec('SET FOREIGN_KEY_CHECKS=0');
|
||||
|
||||
// Tabelle con organization_id (ordine: dipendenti prima dei parent)
|
||||
$orgTables = [
|
||||
'incident_timeline' => 'incident_id IN (SELECT id FROM incidents WHERE organization_id > 4)',
|
||||
'incidents' => 'organization_id > 4',
|
||||
'risk_treatments' => 'risk_id IN (SELECT id FROM risks WHERE organization_id > 4)',
|
||||
'risks' => 'organization_id > 4',
|
||||
'assessment_responses' => 'assessment_id IN (SELECT id FROM assessments WHERE organization_id > 4)',
|
||||
'assessments' => 'organization_id > 4',
|
||||
'policies' => 'organization_id > 4',
|
||||
'suppliers' => 'organization_id > 4',
|
||||
'training_assignments' => 'course_id IN (SELECT id FROM training_courses WHERE organization_id > 4)',
|
||||
'training_courses' => 'organization_id > 4',
|
||||
'assets' => 'organization_id > 4',
|
||||
'compliance_controls' => 'organization_id > 4',
|
||||
'evidence_files' => 'organization_id > 4',
|
||||
'capa_actions' => 'ncr_id IN (SELECT id FROM non_conformities WHERE organization_id > 4)',
|
||||
'non_conformities' => 'organization_id > 4',
|
||||
'whistleblowing_timeline' => 'report_id IN (SELECT id FROM whistleblowing_reports WHERE organization_id > 4)',
|
||||
'whistleblowing_reports' => 'organization_id > 4',
|
||||
'normative_ack' => 'organization_id > 4',
|
||||
'webhook_deliveries' => 'subscription_id IN (SELECT id FROM webhook_subscriptions WHERE organization_id > 4)',
|
||||
'webhook_subscriptions' => 'organization_id > 4',
|
||||
'api_keys' => 'organization_id > 4',
|
||||
'feedback_reports' => 'organization_id > 4',
|
||||
'ai_interactions' => 'organization_id > 4',
|
||||
// audit_logs: trigger immutabile blocca DELETE — skip (dati storici accettabili)
|
||||
];
|
||||
|
||||
$deleted = 0;
|
||||
foreach ($orgTables as $table => $where) {
|
||||
try {
|
||||
$n = $pdo->exec("DELETE FROM `{$table}` WHERE {$where}");
|
||||
$deleted += ($n ?: 0);
|
||||
} catch (\Throwable $e) {
|
||||
simLog(" skip {$table}: " . $e->getMessage(), 'skip');
|
||||
}
|
||||
}
|
||||
|
||||
// Email log (no organization_id)
|
||||
try { $pdo->exec("DELETE FROM email_log WHERE recipient LIKE '%.demo%'"); } catch (\Throwable) {}
|
||||
|
||||
// User organizations + refresh tokens demo
|
||||
try { $pdo->exec('DELETE FROM user_organizations WHERE organization_id > 4'); } catch (\Throwable) {}
|
||||
try {
|
||||
$pdo->exec("DELETE rt FROM refresh_tokens rt
|
||||
JOIN users u ON rt.user_id = u.id
|
||||
WHERE u.email LIKE '%.demo%'");
|
||||
} catch (\Throwable) {}
|
||||
|
||||
// Utenti demo
|
||||
try { $n = $pdo->exec("DELETE FROM users WHERE email LIKE '%.demo%'"); $deleted += ($n ?: 0); } catch (\Throwable) {}
|
||||
|
||||
// Organizzazioni demo
|
||||
try { $n = $pdo->exec('DELETE FROM organizations WHERE id > 4'); $deleted += ($n ?: 0); } catch (\Throwable) {}
|
||||
|
||||
$pdo->exec('SET FOREIGN_KEY_CHECKS=1');
|
||||
|
||||
// Pulisce i file rate limit (register + login) per permettere re-esecuzione immediata
|
||||
$rateDir = '/tmp/nis2_ratelimit/';
|
||||
$cleared = 0;
|
||||
if (is_dir($rateDir)) {
|
||||
foreach (glob($rateDir . '*.json') ?: [] as $f) {
|
||||
if (@unlink($f)) $cleared++;
|
||||
}
|
||||
}
|
||||
|
||||
// Verifica org rimaste
|
||||
$orgCount = $pdo->query('SELECT COUNT(*) FROM organizations')->fetchColumn();
|
||||
simLog("Auto-reset completato — {$deleted} record rimossi, {$orgCount} org base mantenute" . ($cleared ? ", {$cleared} rate-limit file rimossi" : ''), 'ok');
|
||||
}
|
||||
|
||||
// ────────────────────────────────────────────────────────────────────────────
|
||||
// FASE 0 — Health Check
|
||||
// ────────────────────────────────────────────────────────────────────────────
|
||||
@ -623,6 +741,15 @@ if ($code === 200 && !empty($hRes['status'])) {
|
||||
warn("API health check degradato (HTTP $code) — continuo comunque");
|
||||
}
|
||||
|
||||
// ────────────────────────────────────────────────────────────────────────────
|
||||
// FASE 0b — Auto-Reset Dati Demo Precedenti
|
||||
// (skip se si lancia solo SIM-06 che è indipendente)
|
||||
// ────────────────────────────────────────────────────────────────────────────
|
||||
if ($SIM_FILTER !== 'SIM06') {
|
||||
simPhase(0, 'Auto-Reset Dati Demo Precedenti');
|
||||
autoResetDemo();
|
||||
}
|
||||
|
||||
// ────────────────────────────────────────────────────────────────────────────
|
||||
// SIM-01→05 — skip se si esegue solo SIM-06
|
||||
// ────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
Loading…
Reference in New Issue
Block a user