From 8d7a50abbe20bd6b14d35bd095144d9b39d1a530 Mon Sep 17 00:00:00 2001 From: DevEnv nis2-agile Date: Sun, 31 May 2026 10:37:38 +0200 Subject: [PATCH] [FEAT] Seed template "NIS2 base" fornitori (26 domande GV.SC) idempotente per-org Script scripts/seed_supplier_template.php: crea il template predefinito NIS2 base (is_default, pass_threshold 70) + 26 domande dal JSON canonico docs/supplier-portal/template-nis2-base.questions.json, mappate alle misure ACN GV.SC (Allegato 2 Det. 164179/2025) con nis2_ref/vuln_flag/high_criticality_only. Idempotente: template per nome, domande per question_code. Applicato a org 129 (Agile Technology, dogfooding): template id=1, 26 domande. Re-run verificato: 0 inserite, 26 gia presenti. Co-Authored-By: Claude Opus 4.8 --- scripts/seed_supplier_template.php | 100 +++++++++++++++++++++++++++++ 1 file changed, 100 insertions(+) create mode 100644 scripts/seed_supplier_template.php diff --git a/scripts/seed_supplier_template.php b/scripts/seed_supplier_template.php new file mode 100644 index 0000000..56c9193 --- /dev/null +++ b/scripts/seed_supplier_template.php @@ -0,0 +1,100 @@ + + * (scripts/ NON e bind-mountato nel container: copiare in public/ per l'esecuzione, + * oppure eseguire via CLI host con il giusto include path). + * + * Sorgente domande: docs/supplier-portal/template-nis2-base.questions.json (26 domande, + * mappate alle misure ACN GV.SC, Allegato 2 Det. 164179/2025). + * + * Idempotente: se esiste gia un template is_default con name "NIS2 base" per l'org, + * non duplica (verifica per nome) e salta le domande gia presenti (per question_code). + */ + +$ROOT = '/var/www/nis2-agile'; +require $ROOT . '/application/config/env.php'; +require $ROOT . '/application/config/config.php'; +require $ROOT . '/application/config/database.php'; + +$orgId = (int) ($argv[1] ?? 0); +if ($orgId <= 0) { + fwrite(STDERR, "ERRORE: org_id mancante. Uso: php seed_supplier_template.php \n"); + exit(1); +} + +$org = Database::fetchOne('SELECT id, name FROM organizations WHERE id = ?', [$orgId]); +if (!$org) { + fwrite(STDERR, "ERRORE: organizzazione {$orgId} non trovata.\n"); + exit(1); +} + +$jsonPath = $ROOT . '/docs/supplier-portal/template-nis2-base.questions.json'; +if (!is_file($jsonPath)) { + fwrite(STDERR, "ERRORE: file domande non trovato: {$jsonPath}\n"); + exit(1); +} +$data = json_decode((string) file_get_contents($jsonPath), true); +$questions = $data['questions'] ?? []; +if (empty($questions)) { + fwrite(STDERR, "ERRORE: nessuna domanda nel JSON.\n"); + exit(1); +} + +// 1. Template (idempotente per nome) +$tplName = 'NIS2 base - Sicurezza catena di fornitura'; +$tpl = Database::fetchOne( + 'SELECT id FROM questionnaire_templates WHERE organization_id = ? AND name = ?', + [$orgId, $tplName] +); +if ($tpl) { + $tplId = (int) $tpl['id']; + echo "Template gia presente (id={$tplId}) per org {$orgId} ({$org['name']}).\n"; +} else { + $tplId = Database::insert('questionnaire_templates', [ + 'organization_id' => $orgId, + 'category_id' => null, + 'name' => $tplName, + 'description' => 'Template predefinito mappato alle misure ACN GV.SC (Allegato 2, Det. 164179/2025) e Art. 21.2(d)/21.3 Dir. (UE) 2022/2555. 26 domande, scoring per-vulnerabilita.', + 'current_version' => '1.0', + 'status' => 'active', + 'pass_threshold' => 70, + 'is_default' => 1, + 'created_by' => null, + ]); + echo "Template creato (id={$tplId}) per org {$orgId} ({$org['name']}).\n"; +} + +// 2. Domande (idempotente per question_code) +$inserted = 0; $skipped = 0; +foreach ($questions as $q) { + $code = $q['code'] ?? null; + if ($code === null) { continue; } + $exists = Database::fetchOne( + 'SELECT id FROM questionnaire_questions WHERE template_id = ? AND question_code = ?', + [$tplId, $code] + ); + if ($exists) { $skipped++; continue; } + + Database::insert('questionnaire_questions', [ + 'template_id' => $tplId, + 'organization_id' => $orgId, + 'question_code' => $code, + 'question_text' => $q['text'] ?? '', + 'question_type' => $q['type'] ?? 'yes_no_partial', + 'options' => isset($q['options']) && $q['options'] !== null + ? json_encode($q['options'], JSON_UNESCAPED_UNICODE) : null, + 'weight' => (float) ($q['weight'] ?? 1), + 'is_required' => !empty($q['required']) ? 1 : 0, + 'order_index' => (int) ($q['order_index'] ?? 0), + 'nis2_ref' => $q['nis2_ref'] ?? null, + 'vuln_flag' => $q['vuln_flag'] ?? null, + 'high_criticality_only' => !empty($q['high_criticality_only']) ? 1 : 0, + ]); + $inserted++; +} + +$total = Database::fetchOne('SELECT COUNT(*) AS n FROM questionnaire_questions WHERE template_id = ?', [$tplId]); +echo "Domande: inserite={$inserted}, gia presenti={$skipped}, totale nel template={$total['n']}.\n"; +echo "OK.\n";