[FEAT] Epic C / C1 operativo — copia procedure di default al 1° accesso (mig.048)

PolicyController::list ora, al primo accesso al modulo Procedure di un'org reale,
copia le 42 procedure di default (cfg_nis2_procedure) in policies come contenuto
editabile/cancellabile (decisione utente: copia per-org al 1° accesso).
- organizations.framework_procedures_seeded_at = idempotenza (no ri-copia).
- categoria policies assegnata da mappa famiglia NIST->ENUM; nis2_article=misura;
  content riporta misure coperte + nota 'personalizzabile e cancellabile'.
- Salta org demo/sandbox (isDemo) per non inquinare il dataset dimostrativo.
- Best-effort: errore nel seed NON blocca la lista (log + return).
Smoke prod: org 151 3->45 policy (42 default, 10 categorie), idempotente al 2° accesso,
golden 151 poi ripristinato (cleanup). Additivo.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
DevEnv nis2-agile
2026-06-16 18:18:28 +02:00
co-authored by Claude Opus 4.8
parent b34bf3f9f6
commit 4570ce3ba1
2 changed files with 84 additions and 0 deletions
@@ -11,9 +11,25 @@ require_once APP_PATH . '/services/WebhookService.php';
class PolicyController extends BaseController class PolicyController extends BaseController
{ {
// Epic C / C1 operativo: mappa famiglia NIST -> categoria policies (ENUM)
// per assegnare una categoria sensata alle procedure di default copiate.
private const PROC_NIST_CATEGORY = [
'GV.OC' => 'information_security', 'GV.RM' => 'information_security',
'GV.RR' => 'information_security', 'GV.PO' => 'information_security',
'GV.SC' => 'supply_chain',
'ID.AM' => 'asset_management', 'ID.RA' => 'vulnerability_management',
'ID.IM' => 'business_continuity',
'PR.AA' => 'access_control', 'PR.AT' => 'hr_security',
'PR.DS' => 'encryption', 'PR.PS' => 'information_security',
'PR.IR' => 'network_security', 'DE.CM' => 'information_security',
'RS.MA' => 'incident_response', 'RS.CO' => 'incident_response',
'RC.RP' => 'business_continuity', 'RC.CO' => 'business_continuity',
];
public function list(): void public function list(): void
{ {
$this->requireOrgAccess(); $this->requireOrgAccess();
$this->ensureFrameworkProcedures();
$where = 'organization_id = ?'; $where = 'organization_id = ?';
$params = [$this->getCurrentOrgId()]; $params = [$this->getCurrentOrgId()];
@@ -39,6 +55,63 @@ class PolicyController extends BaseController
$this->jsonSuccess($policies); $this->jsonSuccess($policies);
} }
/**
* Epic C / C1 operativo — copia le 42 procedure di default (cfg_nis2_procedure)
* nell'elenco procedure dell'org al PRIMO accesso al modulo, come contenuto
* editabile/cancellabile. Idempotente (flag organizations.framework_procedures_seeded_at).
* Salta le org demo/sandbox (no inquinamento del dataset dimostrativo). Best-effort:
* un errore NON blocca la lista (log + return).
*/
private function ensureFrameworkProcedures(): void
{
if (!empty($this->isDemo)) { return; }
$orgId = $this->getCurrentOrgId();
if (!$orgId) { return; }
try {
$org = Database::fetchOne(
'SELECT framework_procedures_seeded_at FROM organizations WHERE id = ?',
[$orgId]
);
if (!$org || $org['framework_procedures_seeded_at'] !== null) { return; }
$procs = Database::fetchAll(
"SELECT p.proc_code, p.proc_descr,
(SELECT m.nist_code FROM cfg_nis2_requisiti q
JOIN cfg_nis2_misure m ON m.misura_code = q.misura_code
WHERE q.proc_code = p.proc_code ORDER BY q.n LIMIT 1) AS nist_code,
(SELECT q2.misura_code FROM cfg_nis2_requisiti q2
WHERE q2.proc_code = p.proc_code ORDER BY q2.n LIMIT 1) AS first_misura,
(SELECT GROUP_CONCAT(DISTINCT q3.misura_code ORDER BY q3.misura_code SEPARATOR ', ')
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
foreach ($procs as $p) {
$cat = self::PROC_NIST_CATEGORY[$p['nist_code'] ?? ''] ?? 'information_security';
$content = $p['proc_descr']
. "\n\nProcedura di default del framework NIS2 (Determinazione ACN n. 164179 del 14 aprile 2025)."
. ($p['misure'] ? ' Copre le misure: ' . $p['misure'] . '.' : '')
. ' Contenuto iniziale personalizzabile e cancellabile.';
Database::insert('policies', [
'organization_id' => $orgId,
'title' => mb_substr($p['proc_descr'], 0, 255),
'category' => $cat,
'nis2_article' => mb_substr($p['first_misura'] ?? '', 0, 20),
'content' => $content,
'status' => 'draft',
'ai_generated' => 0,
]);
}
Database::query(
'UPDATE organizations SET framework_procedures_seeded_at = NOW() WHERE id = ?',
[$orgId]
);
} catch (Throwable $e) {
error_log('[PolicyController] seed procedure default fallito (org ' . $orgId . '): ' . $e->getMessage());
}
}
public function create(): void public function create(): void
{ {
$this->requireOrgRole(['org_admin', 'compliance_manager']); $this->requireOrgRole(['org_admin', 'compliance_manager']);
+11
View File
@@ -0,0 +1,11 @@
-- =====================================================================
-- 048 — Flag idempotenza per la copia procedure di default (Epic C / C1 operativo)
-- =====================================================================
-- Le 42 procedure di default (cfg_nis2_procedure) vengono copiate nell'elenco
-- procedure (policies) dell'org al PRIMO accesso al modulo, come contenuto
-- editabile/cancellabile. Questo flag evita la ri-copia.
-- Additivo, reversibile (ALTER ... DROP COLUMN framework_procedures_seeded_at).
-- =====================================================================
ALTER TABLE organizations
ADD COLUMN framework_procedures_seeded_at DATETIME NULL DEFAULT NULL;