[FEAT] Modello Organizzativo SGSI (ISO 27001/27017/27018) + SoA pre-popolato da NIS2
Nuovo modulo guidato in 6 step (cl. 4-10 + Statement of Applicability): - migration 037 (isms_models/roles/soa/documents) + 038 (dataset 111 controlli: 93 Annex A:2022 + 7 CLD/27017 + 11 PII/27018) + runner scripts/migrate-isms.php - IsmsModelController (16 endpoint) registrato in index.php - SoA pre-popolato dalle risposte Gap Analysis NIS2 (mapping iso27001_control) - estensioni cloud condizionali 27017/27018 via flag uses_public_cloud/ is_cloud_provider/processes_pii_in_cloud - AIService::generateIsmsDocument + fonti ISO in nis2_sources.php - frontend isms.html/isms.js + api client + sidebar + help + i18n IT/EN - ingest KB ISO (scope SYSTEM, solo titoli/sintesi: no testo coperto da copyright) - version.json 1.14.0; doc studio + deploy handoff Strumento di supporto/pre-audit (non certificazione). Migration DA APPLICARE su host. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,173 @@
|
||||
<?php
|
||||
/**
|
||||
* NIS2 Agile - Ingest fonti ISO 27000 nella Knowledge Base (RAG)
|
||||
* ----------------------------------------------------------------------------
|
||||
* Indicizza nella collection Qdrant `nis2_kb` (scope SYSTEM) il CATALOGO CURATO
|
||||
* dei controlli ISO (dalla tabella iso27001_annex_controls) + le descrizioni
|
||||
* delle fonti ISO registrate in application/config/nis2_sources.php, cosi che
|
||||
* AIService::askWithRag() e il modulo SGSI abbiano grounding sui controlli
|
||||
* 27001/27017/27018.
|
||||
*
|
||||
* IMPORTANTE (copyright): NON viene indicizzato il testo letterale delle norme
|
||||
* ISO (coperto da copyright), ma solo titoli/sintesi proprietari e riferimenti.
|
||||
*
|
||||
* PREREQUISITI: migration 038 applicata (tabella iso27001_annex_controls
|
||||
* popolata) + accesso a Qdrant + Voyage. ESEGUIRE SU HETZNER:
|
||||
* docker exec -i nis2-app php /var/www/nis2-agile/scripts/ingest-iso-sources.php
|
||||
*
|
||||
* Opzioni:
|
||||
* --dry-run costruisce i chunk e stampa le statistiche senza upsert
|
||||
* ============================================================================
|
||||
*/
|
||||
|
||||
if (PHP_SAPI !== 'cli') { fwrite(STDERR, "Solo CLI\n"); exit(1); }
|
||||
|
||||
if (!defined('BASE_PATH')) define('BASE_PATH', dirname(__DIR__));
|
||||
if (!defined('APP_PATH')) define('APP_PATH', BASE_PATH . '/application');
|
||||
|
||||
require_once APP_PATH . '/config/env.php';
|
||||
require_once APP_PATH . '/config/config.php';
|
||||
require_once APP_PATH . '/config/database.php';
|
||||
require_once APP_PATH . '/services/EmbedService.php';
|
||||
require_once APP_PATH . '/services/VectorService.php';
|
||||
|
||||
$opts = getopt('', ['dry-run']);
|
||||
$dryRun = isset($opts['dry-run']);
|
||||
|
||||
function logln(string $m): void { echo '[' . date('Y-m-d H:i:s') . "] $m\n"; }
|
||||
|
||||
function uuid(): string
|
||||
{
|
||||
$b = random_bytes(16);
|
||||
$b[6] = chr((ord($b[6]) & 0x0f) | 0x40);
|
||||
$b[8] = chr((ord($b[8]) & 0x3f) | 0x80);
|
||||
return vsprintf('%s%s-%s-%s-%s-%s%s%s', str_split(bin2hex($b), 4));
|
||||
}
|
||||
|
||||
function chunkText(string $text, int $size = 2000, int $overlap = 200): array
|
||||
{
|
||||
$text = mb_convert_encoding($text, 'UTF-8', 'UTF-8');
|
||||
$chunks = []; $len = mb_strlen($text, 'UTF-8'); $start = 0;
|
||||
while ($start < $len) {
|
||||
$take = min($size, $len - $start);
|
||||
$piece = mb_substr($text, $start, $take, 'UTF-8');
|
||||
if (trim($piece) !== '') $chunks[] = $piece;
|
||||
if ($start + $take >= $len) break;
|
||||
$start += ($size - $overlap);
|
||||
}
|
||||
return $chunks;
|
||||
}
|
||||
|
||||
$sources = require APP_PATH . '/config/nis2_sources.php';
|
||||
$isoKeys = ['iso_27001_2022','iso_27002_2022','iso_27017_2015','iso_27018_2019'];
|
||||
|
||||
// Costruisce un documento di testo per ciascuno standard ISO: descrizione fonte
|
||||
// + elenco dei controlli (codice, titolo IT/EN, tema) presi dal DB curato.
|
||||
$standardMap = [
|
||||
'iso_27001_2022' => 'iso27001',
|
||||
'iso_27017_2015' => 'iso27017',
|
||||
'iso_27018_2019' => 'iso27018',
|
||||
];
|
||||
|
||||
logln('=== Ingest fonti ISO 27000 nella KB (scope SYSTEM) ===');
|
||||
if ($dryRun) logln('MODALITA DRY-RUN: nessun upsert.');
|
||||
|
||||
$embed = null; $vector = null;
|
||||
if (!$dryRun) {
|
||||
$embed = new EmbedService();
|
||||
$vector = new VectorService();
|
||||
$vector->ensureCollection($embed->dims);
|
||||
}
|
||||
|
||||
$totalChunks = 0; $done = 0;
|
||||
foreach ($isoKeys as $key) {
|
||||
if (!isset($sources[$key])) { logln("SKIP {$key}: non nel registry"); continue; }
|
||||
$src = $sources[$key];
|
||||
|
||||
$body = "FONTE: {$src['citation']}\nAUTORITA: {$src['authority']}\n\n{$src['full']}\n\n";
|
||||
|
||||
// Catalogo controlli (se questo standard ha una mappatura DB)
|
||||
if (isset($standardMap[$key])) {
|
||||
$std = $standardMap[$key];
|
||||
try {
|
||||
$rows = Database::fetchAll(
|
||||
'SELECT control_code, theme, title_it, title_en, iso27002_ref, condition_tag
|
||||
FROM iso27001_annex_controls WHERE standard = ? ORDER BY sort_order',
|
||||
[$std]
|
||||
);
|
||||
} catch (Throwable $e) {
|
||||
logln(" ERRORE lettura iso27001_annex_controls (migration 038 applicata?): " . $e->getMessage());
|
||||
$rows = [];
|
||||
}
|
||||
if ($rows) {
|
||||
$body .= "CATALOGO CONTROLLI ({$src['short']}):\n";
|
||||
foreach ($rows as $r) {
|
||||
$cond = $r['condition_tag'] ? " [condizionale: {$r['condition_tag']}]" : '';
|
||||
$ref = $r['iso27002_ref'] ? " ({$r['iso27002_ref']})" : '';
|
||||
$body .= "- {$r['control_code']} - {$r['title_it']} / {$r['title_en']} [tema: {$r['theme']}]{$ref}{$cond}\n";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$body = preg_replace('/[ \t]+/', ' ', $body);
|
||||
$chunks = chunkText($body, 2000, 200);
|
||||
logln("Fonte: {$src['short']} -> " . count($chunks) . ' chunk');
|
||||
$totalChunks += count($chunks);
|
||||
|
||||
if ($dryRun) { $done++; continue; }
|
||||
|
||||
// Idempotenza: rimuovi i chunk SYSTEM esistenti per questa fonte
|
||||
try {
|
||||
$vector->deleteByFilter(['must' => [
|
||||
['key' => 'scope', 'match' => ['value' => 'SYSTEM']],
|
||||
['key' => 'source', 'match' => ['value' => $src['citation']]],
|
||||
]]);
|
||||
} catch (Exception $e) { logln(' (warning) delete precedente: ' . $e->getMessage()); }
|
||||
|
||||
$docUuid = uuid();
|
||||
$points = [];
|
||||
foreach ($chunks as $i => $chunk) {
|
||||
$vec = null;
|
||||
for ($try = 1; $try <= 5; $try++) {
|
||||
try { $vec = $embed->embed($chunk); break; }
|
||||
catch (Throwable $e) {
|
||||
if ($try === 5) { logln(" ERRORE embed chunk {$i}: " . $e->getMessage()); throw $e; }
|
||||
sleep($try);
|
||||
}
|
||||
}
|
||||
$points[] = [
|
||||
'id' => uuid(),
|
||||
'vector' => $vec,
|
||||
'payload' => [
|
||||
'doc_uuid' => $docUuid,
|
||||
'title' => $src['short'] . ($i > 0 ? ' (parte ' . ($i + 1) . ')' : ''),
|
||||
'chunk' => $chunk,
|
||||
'entity_type' => 'standard_iso',
|
||||
'source' => $src['citation'],
|
||||
'lang' => 'it',
|
||||
'scope' => 'SYSTEM',
|
||||
'consulting_firm_id' => null,
|
||||
'organization_id' => null,
|
||||
'shared_with_orgs' => [],
|
||||
'uploaded_by' => 0,
|
||||
],
|
||||
];
|
||||
}
|
||||
foreach (array_chunk($points, 64) as $batch) {
|
||||
$vector->upsertBatch($batch);
|
||||
}
|
||||
|
||||
try {
|
||||
$stmt = Database::getInstance()->prepare(
|
||||
"INSERT INTO kb_uploaded_documents
|
||||
(qdrant_doc_uuid, scope, consulting_firm_id, organization_id, uploaded_by, title, entity_type, source, lang, chunk_count, shared_with_orgs, status)
|
||||
VALUES (?, 'SYSTEM', NULL, NULL, 0, ?, 'standard_iso', ?, 'it', ?, '[]', 'ready')"
|
||||
);
|
||||
$stmt->execute([$docUuid, $src['short'], $src['citation'], count($chunks)]);
|
||||
} catch (Exception $e) { logln(' (warning) tracking insert: ' . $e->getMessage()); }
|
||||
|
||||
logln(" OK indicizzato (doc_uuid={$docUuid})");
|
||||
$done++;
|
||||
}
|
||||
|
||||
logln("=== Completato: {$done} fonti ISO, {$totalChunks} chunk totali ===");
|
||||
Reference in New Issue
Block a user