[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:
co-authored by
Claude Opus 4.8
parent
b34bf3f9f6
commit
4570ce3ba1
@@ -11,9 +11,25 @@ require_once APP_PATH . '/services/WebhookService.php';
|
||||
|
||||
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
|
||||
{
|
||||
$this->requireOrgAccess();
|
||||
$this->ensureFrameworkProcedures();
|
||||
|
||||
$where = 'organization_id = ?';
|
||||
$params = [$this->getCurrentOrgId()];
|
||||
@@ -39,6 +55,63 @@ class PolicyController extends BaseController
|
||||
$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
|
||||
{
|
||||
$this->requireOrgRole(['org_admin', 'compliance_manager']);
|
||||
|
||||
@@ -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;
|
||||
Reference in New Issue
Block a user