diff --git a/public/simulate.html b/public/simulate.html index 3c604a5..66d54f0 100644 --- a/public/simulate.html +++ b/public/simulate.html @@ -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; } }
-
+
NIS2 Agile - Demo Simulator v1.0 + Demo Simulator v2.0 + Auto-Reset

Simulazione Demo Realistica

-

Genera dati demo NIS2 realistici tramite API reali — 3 aziende, 5 scenari, audit trail certificato SHA-256

+

3 aziende · 6 scenari · reset automatico · audit trail SHA-256 · dati creati via API reali

-
+
+

3 Aziende Demo

@@ -121,14 +160,14 @@
DataCore S.r.l. - IT/Cloud · Essential Entity · 320 dip · Milano + IT/Cloud · Essential · 320 dip · Milano
MedClinic Italia S.p.A. - Sanità · Important Entity · 750 dip · Roma + Sanità · Important · 750 dip · Roma
@@ -141,50 +180,60 @@
+
-

5 Scenari Reali

+

6 Scenari Reali

COMPLETA

Tutti gli scenari

-

Esegue tutti e 5 gli scenari in sequenza: onboarding, incidenti, data breach, whistleblowing, audit chain.

+

SIM-01→06 in sequenza: onboarding, incidenti, data breach, whistleblowing, audit chain, B2B.

SIM-01

Onboarding + Gap Assessment

-

Registra le 3 aziende, classifica NIS2 (Essential/Important), esegue assessment 80 domande Art.21.

+

Registra 3 aziende, classifica NIS2, esegue assessment 80 domande Art.21.

SIM-02

Ransomware Art.23 DataCore

-

Attacco ransomware su infrastruttura cloud. Timeline 24h/72h/30d con notifiche ACN e CSIRT.

+

Attacco critico su cloud. Timeline 24h/72h/30d con notifiche CSIRT.

SIM-03

Data Breach Supply Chain

-

Fornitore LIS compromesso, esfiltrazione 2.340 pazienti. GDPR Art.33 + NIS2 Art.23 in parallelo.

+

Fornitore LIS compromesso. GDPR Art.33 + NIS2 Art.23 in parallelo.

SIM-04

Whistleblowing SCADA EnerNet

-

Segnalazione anonima accesso non autorizzato SCADA. Token tracking, investigazione, chiusura.

+

Segnalazione anonima accesso non autorizzato SCADA. Token tracking.

SIM-05

Audit Trail Hash Chain

-

Verifica integrità SHA-256 hash chain per le 3 org. Export certificato NIS2 + ISO 27001.

+

Verifica integrità SHA-256 hash chain per le 3 org. Export certificato.

+
+
+
SIM-06
+

B2B License Provisioning

+

Invite token → org + user + API key creati atomicamente. SSO JWT.

+

Controlli

- + -
-
+
Reset automatico dati demo precedenti all'avvio
+
In attesa @@ -195,6 +244,12 @@
0
Warn
0
Fail
+ + +
+
Ultimi run
+
+
@@ -207,55 +262,91 @@
- NIS2 Agile Simulator — Console - + NIS2 Agile Simulator v2.0 — Console +
-
NIS2 Agile Demo Simulator v1.0 — Pronto
+
NIS2 Agile Demo Simulator v2.0 — Pronto
Seleziona uno scenario e premi "Avvia Simulazione".
-
Scenari disponibili:
+
Scenari disponibili:
SIM-01 Onboarding + Gap Assessment 80 domande
SIM-02 Incidente Ransomware Art.23 (24h/72h/30d)
SIM-03 Data Breach Supply Chain + GDPR
SIM-04 Whistleblowing anonimo SCADA
SIM-05 Audit Trail Hash Chain Verification
-
Tutti i dati vengono creati tramite API reali (nessun INSERT SQL diretto).
+
SIM-06 B2B License Provisioning (invite token)
+
↺ Ogni run pulisce automaticamente i dati demo precedenti.
+
Tutti i dati vengono creati tramite API reali (nessun INSERT SQL diretto).
diff --git a/simulate-nis2.php b/simulate-nis2.php index fc4a777..83bf325 100644 --- a/simulate-nis2.php +++ b/simulate-nis2.php @@ -606,6 +606,95 @@ 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'); + + // Verifica org rimaste + $orgCount = $pdo->query('SELECT COUNT(*) FROM organizations')->fetchColumn(); + simLog("Auto-reset completato — {$deleted} record rimossi, {$orgCount} org base mantenute", 'ok'); +} + // ──────────────────────────────────────────────────────────────────────────── // FASE 0 — Health Check // ──────────────────────────────────────────────────────────────────────────── @@ -623,6 +712,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 // ────────────────────────────────────────────────────────────────────────────