[FEAT] Simulazioni Demo + Audit Trail Certificato SHA-256
- 5 scenari reali: Onboarding, Ransomware Art.23, Data Breach Supply Chain, Whistleblowing SCADA, Audit Hash Chain Verification - simulate-nis2.php: 3 aziende (DataCore/MedClinic/EnerNet), 10 fasi, CLI+SSE - AuditService.php: hash chain SHA-256 stile lg231 (prev_hash+entry_hash) - Migration 010: prev_hash, entry_hash, severity, performed_by su audit_logs - AuditController: GET chain-verify + GET export-certified - reset-demo.sql: reset dati demo idempotente - public/simulate.html: web runner SSE con console dark-theme - Sidebar: link Simulazione Demo + Integrazioni Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 4.6
parent
b23bbc55fd
commit
874eabb6fc
@@ -264,6 +264,8 @@ $actionMap = [
|
||||
'GET:iso27001Mapping' => 'getIsoMapping',
|
||||
'GET:executiveReport' => 'executiveReport',
|
||||
'GET:export' => 'export',
|
||||
'GET:chainVerify' => 'chainVerify',
|
||||
'GET:exportCertified' => 'exportCertified',
|
||||
],
|
||||
|
||||
// ── AdminController ─────────────────────────────
|
||||
|
||||
@@ -213,6 +213,8 @@ function loadSidebar() {
|
||||
items: [
|
||||
{ name: 'Impostazioni', href: 'settings.html', icon: iconCog(), i18nKey: 'nav.settings' },
|
||||
{ name: 'Architettura', href: 'architecture.html', icon: iconCubeTransparent() },
|
||||
{ name: 'Simulazione Demo', href: 'simulate.html', icon: `<svg viewBox="0 0 20 20" fill="currentColor"><path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zM9.555 7.168A1 1 0 008 8v4a1 1 0 001.555.832l3-2a1 1 0 000-1.664l-3-2z" clip-rule="evenodd"/></svg>` },
|
||||
{ name: 'Integrazioni', href: 'integrations/index.html', icon: `<svg viewBox="0 0 20 20" fill="currentColor"><path d="M11 3a1 1 0 100 2h2.586l-6.293 6.293a1 1 0 101.414 1.414L15 6.414V9a1 1 0 102 0V4a1 1 0 00-1-1h-5z"/><path d="M5 5a2 2 0 00-2 2v8a2 2 0 002 2h8a2 2 0 002-2v-3a1 1 0 10-2 0v3H5V7h3a1 1 0 000-2H5z"/></svg>` },
|
||||
]
|
||||
}
|
||||
];
|
||||
|
||||
@@ -0,0 +1,374 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="it">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>NIS2 Agile — Simulazione Demo</title>
|
||||
<style>
|
||||
* { box-sizing: border-box; margin: 0; padding: 0; }
|
||||
:root {
|
||||
--primary: #06b6d4; --green: #22c55e; --red: #ef4444;
|
||||
--yellow: #f59e0b; --gray: #334155; --dark: #0f172a;
|
||||
--card: #1e293b; --border: #334155;
|
||||
}
|
||||
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 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); }
|
||||
|
||||
.container { max-width: 1100px; margin: 0 auto; padding: 32px 24px; display: grid; grid-template-columns: 320px 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; }
|
||||
|
||||
.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: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; }
|
||||
|
||||
.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-dot { width: 8px; height: 8px; border-radius: 50%; flex-shrink: 0; }
|
||||
.dot-cyan { background: #06b6d4; }
|
||||
.dot-purple { background: #8b5cf6; }
|
||||
.dot-green { background: #22c55e; }
|
||||
.company-item span { font-size: 0.8rem; color: #cbd5e1; }
|
||||
.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; }
|
||||
.btn-primary { background: var(--primary); color: #0f172a; }
|
||||
.btn-primary:hover:not(:disabled) { background: #0891b2; }
|
||||
.btn-primary:disabled { opacity: .5; cursor: not-allowed; }
|
||||
.btn-danger { background: #7f1d1d; color: #fca5a5; border: 1px solid #991b1b; }
|
||||
.btn-danger:hover:not(:disabled) { background: #991b1b; }
|
||||
.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); }
|
||||
.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; }
|
||||
.dot-done { background: #06b6d4; }
|
||||
.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; }
|
||||
.num-pass { color: var(--green); }
|
||||
.num-skip { color: #94a3b8; }
|
||||
.num-warn { color: var(--yellow); }
|
||||
.num-fail { color: var(--red); }
|
||||
|
||||
/* 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 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; }
|
||||
.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; }
|
||||
|
||||
/* 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; }
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.container { grid-template-columns: 1fr; }
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<div class="header">
|
||||
<div style="margin-bottom:10px;">
|
||||
<span class="badge badge-cyan">NIS2 Agile</span>
|
||||
<span class="badge badge-green">Demo Simulator v1.0</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>
|
||||
</div>
|
||||
|
||||
<div class="container">
|
||||
|
||||
<!-- Left Panel -->
|
||||
<div style="display:flex;flex-direction:column;gap:16px;">
|
||||
|
||||
<div class="panel">
|
||||
<h3>3 Aziende Demo</h3>
|
||||
<div class="company-list">
|
||||
<div class="company-item">
|
||||
<div class="company-dot dot-cyan"></div>
|
||||
<div>
|
||||
<span>DataCore S.r.l.</span>
|
||||
<small>IT/Cloud · Essential Entity · 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>
|
||||
</div>
|
||||
</div>
|
||||
<div class="company-item">
|
||||
<div class="company-dot dot-green"></div>
|
||||
<div>
|
||||
<span>EnerNet Distribuzione S.r.l.</span>
|
||||
<small>Energia · Essential/Critical · 1800 dip · Torino</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="panel">
|
||||
<h3>5 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>
|
||||
</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>
|
||||
</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>
|
||||
</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>
|
||||
</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>
|
||||
</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>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="panel">
|
||||
<h3>Controlli</h3>
|
||||
<div class="controls">
|
||||
<button id="btnRun" class="btn btn-primary" onclick="runSimulation()">▶ Avvia Simulazione</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="status-bar">
|
||||
<span class="status-dot dot-idle" id="statusDot"></span>
|
||||
<span id="statusText">In attesa</span>
|
||||
</div>
|
||||
<div class="stats-grid" id="statsGrid" style="display:none;">
|
||||
<div class="stat-box"><div class="num num-pass" id="sPass">0</div><div class="lbl">Pass</div></div>
|
||||
<div class="stat-box"><div class="num num-skip" id="sSkip">0</div><div class="lbl">Skip</div></div>
|
||||
<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>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<!-- Right panel — Console -->
|
||||
<div class="console-panel">
|
||||
<div class="console-header">
|
||||
<div class="console-dots">
|
||||
<div class="console-dot console-dot-r"></div>
|
||||
<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>
|
||||
</div>
|
||||
<div id="console">
|
||||
<div class="log-info">NIS2 Agile Demo Simulator v1.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-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>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<script>
|
||||
let evtSource = null;
|
||||
let selectedSim = 'all';
|
||||
let startTime = null;
|
||||
let timerInterval = null;
|
||||
|
||||
function selectSim(el, sim) {
|
||||
document.querySelectorAll('.sim-card').forEach(c => c.classList.remove('active'));
|
||||
el.classList.add('active');
|
||||
selectedSim = sim;
|
||||
}
|
||||
|
||||
function setStatus(text, dotClass) {
|
||||
document.getElementById('statusText').textContent = text;
|
||||
const dot = document.getElementById('statusDot');
|
||||
dot.className = 'status-dot ' + dotClass;
|
||||
}
|
||||
|
||||
function appendLog(msg, type) {
|
||||
const console = document.getElementById('console');
|
||||
const line = document.createElement('div');
|
||||
line.className = 'log-' + (type || 'info');
|
||||
line.textContent = msg;
|
||||
console.appendChild(line);
|
||||
console.scrollTop = console.scrollHeight;
|
||||
}
|
||||
|
||||
function clearConsole() {
|
||||
document.getElementById('console').innerHTML = '<div class="log-info">Console pulita.</div>';
|
||||
}
|
||||
|
||||
function updateProgress(pct) {
|
||||
document.getElementById('progressBar').style.width = pct + '%';
|
||||
}
|
||||
|
||||
function updateStats(stats) {
|
||||
if (!stats) return;
|
||||
document.getElementById('statsGrid').style.display = 'grid';
|
||||
document.getElementById('sPass').textContent = stats.pass || 0;
|
||||
document.getElementById('sSkip').textContent = stats.skip || 0;
|
||||
document.getElementById('sWarn').textContent = stats.warn || 0;
|
||||
document.getElementById('sFail').textContent = stats.fail || 0;
|
||||
}
|
||||
|
||||
function startTimer() {
|
||||
startTime = Date.now();
|
||||
timerInterval = setInterval(() => {
|
||||
const elapsed = ((Date.now() - startTime) / 1000).toFixed(1);
|
||||
document.getElementById('elapsedTime').textContent = elapsed + 's';
|
||||
}, 500);
|
||||
}
|
||||
|
||||
function stopTimer() {
|
||||
if (timerInterval) clearInterval(timerInterval);
|
||||
}
|
||||
|
||||
function runSimulation() {
|
||||
if (evtSource) { evtSource.close(); evtSource = null; }
|
||||
|
||||
const runBtn = document.getElementById('btnRun');
|
||||
const stopBtn = document.getElementById('btnStop');
|
||||
runBtn.disabled = true;
|
||||
stopBtn.disabled = false;
|
||||
|
||||
clearConsole();
|
||||
setStatus('Simulazione in corso...', 'dot-running');
|
||||
updateProgress(5);
|
||||
startTimer();
|
||||
document.getElementById('statsGrid').style.display = 'none';
|
||||
|
||||
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();
|
||||
updateProgress(100);
|
||||
updateStats(data.stats);
|
||||
appendLog('', 'info');
|
||||
appendLog('Simulazione completata.', 'done');
|
||||
document.getElementById('consoleTitle').textContent = 'NIS2 Agile Simulator — Completata';
|
||||
return;
|
||||
}
|
||||
|
||||
const type = data.t || 'info';
|
||||
const msg = data.m || '';
|
||||
|
||||
appendLog(msg, type);
|
||||
|
||||
// Stima progress
|
||||
if (type === 'phase' || msg.includes('FASE')) {
|
||||
phaseCount++;
|
||||
updateProgress(Math.min(5 + phaseCount * 8, 95));
|
||||
}
|
||||
};
|
||||
|
||||
evtSource.onerror = () => {
|
||||
evtSource.close();
|
||||
runBtn.disabled = false;
|
||||
stopBtn.disabled = true;
|
||||
setStatus('Errore connessione', 'dot-error');
|
||||
stopTimer();
|
||||
appendLog('Errore SSE — connessione interrotta.', 'error');
|
||||
};
|
||||
}
|
||||
|
||||
function stopSimulation() {
|
||||
if (evtSource) { evtSource.close(); evtSource = null; }
|
||||
document.getElementById('btnRun').disabled = false;
|
||||
document.getElementById('btnStop').disabled = true;
|
||||
setStatus('Interrotta', 'dot-error');
|
||||
stopTimer();
|
||||
appendLog('Simulazione interrotta manualmente.', 'warn');
|
||||
}
|
||||
|
||||
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'));
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user