[], '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 { // Mappa type → asset_type (colonna DB) e normalizza ENUM // ENUM DB: hardware, software, network, data, service, personnel, facility $typeMap = [ 'ot_system' => 'hardware', 'server' => 'hardware', 'datacenter' => 'facility', 'network' => 'network', 'software' => 'software', 'hardware' => 'hardware', 'data' => 'data', 'service' => 'service', 'facility' => 'facility', 'personnel' => 'personnel', ]; $rawType = $data['type'] ?? $data['asset_type'] ?? 'hardware'; $data['asset_type'] = $typeMap[$rawType] ?? 'hardware'; unset($data['type']); $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'] ?? $res['message'] ?? '')); 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_ids' => [$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' => 'system_failure', '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' => 'other', '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' => 'cyber_attack', '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' => 'other', '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' => 'major', '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' => 'major', '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']);