[FEAT] Simulazioni Demo + Audit Trail Certificato SHA-256
- 5 scenari reali: Onboarding, Ransomware Art.23, Data Breach Supply Chain, Whistleblowing SCADA, Audit Hash Chain Verification - simulate-nis2.php: 3 aziende (DataCore/MedClinic/EnerNet), 10 fasi, CLI+SSE - AuditService.php: hash chain SHA-256 stile lg231 (prev_hash+entry_hash) - Migration 010: prev_hash, entry_hash, severity, performed_by su audit_logs - AuditController: GET chain-verify + GET export-certified - reset-demo.sql: reset dati demo idempotente - public/simulate.html: web runner SSE con console dark-theme - Sidebar: link Simulazione Demo + Integrazioni Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
b23bbc55fd
commit
874eabb6fc
@ -238,4 +238,79 @@ class AuditController extends BaseController
|
||||
echo $csv;
|
||||
exit;
|
||||
}
|
||||
|
||||
/**
|
||||
* GET /api/audit/chain-verify
|
||||
* Verifica l'integrità dell'hash chain per l'organizzazione corrente.
|
||||
* Risponde con: valid, total, hashed, coverage_pct, broken_at, last_hash
|
||||
*/
|
||||
public function chainVerify(): void
|
||||
{
|
||||
$this->requireOrgRole(['org_admin', 'auditor']);
|
||||
$orgId = $this->getCurrentOrgId();
|
||||
|
||||
$db = Database::getInstance();
|
||||
$result = AuditService::verifyChain($db, $orgId);
|
||||
|
||||
// Se la catena è rotta, registra la violazione
|
||||
if (!$result['valid'] && $result['broken_at'] !== null) {
|
||||
$performer = $this->currentUser['email'] ?? 'system';
|
||||
try {
|
||||
$ins = $db->prepare(
|
||||
"INSERT INTO audit_violations
|
||||
(organization_id, detected_by, broken_at_id, chain_length, notes)
|
||||
VALUES (:org, :by, :bid, :len, :notes)"
|
||||
);
|
||||
$ins->execute([
|
||||
'org' => $orgId,
|
||||
'by' => $performer,
|
||||
'bid' => $result['broken_at'],
|
||||
'len' => $result['total'],
|
||||
'notes' => 'Violazione rilevata tramite API chain-verify',
|
||||
]);
|
||||
} catch (Throwable $e) {
|
||||
error_log('[AuditController] chain violation log error: ' . $e->getMessage());
|
||||
}
|
||||
|
||||
$this->logAudit('audit.chain_broken', 'audit_logs', $result['broken_at'], [
|
||||
'total' => $result['total'],
|
||||
'coverage_pct' => $result['coverage_pct'],
|
||||
]);
|
||||
}
|
||||
|
||||
$this->jsonSuccess($result);
|
||||
}
|
||||
|
||||
/**
|
||||
* GET /api/audit/export-certified
|
||||
* Genera un export JSON certificato con hash SHA-256 dell'intero contenuto.
|
||||
* Adatto per ispezioni ACN, audit NIS2 Art.32, certificazione ISO 27001.
|
||||
*/
|
||||
public function exportCertified(): void
|
||||
{
|
||||
$this->requireOrgRole(['org_admin', 'auditor']);
|
||||
$orgId = $this->getCurrentOrgId();
|
||||
$userId = $this->getCurrentUserId();
|
||||
$email = $this->currentUser['email'] ?? 'system';
|
||||
|
||||
$db = Database::getInstance();
|
||||
$result = AuditService::exportCertified(
|
||||
$db,
|
||||
$orgId,
|
||||
$userId,
|
||||
$email,
|
||||
$_GET['purpose'] ?? 'export_certificato',
|
||||
$_GET['from'] ?? null,
|
||||
$_GET['to'] ?? null
|
||||
);
|
||||
|
||||
$this->logAudit('audit.export_certified', 'audit_logs', null, [
|
||||
'records_count' => $result['records_count'],
|
||||
'chain_valid' => $result['chain_valid'],
|
||||
'export_hash' => $result['export_hash'],
|
||||
]);
|
||||
|
||||
header('Content-Disposition: attachment; filename="nis2_audit_certified_' . date('Y-m-d') . '.json"');
|
||||
$this->jsonSuccess($result);
|
||||
}
|
||||
}
|
||||
|
||||
@ -7,6 +7,7 @@
|
||||
*/
|
||||
|
||||
require_once APP_PATH . '/config/database.php';
|
||||
require_once APP_PATH . '/services/AuditService.php';
|
||||
|
||||
class BaseController
|
||||
{
|
||||
@ -533,16 +534,23 @@ class BaseController
|
||||
*/
|
||||
protected function logAudit(string $action, ?string $entityType = null, ?int $entityId = null, ?array $details = null): void
|
||||
{
|
||||
Database::insert('audit_logs', [
|
||||
'user_id' => $this->getCurrentUserId(),
|
||||
'organization_id' => $this->currentOrgId,
|
||||
'action' => $action,
|
||||
'entity_type' => $entityType,
|
||||
'entity_id' => $entityId,
|
||||
'details' => $details ? json_encode($details) : null,
|
||||
'ip_address' => $_SERVER['REMOTE_ADDR'] ?? null,
|
||||
'user_agent' => $_SERVER['HTTP_USER_AGENT'] ?? null,
|
||||
]);
|
||||
$orgId = $this->currentOrgId;
|
||||
$userId = $this->getCurrentUserId();
|
||||
$severity = AuditService::resolveSeverity($action, $details);
|
||||
$email = $this->currentUser['email'] ?? null;
|
||||
|
||||
AuditService::log(
|
||||
$orgId ?? 0,
|
||||
$userId,
|
||||
$action,
|
||||
$entityType,
|
||||
$entityId,
|
||||
$details,
|
||||
$_SERVER['REMOTE_ADDR'] ?? '',
|
||||
$_SERVER['HTTP_USER_AGENT'] ?? null,
|
||||
$severity,
|
||||
$email
|
||||
);
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════════
|
||||
|
||||
310
application/services/AuditService.php
Normal file
310
application/services/AuditService.php
Normal file
@ -0,0 +1,310 @@
|
||||
<?php
|
||||
/**
|
||||
* NIS2 Agile — Audit Trail Certificato SHA-256
|
||||
*
|
||||
* Hash chain: ogni record include l'hash del record precedente per la stessa
|
||||
* organization_id. La manomissione di qualsiasi record rompe la catena
|
||||
* e viene rilevata da verifyChain().
|
||||
*
|
||||
* Adattato dal pattern AuditTrailService di 231 Agile (lg231-agile/shared/audit-lib).
|
||||
*
|
||||
* Uso:
|
||||
* AuditService::log($orgId, $userId, 'incident.create', 'incident', 42, ['severity'=>'high']);
|
||||
* $result = AuditService::verifyChain($db, $orgId);
|
||||
*/
|
||||
class AuditService
|
||||
{
|
||||
/**
|
||||
* Registra un'azione nell'audit trail con hash chain SHA-256.
|
||||
* Non lancia eccezioni — il fallimento del log non blocca l'operazione principale.
|
||||
*
|
||||
* @param int $orgId organization_id
|
||||
* @param int|null $userId user_id (null = sistema/anonimo)
|
||||
* @param string $action Azione es. 'incident.create', 'risk.update', 'policy.approve'
|
||||
* @param string|null $entityType Tipo entità: 'incident','risk','policy','supplier','asset',...
|
||||
* @param int|null $entityId ID entità
|
||||
* @param array|null $details Dati aggiuntivi (serializzati come JSON)
|
||||
* @param string $ipAddress IP del client
|
||||
* @param string|null $userAgent User-Agent
|
||||
* @param string $severity 'info' | 'warning' | 'critical'
|
||||
* @param string|null $performedBy Email/identificatore utente (per export certificato)
|
||||
*/
|
||||
public static function log(
|
||||
int $orgId,
|
||||
?int $userId,
|
||||
string $action,
|
||||
?string $entityType = null,
|
||||
?int $entityId = null,
|
||||
?array $details = null,
|
||||
string $ipAddress = '',
|
||||
?string $userAgent = null,
|
||||
string $severity = 'info',
|
||||
?string $performedBy = null
|
||||
): void {
|
||||
try {
|
||||
$db = Database::getInstance();
|
||||
|
||||
$prevHash = self::getLastHash($db, $orgId);
|
||||
$detailsJson = $details ? self::canonicalJson($details) : '';
|
||||
$createdAt = date('Y-m-d H:i:s');
|
||||
|
||||
// Calcola hash catena: deterministico su dati noti prima dell'INSERT
|
||||
$entryHash = hash('sha256',
|
||||
$orgId .
|
||||
($userId ?? 'system') .
|
||||
$action .
|
||||
($entityType ?? '') .
|
||||
($entityId ?? '') .
|
||||
$detailsJson .
|
||||
$createdAt .
|
||||
($prevHash ?? '')
|
||||
);
|
||||
|
||||
$stmt = $db->prepare(
|
||||
"INSERT INTO audit_logs
|
||||
(organization_id, user_id, action, entity_type, entity_id,
|
||||
details, ip_address, user_agent,
|
||||
prev_hash, entry_hash, severity, performed_by, created_at)
|
||||
VALUES
|
||||
(:org, :uid, :act, :et, :eid,
|
||||
:det, :ip, :ua,
|
||||
:prev, :hash, :sev, :by, :ts)"
|
||||
);
|
||||
$stmt->execute([
|
||||
'org' => $orgId,
|
||||
'uid' => $userId,
|
||||
'act' => $action,
|
||||
'et' => $entityType,
|
||||
'eid' => $entityId,
|
||||
'det' => $details ? json_encode($details, JSON_UNESCAPED_UNICODE) : null,
|
||||
'ip' => $ipAddress ?: ($_SERVER['REMOTE_ADDR'] ?? ''),
|
||||
'ua' => $userAgent ?: ($_SERVER['HTTP_USER_AGENT'] ?? null),
|
||||
'prev' => $prevHash,
|
||||
'hash' => $entryHash,
|
||||
'sev' => $severity,
|
||||
'by' => $performedBy,
|
||||
'ts' => $createdAt,
|
||||
]);
|
||||
} catch (Throwable $e) {
|
||||
error_log('[AuditService] log error: ' . $e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Verifica l'integrità della hash chain per un'organizzazione.
|
||||
* Ricalcola ogni entry_hash e confronta con il valore salvato.
|
||||
*
|
||||
* @param PDO $db Connessione PDO
|
||||
* @param int $orgId organization_id
|
||||
* @return array {valid: bool, total: int, hashed: int, coverage_pct: float,
|
||||
* broken_at: int|null, last_hash: string|null, verified_at: string}
|
||||
*/
|
||||
public static function verifyChain(PDO $db, int $orgId): array
|
||||
{
|
||||
$stmt = $db->prepare(
|
||||
"SELECT id, organization_id, user_id, action, entity_type, entity_id,
|
||||
details, prev_hash, entry_hash, severity, performed_by, created_at
|
||||
FROM audit_logs
|
||||
WHERE organization_id = :org
|
||||
ORDER BY id ASC"
|
||||
);
|
||||
$stmt->execute(['org' => $orgId]);
|
||||
$rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||
|
||||
$valid = true;
|
||||
$brokenAt = null;
|
||||
$lastHash = null;
|
||||
$hashed = 0;
|
||||
|
||||
foreach ($rows as $row) {
|
||||
if (!empty($row['entry_hash'])) {
|
||||
$hashed++;
|
||||
}
|
||||
|
||||
// Record senza hash (pre-migrazione) → skip verifica ma non rompe catena
|
||||
if (empty($row['entry_hash'])) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$detailsDecoded = $row['details'] ? json_decode($row['details'], true) : null;
|
||||
$detailsJson = $detailsDecoded ? self::canonicalJson($detailsDecoded) : '';
|
||||
|
||||
$expected = hash('sha256',
|
||||
$row['organization_id'] .
|
||||
($row['user_id'] ?? 'system') .
|
||||
$row['action'] .
|
||||
($row['entity_type'] ?? '') .
|
||||
($row['entity_id'] ?? '') .
|
||||
$detailsJson .
|
||||
$row['created_at'] .
|
||||
($row['prev_hash'] ?? '')
|
||||
);
|
||||
|
||||
if ($expected !== $row['entry_hash']) {
|
||||
$valid = false;
|
||||
$brokenAt = (int) $row['id'];
|
||||
break;
|
||||
}
|
||||
|
||||
$lastHash = $row['entry_hash'];
|
||||
}
|
||||
|
||||
$total = count($rows);
|
||||
return [
|
||||
'valid' => $valid,
|
||||
'total' => $total,
|
||||
'hashed' => $hashed,
|
||||
'coverage_pct' => $total > 0 ? round($hashed / $total * 100, 1) : 0.0,
|
||||
'broken_at' => $brokenAt,
|
||||
'last_hash' => $lastHash,
|
||||
'verified_at' => date('Y-m-d H:i:s'),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Esporta il registro audit in formato JSON certificato con SHA-256.
|
||||
*
|
||||
* @return array {data: array, export_hash: string, records_count: int,
|
||||
* chain_valid: bool, exported_at: string}
|
||||
*/
|
||||
public static function exportCertified(
|
||||
PDO $db,
|
||||
int $orgId,
|
||||
int $exportedBy,
|
||||
string $performedBy = 'system',
|
||||
string $purpose = 'export_certificato',
|
||||
?string $dateFrom = null,
|
||||
?string $dateTo = null
|
||||
): array {
|
||||
$sql = "SELECT * FROM audit_logs WHERE organization_id = :org";
|
||||
$params = ['org' => $orgId];
|
||||
|
||||
if ($dateFrom) { $sql .= " AND created_at >= :from"; $params['from'] = $dateFrom . ' 00:00:00'; }
|
||||
if ($dateTo) { $sql .= " AND created_at <= :to"; $params['to'] = $dateTo . ' 23:59:59'; }
|
||||
$sql .= " ORDER BY id ASC";
|
||||
|
||||
$stmt = $db->prepare($sql);
|
||||
$stmt->execute($params);
|
||||
$rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||
|
||||
$chainResult = self::verifyChain($db, $orgId);
|
||||
$exportedAt = date('Y-m-d\TH:i:sP');
|
||||
|
||||
$exportData = [
|
||||
'meta' => [
|
||||
'standard' => 'NIS2 EU 2022/2555 — D.Lgs.138/2024 — ISO27001:2022',
|
||||
'generator' => 'NIS2 Agile AuditService v1.0',
|
||||
'org_id' => $orgId,
|
||||
'exported_by' => $performedBy,
|
||||
'exported_at' => $exportedAt,
|
||||
'purpose' => $purpose,
|
||||
'records_count' => count($rows),
|
||||
'chain_valid' => $chainResult['valid'],
|
||||
'chain_coverage' => $chainResult['coverage_pct'],
|
||||
'last_hash' => $chainResult['last_hash'],
|
||||
'date_from' => $dateFrom,
|
||||
'date_to' => $dateTo,
|
||||
],
|
||||
'records' => $rows,
|
||||
];
|
||||
|
||||
$exportJson = json_encode($exportData, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT);
|
||||
$exportHash = hash('sha256', $exportJson);
|
||||
|
||||
// Salva metadati export
|
||||
try {
|
||||
$ins = $db->prepare(
|
||||
"INSERT INTO audit_exports
|
||||
(organization_id, exported_by, performed_by, format, records_count,
|
||||
date_from, date_to, export_hash, chain_valid, purpose)
|
||||
VALUES (:org, :uid, :by, 'json', :cnt, :df, :dt, :hash, :cv, :pur)"
|
||||
);
|
||||
$ins->execute([
|
||||
'org' => $orgId,
|
||||
'uid' => $exportedBy,
|
||||
'by' => $performedBy,
|
||||
'cnt' => count($rows),
|
||||
'df' => $dateFrom,
|
||||
'dt' => $dateTo,
|
||||
'hash' => $exportHash,
|
||||
'cv' => $chainResult['valid'] ? 1 : 0,
|
||||
'pur' => $purpose,
|
||||
]);
|
||||
} catch (Throwable $e) {
|
||||
error_log('[AuditService] exportCertified save error: ' . $e->getMessage());
|
||||
}
|
||||
|
||||
return [
|
||||
'data' => $exportData,
|
||||
'export_hash' => $exportHash,
|
||||
'records_count' => count($rows),
|
||||
'chain_valid' => $chainResult['valid'],
|
||||
'exported_at' => $exportedAt,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Recupera l'entry_hash dell'ultimo record per questa org (chain linking).
|
||||
*/
|
||||
private static function getLastHash(PDO $db, int $orgId): ?string
|
||||
{
|
||||
$stmt = $db->prepare(
|
||||
"SELECT entry_hash FROM audit_logs
|
||||
WHERE organization_id = :org AND entry_hash != ''
|
||||
ORDER BY id DESC LIMIT 1"
|
||||
);
|
||||
$stmt->execute(['org' => $orgId]);
|
||||
$row = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||
return $row ? ($row['entry_hash'] ?: null) : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Serializzazione JSON canonica con chiavi ordinate ricorsivamente.
|
||||
* Garantisce che logAction e verifyChain producano la stessa stringa di input all'hash.
|
||||
*/
|
||||
private static function canonicalJson(array $data): string
|
||||
{
|
||||
$data = self::sortKeysRecursively($data);
|
||||
return json_encode($data, JSON_UNESCAPED_UNICODE);
|
||||
}
|
||||
|
||||
private static function sortKeysRecursively(array $data): array
|
||||
{
|
||||
ksort($data);
|
||||
foreach ($data as $k => $v) {
|
||||
if (is_array($v)) {
|
||||
$data[$k] = self::sortKeysRecursively($v);
|
||||
}
|
||||
}
|
||||
return $data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Mappa action → severity per auto-classificazione degli eventi.
|
||||
*/
|
||||
public static function resolveSeverity(string $action, ?array $details = null): string
|
||||
{
|
||||
$criticalActions = [
|
||||
'incident.create', 'incident.significant', 'incident.deadline_warning',
|
||||
'risk.create_high', 'risk.create_critical',
|
||||
'whistleblowing.received', 'whistleblowing.critical',
|
||||
'audit.chain_broken', 'auth.login_failed_repeated',
|
||||
];
|
||||
$warningActions = [
|
||||
'risk.create_medium', 'incident.update', 'policy.rejected',
|
||||
'supplier.risk_flagged', 'normative.update', 'auth.password_changed',
|
||||
];
|
||||
|
||||
if (in_array($action, $criticalActions, true)) return 'critical';
|
||||
if (in_array($action, $warningActions, true)) return 'warning';
|
||||
|
||||
// Escalate su risk level nei details
|
||||
if ($details) {
|
||||
$level = $details['risk_level'] ?? $details['severity'] ?? $details['priority'] ?? null;
|
||||
if (in_array($level, ['critical', 'high'], true)) return 'critical';
|
||||
if (in_array($level, ['medium'], true)) return 'warning';
|
||||
}
|
||||
|
||||
return 'info';
|
||||
}
|
||||
}
|
||||
113
docs/sql/010_audit_hash_chain.sql
Normal file
113
docs/sql/010_audit_hash_chain.sql
Normal file
@ -0,0 +1,113 @@
|
||||
-- ============================================================
|
||||
-- NIS2 Agile - Migration 010: Audit Trail Certificato SHA-256
|
||||
-- Hash chain per immutabilità certificabile (ispirato da lg231 AuditTrailService)
|
||||
-- Estende la tabella audit_logs con prev_hash, entry_hash, severity, performed_by
|
||||
-- ============================================================
|
||||
-- Eseguire su Hetzner:
|
||||
-- mysql -u nis2_agile_user -p nis2_agile_db < docs/sql/010_audit_hash_chain.sql
|
||||
|
||||
USE nis2_agile_db;
|
||||
|
||||
-- ── Aggiunta colonne (idempotente via information_schema) ──────────────────
|
||||
|
||||
-- prev_hash: hash del record precedente per la stessa organization_id
|
||||
SET @has_prev = (
|
||||
SELECT COUNT(*) FROM information_schema.COLUMNS
|
||||
WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'audit_logs' AND COLUMN_NAME = 'prev_hash'
|
||||
);
|
||||
SET @sql_prev = IF(@has_prev = 0,
|
||||
'ALTER TABLE audit_logs ADD COLUMN prev_hash VARCHAR(64) NULL AFTER details',
|
||||
'SELECT ''prev_hash already exists'' AS note'
|
||||
);
|
||||
PREPARE s FROM @sql_prev; EXECUTE s; DEALLOCATE PREPARE s;
|
||||
|
||||
-- entry_hash: SHA-256 di questo record (include prev_hash per la catena)
|
||||
SET @has_entry = (
|
||||
SELECT COUNT(*) FROM information_schema.COLUMNS
|
||||
WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'audit_logs' AND COLUMN_NAME = 'entry_hash'
|
||||
);
|
||||
SET @sql_entry = IF(@has_entry = 0,
|
||||
'ALTER TABLE audit_logs ADD COLUMN entry_hash VARCHAR(64) NOT NULL DEFAULT \'\' AFTER prev_hash',
|
||||
'SELECT ''entry_hash already exists'' AS note'
|
||||
);
|
||||
PREPARE s FROM @sql_entry; EXECUTE s; DEALLOCATE PREPARE s;
|
||||
|
||||
-- severity: criticità dell'evento per filtro dashboard
|
||||
SET @has_sev = (
|
||||
SELECT COUNT(*) FROM information_schema.COLUMNS
|
||||
WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'audit_logs' AND COLUMN_NAME = 'severity'
|
||||
);
|
||||
SET @sql_sev = IF(@has_sev = 0,
|
||||
"ALTER TABLE audit_logs ADD COLUMN severity ENUM('info','warning','critical') NOT NULL DEFAULT 'info' AFTER entry_hash",
|
||||
'SELECT ''severity already exists'' AS note'
|
||||
);
|
||||
PREPARE s FROM @sql_sev; EXECUTE s; DEALLOCATE PREPARE s;
|
||||
|
||||
-- performed_by: email/identificatore utente (denormalizzato per export certificato)
|
||||
SET @has_by = (
|
||||
SELECT COUNT(*) FROM information_schema.COLUMNS
|
||||
WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'audit_logs' AND COLUMN_NAME = 'performed_by'
|
||||
);
|
||||
SET @sql_by = IF(@has_by = 0,
|
||||
'ALTER TABLE audit_logs ADD COLUMN performed_by VARCHAR(255) NULL AFTER severity',
|
||||
'SELECT ''performed_by already exists'' AS note'
|
||||
);
|
||||
PREPARE s FROM @sql_by; EXECUTE s; DEALLOCATE PREPARE s;
|
||||
|
||||
-- ── Indici ────────────────────────────────────────────────────────────────
|
||||
-- Indice su entry_hash per verifica integrità e ricerca
|
||||
SET @has_idx_hash = (
|
||||
SELECT COUNT(*) FROM information_schema.STATISTICS
|
||||
WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'audit_logs' AND INDEX_NAME = 'idx_entry_hash'
|
||||
);
|
||||
SET @sql_idx = IF(@has_idx_hash = 0,
|
||||
'ALTER TABLE audit_logs ADD INDEX idx_entry_hash (entry_hash)',
|
||||
'SELECT ''idx_entry_hash already exists'' AS note'
|
||||
);
|
||||
PREPARE s FROM @sql_idx; EXECUTE s; DEALLOCATE PREPARE s;
|
||||
|
||||
-- Indice su severity per filtro dashboard
|
||||
SET @has_idx_sev = (
|
||||
SELECT COUNT(*) FROM information_schema.STATISTICS
|
||||
WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'audit_logs' AND INDEX_NAME = 'idx_severity'
|
||||
);
|
||||
SET @sql_isev = IF(@has_idx_sev = 0,
|
||||
'ALTER TABLE audit_logs ADD INDEX idx_severity (severity, organization_id)',
|
||||
'SELECT ''idx_severity already exists'' AS note'
|
||||
);
|
||||
PREPARE s FROM @sql_isev; EXECUTE s; DEALLOCATE PREPARE s;
|
||||
|
||||
-- ── Tabella export certificati audit ─────────────────────────────────────
|
||||
CREATE TABLE IF NOT EXISTS audit_exports (
|
||||
id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
|
||||
organization_id INT NOT NULL,
|
||||
exported_by INT NULL, -- user_id (NULL = sistema)
|
||||
performed_by VARCHAR(255) NULL, -- email utente
|
||||
format ENUM('json','xml','csv') NOT NULL DEFAULT 'json',
|
||||
records_count INT UNSIGNED NOT NULL DEFAULT 0,
|
||||
date_from DATE NULL,
|
||||
date_to DATE NULL,
|
||||
export_hash VARCHAR(64) NOT NULL, -- SHA-256 del contenuto export
|
||||
chain_valid TINYINT(1) NOT NULL DEFAULT 1, -- integrità al momento export
|
||||
purpose VARCHAR(100) NOT NULL DEFAULT 'export_certificato',
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
|
||||
INDEX idx_org (organization_id),
|
||||
INDEX idx_created (created_at)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
|
||||
-- ── Tabella violazioni integrità catena ───────────────────────────────────
|
||||
CREATE TABLE IF NOT EXISTS audit_violations (
|
||||
id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
|
||||
organization_id INT NOT NULL,
|
||||
detected_by VARCHAR(255) NOT NULL DEFAULT 'system',
|
||||
broken_at_id INT NULL, -- audit_logs.id dove la catena si rompe
|
||||
chain_length INT UNSIGNED NOT NULL DEFAULT 0, -- record totali verificati
|
||||
notes TEXT NULL,
|
||||
resolved TINYINT(1) NOT NULL DEFAULT 0,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
|
||||
INDEX idx_org (organization_id)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
|
||||
SELECT 'Migration 010 completata: audit_logs con hash chain SHA-256 certificato' AS result;
|
||||
106
docs/sql/reset-demo.sql
Normal file
106
docs/sql/reset-demo.sql
Normal file
@ -0,0 +1,106 @@
|
||||
-- ============================================================
|
||||
-- NIS2 Agile — Reset Dati Demo
|
||||
-- Cancella tutti i dati generati dalla simulazione, mantenendo
|
||||
-- solo le organizzazioni seed (id <= 4) e i loro utenti.
|
||||
--
|
||||
-- Eseguire su Hetzner:
|
||||
-- ssh -i docs/credentials/hetzner_key root@135.181.149.254
|
||||
-- mysql -u nis2_agile_user -p nis2_agile_db < /var/www/nis2-agile/docs/sql/reset-demo.sql
|
||||
-- ============================================================
|
||||
|
||||
USE nis2_agile_db;
|
||||
|
||||
SET FOREIGN_KEY_CHECKS = 0;
|
||||
|
||||
-- ── Dati operativi generati dalla simulazione ─────────────────────────────
|
||||
|
||||
-- Notifiche incidenti / timeline
|
||||
DELETE FROM incident_timeline WHERE organization_id > 4;
|
||||
DELETE FROM incidents WHERE organization_id > 4;
|
||||
|
||||
-- Rischi e trattamenti
|
||||
DELETE FROM risk_treatments WHERE risk_id IN (SELECT id FROM risks WHERE organization_id > 4);
|
||||
DELETE FROM risks WHERE organization_id > 4;
|
||||
|
||||
-- Assessment e risposte
|
||||
DELETE FROM assessment_responses WHERE assessment_id IN (SELECT id FROM assessments WHERE organization_id > 4);
|
||||
DELETE FROM assessments WHERE organization_id > 4;
|
||||
|
||||
-- Policy
|
||||
DELETE FROM policies WHERE organization_id > 4;
|
||||
|
||||
-- Fornitori
|
||||
DELETE FROM suppliers WHERE organization_id > 4;
|
||||
|
||||
-- Training
|
||||
DELETE FROM training_assignments WHERE course_id IN (SELECT id FROM training_courses WHERE organization_id > 4);
|
||||
DELETE FROM training_courses WHERE organization_id > 4;
|
||||
|
||||
-- Asset
|
||||
DELETE FROM assets WHERE organization_id > 4;
|
||||
|
||||
-- Controlli compliance
|
||||
DELETE FROM compliance_controls WHERE organization_id > 4;
|
||||
|
||||
-- Evidenze
|
||||
DELETE FROM evidence_files WHERE organization_id > 4;
|
||||
|
||||
-- Non conformità e CAPA
|
||||
DELETE FROM corrective_actions WHERE non_conformity_id IN (SELECT id FROM non_conformities WHERE organization_id > 4);
|
||||
DELETE FROM non_conformities WHERE organization_id > 4;
|
||||
|
||||
-- Whistleblowing
|
||||
DELETE FROM whistleblowing_timeline
|
||||
WHERE report_id IN (SELECT id FROM whistleblowing_reports WHERE organization_id > 4);
|
||||
DELETE FROM whistleblowing_reports WHERE organization_id > 4;
|
||||
|
||||
-- Normativa ACK
|
||||
DELETE FROM normative_ack WHERE organization_id > 4;
|
||||
|
||||
-- API keys e webhook
|
||||
DELETE FROM webhook_deliveries
|
||||
WHERE subscription_id IN (SELECT id FROM webhook_subscriptions WHERE organization_id > 4);
|
||||
DELETE FROM webhook_subscriptions WHERE organization_id > 4;
|
||||
DELETE FROM api_keys WHERE organization_id > 4;
|
||||
|
||||
-- Audit log (solo dati demo, non i record di sistema id <= 100)
|
||||
DELETE FROM audit_logs WHERE organization_id > 4;
|
||||
DELETE FROM audit_exports WHERE organization_id > 4;
|
||||
DELETE FROM audit_violations WHERE organization_id > 4;
|
||||
|
||||
-- AI interactions
|
||||
DELETE FROM ai_interactions WHERE organization_id > 4;
|
||||
|
||||
-- Email log
|
||||
DELETE FROM email_log WHERE organization_id > 4;
|
||||
|
||||
-- ── Membership utenti ─────────────────────────────────────────────────────
|
||||
|
||||
-- Rimuove le associazioni utente-organizzazione demo
|
||||
DELETE FROM user_organizations WHERE organization_id > 4;
|
||||
|
||||
-- Rimuove token refresh degli utenti demo
|
||||
DELETE rt FROM refresh_tokens rt
|
||||
JOIN users u ON rt.user_id = u.id
|
||||
WHERE u.email LIKE '%.demo%';
|
||||
|
||||
-- Rimuove utenti demo (email terminano con .demo)
|
||||
DELETE FROM users WHERE email LIKE '%.demo%';
|
||||
|
||||
-- ── Organizzazioni demo ───────────────────────────────────────────────────
|
||||
DELETE FROM organizations WHERE id > 4;
|
||||
|
||||
-- ── Ripristino FK ─────────────────────────────────────────────────────────
|
||||
SET FOREIGN_KEY_CHECKS = 1;
|
||||
|
||||
-- ── Verifica stato ────────────────────────────────────────────────────────
|
||||
SELECT
|
||||
(SELECT COUNT(*) FROM organizations) AS organizations,
|
||||
(SELECT COUNT(*) FROM users) AS users,
|
||||
(SELECT COUNT(*) FROM incidents) AS incidents,
|
||||
(SELECT COUNT(*) FROM risks) AS risks,
|
||||
(SELECT COUNT(*) FROM audit_logs) AS audit_logs,
|
||||
(SELECT COUNT(*) FROM whistleblowing_reports) AS whistleblowing
|
||||
;
|
||||
|
||||
SELECT 'Reset demo completato. Dati seed mantenuti (id <= 4).' AS stato;
|
||||
@ -264,6 +264,8 @@ $actionMap = [
|
||||
'GET:iso27001Mapping' => 'getIsoMapping',
|
||||
'GET:executiveReport' => 'executiveReport',
|
||||
'GET:export' => 'export',
|
||||
'GET:chainVerify' => 'chainVerify',
|
||||
'GET:exportCertified' => 'exportCertified',
|
||||
],
|
||||
|
||||
// ── AdminController ─────────────────────────────
|
||||
|
||||
@ -213,6 +213,8 @@ function loadSidebar() {
|
||||
items: [
|
||||
{ name: 'Impostazioni', href: 'settings.html', icon: iconCog(), i18nKey: 'nav.settings' },
|
||||
{ name: 'Architettura', href: 'architecture.html', icon: iconCubeTransparent() },
|
||||
{ name: 'Simulazione Demo', href: 'simulate.html', icon: `<svg viewBox="0 0 20 20" fill="currentColor"><path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zM9.555 7.168A1 1 0 008 8v4a1 1 0 001.555.832l3-2a1 1 0 000-1.664l-3-2z" clip-rule="evenodd"/></svg>` },
|
||||
{ name: 'Integrazioni', href: 'integrations/index.html', icon: `<svg viewBox="0 0 20 20" fill="currentColor"><path d="M11 3a1 1 0 100 2h2.586l-6.293 6.293a1 1 0 101.414 1.414L15 6.414V9a1 1 0 102 0V4a1 1 0 00-1-1h-5z"/><path d="M5 5a2 2 0 00-2 2v8a2 2 0 002 2h8a2 2 0 002-2v-3a1 1 0 10-2 0v3H5V7h3a1 1 0 000-2H5z"/></svg>` },
|
||||
]
|
||||
}
|
||||
];
|
||||
|
||||
374
public/simulate.html
Normal file
374
public/simulate.html
Normal file
@ -0,0 +1,374 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="it">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>NIS2 Agile — Simulazione Demo</title>
|
||||
<style>
|
||||
* { box-sizing: border-box; margin: 0; padding: 0; }
|
||||
:root {
|
||||
--primary: #06b6d4; --green: #22c55e; --red: #ef4444;
|
||||
--yellow: #f59e0b; --gray: #334155; --dark: #0f172a;
|
||||
--card: #1e293b; --border: #334155;
|
||||
}
|
||||
body { background: var(--dark); color: #e2e8f0; font-family: -apple-system,BlinkMacSystemFont,'Segoe UI',system-ui,sans-serif; min-height: 100vh; }
|
||||
|
||||
.header { background: linear-gradient(135deg,#0c4a6e,#0e7490); padding: 32px 48px; border-bottom: 1px solid #0e7490; }
|
||||
.header h1 { font-size: 1.5rem; font-weight: 800; color: #fff; margin-bottom: 4px; }
|
||||
.header p { color: #7dd3fc; font-size: 0.875rem; }
|
||||
.badge { display: inline-block; padding: 2px 10px; border-radius: 20px; font-size: 0.7rem; font-weight: 700; margin-right: 8px; }
|
||||
.badge-cyan { background: rgba(6,182,212,.2); color: #67e8f9; border: 1px solid rgba(6,182,212,.3); }
|
||||
.badge-green { background: rgba(34,197,94,.2); color: #86efac; border: 1px solid rgba(34,197,94,.3); }
|
||||
|
||||
.container { max-width: 1100px; margin: 0 auto; padding: 32px 24px; display: grid; grid-template-columns: 320px 1fr; gap: 24px; }
|
||||
|
||||
/* Left panel */
|
||||
.panel { background: var(--card); border: 1px solid var(--border); border-radius: 12px; padding: 20px; }
|
||||
.panel h3 { font-size: 0.875rem; font-weight: 700; color: #94a3b8; text-transform: uppercase; letter-spacing: .05em; margin-bottom: 16px; }
|
||||
|
||||
.sim-card { background: #0f172a; border: 1px solid var(--border); border-radius: 8px; padding: 14px; margin-bottom: 10px; cursor: pointer; transition: border-color .2s; }
|
||||
.sim-card:hover { border-color: var(--primary); }
|
||||
.sim-card.active { border-color: var(--primary); background: #0c2230; }
|
||||
.sim-card h4 { font-size: 0.8125rem; font-weight: 700; color: #e2e8f0; margin-bottom: 4px; }
|
||||
.sim-card p { font-size: 0.75rem; color: #64748b; line-height: 1.5; }
|
||||
.sim-card .sim-badge { font-size: 0.65rem; padding: 1px 7px; border-radius: 10px; font-weight: 700; margin-bottom: 6px; display: inline-block; }
|
||||
|
||||
.company-list { margin-bottom: 20px; }
|
||||
.company-item { display: flex; align-items: center; gap: 10px; padding: 10px 12px; border-radius: 8px; background: #0f172a; border: 1px solid var(--border); margin-bottom: 8px; }
|
||||
.company-dot { width: 8px; height: 8px; border-radius: 50%; flex-shrink: 0; }
|
||||
.dot-cyan { background: #06b6d4; }
|
||||
.dot-purple { background: #8b5cf6; }
|
||||
.dot-green { background: #22c55e; }
|
||||
.company-item span { font-size: 0.8rem; color: #cbd5e1; }
|
||||
.company-item small { font-size: 0.7rem; color: #64748b; display: block; }
|
||||
|
||||
/* Controls */
|
||||
.controls { display: flex; flex-direction: column; gap: 10px; margin-bottom: 16px; }
|
||||
.btn { padding: 10px 20px; border: none; border-radius: 8px; font-size: 0.875rem; font-weight: 700; cursor: pointer; transition: all .2s; }
|
||||
.btn-primary { background: var(--primary); color: #0f172a; }
|
||||
.btn-primary:hover:not(:disabled) { background: #0891b2; }
|
||||
.btn-primary:disabled { opacity: .5; cursor: not-allowed; }
|
||||
.btn-danger { background: #7f1d1d; color: #fca5a5; border: 1px solid #991b1b; }
|
||||
.btn-danger:hover:not(:disabled) { background: #991b1b; }
|
||||
.btn-gray { background: #1e293b; color: #94a3b8; border: 1px solid var(--border); }
|
||||
.btn-gray:hover { background: #334155; }
|
||||
|
||||
.status-bar { padding: 10px 12px; border-radius: 8px; font-size: 0.8rem; background: #0f172a; border: 1px solid var(--border); }
|
||||
.status-dot { width: 8px; height: 8px; border-radius: 50%; display: inline-block; margin-right: 6px; }
|
||||
.dot-idle { background: #475569; }
|
||||
.dot-running { background: #22c55e; animation: pulse 1s infinite; }
|
||||
.dot-done { background: #06b6d4; }
|
||||
.dot-error { background: #ef4444; }
|
||||
@keyframes pulse { 0%,100%{opacity:1} 50%{opacity:.4} }
|
||||
|
||||
.stats-grid { display: grid; grid-template-columns: repeat(4,1fr); gap: 8px; margin-top: 12px; }
|
||||
.stat-box { background: #0f172a; border: 1px solid var(--border); border-radius: 6px; padding: 10px; text-align: center; }
|
||||
.stat-box .num { font-size: 1.25rem; font-weight: 800; }
|
||||
.stat-box .lbl { font-size: 0.65rem; color: #64748b; text-transform: uppercase; margin-top: 2px; }
|
||||
.num-pass { color: var(--green); }
|
||||
.num-skip { color: #94a3b8; }
|
||||
.num-warn { color: var(--yellow); }
|
||||
.num-fail { color: var(--red); }
|
||||
|
||||
/* Right panel — Console */
|
||||
.console-panel { display: flex; flex-direction: column; gap: 0; }
|
||||
.console-header { background: #1e293b; border: 1px solid var(--border); border-radius: 12px 12px 0 0; padding: 12px 16px; display: flex; align-items: center; justify-content: space-between; }
|
||||
.console-header span { font-size: 0.8rem; color: #94a3b8; }
|
||||
.console-dots { display: flex; gap: 6px; }
|
||||
.console-dot { width: 10px; height: 10px; border-radius: 50%; }
|
||||
.console-dot-r { background: #ef4444; }
|
||||
.console-dot-y { background: #f59e0b; }
|
||||
.console-dot-g { background: #22c55e; }
|
||||
#console { background: #0a0f1a; border: 1px solid var(--border); border-top: none; border-radius: 0 0 12px 12px; padding: 16px; height: 520px; overflow-y: auto; font-family: 'Cascadia Code','Consolas',monospace; font-size: 0.78rem; line-height: 1.7; }
|
||||
.log-phase { color: #38bdf8; font-weight: 700; border-top: 1px solid #1e3a5f; padding-top: 8px; margin-top: 4px; }
|
||||
.log-ok { color: #86efac; }
|
||||
.log-skip { color: #64748b; }
|
||||
.log-warn { color: #fcd34d; }
|
||||
.log-error { color: #fca5a5; }
|
||||
.log-email { color: #67e8f9; }
|
||||
.log-info { color: #94a3b8; }
|
||||
.log-done { color: #a78bfa; font-weight: 700; border-top: 1px solid #3730a3; padding-top: 8px; margin-top: 4px; }
|
||||
|
||||
/* Progress bar */
|
||||
.progress-wrap { height: 4px; background: #1e293b; border-radius: 2px; overflow: hidden; margin-bottom: 16px; }
|
||||
.progress-bar { height: 100%; background: var(--primary); width: 0%; transition: width .3s; }
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.container { grid-template-columns: 1fr; }
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<div class="header">
|
||||
<div style="margin-bottom:10px;">
|
||||
<span class="badge badge-cyan">NIS2 Agile</span>
|
||||
<span class="badge badge-green">Demo Simulator v1.0</span>
|
||||
</div>
|
||||
<h1>Simulazione Demo Realistica</h1>
|
||||
<p>Genera dati demo NIS2 realistici tramite API reali — 3 aziende, 5 scenari, audit trail certificato SHA-256</p>
|
||||
</div>
|
||||
|
||||
<div class="container">
|
||||
|
||||
<!-- Left Panel -->
|
||||
<div style="display:flex;flex-direction:column;gap:16px;">
|
||||
|
||||
<div class="panel">
|
||||
<h3>3 Aziende Demo</h3>
|
||||
<div class="company-list">
|
||||
<div class="company-item">
|
||||
<div class="company-dot dot-cyan"></div>
|
||||
<div>
|
||||
<span>DataCore S.r.l.</span>
|
||||
<small>IT/Cloud · Essential Entity · 320 dip · Milano</small>
|
||||
</div>
|
||||
</div>
|
||||
<div class="company-item">
|
||||
<div class="company-dot dot-purple"></div>
|
||||
<div>
|
||||
<span>MedClinic Italia S.p.A.</span>
|
||||
<small>Sanità · Important Entity · 750 dip · Roma</small>
|
||||
</div>
|
||||
</div>
|
||||
<div class="company-item">
|
||||
<div class="company-dot dot-green"></div>
|
||||
<div>
|
||||
<span>EnerNet Distribuzione S.r.l.</span>
|
||||
<small>Energia · Essential/Critical · 1800 dip · Torino</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="panel">
|
||||
<h3>5 Scenari Reali</h3>
|
||||
|
||||
<div class="sim-card active" onclick="selectSim(this, 'all')">
|
||||
<div class="sim-badge" style="background:rgba(6,182,212,.15);color:#67e8f9;border:1px solid rgba(6,182,212,.3);">COMPLETA</div>
|
||||
<h4>Tutti gli scenari</h4>
|
||||
<p>Esegue tutti e 5 gli scenari in sequenza: onboarding, incidenti, data breach, whistleblowing, audit chain.</p>
|
||||
</div>
|
||||
<div class="sim-card" onclick="selectSim(this, 'sim01')">
|
||||
<div class="sim-badge" style="background:rgba(34,197,94,.1);color:#86efac;border:1px solid rgba(34,197,94,.3);">SIM-01</div>
|
||||
<h4>Onboarding + Gap Assessment</h4>
|
||||
<p>Registra le 3 aziende, classifica NIS2 (Essential/Important), esegue assessment 80 domande Art.21.</p>
|
||||
</div>
|
||||
<div class="sim-card" onclick="selectSim(this, 'sim02')">
|
||||
<div class="sim-badge" style="background:rgba(239,68,68,.1);color:#fca5a5;border:1px solid rgba(239,68,68,.3);">SIM-02</div>
|
||||
<h4>Ransomware Art.23 DataCore</h4>
|
||||
<p>Attacco ransomware su infrastruttura cloud. Timeline 24h/72h/30d con notifiche ACN e CSIRT.</p>
|
||||
</div>
|
||||
<div class="sim-card" onclick="selectSim(this, 'sim03')">
|
||||
<div class="sim-badge" style="background:rgba(139,92,246,.1);color:#ddd6fe;border:1px solid rgba(139,92,246,.3);">SIM-03</div>
|
||||
<h4>Data Breach Supply Chain</h4>
|
||||
<p>Fornitore LIS compromesso, esfiltrazione 2.340 pazienti. GDPR Art.33 + NIS2 Art.23 in parallelo.</p>
|
||||
</div>
|
||||
<div class="sim-card" onclick="selectSim(this, 'sim04')">
|
||||
<div class="sim-badge" style="background:rgba(245,158,11,.1);color:#fcd34d;border:1px solid rgba(245,158,11,.3);">SIM-04</div>
|
||||
<h4>Whistleblowing SCADA EnerNet</h4>
|
||||
<p>Segnalazione anonima accesso non autorizzato SCADA. Token tracking, investigazione, chiusura.</p>
|
||||
</div>
|
||||
<div class="sim-card" onclick="selectSim(this, 'sim05')">
|
||||
<div class="sim-badge" style="background:rgba(6,182,212,.1);color:#67e8f9;border:1px solid rgba(6,182,212,.3);">SIM-05</div>
|
||||
<h4>Audit Trail Hash Chain</h4>
|
||||
<p>Verifica integrità SHA-256 hash chain per le 3 org. Export certificato NIS2 + ISO 27001.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="panel">
|
||||
<h3>Controlli</h3>
|
||||
<div class="controls">
|
||||
<button id="btnRun" class="btn btn-primary" onclick="runSimulation()">▶ Avvia Simulazione</button>
|
||||
<button id="btnStop" class="btn btn-danger" onclick="stopSimulation()" disabled>■ Interrompi</button>
|
||||
<button class="btn btn-gray" onclick="clearConsole()">⌫ Pulisci Console</button>
|
||||
<button class="btn btn-gray" onclick="resetDemo()">↺ Reset Dati Demo</button>
|
||||
</div>
|
||||
<div class="progress-wrap"><div class="progress-bar" id="progressBar"></div></div>
|
||||
<div class="status-bar">
|
||||
<span class="status-dot dot-idle" id="statusDot"></span>
|
||||
<span id="statusText">In attesa</span>
|
||||
</div>
|
||||
<div class="stats-grid" id="statsGrid" style="display:none;">
|
||||
<div class="stat-box"><div class="num num-pass" id="sPass">0</div><div class="lbl">Pass</div></div>
|
||||
<div class="stat-box"><div class="num num-skip" id="sSkip">0</div><div class="lbl">Skip</div></div>
|
||||
<div class="stat-box"><div class="num num-warn" id="sWarn">0</div><div class="lbl">Warn</div></div>
|
||||
<div class="stat-box"><div class="num num-fail" id="sFail">0</div><div class="lbl">Fail</div></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<!-- Right panel — Console -->
|
||||
<div class="console-panel">
|
||||
<div class="console-header">
|
||||
<div class="console-dots">
|
||||
<div class="console-dot console-dot-r"></div>
|
||||
<div class="console-dot console-dot-y"></div>
|
||||
<div class="console-dot console-dot-g"></div>
|
||||
</div>
|
||||
<span id="consoleTitle">NIS2 Agile Simulator — Console</span>
|
||||
<span id="elapsedTime"></span>
|
||||
</div>
|
||||
<div id="console">
|
||||
<div class="log-info">NIS2 Agile Demo Simulator v1.0 — Pronto</div>
|
||||
<div class="log-info">Seleziona uno scenario e premi "Avvia Simulazione".</div>
|
||||
<div class="log-info" style="margin-top:8px;">Scenari disponibili:</div>
|
||||
<div class="log-ok"> SIM-01 Onboarding + Gap Assessment 80 domande</div>
|
||||
<div class="log-ok"> SIM-02 Incidente Ransomware Art.23 (24h/72h/30d)</div>
|
||||
<div class="log-ok"> SIM-03 Data Breach Supply Chain + GDPR</div>
|
||||
<div class="log-ok"> SIM-04 Whistleblowing anonimo SCADA</div>
|
||||
<div class="log-ok"> SIM-05 Audit Trail Hash Chain Verification</div>
|
||||
<div class="log-info" style="margin-top:8px;">Tutti i dati vengono creati tramite API reali (nessun INSERT SQL diretto).</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<script>
|
||||
let evtSource = null;
|
||||
let selectedSim = 'all';
|
||||
let startTime = null;
|
||||
let timerInterval = null;
|
||||
|
||||
function selectSim(el, sim) {
|
||||
document.querySelectorAll('.sim-card').forEach(c => c.classList.remove('active'));
|
||||
el.classList.add('active');
|
||||
selectedSim = sim;
|
||||
}
|
||||
|
||||
function setStatus(text, dotClass) {
|
||||
document.getElementById('statusText').textContent = text;
|
||||
const dot = document.getElementById('statusDot');
|
||||
dot.className = 'status-dot ' + dotClass;
|
||||
}
|
||||
|
||||
function appendLog(msg, type) {
|
||||
const console = document.getElementById('console');
|
||||
const line = document.createElement('div');
|
||||
line.className = 'log-' + (type || 'info');
|
||||
line.textContent = msg;
|
||||
console.appendChild(line);
|
||||
console.scrollTop = console.scrollHeight;
|
||||
}
|
||||
|
||||
function clearConsole() {
|
||||
document.getElementById('console').innerHTML = '<div class="log-info">Console pulita.</div>';
|
||||
}
|
||||
|
||||
function updateProgress(pct) {
|
||||
document.getElementById('progressBar').style.width = pct + '%';
|
||||
}
|
||||
|
||||
function updateStats(stats) {
|
||||
if (!stats) return;
|
||||
document.getElementById('statsGrid').style.display = 'grid';
|
||||
document.getElementById('sPass').textContent = stats.pass || 0;
|
||||
document.getElementById('sSkip').textContent = stats.skip || 0;
|
||||
document.getElementById('sWarn').textContent = stats.warn || 0;
|
||||
document.getElementById('sFail').textContent = stats.fail || 0;
|
||||
}
|
||||
|
||||
function startTimer() {
|
||||
startTime = Date.now();
|
||||
timerInterval = setInterval(() => {
|
||||
const elapsed = ((Date.now() - startTime) / 1000).toFixed(1);
|
||||
document.getElementById('elapsedTime').textContent = elapsed + 's';
|
||||
}, 500);
|
||||
}
|
||||
|
||||
function stopTimer() {
|
||||
if (timerInterval) clearInterval(timerInterval);
|
||||
}
|
||||
|
||||
function runSimulation() {
|
||||
if (evtSource) { evtSource.close(); evtSource = null; }
|
||||
|
||||
const runBtn = document.getElementById('btnRun');
|
||||
const stopBtn = document.getElementById('btnStop');
|
||||
runBtn.disabled = true;
|
||||
stopBtn.disabled = false;
|
||||
|
||||
clearConsole();
|
||||
setStatus('Simulazione in corso...', 'dot-running');
|
||||
updateProgress(5);
|
||||
startTimer();
|
||||
document.getElementById('statsGrid').style.display = 'none';
|
||||
|
||||
const url = `../simulate-nis2.php?sim=${selectedSim}&t=${Date.now()}`;
|
||||
evtSource = new EventSource(url);
|
||||
|
||||
const phaseKeywords = ['FASE', '══'];
|
||||
let phaseCount = 0;
|
||||
|
||||
evtSource.onmessage = (e) => {
|
||||
const data = JSON.parse(e.data);
|
||||
|
||||
if (data.t === 'done') {
|
||||
evtSource.close();
|
||||
runBtn.disabled = false;
|
||||
stopBtn.disabled = true;
|
||||
setStatus('Completata', 'dot-done');
|
||||
stopTimer();
|
||||
updateProgress(100);
|
||||
updateStats(data.stats);
|
||||
appendLog('', 'info');
|
||||
appendLog('Simulazione completata.', 'done');
|
||||
document.getElementById('consoleTitle').textContent = 'NIS2 Agile Simulator — Completata';
|
||||
return;
|
||||
}
|
||||
|
||||
const type = data.t || 'info';
|
||||
const msg = data.m || '';
|
||||
|
||||
appendLog(msg, type);
|
||||
|
||||
// Stima progress
|
||||
if (type === 'phase' || msg.includes('FASE')) {
|
||||
phaseCount++;
|
||||
updateProgress(Math.min(5 + phaseCount * 8, 95));
|
||||
}
|
||||
};
|
||||
|
||||
evtSource.onerror = () => {
|
||||
evtSource.close();
|
||||
runBtn.disabled = false;
|
||||
stopBtn.disabled = true;
|
||||
setStatus('Errore connessione', 'dot-error');
|
||||
stopTimer();
|
||||
appendLog('Errore SSE — connessione interrotta.', 'error');
|
||||
};
|
||||
}
|
||||
|
||||
function stopSimulation() {
|
||||
if (evtSource) { evtSource.close(); evtSource = null; }
|
||||
document.getElementById('btnRun').disabled = false;
|
||||
document.getElementById('btnStop').disabled = true;
|
||||
setStatus('Interrotta', 'dot-error');
|
||||
stopTimer();
|
||||
appendLog('Simulazione interrotta manualmente.', 'warn');
|
||||
}
|
||||
|
||||
function resetDemo() {
|
||||
if (!confirm('Reset dati demo? Verranno eliminati tutti i dati con organization_id > 4.')) return;
|
||||
fetch('../api/admin/reset-demo', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': 'Bearer ' + (localStorage.getItem('nis2_access_token') || ''),
|
||||
},
|
||||
})
|
||||
.then(r => r.json())
|
||||
.then(d => {
|
||||
if (d.success) {
|
||||
appendLog('Reset demo completato.', 'ok');
|
||||
} else {
|
||||
appendLog('Reset demo fallito: ' + (d.error || 'errore'), 'error');
|
||||
appendLog('Tip: eseguire manualmente docs/sql/reset-demo.sql su Hetzner.', 'warn');
|
||||
}
|
||||
})
|
||||
.catch(() => appendLog('Reset demo: errore di rete — eseguire manualmente su server.', 'warn'));
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
983
simulate-nis2.php
Normal file
983
simulate-nis2.php
Normal file
@ -0,0 +1,983 @@
|
||||
<?php
|
||||
/**
|
||||
* NIS2 Agile — Simulazione Demo Realistica
|
||||
*
|
||||
* Costruisce dati demo realistici attraverso le API reali (no INSERT SQL diretto).
|
||||
* Simula 3 aziende con settori NIS2 differenti, complessità crescente:
|
||||
*
|
||||
* A) DataCore S.r.l. — IT/Cloud Essential Entity, 320 dip, Milano
|
||||
* B) MedClinic Italia S.p.A. — Sanità Important Entity, 750 dip, Roma
|
||||
* C) EnerNet Distribuzione S.r.l — Energia Essential/Critical, 1800 dip, Torino
|
||||
*
|
||||
* 5 Scenari reali:
|
||||
* SIM-01 Onboarding + Gap Assessment completo (tutte e 3 le aziende)
|
||||
* SIM-02 Incidente Ransomware Art.23 DataCore (timeline 24h/72h/30d)
|
||||
* SIM-03 Data Breach Supply Chain MedClinic (fornitore IT compromesso)
|
||||
* SIM-04 Whistleblowing anonimo EnerNet (accesso non autorizzato SCADA)
|
||||
* SIM-05 Audit Trail hash chain verification (tutte e 3 le aziende)
|
||||
*
|
||||
* Utilizzo CLI:
|
||||
* php simulate-nis2.php [--reset] [--sim=N] [--company=slug]
|
||||
*
|
||||
* Dal browser (SSE streaming):
|
||||
* public/simulate.html → pulsante "Avvia Simulazione"
|
||||
*
|
||||
* Tutte le email vengono redirezionate a DEMO_EMAIL.
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
// ── Configurazione ──────────────────────────────────────────────────────────
|
||||
|
||||
define('SIM_VERSION', '1.0.0');
|
||||
define('DEMO_EMAIL', getenv('NIS2_DEMO_EMAIL') ?: 'demo@nis2agile.it');
|
||||
define('DEMO_PWD', 'NIS2Demo2026!');
|
||||
define('IS_CLI', php_sapi_name() === 'cli');
|
||||
define('IS_WEB', !IS_CLI);
|
||||
|
||||
// URL base API — rileva automaticamente ambiente
|
||||
if (IS_CLI) {
|
||||
// Sul server Hetzner: Apache serve /var/www/nis2-agile su https://nis2.certisource.it/
|
||||
define('API_BASE', getenv('NIS2_API_BASE') ?: 'https://nis2.certisource.it/api');
|
||||
} else {
|
||||
// In web: usa URL relativo allo stesso host
|
||||
$proto = (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off') ? 'https' : 'http';
|
||||
$host = $_SERVER['HTTP_HOST'] ?? 'localhost';
|
||||
define('API_BASE', "{$proto}://{$host}/api");
|
||||
}
|
||||
|
||||
// ── Output SSE ──────────────────────────────────────────────────────────────
|
||||
|
||||
if (IS_WEB) {
|
||||
header('Content-Type: text/event-stream');
|
||||
header('Cache-Control: no-cache');
|
||||
header('X-Accel-Buffering: no');
|
||||
ob_implicit_flush(true);
|
||||
while (ob_get_level()) ob_end_flush();
|
||||
}
|
||||
|
||||
// Stato globale simulazione
|
||||
$S = [
|
||||
'jwt' => [], // ['email' => token]
|
||||
'orgs' => [], // ['slug' => ['id', 'name', 'jwt']]
|
||||
'users' => [], // ['email' => ['id', 'jwt']]
|
||||
'stats' => ['pass' => 0, 'skip' => 0, 'fail' => 0, 'warn' => 0],
|
||||
'sim_start'=> microtime(true),
|
||||
];
|
||||
|
||||
// ── Output helpers ──────────────────────────────────────────────────────────
|
||||
|
||||
function simLog(string $msg, string $type = 'info'): void
|
||||
{
|
||||
$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 {
|
||||
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): 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;
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
// ── Idempotent helpers ───────────────────────────────────────────────────────
|
||||
|
||||
/** Registra un utente se non esiste già, ritorna il JWT. */
|
||||
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'];
|
||||
}
|
||||
|
||||
// Prova login
|
||||
$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, 'id' => $loginRes['data']['user']['id'] ?? null];
|
||||
skip("Login $email (già registrato)");
|
||||
return $jwt;
|
||||
}
|
||||
|
||||
// Registrazione
|
||||
$regRes = api('POST', '/auth/register', [
|
||||
'first_name' => $firstName,
|
||||
'last_name' => $lastName,
|
||||
'email' => DEMO_EMAIL !== $email ? DEMO_EMAIL : $email,
|
||||
'password' => $password,
|
||||
'role' => $role,
|
||||
]);
|
||||
// Override email per demo (sempre DEMO_EMAIL ma tracking per slug)
|
||||
$regRes2 = api('POST', '/auth/register', [
|
||||
'first_name' => $firstName,
|
||||
'last_name' => $lastName,
|
||||
'email' => $email,
|
||||
'password' => $password,
|
||||
'role' => $role,
|
||||
]);
|
||||
if (!empty($regRes2['data']['access_token'])) {
|
||||
$jwt = $regRes2['data']['access_token'];
|
||||
$uid = $regRes2['data']['user']['id'] ?? null;
|
||||
$S['users'][$email] = ['jwt' => $jwt, 'id' => $uid];
|
||||
ok("Registrato: $firstName $lastName <$email>");
|
||||
return $jwt;
|
||||
}
|
||||
if (!empty($regRes2['success'])) {
|
||||
// Login dopo registrazione
|
||||
$loginRes2 = api('POST', '/auth/login', ['email' => $email, 'password' => $password]);
|
||||
if (!empty($loginRes2['data']['access_token'])) {
|
||||
$jwt = $loginRes2['data']['access_token'];
|
||||
$S['users'][$email] = ['jwt' => $jwt, 'id' => $loginRes2['data']['user']['id'] ?? null];
|
||||
ok("Registrato + login: $firstName $lastName <$email>");
|
||||
return $jwt;
|
||||
}
|
||||
}
|
||||
|
||||
fail("Registrazione fallita per $email: " . ($regRes2['error'] ?? 'errore'));
|
||||
return null;
|
||||
}
|
||||
|
||||
/** Crea un'organizzazione se non esiste (check per name), ritorna org_id. */
|
||||
function ensureOrg(string $jwt, array $data): ?int
|
||||
{
|
||||
global $S;
|
||||
$name = $data['name'];
|
||||
|
||||
// Lista organizzazioni esistenti
|
||||
$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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$createRes = api('POST', '/organizations/create', $data, $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;
|
||||
}
|
||||
|
||||
/** Completa onboarding org. */
|
||||
function completeOnboarding(string $jwt, int $orgId, array $data): void
|
||||
{
|
||||
$res = api('POST', '/onboarding/complete', $data, $jwt, $orgId);
|
||||
if (apiOk($res, 'onboarding')) {
|
||||
ok("Onboarding completato org #$orgId");
|
||||
}
|
||||
}
|
||||
|
||||
/** Classifica l'org come NIS2 Essential/Important. */
|
||||
function classifyOrg(string $jwt, int $orgId, array $data): void
|
||||
{
|
||||
$res = api('POST', '/organizations/classify', $data, $jwt, $orgId);
|
||||
if (apiOk($res, 'classify')) {
|
||||
ok("Classificazione NIS2: {$data['nis2_type']} — Settore: {$data['sector']}");
|
||||
}
|
||||
}
|
||||
|
||||
/** Crea assessment e risponde a tutte le domande. */
|
||||
function runAssessment(string $jwt, int $orgId, string $orgName, array $responses): void
|
||||
{
|
||||
// Crea assessment
|
||||
$createRes = api('POST', '/assessments/create', [
|
||||
'title' => "Gap Assessment NIS2 — $orgName",
|
||||
'description' => 'Assessment automatico da simulazione demo',
|
||||
], $jwt, $orgId);
|
||||
|
||||
if (empty($createRes['data']['id'])) {
|
||||
fail("Assessment create fallito per $orgName");
|
||||
return;
|
||||
}
|
||||
$assessId = (int) $createRes['data']['id'];
|
||||
ok("Assessment creato #$assessId per $orgName");
|
||||
|
||||
// Rispondi a domande
|
||||
$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");
|
||||
|
||||
// Completa assessment
|
||||
$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%");
|
||||
}
|
||||
}
|
||||
|
||||
/** Crea un rischio. Ritorna risk_id o null. */
|
||||
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'];
|
||||
$level = $data['risk_level'] ?? '?';
|
||||
ok("Rischio creato [{$level}]: {$data['title']} #$id");
|
||||
return $id;
|
||||
}
|
||||
fail("Rischio fallito: {$data['title']} — " . ($res['error'] ?? ''));
|
||||
return null;
|
||||
}
|
||||
|
||||
/** Crea un incidente. Ritorna incident_id o 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;
|
||||
}
|
||||
fail("Incidente fallito: {$data['title']} — " . ($res['error'] ?? ''));
|
||||
return null;
|
||||
}
|
||||
|
||||
/** Crea una policy. Ritorna policy_id o 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;
|
||||
}
|
||||
fail("Policy fallita: {$data['title']} — " . ($res['error'] ?? ''));
|
||||
return null;
|
||||
}
|
||||
|
||||
/** Crea un fornitore. Ritorna supplier_id o 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;
|
||||
}
|
||||
fail("Fornitore fallito: {$data['name']} — " . ($res['error'] ?? ''));
|
||||
return null;
|
||||
}
|
||||
|
||||
/** Verifica audit trail hash chain. */
|
||||
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'] ? 'INTEGRA' : 'CORROTTA';
|
||||
$cov = $d['coverage_pct'] ?? 0;
|
||||
$total = $d['total'] ?? 0;
|
||||
$hashed = $d['hashed'] ?? 0;
|
||||
$type = $d['valid'] ? 'ok' : 'error';
|
||||
|
||||
simLog("Catena [{$label}] — {$valid} — {$hashed}/{$total} record hashati ({$cov}%)", $type);
|
||||
|
||||
if (!$d['valid']) {
|
||||
fail("ATTENZIONE: hash chain rotta al record #" . ($d['broken_at'] ?? '?'));
|
||||
}
|
||||
}
|
||||
|
||||
// ── Definizione Aziende Demo ─────────────────────────────────────────────────
|
||||
|
||||
$COMPANIES = [
|
||||
|
||||
// ── A) DataCore S.r.l. ──────────────────────────────────────────────────
|
||||
'datacore' => [
|
||||
'name' => 'DataCore S.r.l.',
|
||||
'legal_form' => 'S.r.l.',
|
||||
'vat_number' => '09876543210',
|
||||
'ateco_code' => '62.01',
|
||||
'ateco_desc' => 'Produzione di software non connesso all\'edizione',
|
||||
'employees' => 320,
|
||||
'annual_turnover'=> 18500000,
|
||||
'sector' => 'ict',
|
||||
'nis2_type' => 'essential',
|
||||
'city' => 'Milano',
|
||||
'province' => 'MI',
|
||||
'region' => 'Lombardia',
|
||||
'target_score' => 74,
|
||||
'complexity' => 'ALTA',
|
||||
'users' => [
|
||||
['first' => 'Alessandro', 'last' => 'Ferretti', 'email' => 'admin@datacore-srl.demo', 'role' => 'org_admin'],
|
||||
['first' => 'Ing. Chiara','last' => 'Bianchi', 'email' => 'ciso@datacore-srl.demo', 'role' => 'compliance_manager'],
|
||||
['first' => 'Marco', 'last' => 'Negri', 'email' => 'auditor@datacore-srl.demo', 'role' => 'auditor'],
|
||||
],
|
||||
'risks' => [
|
||||
['title' => 'Ransomware su infrastruttura cloud', 'category' => 'technical', 'likelihood' => 4, 'impact' => 5, 'nis2_article' => '21.2.b'],
|
||||
['title' => 'Accesso non autorizzato a sistemi di produzione', 'category' => 'technical', 'likelihood' => 3, 'impact' => 5, 'nis2_article' => '21.2.i'],
|
||||
['title' => 'Data breach clienti enterprise (API key leak)', 'category' => 'data_breach', 'likelihood' => 3, 'impact' => 4, 'nis2_article' => '21.2.b'],
|
||||
['title' => 'Vulnerabilità zero-day in dipendenze npm', 'category' => 'technical', 'likelihood' => 4, 'impact' => 3, 'nis2_article' => '21.2.e'],
|
||||
['title' => 'Supply chain compromise — fornitore DevOps', 'category' => 'supply_chain', 'likelihood' => 2, 'impact' => 5, 'nis2_article' => '21.2.d'],
|
||||
['title' => 'DDoS su API gateway prod', 'category' => 'availability', 'likelihood' => 3, 'impact' => 3, 'nis2_article' => '21.2.c'],
|
||||
],
|
||||
'policies' => [
|
||||
['title' => 'Politica Gestione Incidenti di Sicurezza — Art.21.2.b NIS2', 'type' => 'incident_response', 'nis2_article' => '21.2.b'],
|
||||
['title' => 'Politica Crittografia e Gestione Chiavi — Art.21.2.h NIS2', 'type' => 'cryptography', 'nis2_article' => '21.2.h'],
|
||||
['title' => 'Politica Accesso Privilegiato e MFA — Art.21.2.j NIS2', 'type' => 'access_control', 'nis2_article' => '21.2.j'],
|
||||
],
|
||||
'suppliers' => [
|
||||
['name' => 'CloudVault SIEM S.r.l.', 'service_type' => 'security_service', 'risk_level' => 'high', 'critical' => 1],
|
||||
['name' => 'DevOps Pipeline S.r.l.', 'service_type' => 'software_dev', 'risk_level' => 'medium', 'critical' => 1],
|
||||
['name' => 'Fibernet Telecomunicazioni', 'service_type' => 'network_provider', 'risk_level' => 'medium', 'critical' => 0],
|
||||
],
|
||||
'wb_events' => [],
|
||||
],
|
||||
|
||||
// ── B) MedClinic Italia S.p.A. ──────────────────────────────────────────
|
||||
'medclinic' => [
|
||||
'name' => 'MedClinic Italia S.p.A.',
|
||||
'legal_form' => 'S.p.A.',
|
||||
'vat_number' => '07654321098',
|
||||
'ateco_code' => '86.10',
|
||||
'ateco_desc' => 'Servizi ospedalieri',
|
||||
'employees' => 750,
|
||||
'annual_turnover'=> 42000000,
|
||||
'sector' => 'healthcare',
|
||||
'nis2_type' => 'important',
|
||||
'city' => 'Roma',
|
||||
'province' => 'RM',
|
||||
'region' => 'Lazio',
|
||||
'target_score' => 68,
|
||||
'complexity' => 'ALTA',
|
||||
'users' => [
|
||||
['first' => 'Dott. Roberto', 'last' => 'Conti', 'email' => 'admin@medclinic-spa.demo', 'role' => 'org_admin'],
|
||||
['first' => 'Dott.ssa Laura','last' => 'Moretti', 'email' => 'ciso@medclinic-spa.demo', 'role' => 'compliance_manager'],
|
||||
],
|
||||
'risks' => [
|
||||
['title' => 'Violazione dati sanitari pazienti (GDPR+NIS2)', 'category' => 'data_breach', 'likelihood' => 4, 'impact' => 5, 'nis2_article' => '21.2.b'],
|
||||
['title' => 'Fermo sistemi HIS (Hospital Information System)', 'category' => 'availability', 'likelihood' => 3, 'impact' => 5, 'nis2_article' => '21.2.c'],
|
||||
['title' => 'Compromissione fornitore LIS (laboratorio analisi)', 'category' => 'supply_chain', 'likelihood' => 3, 'impact' => 5, 'nis2_article' => '21.2.d'],
|
||||
['title' => 'Accesso abusivo cartelle cliniche digitali', 'category' => 'technical', 'likelihood' => 4, 'impact' => 4, 'nis2_article' => '21.2.i'],
|
||||
['title' => 'Phishing staff medico (credential harvesting)', 'category' => 'human_factor', 'likelihood' => 5, 'impact' => 3, 'nis2_article' => '21.2.g'],
|
||||
],
|
||||
'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 — Art.21.2.c', 'type' => 'business_continuity','nis2_article' => '21.2.c'],
|
||||
],
|
||||
'suppliers' => [
|
||||
['name' => 'HealthSoft LIS S.r.l.', 'service_type' => 'software_vendor', 'risk_level' => 'critical', 'critical' => 1],
|
||||
['name' => 'MedDevice IoT S.p.A.', 'service_type' => 'hardware_vendor', 'risk_level' => 'high', 'critical' => 1],
|
||||
['name' => 'CloudBackup Med S.r.l.', 'service_type' => 'cloud_storage', 'risk_level' => 'medium', 'critical' => 0],
|
||||
],
|
||||
'wb_events' => [],
|
||||
],
|
||||
|
||||
// ── C) EnerNet Distribuzione S.r.l. ─────────────────────────────────────
|
||||
'enernet' => [
|
||||
'name' => 'EnerNet Distribuzione S.r.l.',
|
||||
'legal_form' => 'S.r.l.',
|
||||
'vat_number' => '05432109876',
|
||||
'ateco_code' => '35.13',
|
||||
'ateco_desc' => 'Distribuzione di energia elettrica',
|
||||
'employees' => 1800,
|
||||
'annual_turnover'=> 125000000,
|
||||
'sector' => 'energy',
|
||||
'nis2_type' => 'essential',
|
||||
'city' => 'Torino',
|
||||
'province' => 'TO',
|
||||
'region' => 'Piemonte',
|
||||
'target_score' => 79,
|
||||
'complexity' => 'ALTISSIMA',
|
||||
'users' => [
|
||||
['first' => 'Ing. Paolo', 'last' => 'Martinelli', 'email' => 'admin@enernet-srl.demo', 'role' => 'org_admin'],
|
||||
['first' => 'Dott. Simona', 'last' => 'Galli', 'email' => 'ciso@enernet-srl.demo', 'role' => 'compliance_manager'],
|
||||
['first' => 'Prof. Carlo', 'last' => 'Ricci', 'email' => 'auditor@enernet-srl.demo', 'role' => 'auditor'],
|
||||
['first' => 'Anna', 'last' => 'Zanetti', 'email' => 'board@enernet-srl.demo', 'role' => 'board_member'],
|
||||
],
|
||||
'risks' => [
|
||||
['title' => 'Attacco a sistemi SCADA/OT di controllo rete elettrica', 'category' => 'technical', 'likelihood' => 3, 'impact' => 5, 'nis2_article' => '21.2.b'],
|
||||
['title' => 'Interruzione fornitura energia (blackout doloso)', 'category' => 'availability', 'likelihood' => 2, 'impact' => 5, 'nis2_article' => '21.2.c'],
|
||||
['title' => 'Accesso non autorizzato sistemi AMI (smart meter)', 'category' => 'technical', 'likelihood' => 3, 'impact' => 4, 'nis2_article' => '21.2.i'],
|
||||
['title' => 'Supply chain — firmware malevolo contatori', 'category' => 'supply_chain', 'likelihood' => 2, 'impact' => 5, 'nis2_article' => '21.2.d'],
|
||||
['title' => 'Insider threat — tecnico campo con accesso SCADA', 'category' => 'human_factor', 'likelihood' => 2, 'impact' => 5, 'nis2_article' => '21.2.i'],
|
||||
['title' => 'Incidente climatico su infrastruttura fisica (alluvione)', 'category' => 'physical', 'likelihood' => 2, 'impact' => 4, 'nis2_article' => '21.2.c'],
|
||||
['title' => 'Vulnerabilità protocollo IEC 61850 su sottostazioni', 'category' => 'technical', 'likelihood' => 3, 'impact' => 4, 'nis2_article' => '21.2.a'],
|
||||
],
|
||||
'policies' => [
|
||||
['title' => 'Politica Sicurezza Sistemi OT/SCADA — Art.21.2.a NIS2', 'type' => 'risk_management', 'nis2_article' => '21.2.a'],
|
||||
['title' => 'Politica Business Continuity Rete Elettrica — Art.21.2.c', 'type' => 'business_continuity','nis2_article' => '21.2.c'],
|
||||
['title' => 'Politica Sicurezza Supply Chain — Art.21.2.d NIS2', 'type' => 'supply_chain', 'nis2_article' => '21.2.d'],
|
||||
],
|
||||
'suppliers' => [
|
||||
['name' => 'ABB Automazione S.p.A.', 'service_type' => 'ot_vendor', 'risk_level' => 'critical', 'critical' => 1],
|
||||
['name' => 'SmartMeter Tech S.r.l.', 'service_type' => 'hardware_vendor', 'risk_level' => 'high', 'critical' => 1],
|
||||
['name' => 'NordTelecom Fibra S.p.A.', 'service_type' => 'network_provider', 'risk_level' => 'medium', 'critical' => 1],
|
||||
['name' => 'Alstom Grid Italia S.r.l.', 'service_type' => 'ot_vendor', 'risk_level' => 'critical','critical' => 1],
|
||||
],
|
||||
'wb_events' => [
|
||||
[
|
||||
'category' => 'unauthorized_access',
|
||||
'title' => 'Accesso non autorizzato sistema SCADA sottostazione Moncalieri',
|
||||
'description' => 'Tecnico esterno di manutenzione ha tentato accesso a sistema SCADA secondario con credenziali non autorizzate. Accesso bloccato da firewall OT ma tentativo registrato nei log. Possibile insider threat o furto credenziali.',
|
||||
'priority' => 'critical',
|
||||
'nis2_article'=> '21.2.i',
|
||||
'is_anonymous'=> 1,
|
||||
],
|
||||
],
|
||||
],
|
||||
];
|
||||
|
||||
// ── Risposte assessment standard per settore ─────────────────────────────────
|
||||
// Formato: question_code => valore (1=no, 2=parziale, 3=sì)
|
||||
|
||||
function getAssessmentResponses(string $sector): array
|
||||
{
|
||||
$base = [];
|
||||
// 80 domande NIS2 (categorie RM01-RM10, da 01 a 08 per categoria)
|
||||
$categories = ['RM', 'IH', 'BC', 'SC', 'SD', 'EA', 'TR', 'CR', 'AM', 'MA'];
|
||||
foreach ($categories as $cat) {
|
||||
for ($i = 1; $i <= 8; $i++) {
|
||||
$base[sprintf('%s%02d', $cat, $i)] = 1; // default: non implementato
|
||||
}
|
||||
}
|
||||
|
||||
// Sovrascritture per settore (rendono la simulazione realistica)
|
||||
$overrides = match ($sector) {
|
||||
'ict' => [
|
||||
'RM01' => 3, 'RM02' => 3, 'RM03' => 2, 'RM04' => 2,
|
||||
'IH01' => 3, 'IH02' => 2, 'IH03' => 2,
|
||||
'SD01' => 3, 'SD02' => 3, 'SD03' => 2,
|
||||
'CR01' => 3, 'CR02' => 3,
|
||||
'AM01' => 3, 'AM02' => 2, 'AM03' => 2,
|
||||
'MA01' => 3, 'MA02' => 2,
|
||||
],
|
||||
'healthcare' => [
|
||||
'RM01' => 2, 'RM02' => 2,
|
||||
'AM01' => 3, 'AM02' => 3,
|
||||
'TR01' => 2, 'TR02' => 2,
|
||||
'BC01' => 2, 'BC02' => 1,
|
||||
'CR01' => 2,
|
||||
],
|
||||
'energy' => [
|
||||
'RM01' => 3, 'RM02' => 2, 'RM03' => 3,
|
||||
'BC01' => 3, 'BC02' => 3, 'BC03' => 2,
|
||||
'AM01' => 3, 'AM02' => 3, 'AM03' => 3,
|
||||
'SC01' => 2, 'SC02' => 2,
|
||||
'MA01' => 3, 'MA02' => 3, 'MA03' => 2,
|
||||
],
|
||||
default => [],
|
||||
};
|
||||
|
||||
return array_merge($base, $overrides);
|
||||
}
|
||||
|
||||
// ────────────────────────────────────────────────────────────────────────────
|
||||
// FASE 0 — Health Check
|
||||
// ────────────────────────────────────────────────────────────────────────────
|
||||
simPhase(0, 'Health Check API');
|
||||
|
||||
$health = api('GET', '/api-status.php' === '' ? '/health' : '');
|
||||
// Usa api-status.php direttamente
|
||||
$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");
|
||||
}
|
||||
|
||||
// ────────────────────────────────────────────────────────────────────────────
|
||||
// SIM-01 — FASE 1+2: Registrazione Aziende + Onboarding
|
||||
// ────────────────────────────────────────────────────────────────────────────
|
||||
simPhase(1, 'SIM-01 — Registrazione Aziende Demo + Onboarding');
|
||||
info('Creazione di 3 aziende NIS2 con settori e classificazioni differenti...');
|
||||
|
||||
foreach ($COMPANIES as $slug => $comp) {
|
||||
simLog("── Azienda [{$comp['nis2_type']}]: {$comp['name']} ({$comp['sector']})", 'phase');
|
||||
|
||||
// Registra admin
|
||||
$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];
|
||||
|
||||
// Crea organizzazione
|
||||
$orgId = ensureOrg($jwt, [
|
||||
'name' => $comp['name'],
|
||||
'legal_form' => $comp['legal_form'],
|
||||
'vat_number' => $comp['vat_number'],
|
||||
'ateco_code' => $comp['ateco_code'],
|
||||
'sector' => $comp['sector'],
|
||||
'employees_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;
|
||||
|
||||
// Onboarding
|
||||
completeOnboarding($jwt, $orgId, [
|
||||
'org_name' => $comp['name'],
|
||||
'vat_number' => $comp['vat_number'],
|
||||
'sector' => $comp['sector'],
|
||||
'employees_count' => $comp['employees'],
|
||||
'annual_turnover_eur' => $comp['annual_turnover'],
|
||||
]);
|
||||
|
||||
// Classificazione NIS2
|
||||
classifyOrg($jwt, $orgId, [
|
||||
'nis2_type' => $comp['nis2_type'],
|
||||
'sector' => $comp['sector'],
|
||||
'is_voluntary'=> 0,
|
||||
'justification'=> "Classificazione automatica: {$comp['nis2_type']} entity — settore {$comp['sector']}",
|
||||
]);
|
||||
|
||||
info(" {$comp['name']}: {$comp['employees']} dip, fatturato " . number_format($comp['annual_turnover']/1000000, 1) . "M€, settore {$comp['sector']}");
|
||||
}
|
||||
|
||||
// ────────────────────────────────────────────────────────────────────────────
|
||||
// SIM-01 — FASE 3: Gap Assessment 80 domande per ogni azienda
|
||||
// ────────────────────────────────────────────────────────────────────────────
|
||||
simPhase(2, 'SIM-01 — Gap Assessment 80 Domande Art.21 NIS2');
|
||||
|
||||
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 = getAssessmentResponses($comp['sector']);
|
||||
runAssessment($jwt, $orgId, $comp['name'], $responses);
|
||||
}
|
||||
|
||||
// ────────────────────────────────────────────────────────────────────────────
|
||||
// FASE 4: Risk Register per ogni azienda
|
||||
// ────────────────────────────────────────────────────────────────────────────
|
||||
simPhase(3, 'Risk Register — Rischi per settore 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']}]:", 'info');
|
||||
|
||||
foreach ($comp['risks'] as $riskDef) {
|
||||
$likelihood = $riskDef['likelihood'];
|
||||
$impact = $riskDef['impact'];
|
||||
$score = $likelihood * $impact;
|
||||
$level = match(true) {
|
||||
$score >= 20 => 'critical',
|
||||
$score >= 12 => 'high',
|
||||
$score >= 6 => 'medium',
|
||||
default => 'low',
|
||||
};
|
||||
createRisk($jwt, $orgId, [
|
||||
'title' => $riskDef['title'],
|
||||
'category' => $riskDef['category'],
|
||||
'likelihood' => $likelihood,
|
||||
'impact' => $impact,
|
||||
'risk_score' => $score,
|
||||
'risk_level' => $level,
|
||||
'nis2_article' => $riskDef['nis2_article'],
|
||||
'description' => "Rischio identificato durante gap assessment NIS2 — {$riskDef['title']}",
|
||||
'status' => 'open',
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
// ────────────────────────────────────────────────────────────────────────────
|
||||
// FASE 5: Policy per ogni azienda
|
||||
// ────────────────────────────────────────────────────────────────────────────
|
||||
simPhase(4, 'Policies NIS2 Art.21 per ogni azienda');
|
||||
|
||||
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) {
|
||||
$polId = createPolicy($jwt, $orgId, [
|
||||
'title' => $polDef['title'],
|
||||
'type' => $polDef['type'],
|
||||
'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 simulazione demo.",
|
||||
'status' => 'draft',
|
||||
]);
|
||||
// Approva la policy
|
||||
if ($polId) {
|
||||
$approveRes = api('POST', "/policies/{$polId}/approve", [
|
||||
'notes' => 'Approvazione automatica da simulazione demo NIS2',
|
||||
], $jwt, $orgId);
|
||||
if (apiOk($approveRes, "policy.approve")) {
|
||||
ok("Policy approvata: {$polDef['title']}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ────────────────────────────────────────────────────────────────────────────
|
||||
// FASE 6: Fornitori + Assessment supply chain
|
||||
// ────────────────────────────────────────────────────────────────────────────
|
||||
simPhase(5, 'Supply Chain — Fornitori critici per ogni azienda');
|
||||
|
||||
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'],
|
||||
'risk_level' => $supDef['risk_level'],
|
||||
'is_critical' => $supDef['critical'],
|
||||
'nis2_relevant' => 1,
|
||||
'contract_status' => 'active',
|
||||
'contact_email' => DEMO_EMAIL,
|
||||
]);
|
||||
|
||||
// Assessment fornitore
|
||||
if ($supId) {
|
||||
$assessRes = api('POST', "/supply-chain/{$supId}/assess", [
|
||||
'has_security_controls' => 1,
|
||||
'has_incident_procedure' => $supDef['risk_level'] !== 'low' ? 1 : 0,
|
||||
'gdpr_compliant' => 1,
|
||||
'nis2_contractual_clauses' => $supDef['critical'] ? 1 : 0,
|
||||
'last_audit_date' => date('Y-m-d', strtotime('-6 months')),
|
||||
'notes' => "Assessment automatico — fornitore {$supDef['service_type']}",
|
||||
], $jwt, $orgId);
|
||||
if (apiOk($assessRes, "supplier.assess")) {
|
||||
ok("Assessment fornitore: {$supDef['name']}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ────────────────────────────────────────────────────────────────────────────
|
||||
// SIM-02 — FASE 7: Incidente Ransomware DataCore (Art.23 timeline 24h/72h/30d)
|
||||
// ────────────────────────────────────────────────────────────────────────────
|
||||
simPhase(6, 'SIM-02 — Incidente Ransomware DataCore (Art.23 NIS2)');
|
||||
info('Scenario: attacco ransomware su infrastruttura cloud DataCore.');
|
||||
info('Trigger: crittografia 3TB dati clienti enterprise + downtime 18h.');
|
||||
info('Timeline Art.23: early warning 24h → notification 72h → final report 30d');
|
||||
|
||||
if (!empty($S['orgs']['datacore']['id'])) {
|
||||
$jwt = $S['orgs']['datacore']['jwt'];
|
||||
$orgId = $S['orgs']['datacore']['id'];
|
||||
|
||||
$incId = createIncident($jwt, $orgId, [
|
||||
'title' => 'Attacco Ransomware — Crittografia infrastruttura cloud prod',
|
||||
'description' => 'Alle 03:42 del ' . date('Y-m-d') . ' sistemi SIEM hanno rilevato attività anomala su cluster Kubernetes prod. Analisi forense preliminare indica compromissione tramite vulnerabilità CVE-2024-XXXX in plugin WordPress del portale clienti. Ransomware "LockBit 3.0" ha crittografato 3.2TB di dati su storage condiviso. 47 clienti enterprise impattati. Downtime 18h. Riscatto richiesto: 380.000 USD in Bitcoin.',
|
||||
'severity' => 'critical',
|
||||
'category' => 'ransomware',
|
||||
'affected_systems' => json_encode(['Kubernetes cluster prod', 'NAS storage 3.2TB', 'Portale clienti', 'API Gateway']),
|
||||
'estimated_impact' => 'Dati 47 clienti enterprise inaccessibili per 18h. Potenziale esfiltrazione pre-cifratura.',
|
||||
'is_significant' => 1,
|
||||
'status' => 'investigating',
|
||||
]);
|
||||
|
||||
if ($incId) {
|
||||
// Timeline: early warning 24h
|
||||
$ewRes = api('POST', "/incidents/{$incId}/early-warning", [
|
||||
'notes' => 'Early warning 24h inviato ad ACN secondo Art.23 NIS2. Incidente significativo confermato.',
|
||||
], $jwt, $orgId);
|
||||
apiOk($ewRes, 'incident.early-warning') && ok("Early warning 24h Art.23 inviato ad ACN");
|
||||
|
||||
// Timeline: aggiornamento investigazione
|
||||
$tlRes = api('POST', "/incidents/{$incId}/timeline", [
|
||||
'event_type' => 'update',
|
||||
'description' => 'Analisi forense: vettore iniziale confermato. Contenimento attivato: isolamento cluster compromesso, failover su DR site. Backup verificati (ultimo: 6h prima). RTO stimato: 4h.',
|
||||
], $jwt, $orgId);
|
||||
apiOk($tlRes, 'incident.timeline') && ok("Timeline aggiornata: contenimento attivato");
|
||||
|
||||
// Notifica 72h
|
||||
$notRes = api('POST', "/incidents/{$incId}/notification", [
|
||||
'notes' => 'Notifica 72h ad ACN e CSIRT nazionale. Impatto confermato: 47 clienti, 3.2TB, 18h downtime. Misure contenimento adottate. Indagine forense in corso.',
|
||||
], $jwt, $orgId);
|
||||
apiOk($notRes, 'incident.notification') && ok("Notifica 72h Art.23 completata");
|
||||
|
||||
// Aggiornamento: risoluzione
|
||||
api('PUT', "/incidents/{$incId}", [
|
||||
'status' => 'resolved',
|
||||
'resolution' => 'Sistemi ripristinati da backup verificati. Vulnerabilità patchata. MFA obbligatorio su tutti gli accessi. Piano remediation 30 giorni approvato da CISO.',
|
||||
], $jwt, $orgId);
|
||||
ok("Incidente risolto: sistemi ripristinati da backup");
|
||||
|
||||
// Final report 30d
|
||||
$frRes = api('POST', "/incidents/{$incId}/final-report", [
|
||||
'notes' => 'Final report 30d ad ACN. Root cause: CVE-2024-XXXX. Lesson learned: segmentazione rete OT/IT, WAF avanzato, patch management automatizzato. Costo totale incidente: 127.000€.',
|
||||
], $jwt, $orgId);
|
||||
apiOk($frRes, 'incident.final-report') && ok("Final report 30d Art.23 completato");
|
||||
|
||||
info("Incidente DataCore completamente gestito secondo Art.23 NIS2 (24h/72h/30d)");
|
||||
}
|
||||
} else {
|
||||
warn("SIM-02 skip: org DataCore non disponibile");
|
||||
}
|
||||
|
||||
// ────────────────────────────────────────────────────────────────────────────
|
||||
// SIM-03 — FASE 8: Data Breach MedClinic (Supply Chain Compromise)
|
||||
// ────────────────────────────────────────────────────────────────────────────
|
||||
simPhase(7, 'SIM-03 — Data Breach Supply Chain MedClinic');
|
||||
info('Scenario: fornitore LIS (laboratorio analisi) compromesso.');
|
||||
info('Dati 2.340 pazienti esfiltrati via API non autenticata fornitore.');
|
||||
|
||||
if (!empty($S['orgs']['medclinic']['id'])) {
|
||||
$jwt = $S['orgs']['medclinic']['jwt'];
|
||||
$orgId = $S['orgs']['medclinic']['id'];
|
||||
|
||||
// Trova fornitore LIS e flaggalo ad alto rischio
|
||||
$suppRes = api('GET', '/supply-chain/list', null, $jwt, $orgId);
|
||||
$lisId = null;
|
||||
if (!empty($suppRes['data'])) {
|
||||
foreach ($suppRes['data'] as $s) {
|
||||
if (strpos($s['name'] ?? '', 'HealthSoft') !== false) {
|
||||
$lisId = (int) $s['id'];
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
if ($lisId) {
|
||||
api('PUT', "/supply-chain/{$lisId}", [
|
||||
'risk_level' => 'critical',
|
||||
'notes' => 'COMPROMISSIONE CONFERMATA: API endpoint /api/results non autenticato esposto su internet. Dati 2.340 pazienti potenzialmente esfiltrati. Contratto sospeso.',
|
||||
], $jwt, $orgId);
|
||||
ok("Fornitore HealthSoft LIS flaggato: CRITICAL");
|
||||
}
|
||||
|
||||
$incId2 = createIncident($jwt, $orgId, [
|
||||
'title' => 'Data Breach — Esfiltrazione dati pazienti via fornitore LIS',
|
||||
'description' => 'Il fornitore di sistemi LIS (Laboratorio Analisi) HealthSoft ha esposto accidentalmente un endpoint API non autenticato. Dati di 2.340 pazienti (nome, codice fiscale, referti analisi) potenzialmente esfiltrati da attore esterno. Scoperto da team IR interno tramite alert SIEM su traffico anomalo verso IP russo (185.x.x.x). Violazione GDPR Art.33+34 + NIS2 Art.23.',
|
||||
'severity' => 'critical',
|
||||
'category' => 'data_breach',
|
||||
'affected_systems' => json_encode(['LIS HealthSoft API', 'Database referti', 'Portale pazienti']),
|
||||
'is_significant' => 1,
|
||||
'status' => 'investigating',
|
||||
]);
|
||||
|
||||
if ($incId2) {
|
||||
api('POST', "/incidents/{$incId2}/early-warning", ['notes' => 'Early warning 24h. Data breach confermato. Segnalazione parallela a Garante Privacy (GDPR Art.33) e ACN (NIS2 Art.23).'], $jwt, $orgId);
|
||||
ok("Early warning 24h MedClinic — ACN + Garante Privacy");
|
||||
|
||||
api('POST', "/incidents/{$incId2}/timeline", [
|
||||
'event_type' => 'update',
|
||||
'description' => 'Fornitore HealthSoft ha confermato la vulnerabilità. Endpoint disabilitato. Audit completo API in corso. Pazienti impattati in fase di notifica individuale (GDPR Art.34).',
|
||||
], $jwt, $orgId);
|
||||
|
||||
api('POST', "/incidents/{$incId2}/notification", ['notes' => 'Notifica 72h ACN completata. Impatto: 2.340 pazienti, dati sanitari. Misure: endpoint bloccato, password reset massivo, monitoraggio dark web attivato.'], $jwt, $orgId);
|
||||
ok("Notifica 72h MedClinic completata");
|
||||
info("Incident MedClinic gestito: supply chain breach Art.23 + GDPR Art.33");
|
||||
}
|
||||
} else {
|
||||
warn("SIM-03 skip: org MedClinic non disponibile");
|
||||
}
|
||||
|
||||
// ────────────────────────────────────────────────────────────────────────────
|
||||
// SIM-04 — FASE 9: Whistleblowing Anonimo EnerNet
|
||||
// ────────────────────────────────────────────────────────────────────────────
|
||||
simPhase(8, 'SIM-04 — Whistleblowing Anonimo EnerNet (Art.32 NIS2)');
|
||||
info('Scenario: tecnico anonimo segnala accesso non autorizzato SCADA sottostazione.');
|
||||
|
||||
if (!empty($S['orgs']['enernet']['id'])) {
|
||||
$jwt = $S['orgs']['enernet']['jwt'];
|
||||
$orgId = $S['orgs']['enernet']['id'];
|
||||
|
||||
foreach ($COMPANIES['enernet']['wb_events'] as $wb) {
|
||||
// Invia segnalazione anonima (no JWT = accesso pubblico anonimo)
|
||||
$wbRes = api('POST', '/whistleblowing/submit', array_merge($wb, ['organization_id' => $orgId]));
|
||||
if (!empty($wbRes['data']['report_code'])) {
|
||||
$code = $wbRes['data']['report_code'];
|
||||
$token = $wbRes['data']['anonymous_token'] ?? '';
|
||||
ok("Segnalazione anonima ricevuta: {$code}");
|
||||
if ($token) info(" Token tracking anonimo: $token");
|
||||
|
||||
// Recupera ID segnalazione (solo admin)
|
||||
$listRes = api('GET', '/whistleblowing/list?status=received', null, $jwt, $orgId);
|
||||
$wbId = null;
|
||||
if (!empty($listRes['data'])) {
|
||||
foreach ($listRes['data'] as $r) {
|
||||
if (($r['report_code'] ?? '') === $code) {
|
||||
$wbId = (int) $r['id'];
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ($wbId) {
|
||||
// Assegna al CISO
|
||||
api('POST', "/whistleblowing/{$wbId}/assign", [
|
||||
'assigned_to' => $S['users'][$COMPANIES['enernet']['users'][1]['email']]['id'] ?? null,
|
||||
'notes' => 'Assegnato a CISO per indagine urgente — priorità CRITICAL',
|
||||
], $jwt, $orgId);
|
||||
ok("Segnalazione #{$code} assegnata al CISO");
|
||||
|
||||
// Aggiorna stato: investigating
|
||||
api('PUT', "/whistleblowing/{$wbId}", [
|
||||
'status' => 'investigating',
|
||||
'notes' => 'Indagine avviata. Log accessi SCADA sotto analisi forense. Tecnico identificato ma non ancora notificato.',
|
||||
], $jwt, $orgId);
|
||||
ok("Segnalazione #{$code} → stato: investigating");
|
||||
|
||||
// Chiudi con risoluzione
|
||||
api('POST', "/whistleblowing/{$wbId}/close", [
|
||||
'resolution_notes' => 'Accesso confermato non autorizzato. Tecnico esterno sospeso dal contratto. Credenziali SCADA revocate e rigenerate. Audit accessi OT completato. Segnalazione ad ACN per incidente rilevante.',
|
||||
'status' => 'resolved',
|
||||
], $jwt, $orgId);
|
||||
ok("Segnalazione #{$code} CHIUSA con risoluzione");
|
||||
|
||||
// Tracking anonimo (simula il segnalante che controlla lo stato)
|
||||
if ($token) {
|
||||
$trackRes = api('GET', "/whistleblowing/track-anonymous?token={$token}");
|
||||
if (!empty($trackRes['data'])) {
|
||||
ok("Tracking anonimo: segnalante può vedere " . count($trackRes['data']['timeline'] ?? []) . " aggiornamenti pubblici");
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
fail("Whistleblowing submit fallito: " . ($wbRes['error'] ?? ''));
|
||||
}
|
||||
}
|
||||
|
||||
// Normativa ACK per EnerNet
|
||||
$normRes = api('GET', '/normative/pending', null, $jwt, $orgId);
|
||||
$pending = $normRes['data']['updates'] ?? [];
|
||||
$acked = 0;
|
||||
foreach ($pending as $norm) {
|
||||
$ackRes = api('POST', "/normative/{$norm['id']}/ack", [
|
||||
'notes' => "Presa visione da CISO EnerNet il " . date('Y-m-d'),
|
||||
], $jwt, $orgId);
|
||||
if (apiOk($ackRes, "normative.ack")) $acked++;
|
||||
}
|
||||
if ($acked > 0) ok("Normativa ACK: $acked aggiornamenti NIS2/ACN confermati da EnerNet");
|
||||
|
||||
} else {
|
||||
warn("SIM-04 skip: org EnerNet non disponibile");
|
||||
}
|
||||
|
||||
// ────────────────────────────────────────────────────────────────────────────
|
||||
// SIM-05 — FASE 10: Verifica Audit Trail Hash Chain
|
||||
// ────────────────────────────────────────────────────────────────────────────
|
||||
simPhase(9, 'SIM-05 — Audit Trail Hash Chain Verification');
|
||||
info('Verifica integrità certificata per tutte e 3 le aziende...');
|
||||
info('Ogni record è linkato SHA-256 al precedente (hash chain certificabile).');
|
||||
|
||||
foreach ($COMPANIES as $slug => $comp) {
|
||||
if (empty($S['orgs'][$slug]['id'])) { skip("Skip chain-verify $slug: org non disponibile"); continue; }
|
||||
$jwt = $S['orgs'][$slug]['jwt'];
|
||||
$orgId = $S['orgs'][$slug]['id'];
|
||||
checkAuditChain($jwt, $orgId, $comp['name']);
|
||||
}
|
||||
|
||||
// ── Report finale ────────────────────────────────────────────────────────────
|
||||
simPhase(10, 'Riepilogo Simulazione');
|
||||
|
||||
$orgsOk = 0;
|
||||
foreach ($COMPANIES as $slug => $comp) {
|
||||
if (!empty($S['orgs'][$slug]['id'])) {
|
||||
$orgsOk++;
|
||||
info(" {$comp['name']}: org_id={$S['orgs'][$slug]['id']} — {$comp['nis2_type']} entity");
|
||||
}
|
||||
}
|
||||
|
||||
info("");
|
||||
info("Dati demo creati:");
|
||||
info(" · $orgsOk aziende NIS2 (Essential/Important)");
|
||||
info(" · " . array_sum(array_map(fn($c) => count($c['risks']), $COMPANIES)) . " rischi registrati");
|
||||
info(" · 2 incidenti Art.23 con timeline 24h/72h/30d");
|
||||
info(" · 1 data breach supply chain");
|
||||
info(" · 1 whistleblowing anonimo gestito e chiuso");
|
||||
info(" · " . array_sum(array_map(fn($c) => count($c['policies']), $COMPANIES)) . " policy approvate");
|
||||
info(" · " . array_sum(array_map(fn($c) => count($c['suppliers']), $COMPANIES)) . " fornitori critici valutati");
|
||||
info(" · Audit trail hash chain verificata per tutte le org");
|
||||
info("");
|
||||
info("URL: " . str_replace('/api', '', API_BASE) . "/dashboard.html");
|
||||
|
||||
simDone($S['stats']);
|
||||
Loading…
Reference in New Issue
Block a user