diff --git a/application/cli/seed_framework.php b/application/cli/seed_framework.php
new file mode 100644
index 0000000..3ea7fce
--- /dev/null
+++ b/application/cli/seed_framework.php
@@ -0,0 +1,121 @@
+exec("SET NAMES utf8mb4 COLLATE utf8mb4_unicode_ci");
+
+$ddl = [
+"CREATE TABLE IF NOT EXISTS cfg_nis2_ambiti (
+ nist_code VARCHAR(16) NOT NULL, nist_descr TEXT NOT NULL,
+ PRIMARY KEY (nist_code)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci",
+"CREATE TABLE IF NOT EXISTS cfg_nis2_procedure (
+ proc_code VARCHAR(16) NOT NULL, proc_descr TEXT NOT NULL,
+ PRIMARY KEY (proc_code)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci",
+"CREATE TABLE IF NOT EXISTS cfg_nis2_rischi (
+ risk_code VARCHAR(16) NOT NULL, risk_descr TEXT NOT NULL,
+ PRIMARY KEY (risk_code)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci",
+"CREATE TABLE IF NOT EXISTS cfg_nis2_misure (
+ misura_code VARCHAR(16) NOT NULL, nist_code VARCHAR(16) NOT NULL,
+ area_politica VARCHAR(255) NOT NULL, misura_descr TEXT NOT NULL, ord INT NOT NULL,
+ applies_important TINYINT(1) NOT NULL DEFAULT 1, applies_essential TINYINT(1) NOT NULL DEFAULT 1,
+ PRIMARY KEY (misura_code), KEY idx_misura_nist (nist_code), KEY idx_misura_ord (ord),
+ CONSTRAINT fk_misura_ambito FOREIGN KEY (nist_code) REFERENCES cfg_nis2_ambiti (nist_code)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci",
+"CREATE TABLE IF NOT EXISTS cfg_nis2_requisiti (
+ id INT NOT NULL AUTO_INCREMENT, n INT NULL, misura_code VARCHAR(16) NOT NULL,
+ requisito_code VARCHAR(32) NOT NULL, requisito_descr TEXT NOT NULL,
+ proc_code VARCHAR(16) NULL, risk_code VARCHAR(16) NULL,
+ applies_important TINYINT(1) NOT NULL DEFAULT 0, applies_essential TINYINT(1) NOT NULL DEFAULT 0,
+ PRIMARY KEY (id), UNIQUE KEY uq_req (misura_code, requisito_code),
+ KEY idx_req_misura (misura_code), KEY idx_req_proc (proc_code), KEY idx_req_risk (risk_code),
+ CONSTRAINT fk_req_misura FOREIGN KEY (misura_code) REFERENCES cfg_nis2_misure (misura_code),
+ CONSTRAINT fk_req_proc FOREIGN KEY (proc_code) REFERENCES cfg_nis2_procedure (proc_code),
+ CONSTRAINT fk_req_risk FOREIGN KEY (risk_code) REFERENCES cfg_nis2_rischi (risk_code)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci",
+];
+foreach ($ddl as $stmt) {
+ try { $pdo->exec($stmt); }
+ catch (PDOException $e) { if (!in_array($e->errorInfo[1] ?? 0, [1050, 1061], true)) { throw $e; } }
+}
+
+$seedPath = __DIR__ . '/../data/nis2_framework_seed.json';
+$seed = json_decode(@file_get_contents($seedPath), true);
+if (!$seed) { fwrite(STDERR, "SEED non leggibile: $seedPath\n"); exit(1); }
+
+$pdo->beginTransaction();
+try {
+ $st = $pdo->prepare("INSERT INTO cfg_nis2_ambiti (nist_code,nist_descr) VALUES (?,?)
+ ON DUPLICATE KEY UPDATE nist_descr=VALUES(nist_descr)");
+ foreach ($seed['ambiti_nist'] as $a) { $st->execute([$a['nist_code'], $a['nist_descr']]); }
+
+ $st = $pdo->prepare("INSERT INTO cfg_nis2_procedure (proc_code,proc_descr) VALUES (?,?)
+ ON DUPLICATE KEY UPDATE proc_descr=VALUES(proc_descr)");
+ foreach ($seed['procedure'] as $p) { $st->execute([$p['proc_code'], $p['proc_descr']]); }
+
+ $st = $pdo->prepare("INSERT INTO cfg_nis2_rischi (risk_code,risk_descr) VALUES (?,?)
+ ON DUPLICATE KEY UPDATE risk_descr=VALUES(risk_descr)");
+ foreach ($seed['rischi'] as $r) { $st->execute([$r['risk_code'], $r['risk_descr']]); }
+
+ $st = $pdo->prepare("INSERT INTO cfg_nis2_misure
+ (misura_code,nist_code,area_politica,misura_descr,ord,applies_important,applies_essential)
+ VALUES (?,?,?,?,?,?,?)
+ ON DUPLICATE KEY UPDATE nist_code=VALUES(nist_code),area_politica=VALUES(area_politica),
+ misura_descr=VALUES(misura_descr),ord=VALUES(ord),
+ applies_important=VALUES(applies_important),applies_essential=VALUES(applies_essential)");
+ foreach ($seed['misure'] as $m) {
+ $st->execute([$m['misura_code'], $m['nist_code'], $m['area_politica'], $m['misura_descr'],
+ $m['ord'], $m['applies_important'], $m['applies_essential']]);
+ }
+
+ $st = $pdo->prepare("INSERT INTO cfg_nis2_requisiti
+ (n,misura_code,requisito_code,requisito_descr,proc_code,risk_code,applies_important,applies_essential)
+ VALUES (?,?,?,?,?,?,?,?)
+ ON DUPLICATE KEY UPDATE n=VALUES(n),requisito_descr=VALUES(requisito_descr),
+ proc_code=VALUES(proc_code),risk_code=VALUES(risk_code),
+ applies_important=VALUES(applies_important),applies_essential=VALUES(applies_essential)");
+ foreach ($seed['requisiti'] as $q) {
+ $st->execute([$q['n'], $q['misura_code'], $q['requisito_code'], $q['requisito_descr'],
+ $q['proc_code'], $q['risk_code'], $q['applies_important'], $q['applies_essential']]);
+ }
+ $pdo->commit();
+} catch (Throwable $e) {
+ $pdo->rollBack();
+ fwrite(STDERR, "SEED FALLITO: " . $e->getMessage() . "\n");
+ exit(1);
+}
+
+$counts = [
+ 'ambiti' => (int) $pdo->query("SELECT COUNT(*) FROM cfg_nis2_ambiti")->fetchColumn(),
+ 'procedure' => (int) $pdo->query("SELECT COUNT(*) FROM cfg_nis2_procedure")->fetchColumn(),
+ 'rischi' => (int) $pdo->query("SELECT COUNT(*) FROM cfg_nis2_rischi")->fetchColumn(),
+ 'misure' => (int) $pdo->query("SELECT COUNT(*) FROM cfg_nis2_misure")->fetchColumn(),
+ 'requisiti' => (int) $pdo->query("SELECT COUNT(*) FROM cfg_nis2_requisiti")->fetchColumn(),
+];
+echo "OK seed-framework — " . json_encode($counts, JSON_UNESCAPED_UNICODE) . "\n";
diff --git a/application/config/nis2_sources.php b/application/config/nis2_sources.php
index bc42f5e..84503ad 100644
--- a/application/config/nis2_sources.php
+++ b/application/config/nis2_sources.php
@@ -76,7 +76,7 @@ return [
'key' => 'acn_specifiche_base_2025',
'short' => 'Specifiche di base ACN (Framework Nazionale 2025)',
'citation' => 'ACN, Misure di sicurezza di base NIS - Allegati 1 e 2 alla Determinazione n. 164179/2025',
- 'full' => 'Misure di sicurezza di base (Determinazione ACN 164179/2025) articolate secondo il Framework Nazionale per la Cybersecurity e la Data Protection (ed. 2025): funzioni Governance (GV), Identificazione (ID), Protezione (PR), Rilevazione (DE), Risposta (RS), Ripristino (RC), poi categorie, sottocategorie e requisiti. Soggetti importanti: 37 misure / 87 requisiti (Allegato 1). Soggetti essenziali: 43 misure / 116 requisiti (Allegato 2), codifica identica con requisiti aggiuntivi. Esempi: GV.RR-04 (cybersicurezza nelle risorse umane, adeguatezza al ruolo), GV.PO-01 (policy di gestione del rischio). Adozione: 18 mesi dalla notifica di inserimento nell elenco NIS.',
+ 'full' => 'Misure di sicurezza di base (Determinazione ACN 164179/2025) articolate secondo il Framework Nazionale per la Cybersecurity e la Data Protection (ed. 2025): funzioni Governance (GV), Identificazione (ID), Protezione (PR), Rilevazione (DE), Risposta (RS), Ripristino (RC), poi categorie, sottocategorie e requisiti. Soggetti importanti: 37 misure / 92 requisiti (Allegato 1). Soggetti essenziali: 43 misure / 116 requisiti (Allegato 2), codifica identica con requisiti aggiuntivi. Esempi: GV.RR-04 (cybersicurezza nelle risorse umane, adeguatezza al ruolo), GV.PO-01 (policy di gestione del rischio). Adozione: 18 mesi dalla notifica di inserimento nell elenco NIS.',
'file' => 'docs/nis2/Determina164179_apr2025.pdf',
'authority' => 'Agenzia per la Cybersicurezza Nazionale (ACN)',
'url' => 'https://www.acn.gov.it/portale/nis/modalita-specifiche-base',
diff --git a/application/controllers/PolicyController.php b/application/controllers/PolicyController.php
index 6c6db86..422c579 100644
--- a/application/controllers/PolicyController.php
+++ b/application/controllers/PolicyController.php
@@ -67,12 +67,18 @@ class PolicyController extends BaseController
if (!empty($this->isDemo)) { return; }
$orgId = $this->getCurrentOrgId();
if (!$orgId) { return; }
+ $pdo = Database::getInstance();
try {
+ // Transazione + lock di riga: serializza richieste concorrenti (niente doppio
+ // seed) e, in caso di fallimento parziale, il rollback annulla gli insert già
+ // fatti (niente procedure duplicate alla ri-prova). Il flag si scrive nello
+ // stesso commit degli insert.
+ $pdo->beginTransaction();
$org = Database::fetchOne(
- 'SELECT framework_procedures_seeded_at FROM organizations WHERE id = ?',
+ 'SELECT framework_procedures_seeded_at FROM organizations WHERE id = ? FOR UPDATE',
[$orgId]
);
- if (!$org || $org['framework_procedures_seeded_at'] !== null) { return; }
+ if (!$org || $org['framework_procedures_seeded_at'] !== null) { $pdo->commit(); return; }
$procs = Database::fetchAll(
"SELECT p.proc_code, p.proc_descr,
@@ -85,7 +91,7 @@ class PolicyController extends BaseController
FROM cfg_nis2_requisiti q3 WHERE q3.proc_code = p.proc_code) AS misure
FROM cfg_nis2_procedure p ORDER BY p.proc_code"
);
- if (!$procs) { return; } // framework non ancora seedato: riprova al prossimo accesso
+ if (!$procs) { $pdo->commit(); return; } // framework non ancora seedato: riprova al prossimo accesso
foreach ($procs as $p) {
$cat = self::PROC_NIST_CATEGORY[$p['nist_code'] ?? ''] ?? 'information_security';
@@ -107,7 +113,9 @@ class PolicyController extends BaseController
'UPDATE organizations SET framework_procedures_seeded_at = NOW() WHERE id = ?',
[$orgId]
);
+ $pdo->commit();
} catch (Throwable $e) {
+ if ($pdo->inTransaction()) { $pdo->rollBack(); }
error_log('[PolicyController] seed procedure default fallito (org ' . $orgId . '): ' . $e->getMessage());
}
}
diff --git a/public/_app-bi-demo.html b/public/_app-bi-demo.html
index 1ce054f..dac6415 100644
--- a/public/_app-bi-demo.html
+++ b/public/_app-bi-demo.html
@@ -70,8 +70,8 @@
-
-
+
+
-
+
+
@@ -164,8 +164,8 @@
}
-
-
+
+
-
-
-
+
+
+
+
-
+
+
@@ -166,8 +166,8 @@
}
-
-
+
+
-
+
+
@@ -378,8 +378,8 @@
}
-
-
+
+
-
+
+
-
-
+
+
-
+
+
-
-
+
+