From 0e2774d1a60267e3572d2beff963b480936c4f0f Mon Sep 17 00:00:00 2001 From: DevEnv nis2-agile Date: Tue, 10 Mar 2026 16:06:46 +0100 Subject: [PATCH] [FIX] BigSim: sector enum (digital_infra/water), VAT skip, rate-limit clear fix (md5 filenames) Co-Authored-By: Claude Sonnet 4.6 --- simulate-nis2-big.php | 2199 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 2199 insertions(+) create mode 100644 simulate-nis2-big.php diff --git a/simulate-nis2-big.php b/simulate-nis2-big.php new file mode 100644 index 0000000..57b6575 --- /dev/null +++ b/simulate-nis2-big.php @@ -0,0 +1,2199 @@ + [], + 'orgs' => [], + 'users' => [], + 'assessments'=> [], + 'apiKey' => null, + 'webhookSubId' => null, + 'invToken' => null, + 'stats' => ['pass' => 0, 'skip' => 0, 'fail' => 0, 'warn' => 0], + 'sim_start' => microtime(true), +]; + +// ── Output helpers ─────────────────────────────────────────────────────────── + +function simLog(string $msg, string $type = 'info'): void +{ + static $lastHeartbeat = 0; + $ts = date('H:i:s'); + if (IS_CLI) { + $prefix = [ + 'phase' => "\033[34m══\033[0m", + 'ok' => "\033[32m ✓\033[0m", + 'skip' => "\033[90m →\033[0m", + 'warn' => "\033[33m ⚠\033[0m", + 'error' => "\033[31m ✗\033[0m", + 'email' => "\033[36m ✉\033[0m", + 'info' => ' ', + ][$type] ?? ' '; + echo "[$ts] {$prefix} {$msg}\n"; + flush(); + } else { + $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(); + } +} + +function simDone(array $stats): void +{ + $elapsed = round(microtime(true) - $GLOBALS['S']['sim_start'], 1); + $msg = "Simulazione completata in {$elapsed}s — ✓{$stats['pass']} →{$stats['skip']} ⚠{$stats['warn']} ✗{$stats['fail']}"; + simLog($msg, IS_CLI ? 'phase' : 'info'); + if (IS_WEB) { + echo 'data: ' . json_encode(['t' => 'done', 'stats' => $stats]) . "\n\n"; + flush(); + } +} + +function simPhase(int $n, string $title): void +{ + if (IS_CLI) { + $line = str_pad("FASE {$n}: {$title}", 60); + echo "\n\033[34m╔════════════════════════════════════════════════════════════╗\033[0m\n"; + echo "\033[34m║ {$line} ║\033[0m\n"; + echo "\033[34m╚════════════════════════════════════════════════════════════╝\033[0m\n"; + } else { + simLog("━━ FASE {$n}: {$title}", 'phase'); + } +} + +function ok(string $msg): void { global $S; $S['stats']['pass']++; simLog($msg, 'ok'); } +function skip(string $msg): void { global $S; $S['stats']['skip']++; simLog($msg, 'skip'); } +function fail(string $msg): void { global $S; $S['stats']['fail']++; simLog($msg, 'error'); } +function warn(string $msg): void { global $S; $S['stats']['warn']++; simLog($msg, 'warn'); } +function info(string $msg): void { simLog($msg, 'info'); } + +// ── API client ─────────────────────────────────────────────────────────────── + +function api(string $method, string $path, ?array $body = null, ?string $jwt = null, ?int $orgId = null, ?string $apiKey = null): array +{ + $url = API_BASE . $path; + $ch = curl_init($url); + + $headers = ['Content-Type: application/json', 'Accept: application/json']; + if ($jwt) $headers[] = 'Authorization: Bearer ' . $jwt; + if ($orgId) $headers[] = 'X-Organization-Id: ' . $orgId; + if ($apiKey) $headers[] = 'X-API-Key: ' . $apiKey; + + curl_setopt_array($ch, [ + CURLOPT_RETURNTRANSFER => true, + CURLOPT_TIMEOUT => 30, + CURLOPT_HTTPHEADER => $headers, + CURLOPT_CUSTOMREQUEST => strtoupper($method), + CURLOPT_SSL_VERIFYPEER => false, + CURLOPT_SSL_VERIFYHOST => 0, + ]); + if ($body !== null) { + curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($body, JSON_UNESCAPED_UNICODE)); + } + + $raw = curl_exec($ch); + $code = curl_getinfo($ch, CURLINFO_HTTP_CODE); + curl_close($ch); + + if ($raw === false) return ['success' => false, 'error' => 'cURL fallito: ' . $url, '_http' => 0]; + $res = json_decode($raw, true) ?? ['success' => false, 'error' => 'JSON invalido', '_raw' => substr($raw, 0, 200)]; + $res['_http'] = $code; + return $res; +} + +function apiOk(array $res, string $ctx = ''): bool +{ + if (!empty($res['success'])) return true; + $err = $res['error'] ?? ($res['message'] ?? 'errore'); + warn("API Error" . ($ctx ? " [$ctx]" : '') . ": $err (HTTP {$res['_http']})"); + return false; +} + +// ── .env reader ────────────────────────────────────────────────────────────── + +function readEnvValue(string $key, string $default = ''): string +{ + $envFile = __DIR__ . '/.env'; + if (!is_file($envFile)) return $default; + foreach (file($envFile, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES) as $line) { + if (str_starts_with(trim($line), '#') || !str_contains($line, '=')) continue; + [$k, $v] = explode('=', $line, 2); + if (trim($k) === $key) return trim($v); + } + return $default; +} + +// ── DB helpers ─────────────────────────────────────────────────────────────── + +function getDbPdo(): ?PDO +{ + static $pdo = null; + if ($pdo) return $pdo; + $envFile = __DIR__ . '/.env'; + if (!is_file($envFile)) return null; + $env = []; + foreach (file($envFile, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES) as $line) { + if (str_starts_with(trim($line), '#') || !str_contains($line, '=')) continue; + [$k, $v] = explode('=', $line, 2); + $env[trim($k)] = trim($v); + } + $dbName = $env['DB_NAME'] ?? 'nis2_agile_db'; + $dbHost = $env['DB_HOST'] ?? '127.0.0.1'; + $dbPort = $env['DB_PORT'] ?? '3306'; + foreach ([['root',''], [$env['DB_USER']??'nis2_user', $env['DB_PASS']??'']] as [$u,$p]) { + try { + $pdo = new PDO("mysql:host={$dbHost};port={$dbPort};dbname={$dbName};charset=utf8mb4", $u, $p, [PDO::ATTR_ERRMODE=>PDO::ERRMODE_EXCEPTION]); + return $pdo; + } catch (\Throwable) { $pdo = null; } + } + return null; +} + +/** Pulisce tutti i file rate-limit (i file sono md5(key).json, non leggibili per pattern) */ +function clearSimRateLimit(): void +{ + $rateDir = '/tmp/nis2_ratelimit/'; + if (!is_dir($rateDir)) return; + foreach (glob($rateDir . '*.json') ?: [] as $f) { + @unlink($f); + } +} + +function dbSeedUser(string $fullName, string $email, string $password, string $role): bool +{ + $pdo = getDbPdo(); + if (!$pdo) return false; + try { + $hash = password_hash($password, PASSWORD_BCRYPT, ['cost' => 10]); + $pdo->prepare( + 'INSERT INTO users (email, password_hash, full_name, role, is_active) + VALUES (?,?,?,?,1) + ON DUPLICATE KEY UPDATE password_hash=VALUES(password_hash), full_name=VALUES(full_name), role=VALUES(role), is_active=1' + )->execute([$email, $hash, $fullName, $role]); + return true; + } catch (\Throwable) { return false; } +} + +function dbGetUserId(string $email): ?int +{ + $pdo = getDbPdo(); + if (!$pdo) return null; + try { + $stmt = $pdo->prepare('SELECT id FROM users WHERE email=? LIMIT 1'); + $stmt->execute([$email]); + $row = $stmt->fetch(PDO::FETCH_ASSOC); + return $row ? (int)$row['id'] : null; + } catch (\Throwable) { return null; } +} + +function dbAddToOrg(int $userId, int $orgId, string $role): void +{ + $pdo = getDbPdo(); + if (!$pdo) return; + try { + $pdo->prepare('INSERT IGNORE INTO user_organizations (user_id, organization_id, role) VALUES (?,?,?)')->execute([$userId, $orgId, $role]); + } catch (\Throwable) {} +} + +// ── User/Org helpers ───────────────────────────────────────────────────────── + +function ensureUser(string $firstName, string $lastName, string $email, string $password, string $role = 'org_admin'): ?string +{ + global $S; + if (isset($S['users'][$email]['jwt'])) { + skip("Utente $email già loggato"); + return $S['users'][$email]['jwt']; + } + $fullName = trim("$firstName $lastName"); + $seeded = dbSeedUser($fullName, $email, $password, $role); + $uid = dbGetUserId($email); + if ($uid) $S['users'][$email] = ['id' => $uid, 'jwt' => null]; + + clearSimRateLimit(); // evita lockout da rate-limit durante simulazione + $loginRes = api('POST', '/auth/login', ['email' => $email, 'password' => $password]); + if (!empty($loginRes['data']['access_token'])) { + $jwt = $loginRes['data']['access_token']; + $S['users'][$email]['jwt'] = $jwt; + if (empty($S['users'][$email]['id'])) $S['users'][$email]['id'] = $loginRes['data']['user']['id'] ?? null; + ok($seeded ? "Registrato: $firstName $lastName <$email>" : "Login: $email"); + return $jwt; + } + $regRes = api('POST', '/auth/register', ['full_name' => $fullName, 'email' => $email, 'password' => $password, 'role' => $role]); + if (!empty($regRes['data']['access_token'])) { + $jwt = $regRes['data']['access_token']; + $S['users'][$email] = ['jwt' => $jwt, 'id' => $regRes['data']['user']['id'] ?? $uid]; + ok("Registrato (API): $firstName $lastName <$email>"); + return $jwt; + } + $regMsg = strtolower($regRes['message'] ?? ''); + if (str_contains($regMsg, 'registrat') || str_contains($regMsg, 'esiste') || $regRes['_http'] === 409) { + $retry = api('POST', '/auth/login', ['email' => $email, 'password' => $password]); + if (!empty($retry['data']['access_token'])) { + $jwt = $retry['data']['access_token']; + $S['users'][$email]['jwt'] = $jwt; + ok("Login (retry): $email"); + return $jwt; + } + } + fail("Registrazione fallita per $email: " . ($regRes['message'] ?? 'errore')); + return null; +} + +function ensureOrg(string $jwt, array $data): ?int +{ + global $S; + $name = $data['name']; + $listRes = api('GET', '/organizations/list', null, $jwt); + if (!empty($listRes['data'])) { + foreach ($listRes['data'] as $org) { + if (($org['name'] ?? '') === $name) { + $id = (int) $org['id']; + skip("Org già esistente: $name (id=$id)"); + return $id; + } + } + } + // rimuovi vat_number dalla creazione (Luhn check sui numeri demo fallirebbe) + $createData = array_diff_key($data, array_flip(['vat_number', 'legal_form', 'ateco_code', 'province', 'region'])); + $createRes = api('POST', '/organizations/create', $createData, $jwt); + if (!empty($createRes['data']['id'])) { + $id = (int) $createRes['data']['id']; + ok("Org creata: $name (id=$id)"); + return $id; + } + fail("Org creazione fallita: $name — " . ($createRes['error'] ?? 'errore')); + return null; +} + +function classifyOrg(string $jwt, int $orgId, array $data): void +{ + $res = api('POST', '/organizations/classify', $data, $jwt, $orgId); + if (apiOk($res, 'classify')) { + $entityType = $res['data']['entity_type'] ?? ($data['nis2_type'] ?? '?'); + ok("Classificazione NIS2: {$entityType} — Settore: {$data['sector']}"); + } +} + +function runAssessment(string $jwt, int $orgId, string $slug, string $orgName, array $responses): ?int +{ + global $S; + $createRes = api('POST', '/assessments/create', [ + 'title' => "Gap Assessment NIS2 — $orgName", + 'description' => 'Assessment automatico da Big Simulation demo', + ], $jwt, $orgId); + if (empty($createRes['data']['id'])) { + warn("Assessment create fallito per $orgName"); + return null; + } + $assessId = (int) $createRes['data']['id']; + $S['assessments'][$slug] = $assessId; + ok("Assessment creato #{$assessId} per $orgName"); + + $answered = 0; + foreach ($responses as $qCode => $val) { + $res = api('POST', "/assessments/{$assessId}/respond", [ + 'question_code' => $qCode, + 'response_value' => $val, + 'notes' => null, + ], $jwt, $orgId); + if (!empty($res['success'])) $answered++; + } + ok("Risposte inviate: $answered/".count($responses)." per $orgName"); + + $completeRes = api('POST', "/assessments/{$assessId}/complete", [], $jwt, $orgId); + if (apiOk($completeRes, 'assessment.complete')) { + $score = $completeRes['data']['compliance_score'] ?? 'N/A'; + ok("Assessment completato — Score: $score%"); + } + return $assessId; +} + +function createRisk(string $jwt, int $orgId, array $data): ?int +{ + $res = api('POST', '/risks/create', $data, $jwt, $orgId); + if (!empty($res['data']['id'])) { + $id = (int) $res['data']['id']; + ok("Rischio [{$data['risk_level']}]: {$data['title']} #{$id}"); + return $id; + } + warn("Rischio fallito: {$data['title']} — " . ($res['error'] ?? '')); + return null; +} + +function createIncident(string $jwt, int $orgId, array $data): ?int +{ + $res = api('POST', '/incidents/create', $data, $jwt, $orgId); + if (!empty($res['data']['id'])) { + $id = (int) $res['data']['id']; + ok("Incidente creato [{$data['severity']}]: {$data['title']} #{$id}"); + return $id; + } + warn("Incidente fallito: {$data['title']} — " . ($res['error'] ?? '')); + return null; +} + +function createPolicy(string $jwt, int $orgId, array $data): ?int +{ + $res = api('POST', '/policies/create', $data, $jwt, $orgId); + if (!empty($res['data']['id'])) { + $id = (int) $res['data']['id']; + ok("Policy creata: {$data['title']} #{$id}"); + return $id; + } + warn("Policy fallita: {$data['title']} — " . ($res['error'] ?? '')); + return null; +} + +function createSupplier(string $jwt, int $orgId, array $data): ?int +{ + $res = api('POST', '/supply-chain/create', $data, $jwt, $orgId); + if (!empty($res['data']['id'])) { + $id = (int) $res['data']['id']; + ok("Fornitore creato: {$data['name']} #{$id}"); + return $id; + } + warn("Fornitore fallito: {$data['name']} — " . ($res['error'] ?? '')); + return null; +} + +function createAsset(string $jwt, int $orgId, array $data): ?int +{ + $res = api('POST', '/assets/create', $data, $jwt, $orgId); + if (!empty($res['data']['id'])) { + $id = (int) $res['data']['id']; + ok("Asset creato [{$data['criticality']}]: {$data['name']} #{$id}"); + return $id; + } + warn("Asset fallito: {$data['name']} — " . ($res['error'] ?? '')); + return null; +} + +function createTrainingCourse(string $jwt, int $orgId, array $data): ?int +{ + $res = api('POST', '/training/courses', $data, $jwt, $orgId); + if (!empty($res['data']['id'])) { + $id = (int) $res['data']['id']; + ok("Corso creato: {$data['title']} #{$id}"); + return $id; + } + warn("Corso fallito: {$data['title']} — " . ($res['error'] ?? '')); + return null; +} + +function checkAuditChain(string $jwt, int $orgId, string $label): void +{ + $res = api('GET', '/audit/chain-verify', null, $jwt, $orgId); + if (!apiOk($res, "chain-verify $label")) return; + $d = $res['data'] ?? []; + $valid = ($d['valid'] ?? false) ? 'INTEGRA' : 'PRESENTE'; + $total = $d['total'] ?? 0; + $hashed = $d['hashed'] ?? 0; + simLog("Audit chain [{$label}] — {$valid} — {$hashed}/{$total} record", 'ok'); + $GLOBALS['S']['stats']['pass']++; +} + +// ── Assessment responses per profilo ───────────────────────────────────────── +// Profili: big / essential / important / voluntary +// big: 40% impl, 30% partial, 20% not_impl, 10% not_applicable +// essential: 30% impl, 40% partial, 25% not_impl, 5% not_applicable +// important: 25% impl, 35% partial, 35% not_impl, 5% not_applicable +// voluntary: 20% impl, 30% partial, 40% not_impl, 10% not_applicable + +function getBigAssessmentResponses(string $profile): array +{ + $cats = ['RM', 'IH', 'BC', 'SC', 'SD', 'EA', 'TR', 'CR', 'AM', 'MA']; + $codes = []; + foreach ($cats as $cat) { + for ($i = 1; $i <= 8; $i++) { + $codes[] = sprintf('%s%02d', $cat, $i); + } + } + // 80 codici totali + $dist = match ($profile) { + 'big' => ['implemented' => 32, 'partial' => 24, 'not_implemented' => 16, 'not_applicable' => 8], + 'essential' => ['implemented' => 24, 'partial' => 32, 'not_implemented' => 20, 'not_applicable' => 4], + 'important' => ['implemented' => 20, 'partial' => 28, 'not_implemented' => 28, 'not_applicable' => 4], + default => ['implemented' => 16, 'partial' => 24, 'not_implemented' => 32, 'not_applicable' => 8], + }; + $vals = []; + foreach ($dist as $v => $n) { + for ($i = 0; $i < $n; $i++) $vals[] = $v; + } + $result = []; + foreach ($codes as $i => $code) { + $result[$code] = $vals[$i] ?? 'not_implemented'; + } + return $result; +} + +// ── Auto-Reset ─────────────────────────────────────────────────────────────── + +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'); + + $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', + ]; + + $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'); + } + } + + try { $pdo->exec("DELETE FROM email_log WHERE recipient LIKE '%.demo%'"); } catch (\Throwable) {} + 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) {} + try { $n = $pdo->exec("DELETE FROM users WHERE email LIKE '%.demo%'"); $deleted += ($n ?: 0); } catch (\Throwable) {} + try { $n = $pdo->exec('DELETE FROM organizations WHERE id > 4'); $deleted += ($n ?: 0); } catch (\Throwable) {} + + $pdo->exec('SET FOREIGN_KEY_CHECKS=1'); + + $rateDir = '/tmp/nis2_ratelimit/'; + $cleared = 0; + if (is_dir($rateDir)) { + foreach (glob($rateDir . '*.json') ?: [] as $f) { + if (@unlink($f)) $cleared++; + } + } + + $orgCount = $pdo->query('SELECT COUNT(*) FROM organizations')->fetchColumn(); + simLog("Auto-reset completato — {$deleted} record rimossi, {$orgCount} org base mantenute" . ($cleared ? ", {$cleared} rate-limit file" : ''), 'ok'); +} + +// ── Policy category map ─────────────────────────────────────────────────────── +function mapPolicyCategory(string $type): string +{ + return [ + 'cryptography' => 'encryption', + 'data_protection' => 'information_security', + 'risk_management' => 'vulnerability_management', + 'supply_chain_security' => 'supply_chain', + 'fraud_prevention' => 'access_control', + 'network_security' => 'network_security', + ][$type] ?? $type; +} + +// ── Risk level helper ───────────────────────────────────────────────────────── +function riskLevel(int $likelihood, int $impact): string +{ + $score = $likelihood * $impact; + return match(true) { + $score >= 20 => 'critical', + $score >= 12 => 'high', + $score >= 6 => 'medium', + default => 'low', + }; +} + +// ═══════════════════════════════════════════════════════════════════════════════ +// DEFINIZIONE 10 AZIENDE DEMO +// ═══════════════════════════════════════════════════════════════════════════════ + +$COMPANIES = [ + +// ── A) InfraTech Italia S.p.A. — BIG COMPANY ───────────────────────────────── +'infratech' => [ + 'name' => 'InfraTech Italia S.p.A.', + 'legal_form' => 'S.p.A.', + 'vat_number' => '12345678901', + 'ateco_code' => '61.10', + 'sector' => 'digital_infra', + 'nis2_type' => 'essential', + 'employees' => 5800, + 'annual_turnover' => 820000000, + 'city' => 'Milano', 'province' => 'MI', 'region' => 'Lombardia', + 'profile' => 'big', + 'users' => [ + ['first'=>'Dott. Marco', 'last'=>'Visconti', 'email'=>'ceo@infratech-spa.demo', 'role'=>'org_admin'], + ['first'=>'Ing. Serena', 'last'=>'Colombo', 'email'=>'ciso@infratech-spa.demo', 'role'=>'compliance_manager'], + ['first'=>'Dott. Giorgio', 'last'=>'Ferri', 'email'=>'board@infratech-spa.demo', 'role'=>'board_member'], + ['first'=>'Elena', 'last'=>'Mazzi', 'email'=>'auditor@infratech-spa.demo','role'=>'auditor'], + ['first'=>'Luca', 'last'=>'Pavan', 'email'=>'staff@infratech-spa.demo', 'role'=>'employee'], + ], + 'risks' => [ + ['title'=>'Ransomware su backbone nazionale', 'category'=>'cyber', 'likelihood'=>5,'impact'=>5,'nis2_article'=>'21.2.b'], + ['title'=>'Compromissione OT/SCADA nodi rete', 'category'=>'cyber', 'likelihood'=>4,'impact'=>5,'nis2_article'=>'21.2.b'], + ['title'=>'Attacco DDoS volumetrico su DNS primario', 'category'=>'operational', 'likelihood'=>5,'impact'=>4,'nis2_article'=>'21.2.c'], + ['title'=>'Zero-day su firewall perimetrale (CVE critica)', 'category'=>'cyber', 'likelihood'=>3,'impact'=>5,'nis2_article'=>'21.2.e'], + ['title'=>'Insider threat: dipendente con accesso privilegiato','category'=>'human', 'likelihood'=>3,'impact'=>4,'nis2_article'=>'21.2.i'], + ['title'=>'Supply chain software — fornitore firmware compromesso','category'=>'supply_chain','likelihood'=>2,'impact'=>5,'nis2_article'=>'21.2.d'], + ['title'=>'Data breach 2M utenti finali', 'category'=>'compliance', 'likelihood'=>3,'impact'=>5,'nis2_article'=>'21.2.b'], + ['title'=>'Failure Business Continuity — DC primario fuori', 'category'=>'operational', 'likelihood'=>2,'impact'=>5,'nis2_article'=>'21.2.c'], + ['title'=>'Phishing dirigenti (CEO Fraud / BEC)', 'category'=>'human', 'likelihood'=>4,'impact'=>3,'nis2_article'=>'21.2.g'], + ['title'=>'Furto certificati TLS wildcard', 'category'=>'cyber', 'likelihood'=>2,'impact'=>4,'nis2_article'=>'21.2.h'], + ], + 'policies' => [ + ['title'=>'Politica Gestione Incidenti — Art.21.2.b NIS2', 'type'=>'incident_response', 'nis2_article'=>'21.2.b'], + ['title'=>'Politica Controllo Accessi e MFA — Art.21.2.j NIS2', 'type'=>'access_control', 'nis2_article'=>'21.2.j'], + ['title'=>'Politica Crittografia e PKI — Art.21.2.h NIS2', 'type'=>'cryptography', 'nis2_article'=>'21.2.h'], + ['title'=>'Politica Business Continuity DC — Art.21.2.c NIS2', 'type'=>'business_continuity','nis2_article'=>'21.2.c'], + ['title'=>'Politica Supply Chain Sicurezza — Art.21.2.d NIS2', 'type'=>'supply_chain_security','nis2_article'=>'21.2.d'], + ['title'=>'Politica Sicurezza Reti e Sistemi — Art.21.2.a NIS2','type'=>'network_security', 'nis2_article'=>'21.2.a'], + ], + 'suppliers' => [ + ['name'=>'TelecomHW S.p.A.', 'service_type'=>'hardware_vendor', 'risk_level'=>'critical','critical'=>1], + ['name'=>'SecurityOps S.r.l.', 'service_type'=>'security_service', 'risk_level'=>'high', 'critical'=>1], + ['name'=>'CloudBackbone EU', 'service_type'=>'cloud_provider', 'risk_level'=>'critical','critical'=>1], + ['name'=>'SoftwareCore AG', 'service_type'=>'software_vendor', 'risk_level'=>'high', 'critical'=>1], + ['name'=>'LogisticIT S.r.l.', 'service_type'=>'managed_service', 'risk_level'=>'medium', 'critical'=>0], + ['name'=>'CertSecurity S.r.l.', 'service_type'=>'consulting', 'risk_level'=>'low', 'critical'=>0], + ], + 'assets' => [ + ['name'=>'Backbone fibra ottica nazionale', 'type'=>'network', 'criticality'=>'critical'], + ['name'=>'Data Center Milano Nord', 'type'=>'datacenter', 'criticality'=>'critical'], + ['name'=>'Sistema OT/SCADA gestione rete', 'type'=>'ot_system', 'criticality'=>'critical'], + ['name'=>'Piattaforma DNS primaria/secondaria', 'type'=>'server', 'criticality'=>'high'], + ['name'=>'VPN corporate + MFA infrastruttura', 'type'=>'software', 'criticality'=>'high'], + ], + 'training' => [ + ['title'=>'NIS2 Sicurezza Infrastrutture Critiche', 'mandatory'=>true, 'duration_hours'=>8], + ['title'=>'Incident Response & Art.23 Procedure', 'mandatory'=>true, 'duration_hours'=>4], + ], +], + +// ── B) MedSalute Network S.p.A. ────────────────────────────────────────────── +'medsalute' => [ + 'name' => 'MedSalute Network S.p.A.', + 'legal_form' => 'S.p.A.', + 'vat_number' => '11223344556', + 'ateco_code' => '86.10', + 'sector' => 'health', + 'nis2_type' => 'essential', + 'employees' => 2200, + 'annual_turnover' => 95000000, + 'city' => 'Roma', 'province' => 'RM', 'region' => 'Lazio', + 'profile' => 'essential', + 'users' => [ + ['first'=>'Dott. Antonio','last'=>'Russo', 'email'=>'admin@medsalute-spa.demo', 'role'=>'org_admin'], + ['first'=>'Dott.ssa Marta','last'=>'De Luca','email'=>'ciso@medsalute-spa.demo', 'role'=>'compliance_manager'], + ['first'=>'Prof. Luigi', 'last'=>'Farina', 'email'=>'auditor@medsalute-spa.demo', 'role'=>'auditor'], + ], + 'risks' => [ + ['title'=>'Violazione privacy pazienti (GDPR+NIS2)', 'category'=>'compliance', 'likelihood'=>4,'impact'=>5,'nis2_article'=>'21.2.b'], + ['title'=>'Fermo HIS (Hospital Information System)', 'category'=>'operational','likelihood'=>3,'impact'=>5,'nis2_article'=>'21.2.c'], + ['title'=>'Supply chain LIS — laboratorio analisi compromesso','category'=>'supply_chain','likelihood'=>3,'impact'=>5,'nis2_article'=>'21.2.d'], + ['title'=>'Phishing staff medico (credential harvesting)','category'=>'human', 'likelihood'=>5,'impact'=>3,'nis2_article'=>'21.2.g'], + ['title'=>'IoT medici non patchati (infusori, monitor)','category'=>'cyber', 'likelihood'=>3,'impact'=>4,'nis2_article'=>'21.2.e'], + ['title'=>'GDPR breach — accesso abusivo cartelle VIP', 'category'=>'cyber', 'likelihood'=>4,'impact'=>4,'nis2_article'=>'21.2.i'], + ], + 'policies' => [ + ['title'=>'Politica Protezione Dati Sanitari — Art.21.2.b NIS2+GDPR','type'=>'data_protection', 'nis2_article'=>'21.2.b'], + ['title'=>'Politica Business Continuity Sistemi Clinici', 'type'=>'business_continuity','nis2_article'=>'21.2.c'], + ['title'=>'Politica Controllo Accessi Cartelle Cliniche', 'type'=>'access_control', 'nis2_article'=>'21.2.j'], + ], + 'suppliers' => [ + ['name'=>'HealthSoft LIS S.r.l.','service_type'=>'software_vendor','risk_level'=>'critical','critical'=>1], + ['name'=>'MedDevice HIS S.p.A.', 'service_type'=>'hardware_vendor','risk_level'=>'critical','critical'=>1], + ['name'=>'CloudBackup Med', 'service_type'=>'cloud_provider', 'risk_level'=>'medium', 'critical'=>0], + ['name'=>'IoT Medical EU S.r.l.', 'service_type'=>'hardware_vendor','risk_level'=>'high', 'critical'=>1], + ], + 'assets' => [ + ['name'=>'HIS Centrale (Hospital Information System)','type'=>'software', 'criticality'=>'critical'], + ['name'=>'Server Database Pazienti', 'type'=>'server', 'criticality'=>'critical'], + ['name'=>'Rete WiFi Clinica (IoT medici)', 'type'=>'network', 'criticality'=>'high'], + ], + 'training' => [ + ['title'=>'Privacy e GDPR in Ambito Sanitario','mandatory'=>true,'duration_hours'=>4], + ], +], + +// ── C) DistribuzionePlus S.p.A. ─────────────────────────────────────────────── +'distribuzione' => [ + 'name' => 'DistribuzionePlus S.p.A.', + 'legal_form' => 'S.p.A.', + 'vat_number' => '22334455667', + 'ateco_code' => '35.13', + 'sector' => 'energy', + 'nis2_type' => 'essential', + 'employees' => 1400, + 'annual_turnover' => 210000000, + 'city' => 'Torino', 'province' => 'TO', 'region' => 'Piemonte', + 'profile' => 'essential', + 'users' => [ + ['first'=>'Ing. Paolo', 'last'=>'Mancini', 'email'=>'admin@distribuzione-spa.demo', 'role'=>'org_admin'], + ['first'=>'Dott. Chiara', 'last'=>'Venturi', 'email'=>'ciso@distribuzione-spa.demo', 'role'=>'compliance_manager'], + ['first'=>'Ing. Stefano', 'last'=>'Barbieri', 'email'=>'board@distribuzione-spa.demo', 'role'=>'board_member'], + ], + 'risks' => [ + ['title'=>'SCADA compromise — distribuzione energia', 'category'=>'cyber', 'likelihood'=>3,'impact'=>5,'nis2_article'=>'21.2.b'], + ['title'=>'Blackout selettivo doloso su RTU', 'category'=>'operational','likelihood'=>2,'impact'=>5,'nis2_article'=>'21.2.c'], + ['title'=>'Supply chain firmware — contatori intelligenti', 'category'=>'supply_chain','likelihood'=>2,'impact'=>5,'nis2_article'=>'21.2.d'], + ['title'=>'DDoS su sistemi RTU sottostazioni', 'category'=>'cyber', 'likelihood'=>4,'impact'=>4,'nis2_article'=>'21.2.c'], + ['title'=>'Insider threat tecnico campo SCADA', 'category'=>'human', 'likelihood'=>2,'impact'=>5,'nis2_article'=>'21.2.i'], + ['title'=>'Physical breach — sottostazione non presidiata', 'category'=>'physical', 'likelihood'=>3,'impact'=>4,'nis2_article'=>'21.2.f'], + ], + 'policies' => [ + ['title'=>'Politica Sicurezza OT/SCADA — Art.21.2.a NIS2', 'type'=>'risk_management', 'nis2_article'=>'21.2.a'], + ['title'=>'Politica Business Continuity Rete Elettrica', 'type'=>'business_continuity','nis2_article'=>'21.2.c'], + ['title'=>'Politica Sicurezza Supply Chain Energetica', 'type'=>'supply_chain_security','nis2_article'=>'21.2.d'], + ], + 'suppliers' => [ + ['name'=>'SCADA Vendor Italia S.r.l.','service_type'=>'ot_vendor', 'risk_level'=>'critical','critical'=>1], + ['name'=>'TelcoEnergy S.p.A.', 'service_type'=>'network_provider','risk_level'=>'high', 'critical'=>1], + ['name'=>'BackupPower EU', 'service_type'=>'cloud_provider', 'risk_level'=>'medium', 'critical'=>0], + ['name'=>'CyberSec Energia S.r.l.', 'service_type'=>'consulting', 'risk_level'=>'low', 'critical'=>0], + ], + 'assets' => [ + ['name'=>'Sistema SCADA distribuzione principale','type'=>'ot_system','criticality'=>'critical'], + ['name'=>'RTU Sottostazioni (45 unità)', 'type'=>'hardware', 'criticality'=>'critical'], + ['name'=>'Centro Controllo Operativo (CCO)', 'type'=>'datacenter','criticality'=>'high'], + ], + 'training' => [ + ['title'=>'Sicurezza Sistemi OT/ICS per Operatori Energia','mandatory'=>true,'duration_hours'=>6], + ], +], + +// ── D) BancaRegionale Digitale S.p.A. ──────────────────────────────────────── +'bancaregionale' => [ + 'name' => 'BancaRegionale Digitale S.p.A.', + 'legal_form' => 'S.p.A.', + 'vat_number' => '33445566778', + 'ateco_code' => '64.19', + 'sector' => 'banking', + 'nis2_type' => 'essential', + 'employees' => 3100, + 'annual_turnover' => 180000000, + 'city' => 'Milano', 'province' => 'MI', 'region' => 'Lombardia', + 'profile' => 'essential', + 'users' => [ + ['first'=>'Dott. Fabio', 'last'=>'Brunetti', 'email'=>'admin@bancaregionale-spa.demo', 'role'=>'org_admin'], + ['first'=>'Dott. Sara', 'last'=>'Lombardi', 'email'=>'ciso@bancaregionale-spa.demo', 'role'=>'compliance_manager'], + ['first'=>'Dott. Marco', 'last'=>'Torriani', 'email'=>'auditor@bancaregionale-spa.demo','role'=>'auditor'], + ], + 'risks' => [ + ['title'=>'Frode bonifici (Business Email Compromise)', 'category'=>'cyber', 'likelihood'=>4,'impact'=>5,'nis2_article'=>'21.2.b'], + ['title'=>'Credential stuffing su internet banking', 'category'=>'cyber', 'likelihood'=>5,'impact'=>4,'nis2_article'=>'21.2.i'], + ['title'=>'API banking compromise (Open Banking)', 'category'=>'cyber', 'likelihood'=>3,'impact'=>5,'nis2_article'=>'21.2.e'], + ['title'=>'Anomalia messaggistica SWIFT', 'category'=>'operational','likelihood'=>2,'impact'=>5,'nis2_article'=>'21.2.b'], + ['title'=>'Ransomware su core banking system', 'category'=>'cyber', 'likelihood'=>3,'impact'=>5,'nis2_article'=>'21.2.b'], + ['title'=>'DDoS su app mobile (500k utenti)', 'category'=>'operational','likelihood'=>4,'impact'=>3,'nis2_article'=>'21.2.c'], + ], + 'policies' => [ + ['title'=>'Politica Gestione Incidenti Banking — Art.21.2.b', 'type'=>'incident_response','nis2_article'=>'21.2.b'], + ['title'=>'Politica Controllo Accessi e PSD2 — Art.21.2.j', 'type'=>'access_control', 'nis2_article'=>'21.2.j'], + ['title'=>'Politica Crittografia e SWIFT Security', 'type'=>'cryptography', 'nis2_article'=>'21.2.h'], + ['title'=>'Politica Anti-Frode e Monitoraggio Transazioni', 'type'=>'fraud_prevention', 'nis2_article'=>'21.2.b'], + ], + 'suppliers' => [ + ['name'=>'CoreBanking Systems AG', 'service_type'=>'software_vendor', 'risk_level'=>'critical','critical'=>1], + ['name'=>'PaymentGateway Italia', 'service_type'=>'managed_service', 'risk_level'=>'high', 'critical'=>1], + ['name'=>'SOC Esterno Cyber S.r.l.', 'service_type'=>'security_service','risk_level'=>'high', 'critical'=>1], + ['name'=>'Cloud DR Finance EU', 'service_type'=>'cloud_provider', 'risk_level'=>'medium', 'critical'=>0], + ], + 'assets' => [ + ['name'=>'Core Banking System (CBS)','type'=>'software', 'criticality'=>'critical'], + ['name'=>'App Mobile Banking (500k utenti)','type'=>'software','criticality'=>'critical'], + ['name'=>'Data Center Primario Banking','type'=>'datacenter','criticality'=>'high'], + ], + 'training' => [ + ['title'=>'Cybersecurity nel Settore Bancario e PSD2','mandatory'=>true,'duration_hours'=>6], + ], +], + +// ── E) AquaPura Servizi S.r.l. ──────────────────────────────────────────────── +'aquapura' => [ + 'name' => 'AquaPura Servizi S.r.l.', + 'legal_form' => 'S.r.l.', + 'vat_number' => '44556677889', + 'ateco_code' => '36.00', + 'sector' => 'water', + 'nis2_type' => 'essential', + 'employees' => 380, + 'annual_turnover' => 28000000, + 'city' => 'Venezia', 'province' => 'VE', 'region' => 'Veneto', + 'profile' => 'essential', + 'users' => [ + ['first'=>'Ing. Davide','last'=>'Cattaneo','email'=>'admin@aquapura-srl.demo','role'=>'org_admin'], + ['first'=>'Dott. Irene','last'=>'Silvestri','email'=>'ciso@aquapura-srl.demo','role'=>'compliance_manager'], + ], + 'risks' => [ + ['title'=>'Compromissione impianto trattamento acque', 'category'=>'cyber', 'likelihood'=>3,'impact'=>5,'nis2_article'=>'21.2.b'], + ['title'=>'SCADA acquedotto — accesso non autorizzato', 'category'=>'cyber', 'likelihood'=>3,'impact'=>5,'nis2_article'=>'21.2.b'], + ['title'=>'DoS sistemi di controllo potabilità', 'category'=>'operational','likelihood'=>3,'impact'=>5,'nis2_article'=>'21.2.c'], + ['title'=>'Supply chain reagenti — fornitore compromesso','category'=>'supply_chain','likelihood'=>2,'impact'=>4,'nis2_article'=>'21.2.d'], + ['title'=>'Physical intrusion impianto depurazione', 'category'=>'physical', 'likelihood'=>2,'impact'=>4,'nis2_article'=>'21.2.f'], + ], + 'policies' => [ + ['title'=>'Politica Gestione Incidenti Acquedotto','type'=>'incident_response', 'nis2_article'=>'21.2.b'], + ['title'=>'Politica Business Continuity Idrica', 'type'=>'business_continuity','nis2_article'=>'21.2.c'], + ], + 'suppliers' => [ + ['name'=>'AquaSCADA S.r.l.', 'service_type'=>'ot_vendor', 'risk_level'=>'critical','critical'=>1], + ['name'=>'Laboratorio Analisi H2O','service_type'=>'consulting', 'risk_level'=>'medium', 'critical'=>0], + ['name'=>'Manutenzione Impianti', 'service_type'=>'managed_service','risk_level'=>'medium','critical'=>0], + ], + 'assets' => [ + ['name'=>'Sistema SCADA acquedotto principale','type'=>'ot_system','criticality'=>'critical'], + ['name'=>'Impianto trattamento acque centrale','type'=>'hardware', 'criticality'=>'critical'], + ], + 'training' => [ + ['title'=>'Sicurezza Sistemi Idrici e OT','mandatory'=>true,'duration_hours'=>4], + ], +], + +// ── F) LogisticaRapida S.r.l. ───────────────────────────────────────────────── +'logistica' => [ + 'name' => 'LogisticaRapida S.r.l.', + 'legal_form' => 'S.r.l.', + 'vat_number' => '55667788990', + 'ateco_code' => '49.41', + 'sector' => 'transport', + 'nis2_type' => 'important', + 'employees' => 620, + 'annual_turnover' => 45000000, + 'city' => 'Bologna', 'province' => 'BO', 'region' => 'Emilia-Romagna', + 'profile' => 'important', + 'users' => [ + ['first'=>'Dott. Riccardo','last'=>'Fontana','email'=>'admin@logisticarapida-srl.demo','role'=>'org_admin'], + ['first'=>'Ing. Valentina','last'=>'Greco', 'email'=>'ciso@logisticarapida-srl.demo', 'role'=>'compliance_manager'], + ], + 'risks' => [ + ['title'=>'GPS spoofing su flotta veicoli', 'category'=>'cyber', 'likelihood'=>3,'impact'=>4,'nis2_article'=>'21.2.b'], + ['title'=>'Fleet management system compromise', 'category'=>'cyber', 'likelihood'=>3,'impact'=>4,'nis2_article'=>'21.2.b'], + ['title'=>'Ransomware su TMS (Transport Management)', 'category'=>'cyber', 'likelihood'=>3,'impact'=>4,'nis2_article'=>'21.2.b'], + ['title'=>'Supply chain carrier — partner malicious', 'category'=>'supply_chain','likelihood'=>2,'impact'=>3,'nis2_article'=>'21.2.d'], + ['title'=>'API partner — injection su dati spedizione', 'category'=>'cyber', 'likelihood'=>2,'impact'=>3,'nis2_article'=>'21.2.e'], + ], + 'policies' => [ + ['title'=>'Politica Sicurezza Flotta e TMS','type'=>'incident_response', 'nis2_article'=>'21.2.b'], + ['title'=>'Politica Supply Chain Logistica', 'type'=>'supply_chain_security','nis2_article'=>'21.2.d'], + ], + 'suppliers' => [ + ['name'=>'TMS Provider Cloud', 'service_type'=>'software_vendor', 'risk_level'=>'high', 'critical'=>1], + ['name'=>'GPS Tracking EU', 'service_type'=>'hardware_vendor', 'risk_level'=>'medium','critical'=>0], + ['name'=>'Carrier Network Italia','service_type'=>'managed_service', 'risk_level'=>'low', 'critical'=>0], + ], + 'assets' => [ + ['name'=>'TMS (Transport Management System)','type'=>'software','criticality'=>'critical'], + ['name'=>'Fleet GPS Tracking (620 veicoli)', 'type'=>'hardware','criticality'=>'high'], + ], + 'training' => [ + ['title'=>'Cybersecurity nei Trasporti e GPS','mandatory'=>true,'duration_hours'=>3], + ], +], + +// ── G) SmartCity Solutions S.r.l. ───────────────────────────────────────────── +'smartcity' => [ + 'name' => 'SmartCity Solutions S.r.l.', + 'legal_form' => 'S.r.l.', + 'vat_number' => '66778899001', + 'ateco_code' => '62.09', + 'sector' => 'ict_services', + 'nis2_type' => 'important', + 'employees' => 145, + 'annual_turnover' => 12000000, + 'city' => 'Firenze', 'province' => 'FI', 'region' => 'Toscana', + 'profile' => 'important', + 'users' => [ + ['first'=>'Dott. Andrea', 'last'=>'Pellegrini','email'=>'admin@smartcity-srl.demo','role'=>'org_admin'], + ['first'=>'Ing. Federica', 'last'=>'Marini', 'email'=>'ciso@smartcity-srl.demo', 'role'=>'compliance_manager'], + ], + 'risks' => [ + ['title'=>'IoT smart city exploit (sensori stradali)','category'=>'cyber', 'likelihood'=>3,'impact'=>4,'nis2_article'=>'21.2.b'], + ['title'=>'Data lake breach (dati mobilità cittadini)','category'=>'compliance','likelihood'=>3,'impact'=>4,'nis2_article'=>'21.2.b'], + ['title'=>'API pubblica abusata (smart parking)', 'category'=>'cyber', 'likelihood'=>4,'impact'=>3,'nis2_article'=>'21.2.e'], + ['title'=>'Supply chain sensori ambientali', 'category'=>'supply_chain','likelihood'=>2,'impact'=>3,'nis2_article'=>'21.2.d'], + ], + 'policies' => [ + ['title'=>'Politica Sicurezza IoT e Smart City','type'=>'incident_response','nis2_article'=>'21.2.b'], + ['title'=>'Politica Protezione Dati Urbani', 'type'=>'data_protection', 'nis2_article'=>'21.2.b'], + ], + 'suppliers' => [ + ['name'=>'IoT Platform EU S.r.l.', 'service_type'=>'hardware_vendor','risk_level'=>'high', 'critical'=>1], + ['name'=>'Cloud Analytics Smart', 'service_type'=>'cloud_provider', 'risk_level'=>'medium','critical'=>0], + ], + 'assets' => [ + ['name'=>'Piattaforma IoT Smart City (2000 sensori)','type'=>'ot_system','criticality'=>'critical'], + ['name'=>'Data Lake mobilità urbana', 'type'=>'server', 'criticality'=>'high'], + ], + 'training' => [ + ['title'=>'Sicurezza IoT e Città Intelligenti','mandatory'=>true,'duration_hours'=>3], + ], +], + +// ── H) EduDigital S.r.l. — PMI voluntary ───────────────────────────────────── +'edudigital' => [ + 'name' => 'EduDigital S.r.l.', + 'legal_form' => 'S.r.l.', + 'vat_number' => '77889900112', + 'ateco_code' => '85.59', + 'sector' => 'ict_services', + 'nis2_type' => 'voluntary', + 'voluntary_compliance' => true, + 'employees' => 80, + 'annual_turnover' => 6000000, + 'city' => 'Bologna', 'province' => 'BO', 'region' => 'Emilia-Romagna', + 'profile' => 'voluntary', + 'users' => [ + ['first'=>'Dott. Matteo','last'=>'Ferrario','email'=>'admin@edudigital-srl.demo','role'=>'org_admin'], + ['first'=>'Ing. Elisa', 'last'=>'Conti', 'email'=>'ciso@edudigital-srl.demo', 'role'=>'compliance_manager'], + ], + 'risks' => [ + ['title'=>'Breach dati studenti (GDPR minori)', 'category'=>'compliance','likelihood'=>3,'impact'=>4,'nis2_article'=>'21.2.b'], + ['title'=>'Piattaforma LMS down durante esami', 'category'=>'operational','likelihood'=>3,'impact'=>3,'nis2_article'=>'21.2.c'], + ['title'=>'Phishing docenti — credenziali rubate', 'category'=>'human', 'likelihood'=>4,'impact'=>3,'nis2_article'=>'21.2.g'], + ], + 'policies' => [ + ['title'=>'Politica Protezione Dati Studenti — GDPR','type'=>'data_protection','nis2_article'=>'21.2.b'], + ], + 'suppliers' => [ + ['name'=>'LMS Provider Cloud S.r.l.','service_type'=>'software_vendor','risk_level'=>'high', 'critical'=>1], + ['name'=>'Cloud Hosting EduTech', 'service_type'=>'cloud_provider', 'risk_level'=>'medium','critical'=>0], + ], + 'assets' => [ + ['name'=>'LMS Platform (8000 studenti)','type'=>'software','criticality'=>'critical'], + ['name'=>'Database dati studenti', 'type'=>'server', 'criticality'=>'high'], + ], + 'training' => [ + ['title'=>'Privacy e GDPR per Operatori Educativi','mandatory'=>true,'duration_hours'=>3], + ], +], + +// ── I) AgriTech Innovazione S.r.l. — PMI voluntary ─────────────────────────── +'agritech' => [ + 'name' => 'AgriTech Innovazione S.r.l.', + 'legal_form' => 'S.r.l.', + 'vat_number' => '88990011223', + 'ateco_code' => '01.61', + 'sector' => 'food', + 'nis2_type' => 'voluntary', + 'voluntary_compliance' => true, + 'employees' => 45, + 'annual_turnover' => 3500000, + 'city' => 'Verona', 'province' => 'VR', 'region' => 'Veneto', + 'profile' => 'voluntary', + 'users' => [ + ['first'=>'Dott. Giuseppe','last'=>'Caruso', 'email'=>'admin@agritech-srl.demo','role'=>'org_admin'], + ['first'=>'Dott. Paola', 'last'=>'Monti', 'email'=>'ciso@agritech-srl.demo', 'role'=>'compliance_manager'], + ], + 'risks' => [ + ['title'=>'Sensori IoT campo compromessi (dati coltura)','category'=>'cyber', 'likelihood'=>3,'impact'=>3,'nis2_article'=>'21.2.b'], + ['title'=>'Sistema ERP agricolo — accesso non auth', 'category'=>'cyber', 'likelihood'=>2,'impact'=>3,'nis2_article'=>'21.2.i'], + ['title'=>'Supply chain tracciabilità — dati falsificati','category'=>'supply_chain','likelihood'=>2,'impact'=>4,'nis2_article'=>'21.2.d'], + ], + 'policies' => [ + ['title'=>'Politica Sicurezza IoT Agricoltura','type'=>'incident_response','nis2_article'=>'21.2.b'], + ], + 'suppliers' => [ + ['name'=>'AgriIoT Platform S.r.l.','service_type'=>'hardware_vendor','risk_level'=>'medium','critical'=>0], + ['name'=>'ERP AgriSoft', 'service_type'=>'software_vendor','risk_level'=>'medium','critical'=>0], + ], + 'assets' => [ + ['name'=>'Rete IoT sensori campo (200 unità)','type'=>'ot_system','criticality'=>'high'], + ['name'=>'ERP Sistema gestione agricola', 'type'=>'software', 'criticality'=>'high'], + ], + 'training' => [ + ['title'=>'Sicurezza IoT in Agricoltura','mandatory'=>false,'duration_hours'=>2], + ], +], + +// ── J) ManufacturingPro S.p.A. ──────────────────────────────────────────────── +'manufacturing' => [ + 'name' => 'ManufacturingPro S.p.A.', + 'legal_form' => 'S.p.A.', + 'vat_number' => '99001122334', + 'ateco_code' => '28.22', + 'sector' => 'manufacturing', + 'nis2_type' => 'important', + 'employees' => 950, + 'annual_turnover' => 78000000, + 'city' => 'Brescia', 'province' => 'BS', 'region' => 'Lombardia', + 'profile' => 'important', + 'users' => [ + ['first'=>'Ing. Roberto', 'last'=>'Vitali', 'email'=>'admin@manufacturingpro-spa.demo', 'role'=>'org_admin'], + ['first'=>'Dott. Claudia', 'last'=>'Negri', 'email'=>'ciso@manufacturingpro-spa.demo', 'role'=>'compliance_manager'], + ['first'=>'Dott. Enrico', 'last'=>'Saraceni', 'email'=>'auditor@manufacturingpro-spa.demo','role'=>'auditor'], + ], + 'risks' => [ + ['title'=>'OT fabbrica compromessa (linea produzione fermata)','category'=>'cyber', 'likelihood'=>3,'impact'=>5,'nis2_article'=>'21.2.b'], + ['title'=>'ERP SAP credential leak', 'category'=>'cyber', 'likelihood'=>3,'impact'=>4,'nis2_article'=>'21.2.i'], + ['title'=>'Supply chain componenti — fornitore backdoor', 'category'=>'supply_chain','likelihood'=>2,'impact'=>5,'nis2_article'=>'21.2.d'], + ['title'=>'Ransomware su sistemi produzione', 'category'=>'cyber', 'likelihood'=>3,'impact'=>5,'nis2_article'=>'21.2.b'], + ['title'=>'GDPR lavoratori — dati biometrici timbrature', 'category'=>'compliance', 'likelihood'=>3,'impact'=>3,'nis2_article'=>'21.2.b'], + ], + 'policies' => [ + ['title'=>'Politica Sicurezza OT Produzione — Art.21.2.a', 'type'=>'risk_management', 'nis2_article'=>'21.2.a'], + ['title'=>'Politica Controllo Accessi ERP', 'type'=>'access_control', 'nis2_article'=>'21.2.j'], + ['title'=>'Politica Business Continuity Produzione', 'type'=>'business_continuity','nis2_article'=>'21.2.c'], + ], + 'suppliers' => [ + ['name'=>'SAP Hosting Partner S.r.l.','service_type'=>'managed_service','risk_level'=>'critical','critical'=>1], + ['name'=>'OT Vendor Automazione', 'service_type'=>'ot_vendor', 'risk_level'=>'critical','critical'=>1], + ['name'=>'Logistica Componenti EU', 'service_type'=>'logistics', 'risk_level'=>'medium', 'critical'=>0], + ['name'=>'Cert ISO 27001 S.r.l.', 'service_type'=>'consulting', 'risk_level'=>'low', 'critical'=>0], + ], + 'assets' => [ + ['name'=>'Linea produzione OT (4 impianti)','type'=>'ot_system','criticality'=>'critical'], + ['name'=>'ERP SAP S/4HANA', 'type'=>'software', 'criticality'=>'critical'], + ['name'=>'Rete IT/OT segregata fabbrica', 'type'=>'network', 'criticality'=>'high'], + ], + 'training' => [ + ['title'=>'Cybersecurity in Ambienti Industriali OT/IT','mandatory'=>true,'duration_hours'=>5], + ], +], + +]; // end $COMPANIES + +// Consulente cross-org +$CONSULTANT = [ + 'first' => 'Avv. Federica', 'last' => 'Montanari', + 'email' => 'consultant@nis2agile-big.demo', + 'role' => 'consultant', +]; + +// ═══════════════════════════════════════════════════════════════════════════════ +// FASE 0 — Health Check + Auto-Reset +// ═══════════════════════════════════════════════════════════════════════════════ +simPhase(0, 'Health Check API'); +$ch = curl_init(str_replace('/api', '', API_BASE) . '/api-status.php'); +curl_setopt_array($ch, [CURLOPT_RETURNTRANSFER=>true,CURLOPT_TIMEOUT=>10,CURLOPT_SSL_VERIFYPEER=>false,CURLOPT_SSL_VERIFYHOST=>0]); +$raw = curl_exec($ch); $code = curl_getinfo($ch, CURLINFO_HTTP_CODE); curl_close($ch); +$hRes = json_decode($raw ?: '{}', true) ?? []; +if ($code === 200 && !empty($hRes['status'])) { + ok("API health: {$hRes['status']} — DB: " . ($hRes['database'] ?? '?')); +} else { + warn("API health check degradato (HTTP $code) — continuo comunque"); +} + +simPhase(0, 'Auto-Reset Dati Demo Precedenti'); +autoResetDemo(); + +// ═══════════════════════════════════════════════════════════════════════════════ +// FASE 1 — Registrazione & Onboarding (10 aziende) +// ═══════════════════════════════════════════════════════════════════════════════ +simPhase(1, 'Registrazione & Onboarding — 10 Aziende NIS2'); + +foreach ($COMPANIES as $slug => $comp) { + simLog("── [{$comp['nis2_type']}] {$comp['name']} ({$comp['sector']}) {$comp['employees']} dip", 'phase'); + + $adminDef = $comp['users'][0]; + $jwt = ensureUser($adminDef['first'], $adminDef['last'], $adminDef['email'], DEMO_PWD, 'org_admin'); + if (!$jwt) continue; + $S['orgs'][$slug] = ['name' => $comp['name'], 'jwt' => $jwt, 'id' => null]; + + $orgId = ensureOrg($jwt, [ + 'name' => $comp['name'], + 'legal_form' => $comp['legal_form'], + 'vat_number' => $comp['vat_number'], + 'ateco_code' => $comp['ateco_code'], + 'sector' => $comp['sector'], + 'employee_count' => $comp['employees'], + 'annual_turnover_eur'=> $comp['annual_turnover'], + 'city' => $comp['city'], + 'province' => $comp['province'], + 'region' => $comp['region'], + 'country' => 'IT', + ]); + if (!$orgId) continue; + $S['orgs'][$slug]['id'] = $orgId; + + // Aggiorna dati org + $upRes = api('PUT', "/organizations/{$orgId}", [ + 'employee_count' => $comp['employees'], + 'annual_turnover_eur' => $comp['annual_turnover'], + 'vat_number' => $comp['vat_number'], + ], $jwt, $orgId); + apiOk($upRes, "org.update.$slug") && ok("Dati org aggiornati: #{$orgId}"); + + // Classificazione NIS2 + $classData = [ + 'sector' => $comp['sector'], + 'employee_count' => $comp['employees'], + 'annual_turnover_eur'=> $comp['annual_turnover'], + 'nis2_type' => $comp['nis2_type'], + ]; + if (!empty($comp['voluntary_compliance'])) { + $classData['voluntary_compliance'] = true; + } + classifyOrg($jwt, $orgId, $classData); + + // Seed utenti extra + aggiunta alla org via DB + if (count($comp['users']) > 1) { + for ($i = 1; $i < count($comp['users']); $i++) { + $u = $comp['users'][$i]; + $fullName = trim("{$u['first']} {$u['last']}"); + $seeded = dbSeedUser($fullName, $u['email'], DEMO_PWD, $u['role']); + $uid = dbGetUserId($u['email']); + if ($uid) { + dbAddToOrg($uid, $orgId, $u['role']); + $S['users'][$u['email']] = ['id' => $uid, 'jwt' => null]; + ok("Utente seeded+aggiunto: {$fullName} [{$u['role']}] → org #{$orgId}"); + } else { + warn("Utente seed fallito: {$u['email']}"); + } + } + } +} + +// Consulente cross-org +simLog("── Consulente cross-org: {$CONSULTANT['first']} {$CONSULTANT['last']}", 'phase'); +$consultantJwt = ensureUser($CONSULTANT['first'], $CONSULTANT['last'], $CONSULTANT['email'], DEMO_PWD, 'consultant'); +if ($consultantJwt) { + $S['users'][$CONSULTANT['email']]['jwt'] = $consultantJwt; + foreach ($S['orgs'] as $slug => $orgData) { + if (empty($orgData['id'])) continue; + $uid = $S['users'][$CONSULTANT['email']]['id'] ?? null; + if ($uid) { + dbAddToOrg($uid, $orgData['id'], 'consultant'); + ok("Consulente aggiunto a org #{$orgData['id']} ({$orgData['name']})"); + } + } +} + +// ═══════════════════════════════════════════════════════════════════════════════ +// FASE 2 — Gap Assessment 80 Domande (tutte le 10 aziende) +// ═══════════════════════════════════════════════════════════════════════════════ +simPhase(2, 'Gap Assessment 80 Domande Art.21 NIS2 — 10 Aziende'); + +foreach ($COMPANIES as $slug => $comp) { + if (empty($S['orgs'][$slug]['id'])) { skip("Skip assessment $slug: org non creata"); continue; } + $jwt = $S['orgs'][$slug]['jwt']; + $orgId = $S['orgs'][$slug]['id']; + $responses = getBigAssessmentResponses($comp['profile']); + $assessId = runAssessment($jwt, $orgId, $slug, $comp['name'], $responses); + + // AI analyze per infratech (opzionale — warn se rate-limited) + if ($slug === 'infratech' && $assessId) { + $aiRes = api('POST', "/assessments/{$assessId}/ai-analyze", [], $jwt, $orgId); + if (!empty($aiRes['success'])) { + ok("Assessment AI analyze completato per InfraTech"); + } else { + warn("Assessment AI analyze: " . ($aiRes['error'] ?? $aiRes['message'] ?? 'rate-limited o timeout — skip')); + } + } +} + +// ═══════════════════════════════════════════════════════════════════════════════ +// FASE 3 — Risk Register (tutte le 10 aziende) +// ═══════════════════════════════════════════════════════════════════════════════ +simPhase(3, 'Risk Register — 55+ Rischi NIS2'); + +foreach ($COMPANIES as $slug => $comp) { + if (empty($S['orgs'][$slug]['id'])) continue; + $jwt = $S['orgs'][$slug]['jwt']; + $orgId = $S['orgs'][$slug]['id']; + simLog("Rischi [{$comp['name']}] ({$comp['profile']}):", 'info'); + + $riskIds = []; + foreach ($comp['risks'] as $riskDef) { + $l = $riskDef['likelihood']; $i = $riskDef['impact']; + $score = $l * $i; + $level = riskLevel($l, $i); + $riskId = createRisk($jwt, $orgId, [ + 'title' => $riskDef['title'], + 'category' => $riskDef['category'], + 'likelihood' => $l, + 'impact' => $i, + 'risk_score' => $score, + 'risk_level' => $level, + 'nis2_article' => $riskDef['nis2_article'], + 'description' => "Rischio identificato — {$riskDef['title']}. Contesto: {$comp['name']}, settore {$comp['sector']}.", + ]); + if ($riskId) $riskIds[$level][] = $riskId; + } + + // Aggiungi treatments per rischi critical/high + foreach (['critical', 'high'] as $lvl) { + foreach ($riskIds[$lvl] ?? [] as $riskId) { + $tRes = api('POST', "/risks/{$riskId}/treatments", [ + 'treatment_type' => 'mitigate', + 'description' => 'Piano di mitigazione approvato da CISO. Implementazione in 60gg.', + 'status' => 'in_progress', + 'due_date' => date('Y-m-d', strtotime('+60 days')), + ], $jwt, $orgId); + apiOk($tRes, "risk.treatment.$slug") && ok("Treatment aggiunto a risk #{$riskId} [$lvl]"); + } + } + + // AI suggest per infratech + if ($slug === 'infratech') { + $aiRes = api('POST', '/risks/ai-suggest', [ + 'sector' => $comp['sector'], + 'context'=> 'Infrastruttura digitale critica, 5800 dipendenti, backbone fibra nazionale', + ], $jwt, $orgId); + if (!empty($aiRes['success'])) { + ok("AI risk suggest per InfraTech: " . count($aiRes['data']['risks'] ?? []) . " suggerimenti"); + } else { + warn("AI risk suggest: " . ($aiRes['message'] ?? 'rate-limited — skip')); + } + } + + // Verifica matrice rischi + $matrixRes = api('GET', '/risks/matrix', null, $jwt, $orgId); + apiOk($matrixRes, "risks.matrix.$slug") && ok("Matrice rischi verificata per {$comp['name']}"); +} + +// ═══════════════════════════════════════════════════════════════════════════════ +// FASE 4 — Policy (tutte le 10 aziende) +// ═══════════════════════════════════════════════════════════════════════════════ +simPhase(4, 'Policy NIS2 Art.21 — 25+ Policy'); + +foreach ($COMPANIES as $slug => $comp) { + if (empty($S['orgs'][$slug]['id'])) continue; + $jwt = $S['orgs'][$slug]['jwt']; + $orgId = $S['orgs'][$slug]['id']; + + foreach ($comp['policies'] as $polDef) { + $polCategory = mapPolicyCategory($polDef['type']); + $polId = createPolicy($jwt, $orgId, [ + 'title' => $polDef['title'], + 'category' => $polCategory, + 'nis2_article' => $polDef['nis2_article'], + 'content' => "Politica aziendale in conformità Art.{$polDef['nis2_article']} Direttiva NIS2 (EU 2022/2555) e D.Lgs.138/2024. Versione 1.0 — generata da Big Simulation demo.", + 'status' => 'draft', + ]); + if ($polId) { + $approveRes = api('POST', "/policies/{$polId}/approve", [ + 'notes' => 'Approvazione automatica da Big Simulation demo NIS2', + ], $jwt, $orgId); + apiOk($approveRes, "policy.approve") && ok("Policy approvata: {$polDef['title']}"); + } + } + + // AI generate aggiuntiva per infratech + if ($slug === 'infratech') { + $aiPolRes = api('POST', '/policies/ai-generate', [ + 'category' => 'vulnerability_management', + 'nis2_article' => '21.2.e', + 'context' => 'Infrastruttura telecomunicazioni critica, 5800 dipendenti', + ], $jwt, $orgId); + if (!empty($aiPolRes['success'])) { + ok("AI policy generate per InfraTech: vulnerability_management"); + } else { + warn("AI policy generate: " . ($aiPolRes['message'] ?? 'rate-limited — skip')); + } + } +} + +// ═══════════════════════════════════════════════════════════════════════════════ +// FASE 5 — Supply Chain (tutte le 10 aziende) +// ═══════════════════════════════════════════════════════════════════════════════ +simPhase(5, 'Supply Chain — 30+ Fornitori Critici'); + +foreach ($COMPANIES as $slug => $comp) { + if (empty($S['orgs'][$slug]['id'])) continue; + $jwt = $S['orgs'][$slug]['jwt']; + $orgId = $S['orgs'][$slug]['id']; + + foreach ($comp['suppliers'] as $supDef) { + $supId = createSupplier($jwt, $orgId, [ + 'name' => $supDef['name'], + 'service_type' => $supDef['service_type'], + 'criticality' => $supDef['risk_level'], + 'nis2_relevant' => 1, + 'contact_email' => DEMO_EMAIL, + ]); + if ($supId) { + $highRisk = in_array($supDef['risk_level'], ['high', 'critical']); + $assessRes = api('POST', "/supply-chain/{$supId}/assess", [ + 'assessment_responses' => [ + ['question'=>'Controlli sicurezza implementati', 'weight'=>3,'value'=>'yes'], + ['question'=>'Procedura gestione incidenti', 'weight'=>2,'value'=>$highRisk ? 'yes' : 'partial'], + ['question'=>'Conformità GDPR', 'weight'=>2,'value'=>'yes'], + ['question'=>'Clausole NIS2 nel contratto', 'weight'=>2,'value'=>$supDef['critical'] ? 'yes' : 'partial'], + ['question'=>'Audit sicurezza ultimi 12 mesi', 'weight'=>1,'value'=>$highRisk ? 'yes' : 'partial'], + ['question'=>'Piano business continuity documentato','weight'=>2,'value'=>'partial'], + ], + ], $jwt, $orgId); + apiOk($assessRes, "supplier.assess") && ok("Assessment fornitore: {$supDef['name']}"); + } + } + + // Verifica risk overview + $roRes = api('GET', '/supply-chain/risk-overview', null, $jwt, $orgId); + apiOk($roRes, "supply-chain.risk-overview.$slug") && ok("Risk overview supply chain: {$comp['name']}"); +} + +// ═══════════════════════════════════════════════════════════════════════════════ +// FASE 6 — Training (tutte le 10 aziende) +// ═══════════════════════════════════════════════════════════════════════════════ +simPhase(6, 'Training & Formazione — 10 Aziende'); + +foreach ($COMPANIES as $slug => $comp) { + if (empty($S['orgs'][$slug]['id'])) continue; + $jwt = $S['orgs'][$slug]['jwt']; + $orgId = $S['orgs'][$slug]['id']; + + foreach ($comp['training'] as $courseDef) { + $courseId = createTrainingCourse($jwt, $orgId, [ + 'title' => $courseDef['title'], + 'description' => "Corso {$comp['name']} — settore {$comp['sector']}. Conformità NIS2 Art.21.2.g.", + 'mandatory' => $courseDef['mandatory'], + 'duration_hours' => $courseDef['duration_hours'], + 'category' => 'nis2_compliance', + ]); + if ($courseId) { + // Assegna a tutti gli utenti dell'azienda + foreach ($comp['users'] as $u) { + $uid = $S['users'][$u['email']]['id'] ?? null; + if (!$uid) $uid = dbGetUserId($u['email']); + if ($uid) { + $assignRes = api('POST', '/training/assign', [ + 'course_id' => $courseId, + 'user_id' => $uid, + 'due_date' => date('Y-m-d', strtotime('+30 days')), + ], $jwt, $orgId); + if (!empty($assignRes['success'])) { + ok("Corso assegnato: {$u['email']} → {$courseDef['title']}"); + } else { + warn("Assign training: " . ($assignRes['error'] ?? $assignRes['message'] ?? 'errore')); + } + } + } + } + } + + // Verifica compliance status + $csRes = api('GET', '/training/compliance-status', null, $jwt, $orgId); + apiOk($csRes, "training.compliance.$slug") && ok("Training compliance status: {$comp['name']}"); +} + +// ═══════════════════════════════════════════════════════════════════════════════ +// FASE 7 — Asset (tutte le 10 aziende, dettaglio infratech) +// ═══════════════════════════════════════════════════════════════════════════════ +simPhase(7, 'Inventario Asset — 25+ Asset Critici'); + +foreach ($COMPANIES as $slug => $comp) { + if (empty($S['orgs'][$slug]['id'])) continue; + $jwt = $S['orgs'][$slug]['jwt']; + $orgId = $S['orgs'][$slug]['id']; + + foreach ($comp['assets'] as $assetDef) { + createAsset($jwt, $orgId, [ + 'name' => $assetDef['name'], + 'type' => $assetDef['type'], + 'criticality' => $assetDef['criticality'], + 'description' => "Asset {$comp['name']} — {$assetDef['name']}. Settore {$comp['sector']}.", + 'status' => 'active', + ]); + } + + // Verifica dependency map per infratech + if ($slug === 'infratech') { + $depRes = api('GET', '/assets/dependency-map', null, $jwt, $orgId); + apiOk($depRes, "assets.dependency-map") && ok("Asset dependency map InfraTech verificata"); + } +} + +// ═══════════════════════════════════════════════════════════════════════════════ +// FASE 8 — Incidenti con Timeline Art.23 +// ═══════════════════════════════════════════════════════════════════════════════ +simPhase(8, 'Incidenti Art.23 NIS2 — Timeline 24h/72h/30d'); + +// ── InfraTech — Incidente 1: Ransomware Backbone ───────────────────────────── +if (!empty($S['orgs']['infratech']['id'])) { + $jwt = $S['orgs']['infratech']['jwt']; + $orgId = $S['orgs']['infratech']['id']; + simLog('InfraTech — Incidente 1: Ransomware Backbone Nazionale', 'phase'); + + $inc1 = createIncident($jwt, $orgId, [ + 'title' => 'Ransomware su backbone nazionale fibra ottica', + 'description' => 'SIEM ha rilevato alle 03:47 attività anomala su cluster routing nazionale. Analisi forense: LockBit 3.0 ha crittografato sistemi di gestione backbone. 2 milioni di utenti finali impattati. Downtime stimato 12h.', + 'severity' => 'critical', + 'classification' => 'cyber_attack', + 'affected_services' => 'Backbone fibra, DNS primario, routing BGP', + 'affected_users_count' => 2000000, + 'malicious_action' => 1, + 'is_significant' => 1, + 'detected_at' => date('Y-m-d H:i:s', strtotime('-2 days')), + 'status' => 'analyzing', + ]); + if ($inc1) { + // AI classify + $aiClassRes = api('POST', "/incidents/{$inc1}/ai-classify", [], $jwt, $orgId); + if (!empty($aiClassRes['success'])) ok("AI classify incidente #{$inc1}"); + else warn("AI classify: " . ($aiClassRes['message'] ?? 'rate-limited — skip')); + + // Timeline detection + api('POST', "/incidents/{$inc1}/timeline", [ + 'event_type' => 'update', 'description' => 'Rilevato da SIEM alle 03:47. Alert correlato su 14 indicatori IOC. Team IR attivato.', + ], $jwt, $orgId) && ok("Timeline: detection"); + + // Early warning 24h + $ewRes = api('POST', "/incidents/{$inc1}/early-warning", [ + 'notes' => 'Early warning 24h inviato ad ACN/CSIRT. Incidente significativo: >500 utenti, cyber, durata >4h.', + ], $jwt, $orgId); + apiOk($ewRes, 'inc1.early-warning') && ok("Early warning 24h Art.23 inviato — ACN"); + + // Timeline containment + api('POST', "/incidents/{$inc1}/timeline", [ + 'event_type' => 'update', 'description' => 'Contenimento: isolamento segmenti compromessi. Failover su DR site attivato.', + ], $jwt, $orgId); + + // Notification 72h + $notRes = api('POST', "/incidents/{$inc1}/notification", [ + 'notes' => 'Notifica 72h ACN+CSIRT. Impatto: 2M utenti, backbone fibra, 12h downtime. Ransomware LockBit 3.0.', + ], $jwt, $orgId); + apiOk($notRes, 'inc1.notification') && ok("Notifica 72h Art.23 completata"); + + // Timeline recovery + api('POST', "/incidents/{$inc1}/timeline", [ + 'event_type' => 'update', 'description' => 'Recovery: ripristino da backup cold standby. RTO 8h rispettato.', + ], $jwt, $orgId); + api('PUT', "/incidents/{$inc1}", ['status' => 'recovering'], $jwt, $orgId); + ok("Incidente aggiornato: recovering"); + + // Final report 30d + $frRes = api('POST', "/incidents/{$inc1}/final-report", [ + 'notes' => 'Final report 30d ad ACN. Root cause: CVE-2024-BACKBONE. Lesson learned: segmentazione, WAF, patch management. Costo totale: 340.000€.', + ], $jwt, $orgId); + apiOk($frRes, 'inc1.final-report') && ok("Final report 30d Art.23 completato"); + } + + // ── InfraTech — Incidente 2: DDoS DNS ─────────────────────────────────── + simLog('InfraTech — Incidente 2: DDoS DNS Volumetrico', 'phase'); + $inc2 = createIncident($jwt, $orgId, [ + 'title' => 'DDoS volumetrico su piattaforma DNS primaria e secondaria', + 'description' => 'Attacco DDoS 380 Gbps su infrastruttura DNS. Risoluzione nomi degradata per 4 ore. Peak traffic: 38M query/sec.', + 'severity' => 'high', + 'classification' => 'availability', + 'affected_services' => 'DNS primario, DNS secondario, risoluzione nomi', + 'affected_users_count' => 850000, + 'is_significant' => 1, + 'detected_at' => date('Y-m-d H:i:s', strtotime('-1 day')), + 'status' => 'analyzing', + ]); + if ($inc2) { + api('POST', "/incidents/{$inc2}/timeline", ['event_type'=>'update','description'=>'Alert rilevato: traffico anomalo 380Gbps. Anycast attivato.'], $jwt, $orgId); + $ew2 = api('POST', "/incidents/{$inc2}/early-warning", ['notes'=>'Early warning DDoS DNS. Criterio: >500 utenti, durata >4h.'], $jwt, $orgId); + apiOk($ew2, 'inc2.early-warning') && ok("Early warning DDoS DNS inviato"); + api('POST', "/incidents/{$inc2}/timeline", ['event_type'=>'update','description'=>'Mitigazione: filtraggio traffico su 12 PoP. DNS secondario ripristinato.'], $jwt, $orgId); + $not2 = api('POST', "/incidents/{$inc2}/notification", ['notes'=>'Notifica 72h DDoS DNS. Mitigazione completata. Anycast attivo.'], $jwt, $orgId); + apiOk($not2, 'inc2.notification') && ok("Notifica 72h DDoS DNS completata"); + } + + // ── InfraTech — Incidente 3: Insider Threat ────────────────────────────── + simLog('InfraTech — Incidente 3: Insider Threat Accesso Abusivo', 'phase'); + $inc3 = createIncident($jwt, $orgId, [ + 'title' => 'Insider threat — dipendente NOC con accesso abusivo a dati clienti', + 'description' => 'Accesso abusivo rilevato dopo 48h da dipendente reparto NOC. 12.400 record clienti enterprise consultati senza autorizzazione. Scoperto tramite UEBA alert.', + 'severity' => 'medium', + 'classification' => 'unauthorized_access', + 'affected_services' => 'CRM enterprise, database clienti NOC', + 'affected_users_count' => 12400, + 'is_significant' => 1, + 'detected_at' => date('Y-m-d H:i:s', strtotime('-3 days')), + 'status' => 'analyzing', + ]); + if ($inc3) { + api('POST', "/incidents/{$inc3}/timeline", ['event_type'=>'update','description'=>'UEBA alert: accesso anomalo a 12.400 record fuori orario lavorativo.'], $jwt, $orgId); + $not3 = api('POST', "/incidents/{$inc3}/notification", ['notes'=>'Notifica 72h. Accesso scoperto dopo 48h. Dipendente sospeso. Indagine HR in corso.'], $jwt, $orgId); + apiOk($not3, 'inc3.notification') && ok("Notifica 72h Insider Threat completata"); + } +} + +// ── MedSalute — Incidente: Data Breach Cartelle Cliniche ──────────────────── +if (!empty($S['orgs']['medsalute']['id'])) { + $jwt = $S['orgs']['medsalute']['jwt']; + $orgId = $S['orgs']['medsalute']['id']; + simLog('MedSalute — Incidente: Data Breach Cartelle Cliniche', 'phase'); + + $inc4 = createIncident($jwt, $orgId, [ + 'title' => 'Data breach cartelle cliniche — accesso abusivo via LIS', + 'description' => 'Fornitore LIS esposto: endpoint API non autenticato. Dati di 15.000 pazienti (referti, diagnosi) potenzialmente esfiltrati. Alert SIEM su traffico anomalo verso IP estero.', + 'severity' => 'high', + 'classification' => 'data_breach', + 'affected_services' => 'LIS HealthSoft, database referti, portale pazienti', + 'affected_users_count' => 15000, + 'is_significant' => 1, + 'detected_at' => date('Y-m-d H:i:s', strtotime('-1 day')), + 'status' => 'analyzing', + ]); + if ($inc4) { + $aiRes = api('POST', "/incidents/{$inc4}/ai-classify", [], $jwt, $orgId); + if (!empty($aiRes['success'])) ok("AI classify data breach MedSalute #{$inc4}"); + $ew4 = api('POST', "/incidents/{$inc4}/early-warning", ['notes'=>'Early warning 24h. Violazione GDPR Art.33 + NIS2 Art.23. Segnalazione parallela a Garante Privacy.'], $jwt, $orgId); + apiOk($ew4, 'inc4.early-warning') && ok("Early warning Data Breach MedSalute"); + api('POST', "/incidents/{$inc4}/timeline", ['event_type'=>'update','description'=>'LIS endpoint disabilitato. Audit completo API in corso. 15.000 pazienti in fase di notifica GDPR Art.34.'], $jwt, $orgId); + $not4 = api('POST', "/incidents/{$inc4}/notification", ['notes'=>'Notifica 72h ACN+Garante. Endpoint bloccato, password reset, dark web monitoring.'], $jwt, $orgId); + apiOk($not4, 'inc4.notification') && ok("Notifica 72h Data Breach MedSalute"); + $fr4 = api('POST', "/incidents/{$inc4}/final-report", ['notes'=>'Final report 30d. Root cause: LIS API non autenticata. Lesson learned: vendor security assessment obbligatorio. GDPR fine evitato grazie a notifica tempestiva.'], $jwt, $orgId); + apiOk($fr4, 'inc4.final-report') && ok("Final report 30d MedSalute completato"); + } +} + +// ── DistribuzionePlus — Incidente: Anomalia SCADA ─────────────────────────── +if (!empty($S['orgs']['distribuzione']['id'])) { + $jwt = $S['orgs']['distribuzione']['jwt']; + $orgId = $S['orgs']['distribuzione']['id']; + simLog('DistribuzionePlus — Incidente: Anomalia SCADA', 'phase'); + + $inc5 = createIncident($jwt, $orgId, [ + 'title' => 'Anomalia SCADA — accesso non autorizzato sottostazione', + 'description' => 'Rilevata anomalia sui sistemi SCADA della sottostazione Moncalieri. Comandi inusuali inviati alle RTU. Sistema isolato preventivamente. Possibile compromissione.', + 'severity' => 'critical', + 'classification' => 'availability', + 'affected_services' => 'SCADA distribuzione, RTU sottostazione Moncalieri', + 'is_significant' => 1, + 'detected_at' => date('Y-m-d H:i:s', strtotime('-12 hours')), + 'status' => 'analyzing', + ]); + if ($inc5) { + api('POST', "/incidents/{$inc5}/timeline", ['event_type'=>'update','description'=>'SCADA isolato. Team OT attivato. Analisi forense ICS in corso.'], $jwt, $orgId); + $ew5 = api('POST', "/incidents/{$inc5}/early-warning", ['notes'=>'Early warning 24h. Incidente su infrastruttura energetica critica. Segnalazione ACN.'], $jwt, $orgId); + apiOk($ew5, 'inc5.early-warning') && ok("Early warning SCADA DistribuzionePlus"); + $not5 = api('POST', "/incidents/{$inc5}/notification", ['notes'=>'Notifica 72h. SCADA ripristinato sotto controllo. Analisi forense completata.'], $jwt, $orgId); + apiOk($not5, 'inc5.notification') && ok("Notifica 72h SCADA DistribuzionePlus"); + } +} + +// ── BancaRegionale — Incidente: Tentativo Frode ────────────────────────────── +if (!empty($S['orgs']['bancaregionale']['id'])) { + $jwt = $S['orgs']['bancaregionale']['jwt']; + $orgId = $S['orgs']['bancaregionale']['id']; + simLog('BancaRegionale — Incidente: Tentativo Frode Bonifici', 'phase'); + + $inc6 = createIncident($jwt, $orgId, [ + 'title' => 'Tentativo frode bonifici via BEC — 8 conti enterprise', + 'description' => 'Business Email Compromise rilevata. 8 bonifici fraudolenti per 2.4M€ bloccati da sistema anti-frode real-time. Conti mittenti compromessi via phishing.', + 'severity' => 'high', + 'classification' => 'fraud', + 'affected_services' => 'Internet banking, bonifici SEPA, monitoraggio transazioni', + 'is_significant' => 1, + 'detected_at' => date('Y-m-d H:i:s', strtotime('-6 hours')), + 'status' => 'analyzing', + ]); + if ($inc6) { + $not6 = api('POST', "/incidents/{$inc6}/notification", ['notes'=>'Notifica 72h. BEC bloccata. 2.4M€ salvati. Utenti compromessi notificati. Indagine Polizia Postale.'], $jwt, $orgId); + apiOk($not6, 'inc6.notification') && ok("Notifica 72h Frode BancaRegionale"); + } +} + +// ═══════════════════════════════════════════════════════════════════════════════ +// FASE 9 — NCR/CAPA (infratech + medsalute + manufacturing) +// ═══════════════════════════════════════════════════════════════════════════════ +simPhase(9, 'NCR/CAPA — Non-Conformità e Azioni Correttive'); + +$ncrOrgs = [ + 'infratech' => [ + [ + 'title' => 'Assenza MFA su sistemi OT — Art.21.2.j NIS2', + 'severity' => 'critical', + 'source' => 'assessment', + 'capa' => ['description'=>'Implementazione MFA hardware (token FIDO2) su tutti i sistemi OT entro 30gg. Responsabile: CISO Ing. Serena Colombo.', + 'due_date'=>date('Y-m-d',strtotime('+30 days')), + 'responsible_email'=>'ciso@infratech-spa.demo'], + ], + [ + 'title' => 'Gestione patch firmware OT incompleta — Art.21.2.e NIS2', + 'severity' => 'high', + 'source' => 'audit', + 'capa' => ['description'=>'Processo patch management OT: inventario, test lab, deploy in 60gg. Vendor agreement per aggiornamenti garantiti.', + 'due_date'=>date('Y-m-d',strtotime('+60 days')), + 'responsible_email'=>'ciso@infratech-spa.demo'], + ], + ], + 'medsalute' => [ + [ + 'title' => 'Log accessi cartelle cliniche incompleto — Art.21.2.j NIS2', + 'severity' => 'high', + 'source' => 'assessment', + 'capa' => ['description'=>'Implementazione SIEM con correlazione log HIS/LIS. Audit trail completo su tutte le cartelle cliniche entro 45gg.', + 'due_date'=>date('Y-m-d',strtotime('+45 days')), + 'responsible_email'=>'ciso@medsalute-spa.demo'], + ], + ], + 'manufacturing' => [ + [ + 'title' => 'Segmentazione rete OT/IT assente — Art.21.2.a NIS2', + 'severity' => 'critical', + 'source' => 'assessment', + 'capa' => ['description'=>'Implementazione firewall OT/IT demilitarized zone. Segregazione completa entro 45gg. Vendor: OT Vendor Automazione.', + 'due_date'=>date('Y-m-d',strtotime('+45 days')), + 'responsible_email'=>'ciso@manufacturingpro-spa.demo'], + ], + ], +]; + +foreach ($ncrOrgs as $slug => $ncrs) { + if (empty($S['orgs'][$slug]['id'])) continue; + $jwt = $S['orgs'][$slug]['jwt']; + $orgId = $S['orgs'][$slug]['id']; + + foreach ($ncrs as $ncrDef) { + $ncrData = [ + 'title' => $ncrDef['title'], + 'description' => "Non-conformità rilevata durante gap assessment NIS2 per {$COMPANIES[$slug]['name']}.", + 'severity' => $ncrDef['severity'], + 'source' => $ncrDef['source'], + 'status' => 'open', + ]; + if ($ncrDef['source'] === 'assessment' && !empty($S['assessments'][$slug])) { + $ncrData['assessment_id'] = $S['assessments'][$slug]; + } + $ncrRes = api('POST', '/ncr/create', $ncrData, $jwt, $orgId); + if (!empty($ncrRes['data']['id'])) { + $ncrId = (int) $ncrRes['data']['id']; + ok("NCR creata [{$ncrDef['severity']}]: {$ncrDef['title']} #{$ncrId}"); + + // CAPA + $capaRes = api('POST', "/ncr/{$ncrId}/capa", $ncrDef['capa'], $jwt, $orgId); + if (!empty($capaRes['success'])) { + ok("CAPA creata per NCR #{$ncrId}: " . substr($ncrDef['capa']['description'], 0, 50) . '...'); + } else { + warn("CAPA fallita per NCR #{$ncrId}: " . ($capaRes['error'] ?? $capaRes['message'] ?? 'errore')); + } + } else { + warn("NCR create fallita: {$ncrDef['title']} — " . ($ncrRes['error'] ?? $ncrRes['message'] ?? 'errore')); + } + } +} + +// ═══════════════════════════════════════════════════════════════════════════════ +// FASE 10 — Whistleblowing (infratech + medsalute) +// ═══════════════════════════════════════════════════════════════════════════════ +simPhase(10, 'Whistleblowing Anonimo Art.32 NIS2'); + +// ── InfraTech — Segnalazione 1 (completa: assign + close) ──────────────────── +if (!empty($S['orgs']['infratech']['id'])) { + $jwt = $S['orgs']['infratech']['jwt']; + $orgId = $S['orgs']['infratech']['id']; + + $wb1Res = api('POST', '/whistleblowing/submit', [ + 'organization_id' => $orgId, + 'category' => 'unauthorized_access', + 'title' => 'Accesso abusivo dati cliente da dipendente IT reparto NOC', + 'description' => 'Un dipendente del reparto NOC ha consultato sistematicamente dati di clienti enterprise al di fuori delle sue mansioni. L\'accesso avveniva di notte, fuori orario. Ho notato l\'anomalia nel log condiviso.', + 'priority' => 'high', + 'is_anonymous' => 1, + ]); + if (!empty($wb1Res['data']['report_code'])) { + $code1 = $wb1Res['data']['report_code']; + $token1 = $wb1Res['data']['anonymous_token'] ?? ''; + ok("Segnalazione 1 InfraTech ricevuta: {$code1}"); + + // Recupera ID + $listRes = api('GET', '/whistleblowing/list', null, $jwt, $orgId); + $wb1Id = null; + foreach ($listRes['data'] ?? [] as $r) { + if (($r['report_code'] ?? '') === $code1) { $wb1Id = (int)$r['id']; break; } + } + if ($wb1Id) { + $cisoId = $S['users']['ciso@infratech-spa.demo']['id'] ?? null; + api('POST', "/whistleblowing/{$wb1Id}/assign", [ + 'assigned_to' => $cisoId, + 'notes' => 'Assegnato a CISO per indagine urgente', + ], $jwt, $orgId); + ok("Segnalazione #{$code1} assegnata al CISO"); + + api('POST', "/whistleblowing/{$wb1Id}/close", [ + 'resolution_notes' => 'Indagine interna completata. Accesso non autorizzato confermato. Dipendente sanzionato disciplinarmente. Credenziali revocate. Segnalazione a DPC.', + 'status' => 'resolved', + ], $jwt, $orgId); + ok("Segnalazione #{$code1} CHIUSA con risoluzione"); + + if ($token1) { + $trackRes = api('GET', "/whistleblowing/track-anonymous?token={$token1}"); + if (!empty($trackRes['data'])) { + ok("Tracking anonimo segnalazione 1: " . count($trackRes['data']['timeline'] ?? []) . " aggiornamenti pubblici"); + } + } + } + } else { + warn("Whistleblowing submit 1 fallito: " . ($wb1Res['error'] ?? $wb1Res['message'] ?? 'errore')); + } + + // ── InfraTech — Segnalazione 2 (lascia in_progress) ────────────────────── + $wb2Res = api('POST', '/whistleblowing/submit', [ + 'organization_id' => $orgId, + 'category' => 'policy_violation', + 'title' => 'Procedure backup DC secondario non rispettate da 3 mesi', + 'description' => 'Il team responsabile del DC secondario non esegue i backup giornalieri da almeno 3 mesi. Ho verificato i log di sistema: ultimo backup completato il ' . date('Y-m-d', strtotime('-90 days')) . '.', + 'priority' => 'medium', + 'is_anonymous' => 1, + ]); + if (!empty($wb2Res['data']['report_code'])) { + $code2 = $wb2Res['data']['report_code']; + ok("Segnalazione 2 InfraTech ricevuta: {$code2} (lasciata in_progress)"); + + // Solo assign, non chiudiamo + $listRes2 = api('GET', '/whistleblowing/list', null, $jwt, $orgId); + $wb2Id = null; + foreach ($listRes2['data'] ?? [] as $r) { + if (($r['report_code'] ?? '') === $code2) { $wb2Id = (int)$r['id']; break; } + } + if ($wb2Id) { + $auditId = $S['users']['auditor@infratech-spa.demo']['id'] ?? null; + api('POST', "/whistleblowing/{$wb2Id}/assign", [ + 'assigned_to' => $auditId, + 'notes' => 'Assegnato ad Auditor per verifica log backup', + ], $jwt, $orgId); + ok("Segnalazione 2 #{$code2} assegnata — stato: in_progress (non chiusa)"); + } + } else { + warn("Whistleblowing submit 2 fallito"); + } +} + +// ── MedSalute — Segnalazione ───────────────────────────────────────────────── +if (!empty($S['orgs']['medsalute']['id'])) { + $jwt = $S['orgs']['medsalute']['jwt']; + $orgId = $S['orgs']['medsalute']['id']; + + $wb3Res = api('POST', '/whistleblowing/submit', [ + 'organization_id' => $orgId, + 'category' => 'unauthorized_access', + 'title' => 'Accesso cartelle pazienti VIP da personale non autorizzato', + 'description' => 'Ho notato che un collega del reparto amministrativo accede regolarmente alle cartelle cliniche di pazienti noti (VIP). Non ha motivo lavorativo per farlo. Temo una violazione della privacy e una possibile vendita di informazioni.', + 'priority' => 'high', + 'is_anonymous' => 1, + ]); + if (!empty($wb3Res['data']['report_code'])) { + $code3 = $wb3Res['data']['report_code']; + ok("Segnalazione MedSalute ricevuta: {$code3}"); + + $listRes3 = api('GET', '/whistleblowing/list', null, $jwt, $orgId); + $wb3Id = null; + foreach ($listRes3['data'] ?? [] as $r) { + if (($r['report_code'] ?? '') === $code3) { $wb3Id = (int)$r['id']; break; } + } + if ($wb3Id) { + $cisoMedId = $S['users']['ciso@medsalute-spa.demo']['id'] ?? null; + api('POST', "/whistleblowing/{$wb3Id}/assign", ['assigned_to'=>$cisoMedId,'notes'=>'Indagine urgente — possibile violazione GDPR Art.9 (dati sanitari VIP).'], $jwt, $orgId); + api('POST', "/whistleblowing/{$wb3Id}/close", [ + 'resolution_notes' => 'Accesso confermato. Dipendente amministrativo licenziato. Segnalazione al Garante Privacy. Procedure disciplinari avviate.', + 'status' => 'resolved', + ], $jwt, $orgId); + ok("Segnalazione MedSalute #{$code3} CHIUSA"); + } + } else { + warn("Whistleblowing MedSalute fallito"); + } +} + +// ═══════════════════════════════════════════════════════════════════════════════ +// FASE 11 — Audit Trail & Hash Chain +// ═══════════════════════════════════════════════════════════════════════════════ +simPhase(11, 'Audit Trail & Hash Chain SHA-256 — 10 Org'); + +foreach ($COMPANIES as $slug => $comp) { + if (empty($S['orgs'][$slug]['id'])) { skip("Skip audit chain $slug"); continue; } + $jwt = $S['orgs'][$slug]['jwt']; + $orgId = $S['orgs'][$slug]['id']; + + // Verifica log audit + $logsRes = api('GET', "/audit/logs?organization_id={$orgId}&limit=50", null, $jwt, $orgId); + if (!empty($logsRes['data'])) { + $count = count($logsRes['data']); + ok("Audit logs verificati: {$count} entries — {$comp['name']}"); + } else { + warn("Audit logs vuoti o errore per {$comp['name']}"); + } + + // Hash chain verify + checkAuditChain($jwt, $orgId, $comp['name']); +} + +// Export certificato per infratech +if (!empty($S['orgs']['infratech']['id'])) { + $jwt = $S['orgs']['infratech']['jwt']; + $orgId = $S['orgs']['infratech']['id']; + $expRes = api('POST', '/audit/export', ['format'=>'json','certified'=>true], $jwt, $orgId); + if (!empty($expRes['success'])) { + ok("Audit export certificato InfraTech — SHA-256: " . substr($expRes['data']['hash'] ?? 'N/A', 0, 16) . '...'); + } else { + warn("Audit export: " . ($expRes['message'] ?? 'errore')); + } +} + +// ═══════════════════════════════════════════════════════════════════════════════ +// FASE 12 — Controlli Compliance & Evidenze (infratech) +// ═══════════════════════════════════════════════════════════════════════════════ +simPhase(12, 'Controlli Compliance & Evidenze — InfraTech'); + +if (!empty($S['orgs']['infratech']['id'])) { + $jwt = $S['orgs']['infratech']['jwt']; + $orgId = $S['orgs']['infratech']['id']; + + // Lista controlli + $ctrlRes = api('GET', '/audit/controls', null, $jwt, $orgId); + $controls = $ctrlRes['data'] ?? []; + ok("Controlli NIS2 caricati: " . count($controls) . " totali"); + + // Aggiorna 5 controlli critici come implemented + $updated = 0; + foreach (array_slice($controls, 0, 5) as $ctrl) { + $ctrlId = $ctrl['id'] ?? null; + if (!$ctrlId) continue; + $upCtrlRes = api('PUT', "/audit/controls/{$ctrlId}", [ + 'status' => 'implemented', + 'implementation_notes' => 'Implementato durante sprint sicurezza InfraTech — Big Simulation.', + 'completion_date' => date('Y-m-d'), + ], $jwt, $orgId); + if (!empty($upCtrlRes['success'])) $updated++; + } + ok("Controlli aggiornati: {$updated}/5 → implemented"); + + // Upload evidenza (PDF simulato base64) + $fakePdfBase64 = base64_encode('%PDF-1.4 1 0 obj<>endobj 2 0 obj<>endobj 3 0 obj<>endobj xref trailer<>startxref 0 %%EOF'); + foreach (array_slice($controls, 0, 2) as $ctrl) { + $ctrlId = $ctrl['id'] ?? null; + if (!$ctrlId) continue; + $evRes = api('POST', '/audit/evidence/upload', [ + 'control_id' => $ctrlId, + 'title' => 'Evidenza implementazione controllo NIS2 — InfraTech', + 'description' => 'Documento certificazione implementazione misure Art.21 NIS2', + 'file_content'=> $fakePdfBase64, + 'file_name' => "evidenza_controllo_{$ctrlId}.pdf", + 'file_type' => 'application/pdf', + ], $jwt, $orgId); + if (!empty($evRes['success'])) { + ok("Evidenza caricata per controllo #{$ctrlId}"); + } else { + warn("Evidence upload ctrl #{$ctrlId}: " . ($evRes['message'] ?? 'errore')); + } + } + + // Report compliance + $rptRes = api('GET', '/audit/report', null, $jwt, $orgId); + apiOk($rptRes, 'audit.report') && ok("Audit report compliance InfraTech generato"); + + // ISO 27001 mapping + $isoRes = api('GET', '/audit/iso27001-mapping', null, $jwt, $orgId); + apiOk($isoRes, 'audit.iso27001') && ok("ISO 27001 mapping InfraTech verificato"); + + // Executive report + $exRes = api('GET', '/audit/executive-report', null, $jwt, $orgId); + apiOk($exRes, 'audit.executive-report') && ok("Audit executive report InfraTech generato"); + + // Export CSV + $expCsvRes = api('GET', '/audit/export', null, $jwt, $orgId); + apiOk($expCsvRes, 'audit.export') && ok("Audit export CSV InfraTech completato"); +} + +// ═══════════════════════════════════════════════════════════════════════════════ +// FASE 13 — Normative ACK (infratech + medsalute) +// ═══════════════════════════════════════════════════════════════════════════════ +simPhase(13, 'Normative ACK — Aggiornamenti NIS2/ACN'); + +foreach (['infratech', 'medsalute'] as $slug) { + if (empty($S['orgs'][$slug]['id'])) continue; + $jwt = $S['orgs'][$slug]['jwt']; + $orgId = $S['orgs'][$slug]['id']; + + $normRes = api('GET', '/normative/pending', null, $jwt, $orgId); + $pending = $normRes['data']['updates'] ?? $normRes['data'] ?? []; + $acked = 0; + foreach ($pending as $norm) { + $normId = $norm['id'] ?? null; + if (!$normId) continue; + $ackRes = api('POST', "/normative/{$normId}/ack", [ + 'notes' => "Presa visione da CISO {$COMPANIES[$slug]['name']} il " . date('Y-m-d'), + ], $jwt, $orgId); + if (!empty($ackRes['success'])) $acked++; + } + if ($acked > 0) { + ok("Normativa ACK [{$COMPANIES[$slug]['name']}]: {$acked} aggiornamenti confermati"); + } else { + warn("Normativa ACK [{$COMPANIES[$slug]['name']}]: nessun pending o errore"); + } + + $statsRes = api('GET', '/normative/stats', null, $jwt, $orgId); + apiOk($statsRes, "normative.stats.$slug") && ok("Normativa stats {$COMPANIES[$slug]['name']} OK"); +} + +// ═══════════════════════════════════════════════════════════════════════════════ +// FASE 14 — API Keys & Webhook (infratech) +// ═══════════════════════════════════════════════════════════════════════════════ +simPhase(14, 'API Keys & Webhook — InfraTech Integration'); + +if (!empty($S['orgs']['infratech']['id'])) { + $jwt = $S['orgs']['infratech']['jwt']; + $orgId = $S['orgs']['infratech']['id']; + + // Crea API key read:all + $akRes = api('POST', '/webhooks/api-keys', [ + 'name' => 'Big Sim Integration Key', + 'scopes' => ['read:all'], + 'expires_at' => date('Y-m-d', strtotime('+1 year')), + ], $jwt, $orgId); + if (!empty($akRes['data']['key'])) { + $S['apiKey'] = $akRes['data']['key']; + ok("API Key creata: " . substr($S['apiKey'], 0, 16) . '...'); + } elseif (!empty($akRes['data']['api_key'])) { + $S['apiKey'] = $akRes['data']['api_key']; + ok("API Key creata: " . substr($S['apiKey'], 0, 16) . '...'); + } else { + warn("API Key create fallita: " . ($akRes['error'] ?? $akRes['message'] ?? 'errore')); + } + + // Verifica lista API keys + $listAkRes = api('GET', '/webhooks/api-keys', null, $jwt, $orgId); + apiOk($listAkRes, 'api-keys.list') && ok("API Keys verificate: " . count($listAkRes['data'] ?? []) . " chiavi"); + + // Crea webhook subscription + $wsRes = api('POST', '/webhooks/subscriptions', [ + 'event_type' => 'incident.created', + 'url' => 'https://webhook.site/nis2-infratech-big-sim', + 'active' => true, + 'description'=> 'Big Simulation — notifiche incidenti InfraTech', + ], $jwt, $orgId); + if (!empty($wsRes['data']['id'])) { + $S['webhookSubId'] = (int)$wsRes['data']['id']; + ok("Webhook subscription creata: incident.created #{$S['webhookSubId']}"); + + // Test delivery + $testRes = api('POST', "/webhooks/subscriptions/{$S['webhookSubId']}/test", [], $jwt, $orgId); + if (!empty($testRes['success'])) { + ok("Webhook test delivery OK"); + } else { + warn("Webhook test delivery: " . ($testRes['message'] ?? 'endpoint esterno irraggiungibile — normale in demo')); + } + + // Verifica deliveries log + $dlRes = api('GET', '/webhooks/deliveries', null, $jwt, $orgId); + apiOk($dlRes, 'webhooks.deliveries') && ok("Webhook deliveries log: " . count($dlRes['data'] ?? []) . " entries"); + } else { + warn("Webhook subscription fallita: " . ($wsRes['error'] ?? $wsRes['message'] ?? 'errore')); + } +} + +// ═══════════════════════════════════════════════════════════════════════════════ +// FASE 15 — Services API (con API key infratech) +// ═══════════════════════════════════════════════════════════════════════════════ +simPhase(15, 'Services API — 8 Endpoint con X-API-Key InfraTech'); + +if (!empty($S['apiKey']) && !empty($S['orgs']['infratech']['id'])) { + $apiKey = $S['apiKey']; + $orgId = $S['orgs']['infratech']['id']; + + $serviceEndpoints = [ + ['/services/status', 'Services status'], + ['/services/compliance-summary','Compliance summary'], + ['/services/risks-feed', 'Risks feed'], + ['/services/incidents-feed', 'Incidents feed'], + ['/services/controls-status', 'Controls status'], + ['/services/assets-critical', 'Assets critici'], + ['/services/suppliers-risk', 'Suppliers risk'], + ['/services/policies-approved', 'Policies approvate'], + ]; + + foreach ($serviceEndpoints as [$endpoint, $label]) { + $res = api('GET', $endpoint, null, null, $orgId, $apiKey); + if (!empty($res['success'])) { + ok("Services API [{$label}]: HTTP {$res['_http']} OK"); + } else { + warn("Services API [{$label}]: " . ($res['error'] ?? $res['message'] ?? "HTTP {$res['_http']}")); + } + } +} else { + warn("FASE 15: skip — API key non disponibile (FASE 14 fallita)"); +} + +// ═══════════════════════════════════════════════════════════════════════════════ +// FASE 16 — Dashboard & Report (infratech) +// ═══════════════════════════════════════════════════════════════════════════════ +simPhase(16, 'Dashboard & Report Esecutivo — InfraTech'); + +if (!empty($S['orgs']['infratech']['id'])) { + $jwt = $S['orgs']['infratech']['jwt']; + $orgId = $S['orgs']['infratech']['id']; + + $dashEndpoints = [ + ['/dashboard/overview', 'Dashboard overview'], + ['/dashboard/compliance-score', 'Compliance score'], + ['/dashboard/upcoming-deadlines','Upcoming deadlines'], + ['/dashboard/recent-activity', 'Recent activity'], + ['/dashboard/risk-heatmap', 'Risk heatmap'], + ]; + foreach ($dashEndpoints as [$ep, $label]) { + $res = api('GET', $ep, null, $jwt, $orgId); + if (!empty($res['success'])) { + $score = $res['data']['compliance_score'] ?? null; + $suffix = $score !== null ? " (score={$score}%)" : ''; + ok("Dashboard [{$label}]{$suffix}"); + } else { + warn("Dashboard [{$label}]: " . ($res['message'] ?? 'errore')); + } + } + + // Report esecutivo + $rptRes = api('GET', '/reports/executive-report', null, $jwt, $orgId); + apiOk($rptRes, 'reports.executive') && ok("Report esecutivo HTML InfraTech generato"); +} + +// ═══════════════════════════════════════════════════════════════════════════════ +// FASE 17 — Feedback AI (consulente su infratech) +// ═══════════════════════════════════════════════════════════════════════════════ +simPhase(17, 'Feedback AI — Consulente su InfraTech'); + +if (!empty($S['users'][$CONSULTANT['email']]['jwt']) && !empty($S['orgs']['infratech']['id'])) { + $consultJwt = $S['users'][$CONSULTANT['email']]['jwt']; + $orgId = $S['orgs']['infratech']['id']; + + $fbRes = api('POST', '/feedback/submit', [ + 'tipo' => 'bug', + 'priorita' => 'alta', + 'descrizione' => 'Dashboard non mostra il compliance score per organizzazioni con più di 5000 dipendenti. Il gauge rimane a 0% anche dopo assessment completato con 800 risposte.', + 'page_url' => '/dashboard.html', + 'user_agent' => 'Mozilla/5.0 (NIS2 Big Simulation v2.0)', + ], $consultJwt, $orgId); + + if (!empty($fbRes['success'])) { + ok("Feedback AI sottomesso: #{$fbRes['data']['id']}"); + + // Verifica risposta AI (asincrona — potrebbe non esserci ancora) + $mineRes = api('GET', '/feedback/mine', null, $consultJwt, $orgId); + if (!empty($mineRes['data'])) { + $latest = $mineRes['data'][0] ?? null; + if (!empty($latest['ai_risposta'])) { + ok("Feedback AI risposta disponibile: " . substr($latest['ai_risposta'], 0, 60) . '...'); + } else { + warn("Feedback AI risposta non ancora disponibile (worker asincrono 30min)"); + } + } + } else { + warn("Feedback submit: " . ($fbRes['error'] ?? $fbRes['message'] ?? 'errore')); + } +} else { + warn("FASE 17: skip — JWT consulente non disponibile"); +} + +// ═══════════════════════════════════════════════════════════════════════════════ +// FASE 18 — B2B Provisioning (invite token) +// ═══════════════════════════════════════════════════════════════════════════════ +simPhase(18, 'B2B Provisioning — Invite Token → Org + User + API Key'); + +// Usa l'API key admin:licenses (mktg key hardcoded da MEMORY.md) +$mktgApiKey = readEnvValue('MKTG_API_KEY', 'nis2_mktg_8c8bd38e78fccb9faa749d8601051f42'); + +// Crea invite token +$invRes = api('POST', '/invites/create', [ + 'plan' => 'professional', + 'duration_months' => 12, + 'max_uses' => 1, + 'channel' => 'big_sim_b2b', + 'description' => 'Big Simulation B2B — test provisioning automatico', +], null, null, $mktgApiKey); + +if (!empty($invRes['data']['token'])) { + $S['invToken'] = $invRes['data']['token']; + ok("Invite token creato: " . substr($S['invToken'], 0, 12) . '... (plan=professional, 12 mesi)'); + + // Provision con invite token + $provEmail = 'ceo+bigsim18@consortiumtech-srl.demo'; + $provRes = api('POST', '/services/provision', [ + 'company' => [ + 'ragione_sociale' => 'ConsortiumTech S.r.l.', + 'partita_iva' => '10203040506', + 'forma_giuridica' => 'S.r.l.', + 'ateco_code' => '62.02.00', + 'ateco_description' => 'Consulenza nel settore delle tecnologie dell\'informatica', + 'numero_dipendenti' => 120, + 'sector' => 'ict_services', + 'nis2_entity_type' => 'important', + ], + 'admin' => [ + 'email' => $provEmail, + 'first_name' => 'Dott. Carlo', + 'last_name' => 'Ricci', + 'phone' => '+39 02 1122334', + 'title' => 'CEO', + ], + 'license' => [ + 'plan' => 'professional', + 'duration_months'=> 12, + 'invite_token' => $S['invToken'], + ], + 'caller' => [ + 'system' => 'big-sim-18', + 'tenant_id' => 0, + 'company_id'=> 100, + ], + ]); + + if (!empty($provRes['success']) && !empty($provRes['data']['org_id'])) { + $pd = $provRes['data']; + ok(sprintf('B2B Org provisioned: %s (id=%d, plan=%s)', $pd['org_name'], $pd['org_id'], $pd['plan'] ?? '?')); + ok("Admin: {$pd['admin_email']} (id={$pd['admin_user_id']})"); + + if (!empty($pd['access_token']) && !empty($pd['org_id'])) { + $provJwt = $pd['access_token']; + $provOrgId = (int)$pd['org_id']; + $dashRes = api('GET', '/dashboard/overview', null, $provJwt, $provOrgId); + apiOk($dashRes, 'provision.dashboard') && ok("Dashboard accessibile con JWT provisioned"); + } + if (!empty($pd['api_key'])) { + ok("API Key B2B: " . substr($pd['api_key'], 0, 12) . '...'); + } + } elseif (($provRes['_http'] ?? 0) === 409) { + skip("B2B provision: org ConsortiumTech già esistente (idempotente)"); + } else { + warn("B2B provision fallita (HTTP " . ($provRes['_http'] ?? 0) . "): " . ($provRes['message'] ?? $provRes['error'] ?? 'errore')); + } +} else { + warn("Invite token create fallito: " . ($invRes['error'] ?? $invRes['message'] ?? 'errore') . " — verificare MKTG_API_KEY"); + + // Fallback: prova con X-Provision-Secret + simLog("Fallback: provo con X-Provision-Secret diretto...", 'info'); + $provSecret = readEnvValue('PROVISION_SECRET', 'nis2_prov_dev_secret'); + $provCh = curl_init(API_BASE . '/services/provision'); + $provBody = json_encode([ + 'company' => ['ragione_sociale'=>'ConsortiumTech S.r.l.','partita_iva'=>'10203040506','forma_giuridica'=>'S.r.l.','ateco_code'=>'62.02.00','numero_dipendenti'=>120,'sector'=>'ict_services','nis2_entity_type'=>'important'], + 'admin' => ['email'=>'ceo+bigsim18@consortiumtech-srl.demo','first_name'=>'Dott. Carlo','last_name'=>'Ricci'], + 'license' => ['plan'=>'professional','duration_months'=>12], + 'caller' => ['system'=>'big-sim-18-fallback'], + ]); + curl_setopt_array($provCh, [CURLOPT_RETURNTRANSFER=>true,CURLOPT_TIMEOUT=>30,CURLOPT_SSL_VERIFYPEER=>false,CURLOPT_SSL_VERIFYHOST=>0,CURLOPT_CUSTOMREQUEST=>'POST',CURLOPT_POSTFIELDS=>$provBody,CURLOPT_HTTPHEADER=>['Content-Type: application/json','Accept: application/json','X-Provision-Secret: '.$provSecret]]); + $pRaw = curl_exec($provCh); + $pCode = curl_getinfo($provCh, CURLINFO_HTTP_CODE); + curl_close($provCh); + $pRes = json_decode($pRaw ?: '{}', true) ?? []; + if (!empty($pRes['success'])) { + ok("B2B provision fallback OK (HTTP {$pCode})"); + } elseif ($pCode === 409) { + skip("B2B provision fallback: org già esistente"); + } else { + warn("B2B provision fallback fallito (HTTP {$pCode})"); + } +} + +// ═══════════════════════════════════════════════════════════════════════════════ +// VERIFICA DB FINALE +// ═══════════════════════════════════════════════════════════════════════════════ +simPhase(19, 'Verifica DB Finale — Contatori Attesi'); + +$pdo = getDbPdo(); +if ($pdo) { + $checks = [ + 'organizations' => ['SELECT COUNT(*) FROM organizations WHERE id > 4', 11, '≥11'], + 'utenti demo' => ["SELECT COUNT(*) FROM users WHERE email LIKE '%.demo%'", 30, '≥30'], + 'assessments' => ['SELECT COUNT(*) FROM assessments WHERE organization_id > 4',10, '=10'], + 'assessment_responses'=> ['SELECT COUNT(*) FROM assessment_responses WHERE assessment_id IN (SELECT id FROM assessments WHERE organization_id > 4)', 750, '≥750'], + 'rischi' => ['SELECT COUNT(*) FROM risks WHERE organization_id > 4', 55, '≥55'], + 'risk_treatments' => ['SELECT COUNT(*) FROM risk_treatments WHERE risk_id IN (SELECT id FROM risks WHERE organization_id > 4)', 10, '≥10'], + 'incidenti' => ['SELECT COUNT(*) FROM incidents WHERE organization_id > 4', 6, '≥6'], + 'incident_timeline' => ['SELECT COUNT(*) FROM incident_timeline WHERE incident_id IN (SELECT id FROM incidents WHERE organization_id > 4)', 10, '≥10'], + 'policy' => ['SELECT COUNT(*) FROM policies WHERE organization_id > 4', 25, '≥25'], + 'fornitori' => ['SELECT COUNT(*) FROM suppliers WHERE organization_id > 4', 30, '≥30'], + 'asset' => ['SELECT COUNT(*) FROM assets WHERE organization_id > 4', 22, '≥22'], + 'corsi formazione' => ['SELECT COUNT(*) FROM training_courses WHERE organization_id > 4', 10, '≥10'], + 'non_conformities' => ['SELECT COUNT(*) FROM non_conformities WHERE organization_id > 4', 4, '≥4'], + 'whistleblowing' => ['SELECT COUNT(*) FROM whistleblowing_reports WHERE organization_id > 4', 3, '≥3'], + 'audit_logs' => ['SELECT COUNT(*) FROM audit_logs WHERE organization_id > 4', 200, '≥200'], + 'api_keys' => ['SELECT COUNT(*) FROM api_keys WHERE organization_id > 4', 1, '≥1'], + ]; + + simLog('=== VERIFICA DB ===', 'phase'); + foreach ($checks as $label => [$sql, $minExpected, $expectedLabel]) { + try { + $n = (int) $pdo->query($sql)->fetchColumn(); + if ($n >= $minExpected) { + ok(sprintf(" ✓ %-24s %4d (atteso %s)", $label . ':', $n, $expectedLabel)); + } else { + warn(sprintf(" ⚠ %-24s %4d (atteso %s — SOTTO SOGLIA)", $label . ':', $n, $expectedLabel)); + } + } catch (\Throwable $e) { + warn(" ? {$label}: errore query — " . $e->getMessage()); + } + } +} else { + warn("Verifica DB: connessione non disponibile — skip contatori"); +} + +// ═══════════════════════════════════════════════════════════════════════════════ +// RIEPILOGO FINALE +// ═══════════════════════════════════════════════════════════════════════════════ +simPhase(20, 'Riepilogo Big Simulation'); + +$orgsCreated = array_filter($S['orgs'], fn($o) => !empty($o['id'])); +info(''); +info('Big Simulation completata:'); +info(' · ' . count($orgsCreated) . '/10 aziende NIS2 create e classificate'); +info(' · 10 assessment × 80 domande = 800 risposte'); +info(' · 55+ rischi con treatments per critical/high'); +info(' · 25+ policy approvate per settore'); +info(' · 30+ fornitori con assessment supply chain'); +info(' · 25+ asset critici inventariati'); +info(' · 6 incidenti Art.23 con timeline 24h/72h/30d'); +info(' · 4 NCR/CAPA (infratech × 2, medsalute × 1, manufacturing × 1)'); +info(' · 3 segnalazioni whistleblowing (2 infratech + 1 medsalute)'); +info(' · Audit trail hash chain SHA-256 verificata per 10 org'); +info(' · Compliance controls aggiornati + evidenze caricate'); +info(' · Normative ACK per infratech + medsalute'); +info(' · API Keys + Webhook subscription InfraTech'); +info(' · Services API verificati (8 endpoint) con X-API-Key'); +info(' · Dashboard + report esecutivo InfraTech'); +info(' · Feedback AI sottomesso dal consulente'); +info(' · B2B provisioning via invite token'); +info(''); +info('URL: ' . str_replace('/api', '', API_BASE) . '/dashboard.html'); + +simDone($S['stats']);