[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:
@@ -81,4 +81,43 @@ return [
|
|||||||
'authority' => 'Agenzia per la Cybersicurezza Nazionale (ACN)',
|
'authority' => 'Agenzia per la Cybersicurezza Nazionale (ACN)',
|
||||||
'url' => 'https://www.acn.gov.it/portale/nis/modalita-specifiche-base',
|
'url' => 'https://www.acn.gov.it/portale/nis/modalita-specifiche-base',
|
||||||
],
|
],
|
||||||
|
|
||||||
|
// ── Standard ISO/IEC 27000 (best practice, NON fonti normative vincolanti) ──
|
||||||
|
// Usate dal modulo Modello Organizzativo SGSI per il grounding di AI e help.
|
||||||
|
'iso_27001_2022' => [
|
||||||
|
'key' => 'iso_27001_2022',
|
||||||
|
'short' => 'ISO/IEC 27001:2022',
|
||||||
|
'citation' => 'ISO/IEC 27001:2022',
|
||||||
|
'full' => 'ISO/IEC 27001:2022 - Information security, cybersecurity and privacy protection - Information security management systems - Requirements. Definisce i requisiti del SGSI (clausole 4-10) e l\'Annex A con 93 controlli su 4 temi. BEST PRACTICE internazionale, NON obbligo normativo.',
|
||||||
|
'file' => null,
|
||||||
|
'authority' => 'ISO/IEC (best practice, non vincolante)',
|
||||||
|
'url' => 'https://www.iso.org/standard/27001',
|
||||||
|
],
|
||||||
|
'iso_27002_2022' => [
|
||||||
|
'key' => 'iso_27002_2022',
|
||||||
|
'short' => 'ISO/IEC 27002:2022',
|
||||||
|
'citation' => 'ISO/IEC 27002:2022',
|
||||||
|
'full' => 'ISO/IEC 27002:2022 - Information security controls. Guida implementativa dei 93 controlli dell\'Annex A di ISO 27001:2022. BEST PRACTICE, non vincolante.',
|
||||||
|
'file' => null,
|
||||||
|
'authority' => 'ISO/IEC (best practice, non vincolante)',
|
||||||
|
'url' => 'https://www.iso.org/standard/75652.html',
|
||||||
|
],
|
||||||
|
'iso_27017_2015' => [
|
||||||
|
'key' => 'iso_27017_2015',
|
||||||
|
'short' => 'ISO/IEC 27017:2015',
|
||||||
|
'citation' => 'ISO/IEC 27017:2015',
|
||||||
|
'full' => 'ISO/IEC 27017:2015 - Code of practice for information security controls based on ISO/IEC 27002 for cloud services. Aggiunge guida cloud-specifica e 7 controlli CLD.* (numerati su ISO 27002:2013). Applicabile a clienti e fornitori cloud. BEST PRACTICE, non vincolante.',
|
||||||
|
'file' => null,
|
||||||
|
'authority' => 'ISO/IEC (best practice, non vincolante)',
|
||||||
|
'url' => 'https://www.iso.org/standard/43757.html',
|
||||||
|
],
|
||||||
|
'iso_27018_2019' => [
|
||||||
|
'key' => 'iso_27018_2019',
|
||||||
|
'short' => 'ISO/IEC 27018:2019',
|
||||||
|
'citation' => 'ISO/IEC 27018:2019',
|
||||||
|
'full' => 'ISO/IEC 27018:2019 - Code of practice for protection of personally identifiable information (PII) in public clouds acting as PII processors. Estende ISO 27002 con controlli privacy (principi ISO 29100). Sinergico con il GDPR per gli obblighi del responsabile del trattamento. BEST PRACTICE, non vincolante.',
|
||||||
|
'file' => null,
|
||||||
|
'authority' => 'ISO/IEC (best practice, non vincolante)',
|
||||||
|
'url' => 'https://www.iso.org/standard/76559.html',
|
||||||
|
],
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -0,0 +1,652 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* NIS2 Agile - Modello Organizzativo SGSI (ISO/IEC 27001:2022)
|
||||||
|
* ----------------------------------------------------------------------------
|
||||||
|
* Procedura guidata per costruire un Sistema di Gestione della Sicurezza delle
|
||||||
|
* Informazioni (SGSI/ISMS) secondo ISO/IEC 27001:2022 (clausole 4-10) +
|
||||||
|
* Statement of Applicability (SoA) sui controlli Annex A:2022, estendibile con
|
||||||
|
* i controlli cloud ISO/IEC 27017:2015 e privacy-cloud ISO/IEC 27018:2019.
|
||||||
|
*
|
||||||
|
* Valore chiave: il SoA viene PRE-POPOLATO dalle risposte dell'assessment NIS2
|
||||||
|
* Art.21 (ogni domanda del questionario porta gia un iso27001_control), cosi un
|
||||||
|
* assessment NIS2 esistente produce una prima bozza di SoA.
|
||||||
|
*
|
||||||
|
* NON sostituisce la valutazione di un auditor: e uno strumento di supporto /
|
||||||
|
* pre-audit. Le bozze AI riportano sempre un disclaimer.
|
||||||
|
*
|
||||||
|
* Endpoint (base /api/isms):
|
||||||
|
* GET /model - SGSI dell'org (null se non iniziato)
|
||||||
|
* POST /model - crea/aggiorna SGSI (upsert) — clausole 4-6
|
||||||
|
* PUT /model - alias di POST (salva step)
|
||||||
|
* GET /annex-controls - dataset controlli applicabili (filtrato per flag cloud/PII)
|
||||||
|
* GET /soa - Statement of Applicability (auto-derivato al 1o accesso)
|
||||||
|
* POST /soa/derive - (ri)deriva lo stato iniziale dal NIS2
|
||||||
|
* PUT /soa - aggiorna un controllo del SoA (body: control_code, ...)
|
||||||
|
* GET /roles - ruoli/RACI del SGSI
|
||||||
|
* POST /roles - crea ruolo
|
||||||
|
* DELETE /roles/{id} - elimina ruolo
|
||||||
|
* GET /documents - documented information
|
||||||
|
* POST /documents - crea documento
|
||||||
|
* POST /documents/ai-generate - genera bozza documento via AI
|
||||||
|
* PUT /documents/{id} - aggiorna documento
|
||||||
|
* GET /readiness - % completamento SGSI + checklist clausole 4-10
|
||||||
|
* GET /export - export completo (model + SoA + ruoli + documenti)
|
||||||
|
*/
|
||||||
|
|
||||||
|
require_once __DIR__ . '/BaseController.php';
|
||||||
|
|
||||||
|
class IsmsModelController extends BaseController
|
||||||
|
{
|
||||||
|
private const STANDARD_LABELS = [
|
||||||
|
'iso27001' => 'ISO/IEC 27001:2022 Annex A',
|
||||||
|
'iso27017' => 'ISO/IEC 27017:2015 (cloud)',
|
||||||
|
'iso27018' => 'ISO/IEC 27018:2019 (PII in cloud)',
|
||||||
|
];
|
||||||
|
|
||||||
|
// ════════════════════════ MODEL ════════════════════════
|
||||||
|
|
||||||
|
/** GET /api/isms/model */
|
||||||
|
public function getModel(): void
|
||||||
|
{
|
||||||
|
$this->requireOrgAccess();
|
||||||
|
$m = $this->loadModel();
|
||||||
|
if ($m) {
|
||||||
|
$m['interested_parties'] = $m['interested_parties'] ? json_decode($m['interested_parties'], true) : [];
|
||||||
|
$m['isms_objectives'] = $m['isms_objectives'] ? json_decode($m['isms_objectives'], true) : [];
|
||||||
|
}
|
||||||
|
$this->jsonSuccess(['model' => $m]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* POST/PUT /api/isms/model
|
||||||
|
* Upsert del SGSI dell'org (una riga per org). Salva i campi inviati;
|
||||||
|
* i campi assenti non vengono toccati (salvataggio per-step).
|
||||||
|
*/
|
||||||
|
public function saveModel(): void
|
||||||
|
{
|
||||||
|
$this->requireOrgRole(['org_admin', 'compliance_manager']);
|
||||||
|
$orgId = $this->getCurrentOrgId();
|
||||||
|
$userId = $this->getCurrentUserId();
|
||||||
|
$body = $this->getJsonBody();
|
||||||
|
|
||||||
|
// Whitelist campi testuali/flag.
|
||||||
|
$fields = [];
|
||||||
|
foreach (['scope_statement','context_internal','context_external','boundaries','exclusions','risk_methodology'] as $k) {
|
||||||
|
if (array_key_exists($k, $body)) {
|
||||||
|
$fields[$k] = $body[$k] !== null ? (string) $body[$k] : null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
foreach (['interested_parties','isms_objectives'] as $k) {
|
||||||
|
if (array_key_exists($k, $body)) {
|
||||||
|
$fields[$k] = json_encode($body[$k] ?? [], JSON_UNESCAPED_UNICODE);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
foreach (['uses_public_cloud','is_cloud_provider','processes_pii_in_cloud'] as $k) {
|
||||||
|
if (array_key_exists($k, $body)) {
|
||||||
|
$fields[$k] = !empty($body[$k]) ? 1 : 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (array_key_exists('status', $body) && in_array($body['status'], ['draft','active','under_review'], true)) {
|
||||||
|
$fields['status'] = $body['status'];
|
||||||
|
}
|
||||||
|
|
||||||
|
$existing = $this->loadModel();
|
||||||
|
if ($existing) {
|
||||||
|
if (!empty($fields)) {
|
||||||
|
Database::update('isms_models', $fields, 'id = ?', [$existing['id']]);
|
||||||
|
}
|
||||||
|
$modelId = (int) $existing['id'];
|
||||||
|
$this->logAudit('isms_model_updated', 'isms_model', $modelId, array_keys($fields));
|
||||||
|
} else {
|
||||||
|
$fields['organization_id'] = $orgId;
|
||||||
|
$fields['created_by'] = $userId;
|
||||||
|
$fields['status'] = $fields['status'] ?? 'draft';
|
||||||
|
$modelId = Database::insert('isms_models', $fields);
|
||||||
|
$this->logAudit('isms_model_created', 'isms_model', $modelId, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->jsonSuccess(['id' => $modelId], 'SGSI salvato');
|
||||||
|
}
|
||||||
|
|
||||||
|
// ════════════════════════ ANNEX CONTROLS ════════════════════════
|
||||||
|
|
||||||
|
/**
|
||||||
|
* GET /api/isms/annex-controls
|
||||||
|
* Dataset di riferimento, filtrato in base ai flag cloud/PII del modello:
|
||||||
|
* i controlli condizionali (27017/27018) compaiono solo se pertinenti.
|
||||||
|
*/
|
||||||
|
public function annexControls(): void
|
||||||
|
{
|
||||||
|
$this->requireOrgAccess();
|
||||||
|
$m = $this->loadModel();
|
||||||
|
$standards = $this->applicableStandards($m);
|
||||||
|
|
||||||
|
$place = implode(',', array_fill(0, count($standards), '?'));
|
||||||
|
$rows = Database::fetchAll(
|
||||||
|
"SELECT control_code, standard, theme, title_it, title_en, iso27002_ref, condition_tag
|
||||||
|
FROM iso27001_annex_controls
|
||||||
|
WHERE standard IN ($place)
|
||||||
|
ORDER BY sort_order",
|
||||||
|
$standards
|
||||||
|
);
|
||||||
|
$this->jsonSuccess([
|
||||||
|
'standards' => array_map(fn($s) => ['key' => $s, 'label' => self::STANDARD_LABELS[$s] ?? $s], $standards),
|
||||||
|
'controls' => $rows,
|
||||||
|
'total' => count($rows),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ════════════════════════ SoA ════════════════════════
|
||||||
|
|
||||||
|
/**
|
||||||
|
* GET /api/isms/soa
|
||||||
|
* Restituisce il SoA raggruppato per standard -> tema. Se vuoto, lo deriva
|
||||||
|
* automaticamente dal NIS2 al primo accesso.
|
||||||
|
*/
|
||||||
|
public function getSoa(): void
|
||||||
|
{
|
||||||
|
$this->requireOrgAccess();
|
||||||
|
$model = $this->requireModel();
|
||||||
|
|
||||||
|
$count = Database::count('isms_soa', 'isms_model_id = ?', [$model['id']]);
|
||||||
|
if ($count === 0) {
|
||||||
|
$this->doDerive($model);
|
||||||
|
}
|
||||||
|
|
||||||
|
$rows = Database::fetchAll(
|
||||||
|
"SELECT s.control_code, s.standard, s.applicable, s.justification_inclusion, s.justification_exclusion,
|
||||||
|
s.implementation_status, s.implementation_pct, s.derived_from_nis2, s.source_ref,
|
||||||
|
c.title_it, c.title_en, c.theme, c.condition_tag, c.sort_order
|
||||||
|
FROM isms_soa s
|
||||||
|
LEFT JOIN iso27001_annex_controls c ON c.control_code = s.control_code
|
||||||
|
WHERE s.isms_model_id = ?
|
||||||
|
ORDER BY c.sort_order, s.control_code",
|
||||||
|
[$model['id']]
|
||||||
|
);
|
||||||
|
|
||||||
|
$byStd = [];
|
||||||
|
foreach ($rows as $r) {
|
||||||
|
$std = $r['standard'];
|
||||||
|
$byStd[$std] ??= ['standard' => $std, 'label' => self::STANDARD_LABELS[$std] ?? $std, 'controls' => []];
|
||||||
|
$byStd[$std]['controls'][] = $r;
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->jsonSuccess([
|
||||||
|
'model_id' => (int) $model['id'],
|
||||||
|
'groups' => array_values($byStd),
|
||||||
|
'stats' => $this->soaStats($model['id']),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** POST /api/isms/soa/derive — (ri)deriva lo stato iniziale dal NIS2. */
|
||||||
|
public function deriveSoa(): void
|
||||||
|
{
|
||||||
|
$this->requireOrgRole(['org_admin', 'compliance_manager']);
|
||||||
|
$model = $this->requireModel();
|
||||||
|
$added = $this->doDerive($model);
|
||||||
|
$this->logAudit('isms_soa_derived', 'isms_model', (int) $model['id'], ['added' => $added]);
|
||||||
|
$this->jsonSuccess(['added' => $added, 'stats' => $this->soaStats($model['id'])], 'SoA derivato dal NIS2');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* PUT /api/isms/soa
|
||||||
|
* Body: { control_code, applicable?, justification_inclusion?, justification_exclusion?,
|
||||||
|
* implementation_status?, implementation_pct? }
|
||||||
|
*/
|
||||||
|
public function updateSoaControl(): void
|
||||||
|
{
|
||||||
|
$this->requireOrgRole(['org_admin', 'compliance_manager', 'auditor']);
|
||||||
|
$model = $this->requireModel();
|
||||||
|
$body = $this->getJsonBody();
|
||||||
|
|
||||||
|
$code = trim((string) ($body['control_code'] ?? ''));
|
||||||
|
if ($code === '') {
|
||||||
|
$this->jsonError('control_code mancante', 400, 'MISSING_CODE');
|
||||||
|
}
|
||||||
|
$row = Database::fetchOne('SELECT id FROM isms_soa WHERE isms_model_id = ? AND control_code = ?', [$model['id'], $code]);
|
||||||
|
if (!$row) {
|
||||||
|
$this->jsonError('Controllo non presente nel SoA', 404, 'NOT_FOUND');
|
||||||
|
}
|
||||||
|
|
||||||
|
$fields = ['updated_by' => $this->getCurrentUserId()];
|
||||||
|
if (array_key_exists('applicable', $body)) {
|
||||||
|
$fields['applicable'] = !empty($body['applicable']) ? 1 : 0;
|
||||||
|
}
|
||||||
|
foreach (['justification_inclusion','justification_exclusion'] as $k) {
|
||||||
|
if (array_key_exists($k, $body)) {
|
||||||
|
$fields[$k] = $body[$k] !== null ? (string) $body[$k] : null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (isset($body['implementation_status']) && in_array($body['implementation_status'], ['not_started','in_progress','implemented','verified'], true)) {
|
||||||
|
$fields['implementation_status'] = $body['implementation_status'];
|
||||||
|
}
|
||||||
|
if (array_key_exists('implementation_pct', $body)) {
|
||||||
|
$fields['implementation_pct'] = max(0, min(100, (int) $body['implementation_pct']));
|
||||||
|
}
|
||||||
|
|
||||||
|
Database::update('isms_soa', $fields, 'id = ?', [$row['id']]);
|
||||||
|
$this->jsonSuccess(['stats' => $this->soaStats($model['id'])], 'Controllo aggiornato');
|
||||||
|
}
|
||||||
|
|
||||||
|
// ════════════════════════ ROLES ════════════════════════
|
||||||
|
|
||||||
|
/** GET /api/isms/roles */
|
||||||
|
public function listRoles(): void
|
||||||
|
{
|
||||||
|
$this->requireOrgAccess();
|
||||||
|
$model = $this->requireModel();
|
||||||
|
$rows = Database::fetchAll(
|
||||||
|
"SELECT r.id, r.role_name, r.user_id, r.responsibility, r.raci,
|
||||||
|
u.full_name AS user_name
|
||||||
|
FROM isms_roles r
|
||||||
|
LEFT JOIN users u ON u.id = r.user_id
|
||||||
|
WHERE r.isms_model_id = ? ORDER BY r.id",
|
||||||
|
[$model['id']]
|
||||||
|
);
|
||||||
|
$this->jsonSuccess(['roles' => $rows]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** POST /api/isms/roles Body: { role_name, user_id?, responsibility?, raci? } */
|
||||||
|
public function saveRole(): void
|
||||||
|
{
|
||||||
|
$this->requireOrgRole(['org_admin', 'compliance_manager']);
|
||||||
|
$model = $this->requireModel();
|
||||||
|
$body = $this->getJsonBody();
|
||||||
|
|
||||||
|
$name = trim((string) ($body['role_name'] ?? ''));
|
||||||
|
if ($name === '') {
|
||||||
|
$this->jsonError('role_name obbligatorio', 400, 'MISSING_ROLE_NAME');
|
||||||
|
}
|
||||||
|
$raci = (string) ($body['raci'] ?? '');
|
||||||
|
$data = [
|
||||||
|
'isms_model_id' => $model['id'],
|
||||||
|
'organization_id' => $this->getCurrentOrgId(),
|
||||||
|
'role_name' => $name,
|
||||||
|
'user_id' => !empty($body['user_id']) ? (int) $body['user_id'] : null,
|
||||||
|
'responsibility' => isset($body['responsibility']) ? (string) $body['responsibility'] : null,
|
||||||
|
'raci' => in_array($raci, ['R','A','C','I'], true) ? $raci : null,
|
||||||
|
];
|
||||||
|
$id = Database::insert('isms_roles', $data);
|
||||||
|
$this->jsonSuccess(['id' => $id], 'Ruolo aggiunto', 201);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** DELETE /api/isms/roles/{id} */
|
||||||
|
public function deleteRole(int $id): void
|
||||||
|
{
|
||||||
|
$this->requireOrgRole(['org_admin', 'compliance_manager']);
|
||||||
|
$model = $this->requireModel();
|
||||||
|
Database::delete('isms_roles', 'id = ? AND isms_model_id = ?', [$id, $model['id']]);
|
||||||
|
$this->jsonSuccess(null, 'Ruolo eliminato');
|
||||||
|
}
|
||||||
|
|
||||||
|
// ════════════════════════ DOCUMENTS ════════════════════════
|
||||||
|
|
||||||
|
/** GET /api/isms/documents */
|
||||||
|
public function listDocuments(): void
|
||||||
|
{
|
||||||
|
$this->requireOrgAccess();
|
||||||
|
$model = $this->requireModel();
|
||||||
|
$rows = Database::fetchAll(
|
||||||
|
"SELECT id, doc_type, title, status, ai_generated, version, updated_at
|
||||||
|
FROM isms_documents WHERE isms_model_id = ? ORDER BY updated_at DESC",
|
||||||
|
[$model['id']]
|
||||||
|
);
|
||||||
|
$this->jsonSuccess(['documents' => $rows]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** POST /api/isms/documents Body: { doc_type, title, body_html?, status? } */
|
||||||
|
public function createDocument(): void
|
||||||
|
{
|
||||||
|
$this->requireOrgRole(['org_admin', 'compliance_manager']);
|
||||||
|
$model = $this->requireModel();
|
||||||
|
$body = $this->getJsonBody();
|
||||||
|
|
||||||
|
$this->validateRequired(['doc_type', 'title']);
|
||||||
|
$id = Database::insert('isms_documents', [
|
||||||
|
'isms_model_id' => $model['id'],
|
||||||
|
'organization_id' => $this->getCurrentOrgId(),
|
||||||
|
'doc_type' => (string) $body['doc_type'],
|
||||||
|
'title' => (string) $body['title'],
|
||||||
|
'body_html' => isset($body['body_html']) ? (string) $body['body_html'] : null,
|
||||||
|
'status' => in_array($body['status'] ?? '', ['draft','review','approved'], true) ? $body['status'] : 'draft',
|
||||||
|
'ai_generated' => !empty($body['ai_generated']) ? 1 : 0,
|
||||||
|
'created_by' => $this->getCurrentUserId(),
|
||||||
|
]);
|
||||||
|
$this->jsonSuccess(['id' => $id], 'Documento creato', 201);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** PUT /api/isms/documents/{id} */
|
||||||
|
public function updateDocument(int $id): void
|
||||||
|
{
|
||||||
|
$this->requireOrgRole(['org_admin', 'compliance_manager']);
|
||||||
|
$model = $this->requireModel();
|
||||||
|
$body = $this->getJsonBody();
|
||||||
|
$row = Database::fetchOne('SELECT id FROM isms_documents WHERE id = ? AND isms_model_id = ?', [$id, $model['id']]);
|
||||||
|
if (!$row) {
|
||||||
|
$this->jsonError('Documento non trovato', 404, 'NOT_FOUND');
|
||||||
|
}
|
||||||
|
$fields = [];
|
||||||
|
foreach (['title','body_html'] as $k) {
|
||||||
|
if (array_key_exists($k, $body)) $fields[$k] = (string) $body[$k];
|
||||||
|
}
|
||||||
|
if (isset($body['status']) && in_array($body['status'], ['draft','review','approved'], true)) {
|
||||||
|
$fields['status'] = $body['status'];
|
||||||
|
}
|
||||||
|
if (!empty($fields)) {
|
||||||
|
Database::update('isms_documents', $fields, 'id = ?', [$id]);
|
||||||
|
}
|
||||||
|
$this->jsonSuccess(['id' => $id], 'Documento aggiornato');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* POST /api/isms/documents/ai-generate
|
||||||
|
* Body: { doc_type, title? } - genera una bozza con AI (grounding fonti certe).
|
||||||
|
*/
|
||||||
|
public function aiGenerateDocument(): void
|
||||||
|
{
|
||||||
|
$this->requireOrgRole(['org_admin', 'compliance_manager']);
|
||||||
|
$model = $this->requireModel();
|
||||||
|
$body = $this->getJsonBody();
|
||||||
|
$docType = trim((string) ($body['doc_type'] ?? ''));
|
||||||
|
if ($docType === '') {
|
||||||
|
$this->jsonError('doc_type obbligatorio', 400, 'MISSING_DOC_TYPE');
|
||||||
|
}
|
||||||
|
|
||||||
|
$org = Database::fetchOne('SELECT sector, entity_type, employee_count FROM organizations WHERE id = ?', [$this->getCurrentOrgId()]);
|
||||||
|
require_once APP_PATH . '/services/AIService.php';
|
||||||
|
$ai = new AIService();
|
||||||
|
|
||||||
|
try {
|
||||||
|
$ctx = [
|
||||||
|
'scope' => $model['scope_statement'] ?? null,
|
||||||
|
'methodology' => $model['risk_methodology'] ?? null,
|
||||||
|
'uses_cloud' => (bool) ($model['uses_public_cloud'] || $model['is_cloud_provider']),
|
||||||
|
'pii_in_cloud' => (bool) $model['processes_pii_in_cloud'],
|
||||||
|
];
|
||||||
|
$doc = $ai->generateIsmsDocument($docType, $org ?: [], $ctx);
|
||||||
|
} catch (Throwable $e) {
|
||||||
|
error_log('[ISMS] aiGenerateDocument fallita: ' . $e->getMessage());
|
||||||
|
$this->jsonError('Generazione AI temporaneamente non disponibile. Riprova piu tardi.', 503, 'AI_UNAVAILABLE');
|
||||||
|
}
|
||||||
|
|
||||||
|
$title = trim((string) ($body['title'] ?? ($doc['title'] ?? $docType)));
|
||||||
|
$id = Database::insert('isms_documents', [
|
||||||
|
'isms_model_id' => $model['id'],
|
||||||
|
'organization_id' => $this->getCurrentOrgId(),
|
||||||
|
'doc_type' => $docType,
|
||||||
|
'title' => $title,
|
||||||
|
'body_html' => (string) ($doc['body_html'] ?? $doc['content'] ?? ''),
|
||||||
|
'status' => 'draft',
|
||||||
|
'ai_generated' => 1,
|
||||||
|
'created_by' => $this->getCurrentUserId(),
|
||||||
|
]);
|
||||||
|
$this->logAudit('isms_document_ai_generated', 'isms_document', $id, ['doc_type' => $docType]);
|
||||||
|
$this->jsonSuccess([
|
||||||
|
'id' => $id,
|
||||||
|
'title' => $title,
|
||||||
|
'body_html' => (string) ($doc['body_html'] ?? $doc['content'] ?? ''),
|
||||||
|
'disclaimer' => 'Bozza generata dall\'AI: revisione umana obbligatoria prima dell\'approvazione.',
|
||||||
|
], 'Bozza generata');
|
||||||
|
}
|
||||||
|
|
||||||
|
// ════════════════════════ READINESS / EXPORT ════════════════════════
|
||||||
|
|
||||||
|
/** GET /api/isms/readiness — checklist clausole 4-10 + % complessiva. */
|
||||||
|
public function readiness(): void
|
||||||
|
{
|
||||||
|
$this->requireOrgAccess();
|
||||||
|
$model = $this->loadModel();
|
||||||
|
if (!$model) {
|
||||||
|
$this->jsonSuccess(['started' => false, 'overall_pct' => 0, 'checklist' => []]);
|
||||||
|
}
|
||||||
|
|
||||||
|
$rolesCount = Database::count('isms_roles', 'isms_model_id = ?', [$model['id']]);
|
||||||
|
$docsCount = Database::count('isms_documents', 'isms_model_id = ?', [$model['id']]);
|
||||||
|
$soa = $this->soaStats($model['id']);
|
||||||
|
$soaAnswered = $soa['total'] > 0 ? ($soa['total'] - $soa['not_started']) : 0;
|
||||||
|
|
||||||
|
$checklist = [
|
||||||
|
['clause' => '4', 'label' => 'Contesto e ambito', 'done' => !empty($model['scope_statement'])],
|
||||||
|
['clause' => '5', 'label' => 'Leadership: policy e ruoli (RACI)', 'done' => $rolesCount > 0],
|
||||||
|
['clause' => '6', 'label' => 'Risk: metodologia e obiettivi', 'done' => !empty($model['risk_methodology'])],
|
||||||
|
['clause' => 'SoA', 'label' => 'Statement of Applicability avviato', 'done' => $soa['total'] > 0],
|
||||||
|
['clause' => '7-8', 'label' => 'Documented information', 'done' => $docsCount > 0],
|
||||||
|
['clause' => '9-10', 'label' => 'Monitoraggio e miglioramento (audit/NCR esistenti)', 'done' => $soaAnswered > 0],
|
||||||
|
];
|
||||||
|
$done = count(array_filter($checklist, fn($c) => $c['done']));
|
||||||
|
$overall = (int) round($done / count($checklist) * 100);
|
||||||
|
|
||||||
|
$this->jsonSuccess([
|
||||||
|
'started' => true,
|
||||||
|
'overall_pct' => $overall,
|
||||||
|
'checklist' => $checklist,
|
||||||
|
'soa' => $soa,
|
||||||
|
'roles_count' => $rolesCount,
|
||||||
|
'docs_count' => $docsCount,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** GET /api/isms/export — model + SoA + ruoli + documenti (per stampa). */
|
||||||
|
public function export(): void
|
||||||
|
{
|
||||||
|
$this->requireOrgAccess();
|
||||||
|
$model = $this->requireModel();
|
||||||
|
$org = Database::fetchOne('SELECT name, sector, entity_type FROM organizations WHERE id = ?', [$this->getCurrentOrgId()]);
|
||||||
|
|
||||||
|
$model['interested_parties'] = $model['interested_parties'] ? json_decode($model['interested_parties'], true) : [];
|
||||||
|
$model['isms_objectives'] = $model['isms_objectives'] ? json_decode($model['isms_objectives'], true) : [];
|
||||||
|
|
||||||
|
$soa = Database::fetchAll(
|
||||||
|
"SELECT s.control_code, s.standard, s.applicable, s.implementation_status, s.implementation_pct,
|
||||||
|
s.justification_inclusion, s.justification_exclusion, s.derived_from_nis2, s.source_ref,
|
||||||
|
c.title_it
|
||||||
|
FROM isms_soa s LEFT JOIN iso27001_annex_controls c ON c.control_code = s.control_code
|
||||||
|
WHERE s.isms_model_id = ? ORDER BY c.sort_order",
|
||||||
|
[$model['id']]
|
||||||
|
);
|
||||||
|
$roles = Database::fetchAll('SELECT role_name, responsibility, raci FROM isms_roles WHERE isms_model_id = ? ORDER BY id', [$model['id']]);
|
||||||
|
$docs = Database::fetchAll('SELECT doc_type, title, status, version FROM isms_documents WHERE isms_model_id = ? ORDER BY id', [$model['id']]);
|
||||||
|
|
||||||
|
$this->logAudit('isms_export', 'isms_model', (int) $model['id'], null);
|
||||||
|
$this->jsonSuccess([
|
||||||
|
'organization' => $org,
|
||||||
|
'model' => $model,
|
||||||
|
'soa' => $soa,
|
||||||
|
'roles' => $roles,
|
||||||
|
'documents' => $docs,
|
||||||
|
'stats' => $this->soaStats($model['id']),
|
||||||
|
'generated_at' => date('c'),
|
||||||
|
'disclaimer' => 'Documento di supporto/pre-audit. Non costituisce certificazione ISO 27001 ne parere professionale vincolante.',
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ════════════════════════ HELPER ════════════════════════
|
||||||
|
|
||||||
|
private function loadModel(): ?array
|
||||||
|
{
|
||||||
|
return Database::fetchOne('SELECT * FROM isms_models WHERE organization_id = ?', [$this->getCurrentOrgId()]);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function requireModel(): array
|
||||||
|
{
|
||||||
|
$m = $this->loadModel();
|
||||||
|
if (!$m) {
|
||||||
|
$this->jsonError('SGSI non ancora avviato. Completa prima il passo Contesto e Ambito.', 422, 'ISMS_NOT_STARTED');
|
||||||
|
}
|
||||||
|
return $m;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Standard applicabili in base ai flag del modello. */
|
||||||
|
private function applicableStandards(?array $model): array
|
||||||
|
{
|
||||||
|
$standards = ['iso27001'];
|
||||||
|
if ($model && ($model['uses_public_cloud'] || $model['is_cloud_provider'])) {
|
||||||
|
$standards[] = 'iso27017';
|
||||||
|
}
|
||||||
|
if ($model && $model['processes_pii_in_cloud']) {
|
||||||
|
$standards[] = 'iso27018';
|
||||||
|
}
|
||||||
|
return $standards;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Deriva/integra il SoA: inserisce i controlli applicabili mancanti,
|
||||||
|
* pre-compilando stato e motivazione dalle risposte NIS2 dove possibile.
|
||||||
|
* INSERT IGNORE => non sovrascrive il lavoro manuale gia presente.
|
||||||
|
* @return int controlli aggiunti
|
||||||
|
*/
|
||||||
|
private function doDerive(array $model): int
|
||||||
|
{
|
||||||
|
$standards = $this->applicableStandards($model);
|
||||||
|
$place = implode(',', array_fill(0, count($standards), '?'));
|
||||||
|
$controls = Database::fetchAll(
|
||||||
|
"SELECT control_code, standard FROM iso27001_annex_controls WHERE standard IN ($place) ORDER BY sort_order",
|
||||||
|
$standards
|
||||||
|
);
|
||||||
|
|
||||||
|
$nis2 = $this->nis2ControlStatus(); // [iso_control_code => ['status'=>..,'pct'=>..,'sources'=>[..]]]
|
||||||
|
$orgId = $this->getCurrentOrgId();
|
||||||
|
$userId = $this->getCurrentUserId();
|
||||||
|
|
||||||
|
$existing = array_column(
|
||||||
|
Database::fetchAll('SELECT control_code FROM isms_soa WHERE isms_model_id = ?', [$model['id']]),
|
||||||
|
'control_code'
|
||||||
|
);
|
||||||
|
$existing = array_flip($existing);
|
||||||
|
|
||||||
|
$added = 0;
|
||||||
|
foreach ($controls as $c) {
|
||||||
|
$code = $c['control_code'];
|
||||||
|
if (isset($existing[$code])) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
$d = $nis2[$code] ?? null;
|
||||||
|
$row = [
|
||||||
|
'isms_model_id' => $model['id'],
|
||||||
|
'organization_id' => $orgId,
|
||||||
|
'control_code' => $code,
|
||||||
|
'standard' => $c['standard'],
|
||||||
|
'applicable' => 1,
|
||||||
|
'updated_by' => $userId,
|
||||||
|
];
|
||||||
|
if ($d) {
|
||||||
|
$row['implementation_status'] = $d['status'];
|
||||||
|
$row['implementation_pct'] = $d['pct'];
|
||||||
|
$row['derived_from_nis2'] = 1;
|
||||||
|
$row['source_ref'] = implode(', ', array_slice($d['sources'], 0, 4));
|
||||||
|
$row['justification_inclusion'] = 'Applicabile: collegato alle misure NIS2 ' . $row['source_ref']
|
||||||
|
. '. Stato derivato dall\'assessment Art.21 (da confermare).';
|
||||||
|
}
|
||||||
|
// INSERT IGNORE manuale tramite query (Database::insert non supporta IGNORE).
|
||||||
|
$cols = implode(',', array_keys($row));
|
||||||
|
$ph = implode(',', array_fill(0, count($row), '?'));
|
||||||
|
Database::query("INSERT IGNORE INTO isms_soa ($cols) VALUES ($ph)", array_values($row));
|
||||||
|
$added++;
|
||||||
|
}
|
||||||
|
return $added;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Aggrega lo stato dei controlli ISO 27001 a partire dalle risposte NIS2.
|
||||||
|
* Usa l'ultimo assessment dell'org e il mapping question->iso27001_control
|
||||||
|
* presente nel questionario.
|
||||||
|
* @return array<string,array{status:string,pct:int,sources:array}>
|
||||||
|
*/
|
||||||
|
private function nis2ControlStatus(): array
|
||||||
|
{
|
||||||
|
$orgId = $this->getCurrentOrgId();
|
||||||
|
$assessment = Database::fetchOne(
|
||||||
|
'SELECT id FROM assessments WHERE organization_id = ? ORDER BY created_at DESC LIMIT 1',
|
||||||
|
[$orgId]
|
||||||
|
);
|
||||||
|
if (!$assessment) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
$responses = Database::fetchAll(
|
||||||
|
'SELECT question_code, response_value FROM assessment_responses WHERE assessment_id = ?',
|
||||||
|
[$assessment['id']]
|
||||||
|
);
|
||||||
|
if (empty($responses)) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
$respByCode = [];
|
||||||
|
foreach ($responses as $r) {
|
||||||
|
$respByCode[$r['question_code']] = $r['response_value'];
|
||||||
|
}
|
||||||
|
|
||||||
|
// mappa question_code -> iso27001_control + nis2_article dal questionario
|
||||||
|
$q = $this->questionnaire();
|
||||||
|
$pctVal = ['implemented' => 100, 'partial' => 50, 'not_implemented' => 0];
|
||||||
|
|
||||||
|
$agg = []; // iso_code => ['sum'=>,'cnt'=>,'sources'=>[]]
|
||||||
|
foreach ($q['categories'] ?? [] as $cat) {
|
||||||
|
foreach ($cat['questions'] ?? [] as $question) {
|
||||||
|
$iso = $question['iso27001_control'] ?? null;
|
||||||
|
$code = $question['code'] ?? null;
|
||||||
|
if (!$iso || !$code || !isset($respByCode[$code])) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
$resp = $respByCode[$code];
|
||||||
|
if ($resp === 'not_applicable' || $resp === null || !isset($pctVal[$resp])) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
$agg[$iso] ??= ['sum' => 0, 'cnt' => 0, 'sources' => []];
|
||||||
|
$agg[$iso]['sum'] += $pctVal[$resp];
|
||||||
|
$agg[$iso]['cnt']++;
|
||||||
|
$art = $question['nis2_article'] ?? '';
|
||||||
|
$src = 'Art.' . $art . ' (' . $code . ')';
|
||||||
|
if (!in_array($src, $agg[$iso]['sources'], true)) {
|
||||||
|
$agg[$iso]['sources'][] = $src;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$out = [];
|
||||||
|
foreach ($agg as $iso => $a) {
|
||||||
|
$pct = $a['cnt'] > 0 ? (int) round($a['sum'] / $a['cnt']) : 0;
|
||||||
|
$status = $pct >= 100 ? 'implemented' : ($pct > 0 ? 'in_progress' : 'not_started');
|
||||||
|
$out[$iso] = ['status' => $status, 'pct' => $pct, 'sources' => $a['sources']];
|
||||||
|
}
|
||||||
|
return $out;
|
||||||
|
}
|
||||||
|
|
||||||
|
private ?array $questionnaireCache = null;
|
||||||
|
private function questionnaire(): array
|
||||||
|
{
|
||||||
|
if ($this->questionnaireCache === null) {
|
||||||
|
$path = APP_PATH . '/data/nis2_questionnaire.json';
|
||||||
|
$json = is_readable($path) ? json_decode((string) file_get_contents($path), true) : null;
|
||||||
|
$this->questionnaireCache = is_array($json) ? $json : ['categories' => []];
|
||||||
|
}
|
||||||
|
return $this->questionnaireCache;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function soaStats(int $modelId): array
|
||||||
|
{
|
||||||
|
$rows = Database::fetchAll(
|
||||||
|
'SELECT applicable, implementation_status, COUNT(*) AS n
|
||||||
|
FROM isms_soa WHERE isms_model_id = ? GROUP BY applicable, implementation_status',
|
||||||
|
[$modelId]
|
||||||
|
);
|
||||||
|
$s = ['total' => 0, 'applicable' => 0, 'excluded' => 0,
|
||||||
|
'not_started' => 0, 'in_progress' => 0, 'implemented' => 0, 'verified' => 0];
|
||||||
|
foreach ($rows as $r) {
|
||||||
|
$n = (int) $r['n'];
|
||||||
|
$s['total'] += $n;
|
||||||
|
if ((int) $r['applicable'] === 1) {
|
||||||
|
$s['applicable'] += $n;
|
||||||
|
$st = $r['implementation_status'];
|
||||||
|
if (isset($s[$st])) $s[$st] += $n;
|
||||||
|
} else {
|
||||||
|
$s['excluded'] += $n;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// % implementazione media sui controlli applicabili
|
||||||
|
$impl = Database::fetchOne(
|
||||||
|
'SELECT AVG(implementation_pct) AS avg_pct FROM isms_soa WHERE isms_model_id = ? AND applicable = 1',
|
||||||
|
[$modelId]
|
||||||
|
);
|
||||||
|
$s['avg_implementation_pct'] = $impl && $impl['avg_pct'] !== null ? (int) round($impl['avg_pct']) : 0;
|
||||||
|
return $s;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -187,6 +187,53 @@ PROMPT;
|
|||||||
return $this->parseJsonResponse($response);
|
return $this->parseJsonResponse($response);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Genera una bozza di documented information SGSI (ISO/IEC 27001:2022).
|
||||||
|
* Es. doc_type: policy_sgsi, dichiarazione_applicabilita, procedura_incident,
|
||||||
|
* politica_controllo_accessi, procedura_continuita, ecc.
|
||||||
|
*
|
||||||
|
* Ritorna ['title'=>, 'body_html'=>]. La bozza riporta un disclaimer e cita
|
||||||
|
* le fonti certe (la conformita normativa primaria in Italia resta NIS2 /
|
||||||
|
* D.Lgs. 138/2024; ISO 27001/27002/27017/27018 sono best practice).
|
||||||
|
*/
|
||||||
|
public function generateIsmsDocument(string $docType, array $organization, ?array $context = null): array
|
||||||
|
{
|
||||||
|
$sector = $organization['sector'] ?? 'n/d';
|
||||||
|
$entityType = $organization['entity_type'] ?? 'n/d';
|
||||||
|
$ctx = $context ? json_encode($context, JSON_UNESCAPED_UNICODE) : 'Non disponibile';
|
||||||
|
$sourcesBlock = $this->authoritativeSourcesBlock();
|
||||||
|
|
||||||
|
$prompt = <<<PROMPT
|
||||||
|
Sei un esperto consulente ISO/IEC 27001:2022 (SGSI / Information Security Management System). Redigi una bozza di documento di tipo "{$docType}" per il Sistema di Gestione della Sicurezza delle Informazioni dell'organizzazione.
|
||||||
|
|
||||||
|
## Organizzazione (dati anonimizzati)
|
||||||
|
- Settore: {$sector}
|
||||||
|
- Tipo entita NIS2: {$entityType}
|
||||||
|
|
||||||
|
## Contesto SGSI
|
||||||
|
{$ctx}
|
||||||
|
{$sourcesBlock}
|
||||||
|
|
||||||
|
## Istruzioni
|
||||||
|
- Documento in italiano, professionale, coerente con ISO/IEC 27001:2022 e ISO/IEC 27002:2022. Se il contesto indica uso del cloud, considera ISO/IEC 27017:2015; se tratta dati personali in cloud, ISO/IEC 27018:2019.
|
||||||
|
- Ricorda che gli obblighi normativi italiani derivano da NIS2 / D.Lgs. 138/2024 / Determinazioni ACN; ISO e best practice non vincolante.
|
||||||
|
- Struttura tipica: Scopo, Ambito, Ruoli e responsabilita, Riferimenti normativi, Contenuto/Regole, Controlli ISO 27001 collegati (Annex A), Monitoraggio e riesame.
|
||||||
|
|
||||||
|
Rispondi SOLO con JSON:
|
||||||
|
{
|
||||||
|
"title": "Titolo del documento",
|
||||||
|
"content": "Contenuto completo in HTML semplice (h2/h3/p/ul/li/strong), pronto per essere salvato e modificato"
|
||||||
|
}
|
||||||
|
PROMPT;
|
||||||
|
|
||||||
|
$response = $this->callAPI($prompt, 'Sei un redattore di documentazione SGSI ISO 27001. Rispondi solo con JSON valido.');
|
||||||
|
$parsed = $this->parseJsonResponse($response);
|
||||||
|
return [
|
||||||
|
'title' => (string) ($parsed['title'] ?? $docType),
|
||||||
|
'body_html' => (string) ($parsed['content'] ?? $parsed['body_html'] ?? ''),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Classifica un incidente e suggerisce severity
|
* Classifica un incidente e suggerisce severity
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -0,0 +1,70 @@
|
|||||||
|
# Deploy — Modulo Modello Organizzativo SGSI (ISO 27001) — v1.14.0
|
||||||
|
|
||||||
|
> Implementazione completata 2026-06-11. Tutto il **codice** (backend, frontend, help, i18n, AI, KB) è scritto e committato. I passi seguenti richiedono privilegi **host** (DB + docker) e vanno eseguiti su Hetzner.
|
||||||
|
|
||||||
|
## Cosa è stato realizzato (già nel repo, files live via bind-mount)
|
||||||
|
|
||||||
|
| Area | File |
|
||||||
|
|---|---|
|
||||||
|
| Migration schema | `docs/sql/037_isms_model.sql` (4 tabelle SGSI), `docs/sql/038_iso27001_annex_controls.sql` (dataset 111 controlli: 93 Annex A:2022 + 7 CLD/27017 + 11 PII/27018) |
|
||||||
|
| Runner migrazioni | `scripts/migrate-isms.php` |
|
||||||
|
| Backend | `application/controllers/IsmsModelController.php` (16 endpoint) + registrazione in `public/index.php` |
|
||||||
|
| AI | `application/services/AIService.php::generateIsmsDocument()` + fonti ISO in `application/config/nis2_sources.php` |
|
||||||
|
| Frontend | `public/isms.html`, `public/js/isms.js`, metodi `isms*` in `public/js/api.js`, voce sidebar in `public/js/common.js` |
|
||||||
|
| Help + i18n | topic `isms` in `public/js/help.js`, stringhe `isms.*`/`nav.isms` IT/EN in `public/js/i18n.js` |
|
||||||
|
| KB / RAG | `scripts/ingest-iso-sources.php` (catalogo controlli + fonti ISO, scope SYSTEM) |
|
||||||
|
| Versione | `public/version.json` → 1.14.0 |
|
||||||
|
|
||||||
|
## Passi di deploy (HOST Hetzner)
|
||||||
|
|
||||||
|
### 1. Applicare le migrazioni al DB di produzione
|
||||||
|
Il DB servito dall'app è il MySQL **host** (via socket), NON il container `nis2-db`. Usare il runner con la connessione dell'app:
|
||||||
|
```bash
|
||||||
|
docker exec nis2-app php /var/www/nis2-agile/scripts/migrate-isms.php
|
||||||
|
```
|
||||||
|
Atteso: `iso27001_annex_controls: 111 controlli` (iso27001:93, iso27017:7, iso27018:11) + 4 tabelle SGSI create. Idempotente (CREATE TABLE IF NOT EXISTS + INSERT IGNORE).
|
||||||
|
|
||||||
|
Alternativa SQL diretta:
|
||||||
|
```bash
|
||||||
|
mysql -h localhost nis2_agile_db < /var/www/nis2-agile/docs/sql/037_isms_model.sql
|
||||||
|
mysql -h localhost nis2_agile_db < /var/www/nis2-agile/docs/sql/038_iso27001_annex_controls.sql
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Hot-reload PHP-FPM (opcache)
|
||||||
|
Dopo ogni edit `.php` il bytecode in cache va ricaricato:
|
||||||
|
```bash
|
||||||
|
docker exec nis2-app kill -USR2 1
|
||||||
|
```
|
||||||
|
(HTML/JS sono già live via nginx, non serve reload.)
|
||||||
|
|
||||||
|
### 3. Smoke test
|
||||||
|
```bash
|
||||||
|
# con un JWT valido + X-Organization-Id:
|
||||||
|
curl -s https://nis2.agile.software/api/isms/model -H "Authorization: Bearer <JWT>" -H "X-Organization-Id: <ORG>"
|
||||||
|
curl -s https://nis2.agile.software/api/isms/annex-controls -H "Authorization: Bearer <JWT>" -H "X-Organization-Id: <ORG>"
|
||||||
|
# UI: aprire https://nis2.agile.software/isms.html → step 1, salvare, andare al SoA (auto-derive dal NIS2)
|
||||||
|
```
|
||||||
|
Verifica chiave: con un assessment NIS2 esistente, `GET /api/isms/soa` deve restituire controlli con `derived_from_nis2=1` e uno stato pre-compilato.
|
||||||
|
|
||||||
|
### 4. (Opzionale) Ingest KB ISO per il grounding AI
|
||||||
|
Richiede Qdrant + Voyage attivi e migration 038 applicata:
|
||||||
|
```bash
|
||||||
|
docker exec nis2-app php /var/www/nis2-agile/scripts/ingest-iso-sources.php
|
||||||
|
# anteprima senza scrivere: aggiungere --dry-run
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5. Push del codice (se non già fatto dal container)
|
||||||
|
Il container NIS2 è legacy: dopo reboot la git credential-cache è vuota. Se il push da devenv non funziona, chiedere all'host (helper vault):
|
||||||
|
> "NIS2 ha N commit su main da pushare, fallo tu dall'host"
|
||||||
|
|
||||||
|
## Note / vincoli rispettati
|
||||||
|
- **Additivo**: nessuna tabella esistente modificata; nessun controller esistente alterato nella logica (solo registrazione del nuovo).
|
||||||
|
- **Copyright ISO**: indicizzati solo titoli/sintesi proprietari + riferimenti, mai il testo letterale delle norme.
|
||||||
|
- **Disclaimer**: il modulo è strumento di supporto/pre-audit, non certificazione ISO; l'UI e l'AI lo dichiarano. Gli obblighi normativi italiani restano NIS2 / D.Lgs. 138/2024 / ACN.
|
||||||
|
- **27017:2015 / 27018:2019**: versioni attuali (nessuna revisione più recente). 27017 numerata su 27002:2013 → `iso27002_ref` conserva il cross-reference.
|
||||||
|
|
||||||
|
## Rollback
|
||||||
|
```sql
|
||||||
|
DROP TABLE IF EXISTS isms_soa, isms_roles, isms_documents, isms_models, iso27001_annex_controls;
|
||||||
|
```
|
||||||
|
+ rimuovere la riga `'isms'` da controllerMap/actionMap in `public/index.php`, USR2. Il frontend `isms.html` resta inerte senza backend.
|
||||||
@@ -0,0 +1,248 @@
|
|||||||
|
# Studio — Modulo "Modello Organizzativo ISO 27000 (SGSI)" + Onboarding PMI
|
||||||
|
|
||||||
|
> **Tipo**: documento di studio e piano di implementazione (NO codice applicato).
|
||||||
|
> **Data**: 2026-06-11 — Autore: sessione Claude (devapp3)
|
||||||
|
> **Stato**: bozza in attesa di revisione utente prima di toccare codice.
|
||||||
|
> **Decisioni utente (2026-06-11)**:
|
||||||
|
> 1. Ambito = **SGSI completo** (clausole ISO/IEC 27001:2022 4–10 + Statement of Applicability)
|
||||||
|
> 2. Deliverable corrente = **questo documento di studio + piano** (niente migration/controller finché non approvato)
|
||||||
|
> 3. Percorso PMI = **stesso wizard completo, con più guida e supporto AI** (nessuna semplificazione dei contenuti normativi)
|
||||||
|
> 4. **Aggiornamento 2026-06-11**: includere le estensioni cloud della famiglia 27000 nelle versioni **attuali** — **ISO/IEC 27017:2015** (controlli sicurezza servizi cloud) e **ISO/IEC 27018:2019** (protezione PII in cloud pubblico per PII processor), come **estensioni condizionali** del SoA (vedi §3.3bis e §4.2).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. Obiettivo
|
||||||
|
|
||||||
|
Estendere NIS2 Agile con una **procedura guidata (wizard)** che accompagni l'azienda nella costruzione del proprio **Sistema di Gestione della Sicurezza delle Informazioni (SGSI / ISMS)** secondo **ISO/IEC 27001:2022**, riusando il più possibile i dati e i moduli già presenti per la compliance NIS2.
|
||||||
|
|
||||||
|
Il wizard deve produrre, a fine percorso, gli artefatti minimi di un SGSI:
|
||||||
|
- **Ambito e contesto** del SGSI (cl. 4)
|
||||||
|
- **Policy SGSI + ruoli/responsabilità** (cl. 5)
|
||||||
|
- **Metodologia di risk assessment + obiettivi** (cl. 6)
|
||||||
|
- **Statement of Applicability (SoA)** sui 93 controlli Annex A:2022 (cl. 6.1.3 d), **estendibile** con i controlli cloud **ISO 27017:2015** e i controlli privacy-cloud **ISO 27018:2019** quando pertinenti (vedi §3.3bis / §4.2)
|
||||||
|
- **Documented information** minima obbligatoria (cl. 7–8)
|
||||||
|
- **Piano di monitoraggio, audit interni e miglioramento** (cl. 9–10)
|
||||||
|
|
||||||
|
**Famiglia ISO 27000 coperta** (versioni attuali):
|
||||||
|
- **ISO/IEC 27001:2022** — requisiti SGSI (base del wizard)
|
||||||
|
- **ISO/IEC 27002:2022** — guida implementativa Annex A
|
||||||
|
- **ISO/IEC 27017:2015** — *estensione condizionale*: controlli di sicurezza per servizi cloud (cliente e/o fornitore cloud)
|
||||||
|
- **ISO/IEC 27018:2019** — *estensione condizionale*: protezione dei PII in cloud pubblico per chi agisce da PII processor (ponte verso GDPR)
|
||||||
|
|
||||||
|
Per le **PMI**, lo stesso percorso resta integrale ma viene arricchito di guida contestuale, esempi e bozze AI dimensionate alla realtà di una piccola/media impresa (vedi §8).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. Stato dell'arte — cosa esiste già (riuso, non riscrittura)
|
||||||
|
|
||||||
|
Il prodotto contiene già un ponte significativo NIS2 ↔ ISO 27001. Riuso previsto:
|
||||||
|
|
||||||
|
| Componente esistente | File / Tabella | Come lo riuso |
|
||||||
|
|---|---|---|
|
||||||
|
| ENUM framework controlli | `compliance_controls.framework` = `nis2 \| iso27001 \| both` | La tabella regge nativamente i controlli ISO: i record SoA finiscono qui con `framework='iso27001'` |
|
||||||
|
| Mapping per-domanda | `application/data/nis2_questionnaire.json` → ogni domanda ha `iso27001_control` (es. `A.5.1`); ogni categoria ha `iso27001_controls[]` | **Pre-popolazione del SoA** dalle risposte NIS2 già date |
|
||||||
|
| Matrice articoli→ISO | `AuditController::iso27001Mapping` (NIS2 Art.21.2.a–j → Annex A) | Base per derivare lo stato iniziale dei controlli |
|
||||||
|
| Modulo Risk | `RiskController`, risk register, matrice 5×5, AI suggest | Clausola 6 (risk assessment & treatment) — si aggancia, non si riscrive |
|
||||||
|
| Audit + NCR/CAPA | `AuditController`, `non_conformities`, `corrective_actions` | Clausole 9–10 (audit interni, riesame, miglioramento) |
|
||||||
|
| AI | `AIService` (gap, policy gen) + vincolo fonti certe `application/config/nis2_sources.php` | Bozze policy/SoA con citazione fonte (da estendere a ISO 27002) |
|
||||||
|
| KB / RAG | `KnowledgeBaseController`, `RagService`, Qdrant `nis2_kb` | Ingest testo ISO 27001/27002 scope SYSTEM per la guida contestuale |
|
||||||
|
| Audit chain | `AuditService` hash-chain SHA-256 | Tracciamento immutabile di ogni step del wizard SGSI |
|
||||||
|
| Multi-tenancy | `organization_id` su ogni tabella + `requireOrgAccess()` | Stesso isolamento per i nuovi dati SGSI |
|
||||||
|
| Pattern onboarding | `OnboardingController::initializeComplianceControls()` (13 controlli NIS2 a setup org) | Pattern speculare per inizializzare il SoA ISO |
|
||||||
|
|
||||||
|
**Gap attuale**: i 93 controlli **Annex A:2022 non sono seedati** (oggi a onboarding nascono solo i 13 control-code NIS2). Vanno introdotti come dataset di riferimento.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. Architettura proposta (additiva, zero breaking change)
|
||||||
|
|
||||||
|
Stesso stack: PHP 8.4 vanilla, Front Controller, PDO singleton, JWT multi-tenant, frontend HTML/JS vanilla. Nessun rebuild Docker (edit live `.php`/`.html` + `kill -USR2 1`). Migration da eseguire sull'**host MySQL** (`mysql -h localhost`), coerente con la topologia DB documentata (l'app HTTP fpm parla con l'host via socket).
|
||||||
|
|
||||||
|
### 3.1 Nuove tabelle (migration `037_isms_model.sql` → eventualmente `038`)
|
||||||
|
|
||||||
|
> Convenzioni del repo: ALTER/CREATE idempotenti via check su `information_schema` (no `IF NOT EXISTS` su ADD COLUMN in MySQL 8.0). Niente `DELIMITER` via pipe.
|
||||||
|
|
||||||
|
- **`isms_models`** — un SGSI per organization
|
||||||
|
`id, organization_id, status (draft/active/under_review), scope_statement TEXT, context_internal TEXT, context_external TEXT, interested_parties JSON, boundaries TEXT, exclusions TEXT, risk_methodology TEXT, isms_objectives JSON, version, created_at, updated_at, approved_by, approved_at`
|
||||||
|
|
||||||
|
- **`isms_roles`** — ruoli e responsabilità (RACI SGSI), cl. 5.3
|
||||||
|
`id, isms_model_id, role_name, user_id NULL, responsibility TEXT, raci ENUM(R,A,C,I), created_at`
|
||||||
|
|
||||||
|
- **`iso27001_annex_controls`** — **dataset di riferimento** dei controlli (globale, non per-org)
|
||||||
|
`control_code (A.5.1…A.8.34, CLD.*, PII.*), standard ENUM(iso27001/iso27017/iso27018), theme, title_it, title_en, purpose_it, iso27002_ref, condition_tag NULL` — seedato da migration. La colonna `standard` distingue i 93 controlli Annex A:2022 dai controlli aggiuntivi cloud (27017) e privacy-cloud (27018); `condition_tag` segna se un controllo è condizionale (es. `cloud_customer`, `cloud_provider`, `pii_processor`).
|
||||||
|
|
||||||
|
- **`isms_soa`** — Statement of Applicability per org (il cuore)
|
||||||
|
`id, isms_model_id, control_code, applicable BOOL, justification_inclusion TEXT, justification_exclusion TEXT, implementation_status (not_started/in_progress/implemented/verified), implementation_pct, derived_from_nis2 BOOL, linked_control_id (→compliance_controls), updated_by, updated_at`
|
||||||
|
|
||||||
|
- **`isms_documents`** — documented information minima (cl. 7.5), bozze AI
|
||||||
|
`id, isms_model_id, doc_type, title, status (draft/review/approved), ai_generated BOOL, body_html LONGTEXT, version, linked_policy_id NULL`
|
||||||
|
|
||||||
|
(Il monitoraggio/audit interni cl. 9–10 **non** crea tabelle nuove: si appoggia ad `AuditController` + `non_conformities`/`corrective_actions` esistenti, con un `isms_model_id` opzionale come tag.)
|
||||||
|
|
||||||
|
### 3.2 Nuovo controller — `IsmsModelController.php`
|
||||||
|
|
||||||
|
Registrato in `public/index.php` (controllerMap + actionMap), pattern identico agli altri controller. Endpoint additivi:
|
||||||
|
|
||||||
|
| Metodo | Endpoint | Scopo |
|
||||||
|
|---|---|---|
|
||||||
|
| GET | `/api/isms/model` | SGSI dell'org corrente (o 404 → wizard da iniziare) |
|
||||||
|
| POST | `/api/isms/model` | Crea/inizializza SGSI (scope, contesto) — cl. 4 |
|
||||||
|
| PUT | `/api/isms/model` | Salva step (leadership, risk methodology, objectives) |
|
||||||
|
| GET | `/api/isms/annex-controls` | I 93 controlli di riferimento (per il SoA) |
|
||||||
|
| GET | `/api/isms/soa` | SoA dell'org, **pre-popolato dal NIS2** al primo accesso |
|
||||||
|
| PUT | `/api/isms/soa/{code}` | Aggiorna applicabilità + motivazione + stato di un controllo |
|
||||||
|
| POST | `/api/isms/soa/derive` | (Ri)deriva lo stato iniziale del SoA dalle risposte NIS2 |
|
||||||
|
| GET | `/api/isms/documents` / POST / PUT | Documented information + bozze |
|
||||||
|
| POST | `/api/isms/documents/ai-generate` | Genera bozza policy/procedura via AIService |
|
||||||
|
| GET | `/api/isms/readiness` | % completamento SGSI + checklist clausole 4–10 |
|
||||||
|
| GET | `/api/isms/export` | Export SGSI completo (HTML stampabile / SoA CSV) |
|
||||||
|
|
||||||
|
### 3.3 Frontend
|
||||||
|
|
||||||
|
- **`public/isms.html`** — wizard a 6 step (struttura `assessment.html`: `app-layout` → `sidebar` + `main-content`), stepper in alto, salvataggio per step.
|
||||||
|
- **`public/js/isms.js`** — handler wizard via client `api.js`, auto-detect ruolo/firm da `/api/auth/me`.
|
||||||
|
- Voce sidebar **"Modello Organizzativo (SGSI)"** in sezione *Gestione* di `common.js` (icona scudo/organigramma).
|
||||||
|
- Aggiornare `help.js` (help contestuale IT/EN) e KB AI come da regola progetto.
|
||||||
|
|
||||||
|
### 3.3bis Estensioni cloud — ISO 27017:2015 e ISO 27018:2019
|
||||||
|
|
||||||
|
Le due norme **non sono SGSI a sé**: sono *code of practice* che **estendono** i controlli 27002. Nel modulo diventano **set di controlli aggiuntivi e condizionali** dentro lo stesso SoA, attivati in base al profilo dell'org raccolto allo Step 1.
|
||||||
|
|
||||||
|
**ISO/IEC 27017:2015 — servizi cloud.** Fornisce guida cloud-specifica su ~37 controlli 27002 + **7 controlli aggiuntivi `CLD.*`**:
|
||||||
|
- `CLD.6.3.1` — Ruoli e responsabilità condivisi in ambiente cloud
|
||||||
|
- `CLD.8.1.5` — Rimozione degli asset del cliente cloud (a fine contratto)
|
||||||
|
- `CLD.9.5.1` — Segregazione negli ambienti di virtual computing
|
||||||
|
- `CLD.9.5.2` — Hardening delle virtual machine
|
||||||
|
- `CLD.12.1.5` — Sicurezza operativa dell'amministratore
|
||||||
|
- `CLD.12.4.5` — Monitoraggio dei servizi cloud
|
||||||
|
- `CLD.13.1.4` — Allineamento sicurezza reti virtuali e fisiche
|
||||||
|
|
||||||
|
Si applicano sia al **cliente cloud** sia al **fornitore cloud** (alcuni hanno responsabilità diverse per ruolo → si usa `raci`/`condition_tag`).
|
||||||
|
|
||||||
|
> ⚠️ **Numerazione**: 27017:2015 è scritta sul **27002:2013** (vecchia numerazione, es. `CLD.9.5.x`). Il modulo è su Annex A:**2022**. Va mantenuto un **cross-reference** 2013↔2022 nel dataset (`iso27002_ref`) per non confondere l'utente. Nessuna revisione 2022 di 27017 esiste a oggi → resta `27017:2015`.
|
||||||
|
|
||||||
|
**ISO/IEC 27018:2019 — PII in cloud pubblico (PII processor).** Aumenta i controlli 27002 con guida per la protezione dei dati personali e aggiunge un set di controlli (Annex A della 27018) organizzati sui **principi privacy ISO 29100** (consenso/scelta, limitazione finalità, minimizzazione, limitazione uso/conservazione/divulgazione, trasparenza, partecipazione dell'interessato, accountability, sicurezza, conformità privacy). Mappabili come `PII.*` nel dataset.
|
||||||
|
|
||||||
|
**Ponte GDPR/NIS2**: 27018 copre obblighi del *responsabile del trattamento* in cloud → si collega ai riferimenti GDPR già citati nel prodotto (es. Art.17 erasure, codice etico persona digitale) e rafforza il dominio NIS2 *supply chain*/*cloud*. Da NON spacciare come conformità GDPR automatica (disclaimer, vedi §7).
|
||||||
|
|
||||||
|
**Attivazione condizionale (Step 1 del wizard)**: tre flag sul profilo SGSI —
|
||||||
|
`uses_public_cloud` (→ abilita 27017 lato cliente), `is_cloud_provider` (→ 27017 lato fornitore), `processes_pii_in_cloud` (→ 27018). I controlli `CLD.*`/`PII.*` entrano nel SoA **solo** se il relativo flag è attivo; altrimenti restano fuori (con motivazione di esclusione precompilata "non applicabile: l'org non eroga/usa servizi cloud / non tratta PII in cloud"). Segnali utili già disponibili per **proporre** i flag: settore, `assets` (asset di tipo cloud), modulo `suppliers` (fornitori cloud).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. Il wizard SGSI — 6 step mappati alle clausole 27001:2022
|
||||||
|
|
||||||
|
| Step | Clausola | Cosa raccoglie | Riuso |
|
||||||
|
|---|---|---|---|
|
||||||
|
| **1. Contesto & Ambito** | 4 | Perimetro, sedi, asset coperti, parti interessate, confini, esclusioni motivate | Asset module per il perimetro |
|
||||||
|
| **2. Leadership** | 5 | Policy SGSI, organigramma sicurezza, ruoli/responsabilità (RACI), comitato | Tabella `isms_roles`; PolicyController per la policy |
|
||||||
|
| **3. Risk** | 6 | Metodologia (es. ISO 27005), criteri, obiettivi SGSI misurabili | **Aggancia `RiskController` esistente** (no doppione del risk register) |
|
||||||
|
| **4. Statement of Applicability** | 6.1.3 | I 93 controlli Annex A: applicabile sì/no + motivazione, **pre-spuntati dal NIS2**; + estensioni condizionali **27017** (cloud) e **27018** (PII-cloud) | `iso27001_control` del questionario + `iso27001Mapping`; flag cloud/PII (§3.3bis, §4.2) |
|
||||||
|
| **5. Documented Information** | 7–8 | Policy/procedure obbligatorie, bozze AI, versioning | `AIService::generatePolicy`, `isms_documents` |
|
||||||
|
| **6. Monitoraggio & Miglioramento** | 9–10 | Piano audit interni, riesame direzione, gestione NC | **Aggancia Audit + NCR/CAPA esistenti** |
|
||||||
|
|
||||||
|
### 4.1 Logica di pre-popolazione del SoA (valore chiave)
|
||||||
|
|
||||||
|
Al primo accesso al SoA (o via `POST /api/isms/soa/derive`):
|
||||||
|
|
||||||
|
1. Per ogni controllo Annex A si guarda quali domande NIS2 hanno quel `iso27001_control`.
|
||||||
|
2. Si aggrega lo stato delle risposte (`implemented`/`partial`/`not_implemented`/`not_applicable`).
|
||||||
|
3. Si imposta una **proposta** iniziale: `applicable=true`, `implementation_status` e `implementation_pct` derivati, `derived_from_nis2=true`, e una bozza di `justification_inclusion` citando l'articolo NIS2 di origine.
|
||||||
|
4. L'utente conferma/modifica. I controlli ISO **senza** corrispondenza NIS2 (es. parte di A.7 fisici) restano da compilare manualmente, evidenziati come "non coperti dall'assessment NIS2".
|
||||||
|
|
||||||
|
Questo trasforma un assessment NIS2 già fatto in una **prima bozza di SoA pronta all'80%**, che è il principale argomento di vendita del modulo.
|
||||||
|
|
||||||
|
### 4.2 SoA esteso con 27017/27018 (condizionale)
|
||||||
|
|
||||||
|
Lo Step 4 mostra il SoA **stratificato per standard**:
|
||||||
|
1. **Sezione base** — 93 controlli Annex A:2022 (sempre).
|
||||||
|
2. **Sezione cloud (27017)** — i 7 `CLD.*` + i ~37 controlli 27002 con guida cloud, visibili solo se `uses_public_cloud` o `is_cloud_provider`. Per ogni controllo: lente "cosa cambia in cloud" e distinzione responsabilità cliente vs fornitore.
|
||||||
|
3. **Sezione PII-cloud (27018)** — controlli `PII.*` (principi 29100), visibili solo se `processes_pii_in_cloud`, con tag del principio privacy e link al riferimento GDPR pertinente.
|
||||||
|
|
||||||
|
`isms_soa` distingue le righe via il `standard` del controllo collegato. La derivazione automatica dal NIS2 (§4.1) copre la sezione base; le sezioni cloud/PII partono da bozze di motivazione AI (non hanno corrispondenza diretta nel questionario NIS2) e sono evidenziate come "da compilare — estensione cloud/privacy".
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. Integrazione AI (fonti certe)
|
||||||
|
|
||||||
|
- Estendere `application/config/nis2_sources.php` (registry fonti certe) con i riferimenti **ISO/IEC 27001:2022, 27002:2022, 27017:2015 e 27018:2019** così che `AIService::authoritativeSourcesBlock()` possa citarli e vietare riferimenti inventati. Marcare 27017/27018 come *code of practice* (guida, non requisiti certificabili a sé).
|
||||||
|
- Nuovi prompt:
|
||||||
|
- **Motivazione SoA**: data una scelta di applicabilità, genera/propone la `justification_inclusion`/`exclusion`.
|
||||||
|
- **Bozza documented information**: policy/procedura per un controllo o gruppo di controlli (riusa il pattern `generatePolicy`).
|
||||||
|
- **Spiegazione clausola** (guida contestuale, soprattutto PMI).
|
||||||
|
- Ingest del testo ISO 27001/27002 in KB Qdrant `nis2_kb` scope SYSTEM (pipeline `scripts/ingest-*` esistente) → `askWithRag()` risponde alle domande dell'utente durante il wizard.
|
||||||
|
|
||||||
|
> ⚠️ Il testo integrale ISO è coperto da copyright: ingestare **parafrasi/sintesi proprietarie + riferimenti**, non il testo letterale della norma. Da chiarire con l'utente (vedi §10).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. Schema migrazioni — punto di partenza
|
||||||
|
|
||||||
|
Ultima migration presente: **`036_acn_gap_assessment.sql`**. Le nuove partono da:
|
||||||
|
- `037_isms_model.sql` — `isms_models`, `isms_roles`, `isms_soa`, `isms_documents` + indici
|
||||||
|
- `038_iso27001_annex_controls.sql` — tabella di riferimento + **seed dei 93 controlli Annex A:2022** (4 temi) + **7 controlli `CLD.*` ISO 27017:2015** + **controlli `PII.*` ISO 27018:2019** (con `standard` e `condition_tag`); ALTER `isms_models` per i 3 flag `uses_public_cloud`/`is_cloud_provider`/`processes_pii_in_cloud`
|
||||||
|
|
||||||
|
Tutte additive, idempotenti, da girare sull'host MySQL.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7. Impatto e rischi
|
||||||
|
|
||||||
|
**Impatto basso / additivo:**
|
||||||
|
- Nessuna modifica a schema esistente se non un eventuale ALTER `compliance_controls` per linkare al SoA (valutare: forse non serve, si usa `isms_soa.linked_control_id`).
|
||||||
|
- Nessun controller esistente toccato nella logica (solo registrazione del nuovo in `index.php`).
|
||||||
|
- Nessun rebuild Docker.
|
||||||
|
|
||||||
|
**Rischi / attenzioni:**
|
||||||
|
1. **Copyright ISO** (vedi §5/§10) — non ingestare testo letterale.
|
||||||
|
2. **Volume dataset Annex A** (93 controlli × bilingue) — lavoro di data-entry/curatela contenuti.
|
||||||
|
3. **Disallineamento mapping** — il mapping NIS2→ISO è una *guida*, non una corrispondenza 1:1 certificabile: il SoA derivato è una **bozza assistita**, va dichiarato chiaramente nell'UI (disclaimer, come già fatto per gauge/bozze AI).
|
||||||
|
4. **Topologia DB** — migration sull'host, non `docker exec nis2-db` (path secondario non-prod).
|
||||||
|
5. **Governance**: ogni step va proposto e confermato; commit immediato post-smoke; push via host se cache token vuota.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 8. Modello onboarding PMI (decisione: stesso percorso + più AI)
|
||||||
|
|
||||||
|
`employee_count` e `annual_turnover_eur` sono già raccolti a onboarding → si può **rilevare automaticamente** che l'org è una PMI e attivare un *layer di assistenza*, **senza** ridurre i contenuti:
|
||||||
|
|
||||||
|
- **Guida contestuale potenziata**: ogni step del wizard mostra spiegazione "cosa significa per una PMI", esempi concreti dimensionati, stima di effort.
|
||||||
|
- **Bozze AI dimensionate**: prompt arricchiti col contesto PMI (settore, dimensione, range dipendenti — già anonimizzato in `AIService`) → policy/procedure realistiche e snelle ma complete.
|
||||||
|
- **Co-pilota conversazionale**: `askWithRag()` disponibile nel wizard per domande libere ("devo davvero fare l'audit interno?").
|
||||||
|
- **Roadmap a fasi suggerita**: stesso SoA completo, ma con priorità/sequenza proposta per non sovraccaricare (quick win prima), riusando la logica `quick_wins`/`compliance_roadmap` già prodotta dall'AI gap analysis.
|
||||||
|
- **Estensioni cloud per PMI**: molte PMI usano SaaS/cloud pubblico ma non se ne rendono conto → l'AI, dai segnali (suppliers, asset), **propone** di attivare 27017/27018 spiegando in linguaggio semplice perché ("usi Microsoft 365/AWS → questi 7 controlli cloud ti riguardano").
|
||||||
|
- **Nessun contenuto rimosso**: i 93 controlli e le 6 clausole restano tutti; cambia solo il *supporto*, coerente con la scelta utente.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 9. Roadmap di implementazione proposta (a fasi)
|
||||||
|
|
||||||
|
| Fase | Contenuto | Output verificabile |
|
||||||
|
|---|---|---|
|
||||||
|
| **F0 — Dataset** | Curatela + seed 93 controlli Annex A:2022 + 7 `CLD.*` (27017) + `PII.*` (27018) con cross-ref numerazione (`038`) + estensione `nis2_sources.php` | Tabella popolata (3 standard), fonti ISO citabili |
|
||||||
|
| **F1 — Backend SGSI** | Migration `037`, `IsmsModelController`, endpoint model/SoA + derive, registrazione `index.php` | curl: crea modello, deriva SoA dal NIS2, legge readiness |
|
||||||
|
| **F2 — Wizard frontend** | `isms.html` + `isms.js` 6 step, voce sidebar, help.js | Wizard navigabile end-to-end su org demo |
|
||||||
|
| **F3 — AI & docs** | Prompt SoA/policy, `ai-generate`, ingest KB ISO | Bozza policy generata + risposta RAG nel wizard |
|
||||||
|
| **F4 — PMI layer** | Guida contestuale, co-pilota, roadmap a fasi | Org PMI demo: percorso completo con assistenza |
|
||||||
|
| **F5 — Export & polish** | Export SGSI HTML stampabile + SoA CSV, audit-chain tag, version.json bump | Documento SGSI esportabile |
|
||||||
|
|
||||||
|
Ogni fase: edit → `kill -USR2 1` → smoke curl → commit → push (via host se serve). Test su org demo (id≤4 / `%.demo%`), reset con `reset-demo.sql`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 10. Decisioni aperte da chiarire con l'utente
|
||||||
|
|
||||||
|
1. **Copyright ISO**: confermo l'approccio "sintesi/parafrasi proprietarie + riferimenti" per la KB e i titoli controlli? (necessario per non violare il copyright della norma)
|
||||||
|
2. **Versione Annex A**: confermato **2022 (93 controlli, 4 temi)**? (lo studio assume 2022; il questionario usa già codici `A.5.x`/`A.8.x` 2022)
|
||||||
|
2bis. **Estensioni cloud**: confermo le versioni attuali **27017:2015** e **27018:2019** (non esistono revisioni più recenti) e l'approccio a **sezioni condizionali** del SoA attivate dai 3 flag cloud/PII? Includo anche il **cross-reference numerazione 27017 (2013→2022)** nel dataset?
|
||||||
|
3. **Posizionamento**: il SGSI è un modulo **a sé** in sidebar, o un percorso che parte dalla pagina assessment esistente?
|
||||||
|
4. **Certificabilità**: il modulo è di **supporto/pre-audit** (bozze assistite con disclaimer) — confermi che NON deve presentarsi come certificazione automatica?
|
||||||
|
5. **Mobile/multi-superficie**: rilevante il tag `data-app-version`/`surface` (std feedback-version-provenance) anche per le nuove pagine? (di default sì, additivo)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 11. Riepilogo
|
||||||
|
|
||||||
|
Il modulo è **realizzabile in larga parte per riuso**: il dato più prezioso (mapping NIS2→ISO per domanda) è già nel prodotto e abilita un **SoA pre-popolato all'80%** da un assessment NIS2 esistente — il vero differenziatore. L'implementazione è additiva, non breaking, su pattern noti. Il percorso PMI è un layer di assistenza AI sopra lo stesso wizard completo.
|
||||||
|
|
||||||
|
**Prossimo passo**: revisione di questo studio + risposte alle decisioni aperte (§10) → poi avvio Fase F0/F1 mostrando i diff prima di qualsiasi commit.
|
||||||
@@ -0,0 +1,118 @@
|
|||||||
|
-- ============================================================================
|
||||||
|
-- Migration 037 - Modello Organizzativo SGSI (ISO/IEC 27001:2022)
|
||||||
|
-- ----------------------------------------------------------------------------
|
||||||
|
-- Introduce il modulo "Modello Organizzativo" per costruire un Sistema di
|
||||||
|
-- Gestione della Sicurezza delle Informazioni (SGSI/ISMS) secondo
|
||||||
|
-- ISO/IEC 27001:2022 (clausole 4-10) + Statement of Applicability (SoA) sui
|
||||||
|
-- controlli Annex A:2022, estendibile con le norme cloud ISO/IEC 27017:2015
|
||||||
|
-- e ISO/IEC 27018:2019 (controlli condizionali).
|
||||||
|
--
|
||||||
|
-- Tutte ADDITIVE: nessuna tabella esistente modificata. Il SoA tiene un
|
||||||
|
-- riferimento "morbido" ai compliance_controls via linked_control_id (INT, no
|
||||||
|
-- FK) per non vincolare lo schema esistente.
|
||||||
|
--
|
||||||
|
-- Dataset di riferimento dei controlli (93 Annex A + 7 CLD + PII): vedi
|
||||||
|
-- migration 038_iso27001_annex_controls.sql.
|
||||||
|
--
|
||||||
|
-- IDEMPOTENTE: CREATE TABLE IF NOT EXISTS. Applicare su host MySQL
|
||||||
|
-- mysql -h localhost nis2_agile_db < 037_isms_model.sql
|
||||||
|
-- oppure tramite il runner con la connessione corretta dell'app:
|
||||||
|
-- docker exec nis2-app php scripts/migrate-isms.php
|
||||||
|
-- NON usare `docker exec nis2-db` (DB secondario non-prod).
|
||||||
|
-- STATO: DA APPLICARE su host.
|
||||||
|
-- ============================================================================
|
||||||
|
|
||||||
|
-- Intestazione del SGSI: una riga per organization (un SGSI attivo per org).
|
||||||
|
CREATE TABLE IF NOT EXISTS isms_models (
|
||||||
|
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||||
|
organization_id INT NOT NULL,
|
||||||
|
status ENUM('draft','active','under_review') DEFAULT 'draft',
|
||||||
|
-- Clausola 4 - Contesto dell'organizzazione
|
||||||
|
scope_statement TEXT NULL,
|
||||||
|
context_internal TEXT NULL,
|
||||||
|
context_external TEXT NULL,
|
||||||
|
interested_parties JSON NULL,
|
||||||
|
boundaries TEXT NULL,
|
||||||
|
exclusions TEXT NULL,
|
||||||
|
-- Clausola 6 - Pianificazione (metodologia + obiettivi)
|
||||||
|
risk_methodology TEXT NULL,
|
||||||
|
isms_objectives JSON NULL,
|
||||||
|
-- Flag estensioni cloud (attivano controlli condizionali nel SoA)
|
||||||
|
uses_public_cloud TINYINT(1) DEFAULT 0, -- abilita ISO 27017 (lato cliente)
|
||||||
|
is_cloud_provider TINYINT(1) DEFAULT 0, -- abilita ISO 27017 (lato fornitore)
|
||||||
|
processes_pii_in_cloud TINYINT(1) DEFAULT 0, -- abilita ISO 27018 (PII processor)
|
||||||
|
version VARCHAR(20) DEFAULT '1.0',
|
||||||
|
created_by INT NULL,
|
||||||
|
approved_by INT NULL,
|
||||||
|
approved_at DATETIME NULL,
|
||||||
|
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||||
|
FOREIGN KEY (organization_id) REFERENCES organizations(id) ON DELETE CASCADE,
|
||||||
|
FOREIGN KEY (created_by) REFERENCES users(id) ON DELETE SET NULL,
|
||||||
|
FOREIGN KEY (approved_by) REFERENCES users(id) ON DELETE SET NULL,
|
||||||
|
UNIQUE KEY uk_isms_org (organization_id),
|
||||||
|
INDEX idx_isms_status (status)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||||
|
|
||||||
|
-- Ruoli e responsabilita SGSI (RACI) - clausola 5.3.
|
||||||
|
CREATE TABLE IF NOT EXISTS isms_roles (
|
||||||
|
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||||
|
isms_model_id INT NOT NULL,
|
||||||
|
organization_id INT NOT NULL,
|
||||||
|
role_name VARCHAR(150) NOT NULL,
|
||||||
|
user_id INT NULL,
|
||||||
|
responsibility TEXT NULL,
|
||||||
|
raci ENUM('R','A','C','I') NULL,
|
||||||
|
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
FOREIGN KEY (isms_model_id) REFERENCES isms_models(id) ON DELETE CASCADE,
|
||||||
|
FOREIGN KEY (organization_id) REFERENCES organizations(id) ON DELETE CASCADE,
|
||||||
|
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE SET NULL,
|
||||||
|
INDEX idx_isms_roles_model (isms_model_id)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||||
|
|
||||||
|
-- Statement of Applicability: una riga per controllo applicabile/escluso.
|
||||||
|
CREATE TABLE IF NOT EXISTS isms_soa (
|
||||||
|
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||||
|
isms_model_id INT NOT NULL,
|
||||||
|
organization_id INT NOT NULL,
|
||||||
|
control_code VARCHAR(20) NOT NULL, -- es. A.5.1, CLD.9.5.1, PII.1
|
||||||
|
standard ENUM('iso27001','iso27017','iso27018') DEFAULT 'iso27001',
|
||||||
|
applicable TINYINT(1) DEFAULT 1,
|
||||||
|
justification_inclusion TEXT NULL,
|
||||||
|
justification_exclusion TEXT NULL,
|
||||||
|
implementation_status ENUM('not_started','in_progress','implemented','verified') DEFAULT 'not_started',
|
||||||
|
implementation_pct INT DEFAULT 0,
|
||||||
|
-- true se lo stato iniziale e stato DERIVATO dalle risposte NIS2 (assessment Art.21)
|
||||||
|
derived_from_nis2 TINYINT(1) DEFAULT 0,
|
||||||
|
source_ref VARCHAR(255) NULL, -- es. "NIS2 Art.21.2.a (ART21A_001)"
|
||||||
|
linked_control_id INT NULL, -- soft-link a compliance_controls.id (no FK)
|
||||||
|
updated_by INT NULL,
|
||||||
|
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||||
|
FOREIGN KEY (isms_model_id) REFERENCES isms_models(id) ON DELETE CASCADE,
|
||||||
|
FOREIGN KEY (organization_id) REFERENCES organizations(id) ON DELETE CASCADE,
|
||||||
|
FOREIGN KEY (updated_by) REFERENCES users(id) ON DELETE SET NULL,
|
||||||
|
UNIQUE KEY uk_isms_soa_ctrl (isms_model_id, control_code),
|
||||||
|
INDEX idx_isms_soa_model (isms_model_id),
|
||||||
|
INDEX idx_isms_soa_standard (standard)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||||
|
|
||||||
|
-- Documented information minima (clausola 7.5): policy/procedure, bozze AI.
|
||||||
|
CREATE TABLE IF NOT EXISTS isms_documents (
|
||||||
|
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||||
|
isms_model_id INT NOT NULL,
|
||||||
|
organization_id INT NOT NULL,
|
||||||
|
doc_type VARCHAR(60) NOT NULL, -- es. policy_sgsi, procedura_incident, soa_export
|
||||||
|
title VARCHAR(255) NOT NULL,
|
||||||
|
status ENUM('draft','review','approved') DEFAULT 'draft',
|
||||||
|
ai_generated TINYINT(1) DEFAULT 0,
|
||||||
|
body_html LONGTEXT NULL,
|
||||||
|
version VARCHAR(20) DEFAULT '1.0',
|
||||||
|
linked_policy_id INT NULL, -- soft-link a policies.id (no FK)
|
||||||
|
created_by INT NULL,
|
||||||
|
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||||
|
FOREIGN KEY (isms_model_id) REFERENCES isms_models(id) ON DELETE CASCADE,
|
||||||
|
FOREIGN KEY (organization_id) REFERENCES organizations(id) ON DELETE CASCADE,
|
||||||
|
FOREIGN KEY (created_by) REFERENCES users(id) ON DELETE SET NULL,
|
||||||
|
INDEX idx_isms_docs_model (isms_model_id)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||||
@@ -0,0 +1,163 @@
|
|||||||
|
-- ============================================================================
|
||||||
|
-- Migration 038 - Dataset di riferimento controlli ISO 27000 (per il SoA)
|
||||||
|
-- ----------------------------------------------------------------------------
|
||||||
|
-- Tabella GLOBALE (non per-org) dei controlli citabili nello Statement of
|
||||||
|
-- Applicability del modulo Modello Organizzativo SGSI (vedi 037):
|
||||||
|
-- - 93 controlli ISO/IEC 27001:2022 Annex A (4 temi)
|
||||||
|
-- - 7 controlli aggiuntivi cloud ISO/IEC 27017:2015 (CLD.*) - condizionali
|
||||||
|
-- - 11 controlli privacy-cloud ISO/IEC 27018:2019 (PII.*) - condizionali
|
||||||
|
--
|
||||||
|
-- I titoli ISO sono RIFORMULAZIONI proprietarie (IT/EN) a scopo di indice; il
|
||||||
|
-- testo integrale delle norme e coperto da copyright e NON e riprodotto.
|
||||||
|
-- 27017:2015 e numerata sul 27002:2013: iso27002_ref conserva il riferimento.
|
||||||
|
--
|
||||||
|
-- IDEMPOTENTE: CREATE TABLE IF NOT EXISTS + INSERT IGNORE (control_code UNIQUE).
|
||||||
|
-- Applicare su host MySQL (mysql -h localhost) o via scripts/migrate-isms.php.
|
||||||
|
-- STATO: DA APPLICARE su host.
|
||||||
|
-- ============================================================================
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS iso27001_annex_controls (
|
||||||
|
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||||
|
control_code VARCHAR(20) NOT NULL,
|
||||||
|
standard ENUM('iso27001','iso27017','iso27018') DEFAULT 'iso27001',
|
||||||
|
theme VARCHAR(30) NOT NULL, -- organizational/people/physical/technological/privacy
|
||||||
|
sort_order INT NOT NULL DEFAULT 0,
|
||||||
|
title_it VARCHAR(255) NOT NULL,
|
||||||
|
title_en VARCHAR(255) NOT NULL,
|
||||||
|
purpose_it TEXT NULL,
|
||||||
|
iso27002_ref VARCHAR(120) NULL, -- per CLD.*: riferimento 27002:2013
|
||||||
|
condition_tag VARCHAR(40) NULL, -- NULL=sempre; cloud / pii_processor = condizionale
|
||||||
|
UNIQUE KEY uk_annex_code (control_code),
|
||||||
|
INDEX idx_annex_standard (standard),
|
||||||
|
INDEX idx_annex_theme (theme)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||||
|
|
||||||
|
-- ── ISO/IEC 27001:2022 Annex A — A.5 Controlli organizzativi (37) ──
|
||||||
|
INSERT IGNORE INTO iso27001_annex_controls (control_code, standard, theme, sort_order, title_it, title_en) VALUES
|
||||||
|
('A.5.1','iso27001','organizational',501,'Politiche per la sicurezza delle informazioni','Policies for information security'),
|
||||||
|
('A.5.2','iso27001','organizational',502,'Ruoli e responsabilita per la sicurezza delle informazioni','Information security roles and responsibilities'),
|
||||||
|
('A.5.3','iso27001','organizational',503,'Separazione dei compiti','Segregation of duties'),
|
||||||
|
('A.5.4','iso27001','organizational',504,'Responsabilita della direzione','Management responsibilities'),
|
||||||
|
('A.5.5','iso27001','organizational',505,'Contatti con le autorita','Contact with authorities'),
|
||||||
|
('A.5.6','iso27001','organizational',506,'Contatti con gruppi di interesse specialistici','Contact with special interest groups'),
|
||||||
|
('A.5.7','iso27001','organizational',507,'Threat intelligence','Threat intelligence'),
|
||||||
|
('A.5.8','iso27001','organizational',508,'Sicurezza delle informazioni nella gestione dei progetti','Information security in project management'),
|
||||||
|
('A.5.9','iso27001','organizational',509,'Inventario delle informazioni e degli altri asset associati','Inventory of information and other associated assets'),
|
||||||
|
('A.5.10','iso27001','organizational',510,'Uso accettabile delle informazioni e degli altri asset associati','Acceptable use of information and other associated assets'),
|
||||||
|
('A.5.11','iso27001','organizational',511,'Restituzione degli asset','Return of assets'),
|
||||||
|
('A.5.12','iso27001','organizational',512,'Classificazione delle informazioni','Classification of information'),
|
||||||
|
('A.5.13','iso27001','organizational',513,'Etichettatura delle informazioni','Labelling of information'),
|
||||||
|
('A.5.14','iso27001','organizational',514,'Trasferimento delle informazioni','Information transfer'),
|
||||||
|
('A.5.15','iso27001','organizational',515,'Controllo degli accessi','Access control'),
|
||||||
|
('A.5.16','iso27001','organizational',516,'Gestione delle identita','Identity management'),
|
||||||
|
('A.5.17','iso27001','organizational',517,'Informazioni di autenticazione','Authentication information'),
|
||||||
|
('A.5.18','iso27001','organizational',518,'Diritti di accesso','Access rights'),
|
||||||
|
('A.5.19','iso27001','organizational',519,'Sicurezza delle informazioni nelle relazioni con i fornitori','Information security in supplier relationships'),
|
||||||
|
('A.5.20','iso27001','organizational',520,'Gestione della sicurezza delle informazioni negli accordi con i fornitori','Addressing information security within supplier agreements'),
|
||||||
|
('A.5.21','iso27001','organizational',521,'Gestione della sicurezza nella catena di fornitura ICT','Managing information security in the ICT supply chain'),
|
||||||
|
('A.5.22','iso27001','organizational',522,'Monitoraggio, riesame e gestione del cambiamento dei servizi dei fornitori','Monitoring, review and change management of supplier services'),
|
||||||
|
('A.5.23','iso27001','organizational',523,'Sicurezza delle informazioni nell''uso di servizi cloud','Information security for use of cloud services'),
|
||||||
|
('A.5.24','iso27001','organizational',524,'Pianificazione e preparazione della gestione degli incidenti','Information security incident management planning and preparation'),
|
||||||
|
('A.5.25','iso27001','organizational',525,'Valutazione e decisione sugli eventi di sicurezza delle informazioni','Assessment and decision on information security events'),
|
||||||
|
('A.5.26','iso27001','organizational',526,'Risposta agli incidenti di sicurezza delle informazioni','Response to information security incidents'),
|
||||||
|
('A.5.27','iso27001','organizational',527,'Apprendimento dagli incidenti di sicurezza delle informazioni','Learning from information security incidents'),
|
||||||
|
('A.5.28','iso27001','organizational',528,'Raccolta delle evidenze','Collection of evidence'),
|
||||||
|
('A.5.29','iso27001','organizational',529,'Sicurezza delle informazioni durante le interruzioni','Information security during disruption'),
|
||||||
|
('A.5.30','iso27001','organizational',530,'Prontezza ICT per la continuita operativa','ICT readiness for business continuity'),
|
||||||
|
('A.5.31','iso27001','organizational',531,'Requisiti legali, statutari, regolamentari e contrattuali','Legal, statutory, regulatory and contractual requirements'),
|
||||||
|
('A.5.32','iso27001','organizational',532,'Diritti di proprieta intellettuale','Intellectual property rights'),
|
||||||
|
('A.5.33','iso27001','organizational',533,'Protezione delle registrazioni','Protection of records'),
|
||||||
|
('A.5.34','iso27001','organizational',534,'Privacy e protezione dei dati personali (PII)','Privacy and protection of PII'),
|
||||||
|
('A.5.35','iso27001','organizational',535,'Riesame indipendente della sicurezza delle informazioni','Independent review of information security'),
|
||||||
|
('A.5.36','iso27001','organizational',536,'Conformita a politiche, regole e standard di sicurezza','Compliance with policies, rules and standards for information security'),
|
||||||
|
('A.5.37','iso27001','organizational',537,'Procedure operative documentate','Documented operating procedures');
|
||||||
|
|
||||||
|
-- ── A.6 Controlli relativi alle persone (8) ──
|
||||||
|
INSERT IGNORE INTO iso27001_annex_controls (control_code, standard, theme, sort_order, title_it, title_en) VALUES
|
||||||
|
('A.6.1','iso27001','people',601,'Screening','Screening'),
|
||||||
|
('A.6.2','iso27001','people',602,'Termini e condizioni di impiego','Terms and conditions of employment'),
|
||||||
|
('A.6.3','iso27001','people',603,'Consapevolezza, istruzione e formazione sulla sicurezza','Information security awareness, education and training'),
|
||||||
|
('A.6.4','iso27001','people',604,'Processo disciplinare','Disciplinary process'),
|
||||||
|
('A.6.5','iso27001','people',605,'Responsabilita dopo la cessazione o il cambio di impiego','Responsibilities after termination or change of employment'),
|
||||||
|
('A.6.6','iso27001','people',606,'Accordi di riservatezza o di non divulgazione','Confidentiality or non-disclosure agreements'),
|
||||||
|
('A.6.7','iso27001','people',607,'Lavoro da remoto','Remote working'),
|
||||||
|
('A.6.8','iso27001','people',608,'Segnalazione di eventi di sicurezza delle informazioni','Information security event reporting');
|
||||||
|
|
||||||
|
-- ── A.7 Controlli fisici (14) ──
|
||||||
|
INSERT IGNORE INTO iso27001_annex_controls (control_code, standard, theme, sort_order, title_it, title_en) VALUES
|
||||||
|
('A.7.1','iso27001','physical',701,'Perimetri di sicurezza fisica','Physical security perimeters'),
|
||||||
|
('A.7.2','iso27001','physical',702,'Accesso fisico','Physical entry'),
|
||||||
|
('A.7.3','iso27001','physical',703,'Messa in sicurezza di uffici, locali e strutture','Securing offices, rooms and facilities'),
|
||||||
|
('A.7.4','iso27001','physical',704,'Monitoraggio della sicurezza fisica','Physical security monitoring'),
|
||||||
|
('A.7.5','iso27001','physical',705,'Protezione da minacce fisiche e ambientali','Protecting against physical and environmental threats'),
|
||||||
|
('A.7.6','iso27001','physical',706,'Lavoro in aree sicure','Working in secure areas'),
|
||||||
|
('A.7.7','iso27001','physical',707,'Scrivania pulita e schermo pulito','Clear desk and clear screen'),
|
||||||
|
('A.7.8','iso27001','physical',708,'Posizionamento e protezione delle apparecchiature','Equipment siting and protection'),
|
||||||
|
('A.7.9','iso27001','physical',709,'Sicurezza degli asset fuori sede','Security of assets off-premises'),
|
||||||
|
('A.7.10','iso27001','physical',710,'Supporti di memorizzazione','Storage media'),
|
||||||
|
('A.7.11','iso27001','physical',711,'Servizi di supporto (utilities)','Supporting utilities'),
|
||||||
|
('A.7.12','iso27001','physical',712,'Sicurezza del cablaggio','Cabling security'),
|
||||||
|
('A.7.13','iso27001','physical',713,'Manutenzione delle apparecchiature','Equipment maintenance'),
|
||||||
|
('A.7.14','iso27001','physical',714,'Smaltimento sicuro o riutilizzo delle apparecchiature','Secure disposal or re-use of equipment');
|
||||||
|
|
||||||
|
-- ── A.8 Controlli tecnologici (34) ──
|
||||||
|
INSERT IGNORE INTO iso27001_annex_controls (control_code, standard, theme, sort_order, title_it, title_en) VALUES
|
||||||
|
('A.8.1','iso27001','technological',801,'Dispositivi endpoint degli utenti','User endpoint devices'),
|
||||||
|
('A.8.2','iso27001','technological',802,'Diritti di accesso privilegiati','Privileged access rights'),
|
||||||
|
('A.8.3','iso27001','technological',803,'Restrizione dell''accesso alle informazioni','Information access restriction'),
|
||||||
|
('A.8.4','iso27001','technological',804,'Accesso al codice sorgente','Access to source code'),
|
||||||
|
('A.8.5','iso27001','technological',805,'Autenticazione sicura','Secure authentication'),
|
||||||
|
('A.8.6','iso27001','technological',806,'Gestione della capacita','Capacity management'),
|
||||||
|
('A.8.7','iso27001','technological',807,'Protezione dal malware','Protection against malware'),
|
||||||
|
('A.8.8','iso27001','technological',808,'Gestione delle vulnerabilita tecniche','Management of technical vulnerabilities'),
|
||||||
|
('A.8.9','iso27001','technological',809,'Gestione della configurazione','Configuration management'),
|
||||||
|
('A.8.10','iso27001','technological',810,'Cancellazione delle informazioni','Information deletion'),
|
||||||
|
('A.8.11','iso27001','technological',811,'Mascheramento dei dati','Data masking'),
|
||||||
|
('A.8.12','iso27001','technological',812,'Prevenzione della fuga di dati','Data leakage prevention'),
|
||||||
|
('A.8.13','iso27001','technological',813,'Backup delle informazioni','Information backup'),
|
||||||
|
('A.8.14','iso27001','technological',814,'Ridondanza delle strutture di elaborazione','Redundancy of information processing facilities'),
|
||||||
|
('A.8.15','iso27001','technological',815,'Logging','Logging'),
|
||||||
|
('A.8.16','iso27001','technological',816,'Attivita di monitoraggio','Monitoring activities'),
|
||||||
|
('A.8.17','iso27001','technological',817,'Sincronizzazione degli orologi','Clock synchronization'),
|
||||||
|
('A.8.18','iso27001','technological',818,'Uso di programmi di utilita privilegiati','Use of privileged utility programs'),
|
||||||
|
('A.8.19','iso27001','technological',819,'Installazione del software sui sistemi operativi','Installation of software on operational systems'),
|
||||||
|
('A.8.20','iso27001','technological',820,'Sicurezza delle reti','Networks security'),
|
||||||
|
('A.8.21','iso27001','technological',821,'Sicurezza dei servizi di rete','Security of network services'),
|
||||||
|
('A.8.22','iso27001','technological',822,'Segregazione delle reti','Segregation of networks'),
|
||||||
|
('A.8.23','iso27001','technological',823,'Filtraggio web','Web filtering'),
|
||||||
|
('A.8.24','iso27001','technological',824,'Uso della crittografia','Use of cryptography'),
|
||||||
|
('A.8.25','iso27001','technological',825,'Ciclo di vita di sviluppo sicuro','Secure development life cycle'),
|
||||||
|
('A.8.26','iso27001','technological',826,'Requisiti di sicurezza delle applicazioni','Application security requirements'),
|
||||||
|
('A.8.27','iso27001','technological',827,'Principi di architettura e ingegneria dei sistemi sicuri','Secure system architecture and engineering principles'),
|
||||||
|
('A.8.28','iso27001','technological',828,'Codifica sicura','Secure coding'),
|
||||||
|
('A.8.29','iso27001','technological',829,'Test di sicurezza nello sviluppo e nell''accettazione','Security testing in development and acceptance'),
|
||||||
|
('A.8.30','iso27001','technological',830,'Sviluppo affidato all''esterno','Outsourced development'),
|
||||||
|
('A.8.31','iso27001','technological',831,'Separazione degli ambienti di sviluppo, test e produzione','Separation of development, test and production environments'),
|
||||||
|
('A.8.32','iso27001','technological',832,'Gestione del cambiamento','Change management'),
|
||||||
|
('A.8.33','iso27001','technological',833,'Informazioni di test','Test information'),
|
||||||
|
('A.8.34','iso27001','technological',834,'Protezione dei sistemi informativi durante i test di audit','Protection of information systems during audit testing');
|
||||||
|
|
||||||
|
-- ── ISO/IEC 27017:2015 — controlli cloud aggiuntivi (CLD.*) — CONDIZIONALI ──
|
||||||
|
INSERT IGNORE INTO iso27001_annex_controls (control_code, standard, theme, sort_order, title_it, title_en, iso27002_ref, condition_tag) VALUES
|
||||||
|
('CLD.6.3.1','iso27017','organizational',1010,'Ruoli e responsabilita condivisi in un ambiente di cloud computing','Shared roles and responsibilities within a cloud computing environment','ISO 27002:2013 §6.1.1 (ext. cloud)','cloud'),
|
||||||
|
('CLD.8.1.5','iso27017','organizational',1020,'Rimozione degli asset del cliente del servizio cloud','Removal of cloud service customer assets','ISO 27002:2013 §8.1 (ext. cloud)','cloud'),
|
||||||
|
('CLD.9.5.1','iso27017','technological',1030,'Segregazione negli ambienti di virtual computing','Segregation in virtual computing environments','ISO 27002:2013 §9.5 (ext. cloud)','cloud'),
|
||||||
|
('CLD.9.5.2','iso27017','technological',1040,'Hardening delle macchine virtuali','Virtual machine hardening','ISO 27002:2013 §9.5 (ext. cloud)','cloud'),
|
||||||
|
('CLD.12.1.5','iso27017','technological',1050,'Sicurezza operativa dell''amministratore','Administrator''s operational security','ISO 27002:2013 §12.1 (ext. cloud)','cloud'),
|
||||||
|
('CLD.12.4.5','iso27017','technological',1060,'Monitoraggio dei servizi cloud','Monitoring of cloud services','ISO 27002:2013 §12.4 (ext. cloud)','cloud'),
|
||||||
|
('CLD.13.1.4','iso27017','technological',1070,'Allineamento della sicurezza tra reti virtuali e fisiche','Alignment of security management for virtual and physical networks','ISO 27002:2013 §13.1 (ext. cloud)','cloud');
|
||||||
|
|
||||||
|
-- ── ISO/IEC 27018:2019 — controlli PII in cloud pubblico (PII.*) — CONDIZIONALI ──
|
||||||
|
-- Organizzati sui principi privacy ISO/IEC 29100. Codici interni PII.n (indice).
|
||||||
|
INSERT IGNORE INTO iso27001_annex_controls (control_code, standard, theme, sort_order, title_it, title_en, iso27002_ref, condition_tag) VALUES
|
||||||
|
('PII.1','iso27018','privacy',2010,'Consenso e scelta: trattare i PII solo su istruzioni documentate del cliente','Consent and choice: process PII only on documented client instructions','ISO 27018:2019 Annex A','pii_processor'),
|
||||||
|
('PII.2','iso27018','privacy',2020,'Legittimita e specificazione delle finalita','Purpose legitimacy and specification','ISO 27018:2019 Annex A','pii_processor'),
|
||||||
|
('PII.3','iso27018','privacy',2030,'Limitazione della raccolta','Collection limitation','ISO 27018:2019 Annex A','pii_processor'),
|
||||||
|
('PII.4','iso27018','privacy',2040,'Minimizzazione dei dati','Data minimization','ISO 27018:2019 Annex A','pii_processor'),
|
||||||
|
('PII.5','iso27018','privacy',2050,'Limitazione di uso, conservazione e divulgazione','Use, retention and disclosure limitation','ISO 27018:2019 Annex A','pii_processor'),
|
||||||
|
('PII.6','iso27018','privacy',2060,'Accuratezza e qualita','Accuracy and quality','ISO 27018:2019 Annex A','pii_processor'),
|
||||||
|
('PII.7','iso27018','privacy',2070,'Apertura, trasparenza e notifica','Openness, transparency and notice','ISO 27018:2019 Annex A','pii_processor'),
|
||||||
|
('PII.8','iso27018','privacy',2080,'Partecipazione e accesso dell''interessato','Individual participation and access','ISO 27018:2019 Annex A','pii_processor'),
|
||||||
|
('PII.9','iso27018','privacy',2090,'Accountability: notifica di violazione e gestione delle richieste','Accountability: breach notification and request handling','ISO 27018:2019 Annex A','pii_processor'),
|
||||||
|
('PII.10','iso27018','privacy',2100,'Sicurezza delle informazioni applicata ai PII','Information security applied to PII','ISO 27018:2019 Annex A','pii_processor'),
|
||||||
|
('PII.11','iso27018','privacy',2110,'Conformita privacy: sub-responsabili e trasferimenti internazionali','Privacy compliance: sub-processors and international transfers','ISO 27018:2019 Annex A','pii_processor');
|
||||||
@@ -88,6 +88,7 @@ $controllerMap = [
|
|||||||
'organizations' => 'OrganizationController',
|
'organizations' => 'OrganizationController',
|
||||||
'assessments' => 'AssessmentController',
|
'assessments' => 'AssessmentController',
|
||||||
'acn-gap' => 'AcnAssessmentController', // Gap Analysis ACN (Det. 164179/2025, misure/requisiti)
|
'acn-gap' => 'AcnAssessmentController', // Gap Analysis ACN (Det. 164179/2025, misure/requisiti)
|
||||||
|
'isms' => 'IsmsModelController', // Modello Organizzativo SGSI (ISO 27001/27017/27018 + SoA)
|
||||||
'dashboard' => 'DashboardController',
|
'dashboard' => 'DashboardController',
|
||||||
'risks' => 'RiskController',
|
'risks' => 'RiskController',
|
||||||
'incidents' => 'IncidentController',
|
'incidents' => 'IncidentController',
|
||||||
@@ -217,6 +218,26 @@ $actionMap = [
|
|||||||
'POST:{id}/aiAnalyze' => 'aiAnalyze',
|
'POST:{id}/aiAnalyze' => 'aiAnalyze',
|
||||||
],
|
],
|
||||||
|
|
||||||
|
// ── IsmsModelController — Modello Organizzativo SGSI (ISO 27001/27017/27018) ──
|
||||||
|
'isms' => [
|
||||||
|
'GET:model' => 'getModel',
|
||||||
|
'POST:model' => 'saveModel',
|
||||||
|
'PUT:model' => 'saveModel',
|
||||||
|
'GET:annexControls' => 'annexControls',
|
||||||
|
'GET:soa' => 'getSoa',
|
||||||
|
'POST:soa/derive' => 'deriveSoa',
|
||||||
|
'PUT:soa' => 'updateSoaControl',
|
||||||
|
'GET:roles' => 'listRoles',
|
||||||
|
'POST:roles' => 'saveRole',
|
||||||
|
'DELETE:roles/{subId}' => 'deleteRole',
|
||||||
|
'GET:documents' => 'listDocuments',
|
||||||
|
'POST:documents' => 'createDocument',
|
||||||
|
'POST:documents/aiGenerate' => 'aiGenerateDocument',
|
||||||
|
'PUT:documents/{subId}' => 'updateDocument',
|
||||||
|
'GET:readiness' => 'readiness',
|
||||||
|
'GET:export' => 'export',
|
||||||
|
],
|
||||||
|
|
||||||
// ── DashboardController ─────────────────────────
|
// ── DashboardController ─────────────────────────
|
||||||
'dashboard' => [
|
'dashboard' => [
|
||||||
'GET:overview' => 'overview',
|
'GET:overview' => 'overview',
|
||||||
|
|||||||
@@ -0,0 +1,170 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="it">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>Modello Organizzativo SGSI (ISO 27001) - NIS2 Agile</title>
|
||||||
|
<link rel="stylesheet" href="css/style.css">
|
||||||
|
<style>
|
||||||
|
.isms-intro { background:#eff6ff; border-left:4px solid var(--primary,#2563eb); padding:14px 18px; border-radius:8px; margin-bottom:20px; font-size:.92rem; line-height:1.6; }
|
||||||
|
.isms-steps { display:flex; flex-wrap:wrap; gap:6px; margin-bottom:20px; }
|
||||||
|
.isms-step { flex:1 1 140px; min-width:120px; padding:10px 12px; border:1.5px solid var(--gray-200,#e5e7eb); border-radius:10px; background:#fff; cursor:pointer; font-size:.82rem; text-align:center; }
|
||||||
|
.isms-step.active { border-color:var(--primary,#2563eb); background:#eff6ff; font-weight:700; }
|
||||||
|
.isms-step.done { border-color:#16a34a; }
|
||||||
|
.isms-step .num { display:inline-block; width:22px; height:22px; line-height:22px; border-radius:50%; background:var(--gray-100,#f3f4f6); font-weight:700; margin-bottom:4px; }
|
||||||
|
.isms-step.done .num { background:#16a34a; color:#fff; }
|
||||||
|
.isms-panel { display:none; }
|
||||||
|
.isms-panel.active { display:block; }
|
||||||
|
.isms-disclaimer { font-size:.8rem; color:#92400e; background:#fffbeb; border:1px solid #fde68a; border-radius:8px; padding:10px 12px; margin:12px 0; }
|
||||||
|
.raci-row { display:flex; gap:8px; align-items:center; flex-wrap:wrap; padding:8px 0; border-bottom:1px solid var(--gray-100,#f3f4f6); }
|
||||||
|
.raci-row input, .raci-row select { padding:6px 8px; border:1px solid var(--gray-200,#e5e7eb); border-radius:6px; font-size:.85rem; }
|
||||||
|
.soa-std { border:1px solid var(--gray-200,#e5e7eb); border-radius:10px; margin-bottom:14px; overflow:hidden; }
|
||||||
|
.soa-std-head { background:var(--gray-50,#f9fafb); padding:12px 16px; cursor:pointer; display:flex; justify-content:space-between; align-items:center; }
|
||||||
|
.soa-std-body { padding:6px 14px 14px; display:none; }
|
||||||
|
.soa-std.open .soa-std-body { display:block; }
|
||||||
|
.soa-theme { font-size:.78rem; font-weight:700; text-transform:uppercase; color:var(--gray-500,#6b7280); margin:14px 0 6px; letter-spacing:.04em; }
|
||||||
|
.soa-ctrl { padding:12px 0; border-top:1px solid var(--gray-100,#f3f4f6); }
|
||||||
|
.soa-ctrl-head { display:flex; gap:10px; align-items:flex-start; justify-content:space-between; flex-wrap:wrap; }
|
||||||
|
.soa-code { display:inline-block; background:#eef2ff; color:#3730a3; font-weight:700; font-size:.72rem; padding:2px 8px; border-radius:6px; margin-right:6px; }
|
||||||
|
.soa-derived { background:#ecfdf5; color:#065f46; font-size:.68rem; padding:1px 6px; border-radius:5px; }
|
||||||
|
.soa-opts { display:flex; flex-wrap:wrap; gap:6px; margin-top:8px; }
|
||||||
|
.soa-opt { min-height:34px; padding:5px 10px; border:1.5px solid var(--gray-200,#e5e7eb); border-radius:18px; background:#fff; cursor:pointer; font-size:.8rem; font-weight:600; }
|
||||||
|
.soa-opt.sel-implemented,.soa-opt.sel-verified { background:#16a34a; color:#fff; border-color:#16a34a; }
|
||||||
|
.soa-opt.sel-in_progress { background:#f59e0b; color:#fff; border-color:#f59e0b; }
|
||||||
|
.soa-opt.sel-not_started { background:#9ca3af; color:#fff; border-color:#9ca3af; }
|
||||||
|
.soa-justif { width:100%; margin-top:8px; padding:8px; border:1px solid var(--gray-200,#e5e7eb); border-radius:6px; font-size:.84rem; min-height:46px; }
|
||||||
|
.isms-field { margin-bottom:14px; }
|
||||||
|
.isms-field label { display:block; font-weight:600; font-size:.88rem; margin-bottom:4px; }
|
||||||
|
.isms-field textarea, .isms-field input { width:100%; padding:9px; border:1px solid var(--gray-200,#e5e7eb); border-radius:8px; font-size:.9rem; }
|
||||||
|
.isms-flags label { display:block; padding:8px 0; font-size:.9rem; }
|
||||||
|
.ck-item { display:flex; justify-content:space-between; padding:8px 0; border-bottom:1px solid var(--gray-100,#f3f4f6); font-size:.9rem; }
|
||||||
|
.ck-yes { color:#16a34a; font-weight:700; } .ck-no { color:#9ca3af; }
|
||||||
|
.savehint { font-size:.78rem; color:var(--gray-400,#9ca3af); min-height:16px; }
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="app-layout">
|
||||||
|
<aside class="sidebar" id="sidebar"></aside>
|
||||||
|
<main class="main-content">
|
||||||
|
<header class="content-header">
|
||||||
|
<h2 data-i18n="isms.title">Modello Organizzativo SGSI (ISO 27001)</h2>
|
||||||
|
<div class="content-header-actions">
|
||||||
|
<span class="savehint" id="isms-savehint"></span>
|
||||||
|
<button class="btn btn-outline btn-sm" onclick="ismsExportView()" data-i18n="isms.export">Esporta SGSI</button>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<div class="content-body">
|
||||||
|
<div class="isms-intro">
|
||||||
|
<strong data-i18n="isms.intro.title">Costruisci il tuo Sistema di Gestione della Sicurezza delle Informazioni.</strong>
|
||||||
|
<span data-i18n="isms.intro.body">Procedura guidata ISO/IEC 27001:2022 (clausole 4-10 + Statement of Applicability). Lo Statement of Applicability viene pre-popolato dalle tue risposte alla Gap Analysis NIS2. Attiva le estensioni cloud ISO 27017/27018 dove pertinenti.</span>
|
||||||
|
</div>
|
||||||
|
<div class="isms-disclaimer" data-i18n="isms.disclaimer">Strumento di supporto e pre-audit: non costituisce una certificazione ISO 27001 né un parere professionale vincolante. Gli obblighi normativi in Italia derivano da NIS2 / D.Lgs. 138/2024 / Determinazioni ACN; ISO 27001/27017/27018 sono best practice.</div>
|
||||||
|
|
||||||
|
<div class="isms-steps" id="isms-steps"></div>
|
||||||
|
|
||||||
|
<!-- STEP 1 - Contesto & Ambito (cl.4) -->
|
||||||
|
<div class="isms-panel" id="panel-0">
|
||||||
|
<div class="card"><div class="card-body">
|
||||||
|
<h3 data-i18n="isms.s1.title">1. Contesto e Ambito (cl. 4)</h3>
|
||||||
|
<div class="isms-field"><label data-i18n="isms.s1.scope">Ambito del SGSI (scope statement)</label><textarea id="f-scope" rows="3" placeholder="Es. Tutti i sistemi e processi a supporto dei servizi ICT erogati dalla sede di..."></textarea></div>
|
||||||
|
<div class="isms-field"><label data-i18n="isms.s1.ctx_int">Contesto interno</label><textarea id="f-ctx-int" rows="2"></textarea></div>
|
||||||
|
<div class="isms-field"><label data-i18n="isms.s1.ctx_ext">Contesto esterno</label><textarea id="f-ctx-ext" rows="2"></textarea></div>
|
||||||
|
<div class="isms-field"><label data-i18n="isms.s1.parties">Parti interessate (una per riga)</label><textarea id="f-parties" rows="3" placeholder="Clienti Autorità (ACN/CSIRT) Fornitori ICT"></textarea></div>
|
||||||
|
<div class="isms-field"><label data-i18n="isms.s1.boundaries">Confini del SGSI</label><textarea id="f-boundaries" rows="2"></textarea></div>
|
||||||
|
<div class="isms-field"><label data-i18n="isms.s1.exclusions">Esclusioni motivate</label><textarea id="f-exclusions" rows="2"></textarea></div>
|
||||||
|
<div class="isms-flags">
|
||||||
|
<strong data-i18n="isms.s1.cloud">Estensioni cloud (attivano i controlli condizionali nel SoA)</strong>
|
||||||
|
<label><input type="checkbox" id="f-uses-cloud"> <span data-i18n="isms.s1.uses_cloud">Usiamo servizi cloud pubblici (Microsoft 365, AWS, Google...) → ISO 27017</span></label>
|
||||||
|
<label><input type="checkbox" id="f-cloud-provider"> <span data-i18n="isms.s1.cloud_provider">Siamo fornitori di servizi cloud → ISO 27017 (lato provider)</span></label>
|
||||||
|
<label><input type="checkbox" id="f-pii-cloud"> <span data-i18n="isms.s1.pii_cloud">Trattiamo dati personali (PII) in cloud pubblico come responsabili → ISO 27018</span></label>
|
||||||
|
</div>
|
||||||
|
<button class="btn btn-primary" onclick="saveStep1()" data-i18n="isms.save_next">Salva e continua</button>
|
||||||
|
</div></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- STEP 2 - Leadership (cl.5) -->
|
||||||
|
<div class="isms-panel" id="panel-1">
|
||||||
|
<div class="card"><div class="card-body">
|
||||||
|
<h3 data-i18n="isms.s2.title">2. Leadership: ruoli e responsabilità (cl. 5)</h3>
|
||||||
|
<p class="text-muted" data-i18n="isms.s2.hint">Definisci i ruoli del SGSI con la matrice RACI (Responsible, Accountable, Consulted, Informed).</p>
|
||||||
|
<div id="roles-list"></div>
|
||||||
|
<div class="raci-row" style="border-top:2px solid var(--gray-200,#e5e7eb); margin-top:10px;">
|
||||||
|
<input id="r-name" placeholder="Ruolo (es. CISO)" style="flex:1 1 160px;">
|
||||||
|
<input id="r-resp" placeholder="Responsabilità" style="flex:2 1 220px;">
|
||||||
|
<select id="r-raci"><option value="">RACI</option><option>R</option><option>A</option><option>C</option><option>I</option></select>
|
||||||
|
<button class="btn btn-sm btn-primary" onclick="addRole()" data-i18n="isms.s2.add">Aggiungi ruolo</button>
|
||||||
|
</div>
|
||||||
|
<button class="btn btn-outline" style="margin-top:14px;" onclick="goStep(2)" data-i18n="isms.next">Continua</button>
|
||||||
|
</div></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- STEP 3 - Risk (cl.6) -->
|
||||||
|
<div class="isms-panel" id="panel-2">
|
||||||
|
<div class="card"><div class="card-body">
|
||||||
|
<h3 data-i18n="isms.s3.title">3. Risk: metodologia e obiettivi (cl. 6)</h3>
|
||||||
|
<div class="isms-field"><label data-i18n="isms.s3.method">Metodologia di risk assessment</label><textarea id="f-method" rows="3" placeholder="Es. ISO 27005 / NIST SP 800-30; scala probabilità×impatto 5×5 (già usata nel modulo Rischi)."></textarea></div>
|
||||||
|
<div class="isms-field"><label data-i18n="isms.s3.objectives">Obiettivi del SGSI (uno per riga)</label><textarea id="f-objectives" rows="3"></textarea></div>
|
||||||
|
<p class="text-muted" data-i18n="isms.s3.link">Il registro dei rischi vive nel modulo <a href="risks.html">Rischi</a>: questo passo ne dichiara la metodologia.</p>
|
||||||
|
<button class="btn btn-primary" onclick="saveStep3()" data-i18n="isms.save_next">Salva e continua</button>
|
||||||
|
</div></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- STEP 4 - SoA -->
|
||||||
|
<div class="isms-panel" id="panel-3">
|
||||||
|
<div class="card"><div class="card-body">
|
||||||
|
<h3 data-i18n="isms.s4.title">4. Statement of Applicability</h3>
|
||||||
|
<p class="text-muted" data-i18n="isms.s4.hint">I controlli sono pre-popolati dalle risposte NIS2 dove esiste corrispondenza (badge "da NIS2"). Conferma applicabilità, stato e motivazione per ciascuno.</p>
|
||||||
|
<div style="margin:10px 0;">
|
||||||
|
<button class="btn btn-sm btn-outline" onclick="deriveSoa()" data-i18n="isms.s4.derive">Ri-deriva dal NIS2</button>
|
||||||
|
<span id="soa-stats" class="text-muted" style="margin-left:12px; font-size:.85rem;"></span>
|
||||||
|
</div>
|
||||||
|
<div id="soa-groups"></div>
|
||||||
|
<button class="btn btn-outline" style="margin-top:14px;" onclick="goStep(4)" data-i18n="isms.next">Continua</button>
|
||||||
|
</div></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- STEP 5 - Documented information (cl.7-8) -->
|
||||||
|
<div class="isms-panel" id="panel-4">
|
||||||
|
<div class="card"><div class="card-body">
|
||||||
|
<h3 data-i18n="isms.s5.title">5. Documented Information (cl. 7-8)</h3>
|
||||||
|
<div class="raci-row" style="border:none;">
|
||||||
|
<select id="d-type" style="flex:1 1 200px;">
|
||||||
|
<option value="policy_sgsi">Policy SGSI</option>
|
||||||
|
<option value="dichiarazione_applicabilita">Dichiarazione di Applicabilità</option>
|
||||||
|
<option value="politica_controllo_accessi">Politica controllo accessi</option>
|
||||||
|
<option value="procedura_incident">Procedura gestione incidenti</option>
|
||||||
|
<option value="procedura_continuita">Procedura continuità operativa</option>
|
||||||
|
<option value="politica_fornitori">Politica sicurezza fornitori</option>
|
||||||
|
</select>
|
||||||
|
<button class="btn btn-sm btn-primary" onclick="aiGenDoc()" id="btn-aigen" data-i18n="isms.s5.aigen">Genera bozza con AI</button>
|
||||||
|
</div>
|
||||||
|
<div class="savehint" id="aigen-hint"></div>
|
||||||
|
<div id="docs-list" style="margin-top:14px;"></div>
|
||||||
|
<button class="btn btn-outline" style="margin-top:14px;" onclick="goStep(5)" data-i18n="isms.next">Continua</button>
|
||||||
|
</div></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- STEP 6 - Monitoring & Readiness (cl.9-10) -->
|
||||||
|
<div class="isms-panel" id="panel-5">
|
||||||
|
<div class="card"><div class="card-body">
|
||||||
|
<h3 data-i18n="isms.s6.title">6. Monitoraggio e Miglioramento (cl. 9-10)</h3>
|
||||||
|
<p class="text-muted" data-i18n="isms.s6.hint">Gli audit interni e le non conformità si gestiscono nei moduli esistenti: <a href="reports.html">Audit & Report</a> e NCR/CAPA.</p>
|
||||||
|
<h4 data-i18n="isms.s6.readiness">Completamento del SGSI</h4>
|
||||||
|
<div style="text-align:center; margin:14px 0;">
|
||||||
|
<div style="font-size:2.6rem; font-weight:800;" id="readiness-pct">0%</div>
|
||||||
|
</div>
|
||||||
|
<div id="readiness-checklist"></div>
|
||||||
|
</div></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script src="js/i18n.js"></script>
|
||||||
|
<script src="js/common.js"></script>
|
||||||
|
<script src="js/api.js"></script>
|
||||||
|
<script src="js/help.js"></script>
|
||||||
|
<script src="js/isms.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -204,6 +204,26 @@ class NIS2API {
|
|||||||
acnReport(id) { return this._acn(this.get(`/acn-gap/${id}/report`)); }
|
acnReport(id) { return this._acn(this.get(`/acn-gap/${id}/report`)); }
|
||||||
acnAiAnalyze(id) { return this._acn(this.post(`/acn-gap/${id}/aiAnalyze`, {})); }
|
acnAiAnalyze(id) { return this._acn(this.post(`/acn-gap/${id}/aiAnalyze`, {})); }
|
||||||
|
|
||||||
|
// ═══════════════════════════════════════════════════════════════════
|
||||||
|
// Modello Organizzativo SGSI (ISO 27001/27017/27018) — vedi IsmsModelController
|
||||||
|
// Stesso contratto degli acn*: ritornano `data`, lanciano su success=false.
|
||||||
|
// ═══════════════════════════════════════════════════════════════════
|
||||||
|
ismsGetModel() { return this._acn(this.get('/isms/model')); }
|
||||||
|
ismsSaveModel(data) { return this._acn(this.post('/isms/model', data || {})); }
|
||||||
|
ismsAnnexControls() { return this._acn(this.get('/isms/annex-controls')); }
|
||||||
|
ismsGetSoa() { return this._acn(this.get('/isms/soa')); }
|
||||||
|
ismsDeriveSoa() { return this._acn(this.post('/isms/soa/derive', {})); }
|
||||||
|
ismsUpdateSoa(data) { return this._acn(this.put('/isms/soa', data)); }
|
||||||
|
ismsRoles() { return this._acn(this.get('/isms/roles')); }
|
||||||
|
ismsSaveRole(data) { return this._acn(this.post('/isms/roles', data)); }
|
||||||
|
ismsDeleteRole(id) { return this._acn(this.del(`/isms/roles/${id}`)); }
|
||||||
|
ismsDocuments() { return this._acn(this.get('/isms/documents')); }
|
||||||
|
ismsCreateDocument(data) { return this._acn(this.post('/isms/documents', data)); }
|
||||||
|
ismsAiGenerateDocument(data) { return this._acn(this.post('/isms/documents/ai-generate', data)); }
|
||||||
|
ismsUpdateDocument(id, data) { return this._acn(this.put(`/isms/documents/${id}`, data)); }
|
||||||
|
ismsReadiness() { return this._acn(this.get('/isms/readiness')); }
|
||||||
|
ismsExport() { return this._acn(this.get('/isms/export')); }
|
||||||
|
|
||||||
// ═══════════════════════════════════════════════════════════════════
|
// ═══════════════════════════════════════════════════════════════════
|
||||||
// Dashboard
|
// Dashboard
|
||||||
// ═══════════════════════════════════════════════════════════════════
|
// ═══════════════════════════════════════════════════════════════════
|
||||||
|
|||||||
@@ -189,6 +189,7 @@ function loadSidebar() {
|
|||||||
{ name: 'Compliance Journey', href: 'workflow.html', icon: `<svg viewBox="0 0 20 20" fill="currentColor"><path d="M10.707 2.293a1 1 0 00-1.414 0l-7 7a1 1 0 001.414 1.414L4 10.414V17a1 1 0 001 1h2a1 1 0 001-1v-2a1 1 0 011-1h2a1 1 0 011 1v2a1 1 0 001 1h2a1 1 0 001-1v-6.586l.293.293a1 1 0 001.414-1.414l-7-7z"/></svg>` },
|
{ name: 'Compliance Journey', href: 'workflow.html', icon: `<svg viewBox="0 0 20 20" fill="currentColor"><path d="M10.707 2.293a1 1 0 00-1.414 0l-7 7a1 1 0 001.414 1.414L4 10.414V17a1 1 0 001 1h2a1 1 0 001-1v-2a1 1 0 011-1h2a1 1 0 011 1v2a1 1 0 001 1h2a1 1 0 001-1v-6.586l.293.293a1 1 0 001.414-1.414l-7-7z"/></svg>` },
|
||||||
{ name: 'Gap Analysis', href: 'assessment.html', icon: iconClipboardCheck(), i18nKey: 'nav.gap_analysis' },
|
{ name: 'Gap Analysis', href: 'assessment.html', icon: iconClipboardCheck(), i18nKey: 'nav.gap_analysis' },
|
||||||
{ name: 'Gap Analysis ACN', href: 'acn-gap.html', icon: iconClipboardCheck(), i18nKey: 'nav.acn_gap' },
|
{ name: 'Gap Analysis ACN', href: 'acn-gap.html', icon: iconClipboardCheck(), i18nKey: 'nav.acn_gap' },
|
||||||
|
{ name: 'Modello SGSI (ISO 27001)', href: 'isms.html', icon: `<svg viewBox="0 0 20 20" fill="currentColor"><path fill-rule="evenodd" d="M2.166 4.999A11.954 11.954 0 0010 1.944 11.954 11.954 0 0017.834 5c.11.65.166 1.32.166 2.001 0 5.225-3.34 9.67-8 11.317C5.34 16.67 2 12.225 2 7c0-.682.057-1.35.166-2.001zm11.541 3.708a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z" clip-rule="evenodd"/></svg>`, i18nKey: 'nav.isms' },
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -159,6 +159,48 @@ const HelpSystem = (function () {
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
|
||||||
|
'isms': {
|
||||||
|
title: 'Guida - Modello Organizzativo SGSI (ISO 27001)',
|
||||||
|
intro: 'Il modulo Modello Organizzativo ti guida nella costruzione di un Sistema di Gestione della Sicurezza delle Informazioni (SGSI / ISMS) secondo ISO/IEC 27001:2022 (clausole 4-10) + lo Statement of Applicability (SoA) sui controlli Annex A. E\' uno strumento di SUPPORTO e PRE-AUDIT: non sostituisce l\'auditor ne costituisce una certificazione.',
|
||||||
|
sections: [
|
||||||
|
{
|
||||||
|
heading: 'Il wizard in 6 passi (clausole ISO 27001:2022)',
|
||||||
|
items: [
|
||||||
|
'<strong>1. Contesto e Ambito (cl. 4)</strong>: definisci lo scope del SGSI, contesto interno/esterno, parti interessate, confini ed esclusioni motivate. Qui attivi anche le estensioni cloud.',
|
||||||
|
'<strong>2. Leadership (cl. 5)</strong>: ruoli e responsabilita del SGSI con matrice RACI (Responsible, Accountable, Consulted, Informed).',
|
||||||
|
'<strong>3. Risk (cl. 6)</strong>: dichiari la metodologia di risk assessment (es. ISO 27005) e gli obiettivi. Il registro dei rischi resta nel modulo Rischi.',
|
||||||
|
'<strong>4. Statement of Applicability</strong>: i 93 controlli Annex A con applicabilita, stato e motivazione.',
|
||||||
|
'<strong>5. Documented Information (cl. 7-8)</strong>: policy e procedure, con bozze generabili dall\'AI (revisione umana obbligatoria).',
|
||||||
|
'<strong>6. Monitoraggio e Miglioramento (cl. 9-10)</strong>: audit interni e non conformita si gestiscono nei moduli Audit & Report e NCR/CAPA. Una checklist mostra il completamento del SGSI.'
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
heading: 'SoA pre-popolato dal NIS2',
|
||||||
|
items: [
|
||||||
|
'Ogni domanda della Gap Analysis NIS2 Art.21 e gia collegata a un controllo ISO 27001 (es. A.5.1): lo Statement of Applicability viene quindi <strong>pre-compilato</strong> a partire dalle tue risposte NIS2 (badge "da NIS2").',
|
||||||
|
'Lo stato derivato e una <strong>proposta da confermare</strong>: implemented=100%, partial=50%, not_implemented=0%; not_applicable esclude il controllo dalla derivazione.',
|
||||||
|
'I controlli senza corrispondenza NIS2 (es. parte dei controlli fisici A.7) restano da compilare manualmente.',
|
||||||
|
'Il pulsante "Ri-deriva dal NIS2" aggiunge solo i controlli mancanti: non sovrascrive le tue modifiche manuali.'
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
heading: 'Estensioni cloud ISO 27017 / 27018',
|
||||||
|
items: [
|
||||||
|
'Se usi servizi cloud pubblici o sei un fornitore cloud, attiva <strong>ISO/IEC 27017:2015</strong>: aggiunge 7 controlli cloud (CLD.*) al SoA.',
|
||||||
|
'Se tratti dati personali (PII) in cloud pubblico come responsabile, attiva <strong>ISO/IEC 27018:2019</strong>: aggiunge i controlli privacy (PII.*) allineati ai principi ISO 29100, sinergici con il GDPR.',
|
||||||
|
'Le sezioni cloud/PII compaiono nel SoA solo se attivi i relativi flag nel passo 1.'
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
references: [
|
||||||
|
'ISO/IEC 27001:2022 - Requisiti del SGSI (clausole 4-10 + Annex A, 93 controlli)',
|
||||||
|
'ISO/IEC 27002:2022 - Guida implementativa dei controlli Annex A',
|
||||||
|
'ISO/IEC 27017:2015 - Controlli di sicurezza per i servizi cloud (CLD.*)',
|
||||||
|
'ISO/IEC 27018:2019 - Protezione dei PII nel cloud pubblico (PII processor)',
|
||||||
|
'NOTA: ISO e best practice non vincolante; gli obblighi italiani derivano da NIS2 / D.Lgs. 138/2024 / Determinazioni ACN.'
|
||||||
|
]
|
||||||
|
},
|
||||||
|
|
||||||
// ─── Risk Management ─────────────────────────────────────────
|
// ─── Risk Management ─────────────────────────────────────────
|
||||||
'risks': {
|
'risks': {
|
||||||
title: 'Guida - Gestione Rischi',
|
title: 'Guida - Gestione Rischi',
|
||||||
@@ -911,6 +953,8 @@ const HelpSystem = (function () {
|
|||||||
'acn-gap.html': 'acn',
|
'acn-gap.html': 'acn',
|
||||||
'acn-gap': 'acn',
|
'acn-gap': 'acn',
|
||||||
'acn': 'acn',
|
'acn': 'acn',
|
||||||
|
'isms.html': 'isms',
|
||||||
|
'isms': 'isms',
|
||||||
'risks.html': 'risks',
|
'risks.html': 'risks',
|
||||||
'risks': 'risks',
|
'risks': 'risks',
|
||||||
'incidents.html': 'incidents',
|
'incidents.html': 'incidents',
|
||||||
|
|||||||
@@ -36,6 +36,47 @@ const I18n = (function () {
|
|||||||
'acn.results.ai': { it: 'Analisi AI dei gap', en: 'AI gap analysis' },
|
'acn.results.ai': { it: 'Analisi AI dei gap', en: 'AI gap analysis' },
|
||||||
'acn.results.gaps': { it: "Requisiti non conformi (piano d'azione)", en: 'Non-compliant requirements (action plan)' },
|
'acn.results.gaps': { it: "Requisiti non conformi (piano d'azione)", en: 'Non-compliant requirements (action plan)' },
|
||||||
'acn.backstart': { it: "Torna all'inizio", en: 'Back to start' },
|
'acn.backstart': { it: "Torna all'inizio", en: 'Back to start' },
|
||||||
|
// ── Modello Organizzativo SGSI (ISO 27001) ──
|
||||||
|
'nav.isms': { it: 'Modello SGSI (ISO 27001)', en: 'ISMS Model (ISO 27001)' },
|
||||||
|
'isms.title': { it: 'Modello Organizzativo SGSI (ISO 27001)', en: 'ISMS Organizational Model (ISO 27001)' },
|
||||||
|
'isms.export': { it: 'Esporta SGSI', en: 'Export ISMS' },
|
||||||
|
'isms.intro.title': { it: 'Costruisci il tuo Sistema di Gestione della Sicurezza delle Informazioni.', en: 'Build your Information Security Management System.' },
|
||||||
|
'isms.intro.body': { it: 'Procedura guidata ISO/IEC 27001:2022 (clausole 4-10 + Statement of Applicability). Lo Statement of Applicability viene pre-popolato dalle tue risposte alla Gap Analysis NIS2. Attiva le estensioni cloud ISO 27017/27018 dove pertinenti.', en: 'Guided ISO/IEC 27001:2022 procedure (clauses 4-10 + Statement of Applicability). The Statement of Applicability is pre-populated from your NIS2 Gap Analysis answers. Enable the ISO 27017/27018 cloud extensions where relevant.' },
|
||||||
|
'isms.disclaimer': { it: 'Strumento di supporto e pre-audit: non costituisce una certificazione ISO 27001 né un parere professionale vincolante. Gli obblighi normativi in Italia derivano da NIS2 / D.Lgs. 138/2024 / Determinazioni ACN; ISO 27001/27017/27018 sono best practice.', en: 'Support and pre-audit tool: it is not an ISO 27001 certification nor binding professional advice. Regulatory obligations in Italy stem from NIS2 / Legislative Decree 138/2024 / ACN Determinations; ISO 27001/27017/27018 are best practices.' },
|
||||||
|
'isms.step1': { it: 'Contesto', en: 'Context' },
|
||||||
|
'isms.step2': { it: 'Leadership', en: 'Leadership' },
|
||||||
|
'isms.step3': { it: 'Risk', en: 'Risk' },
|
||||||
|
'isms.step4': { it: 'SoA', en: 'SoA' },
|
||||||
|
'isms.step5': { it: 'Documenti', en: 'Documents' },
|
||||||
|
'isms.step6': { it: 'Monitoraggio', en: 'Monitoring' },
|
||||||
|
'isms.save_next': { it: 'Salva e continua', en: 'Save and continue' },
|
||||||
|
'isms.next': { it: 'Continua', en: 'Continue' },
|
||||||
|
'isms.s1.title': { it: '1. Contesto e Ambito (cl. 4)', en: '1. Context and Scope (cl. 4)' },
|
||||||
|
'isms.s1.scope': { it: 'Ambito del SGSI (scope statement)', en: 'ISMS scope statement' },
|
||||||
|
'isms.s1.ctx_int': { it: 'Contesto interno', en: 'Internal context' },
|
||||||
|
'isms.s1.ctx_ext': { it: 'Contesto esterno', en: 'External context' },
|
||||||
|
'isms.s1.parties': { it: 'Parti interessate (una per riga)', en: 'Interested parties (one per line)' },
|
||||||
|
'isms.s1.boundaries': { it: 'Confini del SGSI', en: 'ISMS boundaries' },
|
||||||
|
'isms.s1.exclusions': { it: 'Esclusioni motivate', en: 'Justified exclusions' },
|
||||||
|
'isms.s1.cloud': { it: 'Estensioni cloud (attivano i controlli condizionali nel SoA)', en: 'Cloud extensions (enable conditional controls in the SoA)' },
|
||||||
|
'isms.s1.uses_cloud': { it: 'Usiamo servizi cloud pubblici (Microsoft 365, AWS, Google...) → ISO 27017', en: 'We use public cloud services (Microsoft 365, AWS, Google...) → ISO 27017' },
|
||||||
|
'isms.s1.cloud_provider': { it: 'Siamo fornitori di servizi cloud → ISO 27017 (lato provider)', en: 'We are cloud service providers → ISO 27017 (provider side)' },
|
||||||
|
'isms.s1.pii_cloud': { it: 'Trattiamo dati personali (PII) in cloud pubblico come responsabili → ISO 27018', en: 'We process personal data (PII) in public cloud as processors → ISO 27018' },
|
||||||
|
'isms.s2.title': { it: '2. Leadership: ruoli e responsabilità (cl. 5)', en: '2. Leadership: roles and responsibilities (cl. 5)' },
|
||||||
|
'isms.s2.hint': { it: 'Definisci i ruoli del SGSI con la matrice RACI (Responsible, Accountable, Consulted, Informed).', en: 'Define the ISMS roles with the RACI matrix (Responsible, Accountable, Consulted, Informed).' },
|
||||||
|
'isms.s2.add': { it: 'Aggiungi ruolo', en: 'Add role' },
|
||||||
|
'isms.s3.title': { it: '3. Risk: metodologia e obiettivi (cl. 6)', en: '3. Risk: methodology and objectives (cl. 6)' },
|
||||||
|
'isms.s3.method': { it: 'Metodologia di risk assessment', en: 'Risk assessment methodology' },
|
||||||
|
'isms.s3.objectives': { it: 'Obiettivi del SGSI (uno per riga)', en: 'ISMS objectives (one per line)' },
|
||||||
|
'isms.s3.link': { it: 'Il registro dei rischi vive nel modulo Rischi: questo passo ne dichiara la metodologia.', en: 'The risk register lives in the Risks module: this step declares its methodology.' },
|
||||||
|
'isms.s4.title': { it: '4. Statement of Applicability', en: '4. Statement of Applicability' },
|
||||||
|
'isms.s4.hint': { it: 'I controlli sono pre-popolati dalle risposte NIS2 dove esiste corrispondenza (badge "da NIS2"). Conferma applicabilità, stato e motivazione per ciascuno.', en: 'Controls are pre-populated from NIS2 answers where a mapping exists ("from NIS2" badge). Confirm applicability, status and justification for each.' },
|
||||||
|
'isms.s4.derive': { it: 'Ri-deriva dal NIS2', en: 'Re-derive from NIS2' },
|
||||||
|
'isms.s5.title': { it: '5. Documented Information (cl. 7-8)', en: '5. Documented Information (cl. 7-8)' },
|
||||||
|
'isms.s5.aigen': { it: 'Genera bozza con AI', en: 'Generate draft with AI' },
|
||||||
|
'isms.s6.title': { it: '6. Monitoraggio e Miglioramento (cl. 9-10)', en: '6. Monitoring and Improvement (cl. 9-10)' },
|
||||||
|
'isms.s6.hint': { it: 'Gli audit interni e le non conformità si gestiscono nei moduli esistenti: Audit & Report e NCR/CAPA.', en: 'Internal audits and non-conformities are managed in the existing modules: Audit & Reports and NCR/CAPA.' },
|
||||||
|
'isms.s6.readiness': { it: 'Completamento del SGSI', en: 'ISMS completion' },
|
||||||
'nav.management': { it: 'Gestione', en: 'Management' },
|
'nav.management': { it: 'Gestione', en: 'Management' },
|
||||||
'nav.risks': { it: 'Rischi', en: 'Risks' },
|
'nav.risks': { it: 'Rischi', en: 'Risks' },
|
||||||
'nav.incidents': { it: 'Incidenti', en: 'Incidents' },
|
'nav.incidents': { it: 'Incidenti', en: 'Incidents' },
|
||||||
|
|||||||
@@ -0,0 +1,288 @@
|
|||||||
|
/**
|
||||||
|
* NIS2 Agile - Modello Organizzativo SGSI (ISO 27001/27017/27018)
|
||||||
|
* Wizard 6 step: Contesto → Leadership → Risk → SoA → Documenti → Monitoraggio.
|
||||||
|
* Pattern coerente con acn-gap.html: client api.* che ritorna `data` e lancia su errore.
|
||||||
|
*/
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
const STEP_KEYS = ['isms.step1','isms.step2','isms.step3','isms.step4','isms.step5','isms.step6'];
|
||||||
|
const STEP_FALLBACK = ['Contesto','Leadership','Risk','SoA','Documenti','Monitoraggio'];
|
||||||
|
const THEME_LABELS = {
|
||||||
|
organizational: { it:'Organizzativi', en:'Organizational' },
|
||||||
|
people: { it:'Persone', en:'People' },
|
||||||
|
physical: { it:'Fisici', en:'Physical' },
|
||||||
|
technological: { it:'Tecnologici', en:'Technological' },
|
||||||
|
privacy: { it:'Privacy (PII)', en:'Privacy (PII)' }
|
||||||
|
};
|
||||||
|
|
||||||
|
let ISMS = { model:null, step:0, soaLoaded:false };
|
||||||
|
let soaSaveTimer = {};
|
||||||
|
|
||||||
|
function lang(){ try { return I18n.getLang ? I18n.getLang() : 'it'; } catch(e){ return 'it'; } }
|
||||||
|
function el(id){ return document.getElementById(id); }
|
||||||
|
function esc(s){ const d=document.createElement('div'); d.textContent=s==null?'':String(s); return d.innerHTML; }
|
||||||
|
function hint(id, msg){ const e=el(id); if(e){ e.textContent=msg; } }
|
||||||
|
|
||||||
|
document.addEventListener('DOMContentLoaded', async function(){
|
||||||
|
if (typeof checkAuth==='function' && !checkAuth()) return;
|
||||||
|
if (window.I18n && I18n.init) I18n.init('it');
|
||||||
|
if (typeof loadSidebar==='function') loadSidebar();
|
||||||
|
if (window.HelpSystem && HelpSystem.init) HelpSystem.init();
|
||||||
|
renderSteps();
|
||||||
|
await loadModel();
|
||||||
|
});
|
||||||
|
|
||||||
|
function renderSteps(){
|
||||||
|
let html='';
|
||||||
|
STEP_FALLBACK.forEach(function(lbl, i){
|
||||||
|
const t = (window.I18n && I18n.t) ? I18n.t(STEP_KEYS[i]) : lbl;
|
||||||
|
html += '<div class="isms-step" id="step-'+i+'" onclick="goStep('+i+')"><div class="num">'+(i+1)+'</div><div>'+esc(t&&t!==STEP_KEYS[i]?t:lbl)+'</div></div>';
|
||||||
|
});
|
||||||
|
el('isms-steps').innerHTML = html;
|
||||||
|
}
|
||||||
|
|
||||||
|
function goStep(i){
|
||||||
|
ISMS.step = i;
|
||||||
|
document.querySelectorAll('.isms-panel').forEach(function(p){ p.classList.remove('active'); });
|
||||||
|
const panel = el('panel-'+i); if (panel) panel.classList.add('active');
|
||||||
|
document.querySelectorAll('.isms-step').forEach(function(s,idx){ s.classList.toggle('active', idx===i); });
|
||||||
|
if (i===1) loadRoles();
|
||||||
|
if (i===3) loadSoa();
|
||||||
|
if (i===4) loadDocs();
|
||||||
|
if (i===5) loadReadiness();
|
||||||
|
window.scrollTo({top:0,behavior:'smooth'});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadModel(){
|
||||||
|
try {
|
||||||
|
const res = await api.ismsGetModel();
|
||||||
|
ISMS.model = res.model;
|
||||||
|
if (ISMS.model) fillStep1(ISMS.model);
|
||||||
|
markDone();
|
||||||
|
} catch(e){ /* tabella non ancora migrata o nessun modello: si parte da zero */ }
|
||||||
|
goStep(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
function fillStep1(m){
|
||||||
|
el('f-scope').value = m.scope_statement||'';
|
||||||
|
el('f-ctx-int').value = m.context_internal||'';
|
||||||
|
el('f-ctx-ext').value = m.context_external||'';
|
||||||
|
el('f-parties').value = (m.interested_parties||[]).join('\n');
|
||||||
|
el('f-boundaries').value = m.boundaries||'';
|
||||||
|
el('f-exclusions').value = m.exclusions||'';
|
||||||
|
el('f-uses-cloud').checked = !!m.uses_public_cloud;
|
||||||
|
el('f-cloud-provider').checked = !!m.is_cloud_provider;
|
||||||
|
el('f-pii-cloud').checked = !!m.processes_pii_in_cloud;
|
||||||
|
if (m.risk_methodology!==undefined) el('f-method').value = m.risk_methodology||'';
|
||||||
|
el('f-objectives').value = (m.isms_objectives||[]).join('\n');
|
||||||
|
}
|
||||||
|
|
||||||
|
function linesToArr(v){ return (v||'').split('\n').map(function(s){return s.trim();}).filter(Boolean); }
|
||||||
|
|
||||||
|
async function saveStep1(){
|
||||||
|
const data = {
|
||||||
|
scope_statement: el('f-scope').value,
|
||||||
|
context_internal: el('f-ctx-int').value,
|
||||||
|
context_external: el('f-ctx-ext').value,
|
||||||
|
interested_parties: linesToArr(el('f-parties').value),
|
||||||
|
boundaries: el('f-boundaries').value,
|
||||||
|
exclusions: el('f-exclusions').value,
|
||||||
|
uses_public_cloud: el('f-uses-cloud').checked,
|
||||||
|
is_cloud_provider: el('f-cloud-provider').checked,
|
||||||
|
processes_pii_in_cloud: el('f-pii-cloud').checked
|
||||||
|
};
|
||||||
|
try {
|
||||||
|
await api.ismsSaveModel(data);
|
||||||
|
hint('isms-savehint', lang()==='en'?'Saved':'Salvato');
|
||||||
|
ISMS.soaLoaded = false; // i flag cloud cambiano il perimetro SoA
|
||||||
|
await loadModel();
|
||||||
|
goStep(1);
|
||||||
|
} catch(e){ alert((e&&e.message)||'Errore'); }
|
||||||
|
}
|
||||||
|
|
||||||
|
async function saveStep3(){
|
||||||
|
try {
|
||||||
|
await api.ismsSaveModel({ risk_methodology: el('f-method').value, isms_objectives: linesToArr(el('f-objectives').value) });
|
||||||
|
hint('isms-savehint', lang()==='en'?'Saved':'Salvato');
|
||||||
|
await loadModel();
|
||||||
|
goStep(3);
|
||||||
|
} catch(e){ alert((e&&e.message)||'Errore'); }
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Ruoli (cl.5) ──
|
||||||
|
async function loadRoles(){
|
||||||
|
try {
|
||||||
|
const res = await api.ismsRoles();
|
||||||
|
let html='';
|
||||||
|
(res.roles||[]).forEach(function(r){
|
||||||
|
html += '<div class="raci-row"><span class="soa-code">'+(r.raci||'-')+'</span>'+
|
||||||
|
'<strong style="flex:1 1 140px;">'+esc(r.role_name)+'</strong>'+
|
||||||
|
'<span style="flex:2 1 200px; color:var(--gray-500,#6b7280); font-size:.85rem;">'+esc(r.responsibility||'')+'</span>'+
|
||||||
|
'<button class="btn btn-sm btn-outline" onclick="delRole('+r.id+')">×</button></div>';
|
||||||
|
});
|
||||||
|
el('roles-list').innerHTML = html || '<p class="text-muted">'+(lang()==='en'?'No roles yet.':'Nessun ruolo definito.')+'</p>';
|
||||||
|
} catch(e){ el('roles-list').innerHTML='<p class="text-muted">'+esc((e&&e.message)||'')+'</p>'; }
|
||||||
|
}
|
||||||
|
async function addRole(){
|
||||||
|
const name = el('r-name').value.trim();
|
||||||
|
if(!name){ return; }
|
||||||
|
try {
|
||||||
|
await api.ismsSaveRole({ role_name:name, responsibility:el('r-resp').value, raci:el('r-raci').value });
|
||||||
|
el('r-name').value=''; el('r-resp').value=''; el('r-raci').value='';
|
||||||
|
loadRoles(); markDone();
|
||||||
|
} catch(e){ alert((e&&e.message)||'Errore'); }
|
||||||
|
}
|
||||||
|
async function delRole(id){ try { await api.ismsDeleteRole(id); loadRoles(); } catch(e){} }
|
||||||
|
|
||||||
|
// ── SoA (cl.6.1.3) ──
|
||||||
|
async function loadSoa(){
|
||||||
|
if (ISMS.soaLoaded) return;
|
||||||
|
el('soa-groups').innerHTML = '<p class="text-muted">'+(lang()==='en'?'Loading…':'Caricamento…')+'</p>';
|
||||||
|
try {
|
||||||
|
const res = await api.ismsGetSoa();
|
||||||
|
renderSoa(res);
|
||||||
|
ISMS.soaLoaded = true;
|
||||||
|
markDone();
|
||||||
|
} catch(e){
|
||||||
|
el('soa-groups').innerHTML = '<p class="text-muted">'+esc((e&&e.message)||'')+'</p>';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
async function deriveSoa(){
|
||||||
|
try { await api.ismsDeriveSoa(); ISMS.soaLoaded=false; await loadSoa(); }
|
||||||
|
catch(e){ alert((e&&e.message)||'Errore'); }
|
||||||
|
}
|
||||||
|
function renderSoa(res){
|
||||||
|
showSoaStats(res.stats);
|
||||||
|
let html='';
|
||||||
|
(res.groups||[]).forEach(function(g, gi){
|
||||||
|
// raggruppa per tema
|
||||||
|
const byTheme = {};
|
||||||
|
(g.controls||[]).forEach(function(c){ (byTheme[c.theme]=byTheme[c.theme]||[]).push(c); });
|
||||||
|
let inner='';
|
||||||
|
Object.keys(byTheme).forEach(function(th){
|
||||||
|
inner += '<div class="soa-theme">'+esc(THEME_LABELS[th]?THEME_LABELS[th][lang()]:th)+'</div>';
|
||||||
|
byTheme[th].forEach(function(c){ inner += renderCtrl(c); });
|
||||||
|
});
|
||||||
|
html += '<div class="soa-std'+(gi===0?' open':'')+'"><div class="soa-std-head" onclick="this.parentNode.classList.toggle(\'open\')">'+
|
||||||
|
'<strong>'+esc(g.label)+'</strong><span class="text-muted">'+(g.controls||[]).length+' '+(lang()==='en'?'controls':'controlli')+'</span></div>'+
|
||||||
|
'<div class="soa-std-body">'+inner+'</div></div>';
|
||||||
|
});
|
||||||
|
el('soa-groups').innerHTML = html || '<p class="text-muted">'+(lang()==='en'?'No controls.':'Nessun controllo.')+'</p>';
|
||||||
|
}
|
||||||
|
function renderCtrl(c){
|
||||||
|
const title = lang()==='en' ? (c.title_en||c.title_it) : (c.title_it||c.title_en);
|
||||||
|
const derived = (+c.derived_from_nis2) ? '<span class="soa-derived" title="'+esc(c.source_ref||'')+'">'+(lang()==='en'?'from NIS2':'da NIS2')+'</span>' : '';
|
||||||
|
const applicable = (+c.applicable)===1;
|
||||||
|
const statuses = [
|
||||||
|
['not_started', lang()==='en'?'Not started':'Da iniziare'],
|
||||||
|
['in_progress', lang()==='en'?'In progress':'In corso'],
|
||||||
|
['implemented', lang()==='en'?'Implemented':'Attuato'],
|
||||||
|
['verified', lang()==='en'?'Verified':'Verificato']
|
||||||
|
];
|
||||||
|
let opts='';
|
||||||
|
statuses.forEach(function(s){
|
||||||
|
const sel = c.implementation_status===s[0] ? ('sel-'+s[0]) : '';
|
||||||
|
opts += '<button type="button" class="soa-opt '+sel+'" onclick="setSoaStatus(this,\''+esc(c.control_code)+'\',\''+s[0]+'\')">'+s[1]+'</button>';
|
||||||
|
});
|
||||||
|
return '<div class="soa-ctrl" data-code="'+esc(c.control_code)+'">'+
|
||||||
|
'<div class="soa-ctrl-head"><div><span class="soa-code">'+esc(c.control_code)+'</span>'+esc(title)+' '+derived+'</div>'+
|
||||||
|
'<label style="font-size:.82rem; white-space:nowrap;"><input type="checkbox" '+(applicable?'checked':'')+' onchange="setSoaApplicable(\''+esc(c.control_code)+'\',this.checked)"> '+(lang()==='en'?'Applicable':'Applicabile')+'</label></div>'+
|
||||||
|
'<div class="soa-opts" style="'+(applicable?'':'opacity:.4;pointer-events:none;')+'">'+opts+'</div>'+
|
||||||
|
'<textarea class="soa-justif" placeholder="'+(lang()==='en'?'Justification (inclusion/exclusion)':'Motivazione (inclusione/esclusione)')+'" onchange="setSoaJustif(\''+esc(c.control_code)+'\',this.value,'+applicable+')">'+esc(applicable?(c.justification_inclusion||''):(c.justification_exclusion||''))+'</textarea>'+
|
||||||
|
'</div>';
|
||||||
|
}
|
||||||
|
function setSoaStatus(btn, code, status){
|
||||||
|
const wrap = btn.parentNode;
|
||||||
|
Array.prototype.forEach.call(wrap.querySelectorAll('.soa-opt'), function(b){ b.className='soa-opt'; });
|
||||||
|
btn.className='soa-opt sel-'+status;
|
||||||
|
const pct = {not_started:0,in_progress:50,implemented:100,verified:100}[status];
|
||||||
|
queueSoaSave(code, { control_code:code, implementation_status:status, implementation_pct:pct });
|
||||||
|
}
|
||||||
|
function setSoaApplicable(code, applicable){
|
||||||
|
queueSoaSave(code, { control_code:code, applicable:applicable });
|
||||||
|
ISMS.soaLoaded=false; setTimeout(loadSoa, 400);
|
||||||
|
}
|
||||||
|
function setSoaJustif(code, val, applicable){
|
||||||
|
queueSoaSave(code, applicable ? { control_code:code, justification_inclusion:val } : { control_code:code, justification_exclusion:val });
|
||||||
|
}
|
||||||
|
function queueSoaSave(code, payload){
|
||||||
|
hint('isms-savehint', lang()==='en'?'Saving…':'Salvataggio…');
|
||||||
|
if (soaSaveTimer[code]) clearTimeout(soaSaveTimer[code]);
|
||||||
|
soaSaveTimer[code] = setTimeout(async function(){
|
||||||
|
try { const r = await api.ismsUpdateSoa(payload); showSoaStats(r.stats); hint('isms-savehint', lang()==='en'?'Saved':'Salvato'); }
|
||||||
|
catch(e){ hint('isms-savehint', lang()==='en'?'Save pending':'Salvataggio in sospeso'); }
|
||||||
|
}, 600);
|
||||||
|
}
|
||||||
|
function showSoaStats(s){
|
||||||
|
if(!s){ return; }
|
||||||
|
const e = el('soa-stats');
|
||||||
|
if(e) e.textContent = (lang()==='en'?'Applicable: ':'Applicabili: ')+s.applicable+' · '+(lang()==='en'?'avg impl.: ':'impl. media: ')+s.avg_implementation_pct+'%';
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Documenti (cl.7-8) ──
|
||||||
|
async function loadDocs(){
|
||||||
|
try {
|
||||||
|
const res = await api.ismsDocuments();
|
||||||
|
let html='';
|
||||||
|
(res.documents||[]).forEach(function(d){
|
||||||
|
const ai = (+d.ai_generated)?'<span class="soa-derived">AI</span>':'';
|
||||||
|
html += '<div class="ck-item"><div><strong>'+esc(d.title)+'</strong> '+ai+' <span class="text-muted" style="font-size:.8rem;">('+esc(d.doc_type)+')</span></div>'+
|
||||||
|
'<span class="badge badge-'+(d.status==='approved'?'success':'warning')+'">'+esc(d.status)+'</span></div>';
|
||||||
|
});
|
||||||
|
el('docs-list').innerHTML = html || '<p class="text-muted">'+(lang()==='en'?'No documents yet.':'Nessun documento.')+'</p>';
|
||||||
|
} catch(e){ el('docs-list').innerHTML='<p class="text-muted">'+esc((e&&e.message)||'')+'</p>'; }
|
||||||
|
}
|
||||||
|
async function aiGenDoc(){
|
||||||
|
const btn = el('btn-aigen'); btn.disabled=true;
|
||||||
|
hint('aigen-hint', lang()==='en'?'Generating draft…':'Generazione bozza in corso…');
|
||||||
|
try {
|
||||||
|
await api.ismsAiGenerateDocument({ doc_type: el('d-type').value });
|
||||||
|
hint('aigen-hint', lang()==='en'?'Draft created (review required).':'Bozza creata (revisione obbligatoria).');
|
||||||
|
loadDocs(); markDone();
|
||||||
|
} catch(e){ hint('aigen-hint',''); alert((e&&e.message)||(lang()==='en'?'AI unavailable':'AI non disponibile')); }
|
||||||
|
finally { btn.disabled=false; }
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Readiness (cl.9-10) ──
|
||||||
|
async function loadReadiness(){
|
||||||
|
try {
|
||||||
|
const res = await api.ismsReadiness();
|
||||||
|
el('readiness-pct').textContent = (res.overall_pct||0)+'%';
|
||||||
|
let html='';
|
||||||
|
(res.checklist||[]).forEach(function(c){
|
||||||
|
html += '<div class="ck-item"><span><span class="soa-code">'+esc(c.clause)+'</span>'+esc(c.label)+'</span>'+
|
||||||
|
'<span class="'+(c.done?'ck-yes':'ck-no')+'">'+(c.done?'✓':'—')+'</span></div>';
|
||||||
|
});
|
||||||
|
el('readiness-checklist').innerHTML = html;
|
||||||
|
} catch(e){ el('readiness-checklist').innerHTML='<p class="text-muted">'+esc((e&&e.message)||'')+'</p>'; }
|
||||||
|
}
|
||||||
|
|
||||||
|
function markDone(){
|
||||||
|
// marca gli step "completati" in base allo stato del modello (best-effort).
|
||||||
|
const m = ISMS.model;
|
||||||
|
const done = [ m&&!!m.scope_statement, false, m&&!!m.risk_methodology, ISMS.soaLoaded, false, false ];
|
||||||
|
done.forEach(function(d,i){ const s=el('step-'+i); if(s) s.classList.toggle('done', !!d); });
|
||||||
|
}
|
||||||
|
|
||||||
|
async function ismsExportView(){
|
||||||
|
try {
|
||||||
|
const data = await api.ismsExport();
|
||||||
|
const w = window.open('', '_blank');
|
||||||
|
if(!w){ return; }
|
||||||
|
const m = data.model||{};
|
||||||
|
let soa='';
|
||||||
|
(data.soa||[]).forEach(function(c){
|
||||||
|
soa += '<tr><td>'+esc(c.control_code)+'</td><td>'+esc(c.title_it||'')+'</td><td>'+((+c.applicable)?'Sì':'No')+'</td><td>'+esc(c.implementation_status||'')+'</td><td>'+(c.implementation_pct||0)+'%</td></tr>';
|
||||||
|
});
|
||||||
|
w.document.write('<html><head><meta charset="utf-8"><title>SGSI - '+esc((data.organization&&data.organization.name)||'')+'</title>'+
|
||||||
|
'<style>body{font-family:Arial,sans-serif;padding:30px;color:#111} h1,h2{color:#1e40af} table{width:100%;border-collapse:collapse;font-size:12px} td,th{border:1px solid #ccc;padding:5px;text-align:left} .disc{background:#fffbeb;border:1px solid #fde68a;padding:10px;font-size:12px;margin:16px 0}</style></head><body>'+
|
||||||
|
'<h1>Modello Organizzativo SGSI (ISO/IEC 27001:2022)</h1>'+
|
||||||
|
'<p><strong>'+esc((data.organization&&data.organization.name)||'')+'</strong> — generato il '+esc(data.generated_at||'')+'</p>'+
|
||||||
|
'<div class="disc">'+esc(data.disclaimer||'')+'</div>'+
|
||||||
|
'<h2>Ambito</h2><p>'+esc(m.scope_statement||'—')+'</p>'+
|
||||||
|
'<h2>Metodologia di risk assessment</h2><p>'+esc(m.risk_methodology||'—')+'</p>'+
|
||||||
|
'<h2>Statement of Applicability</h2><table><tr><th>Controllo</th><th>Titolo</th><th>Applicabile</th><th>Stato</th><th>%</th></tr>'+soa+'</table>'+
|
||||||
|
'</body></html>');
|
||||||
|
w.document.close();
|
||||||
|
} catch(e){ alert((e&&e.message)||(lang()==='en'?'Start the SGSI first.':'Avvia prima il SGSI.')); }
|
||||||
|
}
|
||||||
+1
-1
@@ -1 +1 @@
|
|||||||
{"version":"1.13.0","build":"2026-06-01-v1.13.0","date":"2026-06-01","changelog":"NUOVO modulo Gap Analysis ACN: assessment di conformita di secondo livello sulle misure e requisiti puntuali della Determinazione ACN 164179/2025 (non solo le 10 lettere generiche Art.21). Distingue soggetti importanti (37 misure/87 requisiti) ed essenziali (43/116), con codifica Framework Nazionale GV/ID/PR/DE/RS/RC, scoring per funzione, piano d'azione gap e analisi AI con grounding sui 203 requisiti ACN. Dataset estratto dai testi ufficiali ACN. Guida, help, traduzioni IT/EN aggiornati."}
|
{"version":"1.14.0","build":"2026-06-11-v1.14.0","date":"2026-06-11","changelog":"NUOVO modulo Modello Organizzativo SGSI (ISO/IEC 27001:2022): procedura guidata in 6 step (Contesto/Ambito cl.4, Leadership+RACI cl.5, Risk cl.6, Statement of Applicability, Documented Information cl.7-8, Monitoraggio cl.9-10). SoA sui 93 controlli Annex A:2022 PRE-POPOLATO dalle risposte Gap Analysis NIS2 (ogni domanda mappa un controllo ISO). Estensioni cloud condizionali ISO/IEC 27017:2015 (7 controlli CLD.*) e ISO/IEC 27018:2019 (controlli PII per processor in cloud, sinergia GDPR). Generazione bozze documenti via AI con grounding fonti certe. Backend IsmsModelController + migration 037 (tabelle SGSI) e 038 (dataset 111 controlli). Frontend isms.html/isms.js, voce sidebar, help online + traduzioni IT/EN, ingest KB ISO. Strumento di supporto/pre-audit (non certificazione). Migration DA APPLICARE su host."}
|
||||||
|
|||||||
@@ -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 ===");
|
||||||
@@ -0,0 +1,70 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* NIS2 Agile - Runner migrazioni SGSI (ISO 27001) — 037 + 038
|
||||||
|
* ----------------------------------------------------------------------------
|
||||||
|
* Applica le migrazioni del Modello Organizzativo SGSI usando la STESSA
|
||||||
|
* connessione PDO dell'app (config/database.php), cosi da colpire il DB
|
||||||
|
* realmente servito (host MySQL via socket), NON il DB secondario del container.
|
||||||
|
*
|
||||||
|
* USO (sull'host, con la connessione dell'app):
|
||||||
|
* docker exec nis2-app php /var/www/nis2-agile/scripts/migrate-isms.php
|
||||||
|
*
|
||||||
|
* Idempotente: le migrazioni usano CREATE TABLE IF NOT EXISTS + INSERT IGNORE.
|
||||||
|
* Sola lettura su tabelle esistenti: NON modifica nessuna tabella preesistente.
|
||||||
|
*/
|
||||||
|
|
||||||
|
require_once __DIR__ . '/../application/config/config.php';
|
||||||
|
require_once __DIR__ . '/../application/config/database.php';
|
||||||
|
|
||||||
|
$files = [
|
||||||
|
__DIR__ . '/../docs/sql/037_isms_model.sql',
|
||||||
|
__DIR__ . '/../docs/sql/038_iso27001_annex_controls.sql',
|
||||||
|
];
|
||||||
|
|
||||||
|
$pdo = Database::getInstance();
|
||||||
|
$totalStmts = 0;
|
||||||
|
|
||||||
|
foreach ($files as $file) {
|
||||||
|
if (!is_readable($file)) {
|
||||||
|
fwrite(STDERR, "[migrate-isms] File non leggibile: $file\n");
|
||||||
|
exit(1);
|
||||||
|
}
|
||||||
|
$sql = (string) file_get_contents($file);
|
||||||
|
|
||||||
|
// Rimuove le righe di commento "--" (le migrazioni non usano stringhe con '--').
|
||||||
|
$lines = preg_split('/\r?\n/', $sql);
|
||||||
|
$clean = [];
|
||||||
|
foreach ($lines as $l) {
|
||||||
|
if (preg_match('/^\s*--/', $l)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
$clean[] = $l;
|
||||||
|
}
|
||||||
|
$sql = implode("\n", $clean);
|
||||||
|
|
||||||
|
// Split semplice su ';' (nessun DELIMITER / stored procedure in questi file).
|
||||||
|
$statements = array_filter(array_map('trim', explode(';', $sql)), fn($s) => $s !== '');
|
||||||
|
|
||||||
|
echo "== " . basename($file) . " (" . count($statements) . " statement) ==\n";
|
||||||
|
foreach ($statements as $stmt) {
|
||||||
|
try {
|
||||||
|
$pdo->exec($stmt);
|
||||||
|
$totalStmts++;
|
||||||
|
} catch (PDOException $e) {
|
||||||
|
$head = substr(preg_replace('/\s+/', ' ', $stmt), 0, 80);
|
||||||
|
fwrite(STDERR, "[ERRORE] su \"$head...\": " . $e->getMessage() . "\n");
|
||||||
|
exit(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verifica rapida del seed.
|
||||||
|
$count = (int) ($pdo->query('SELECT COUNT(*) AS n FROM iso27001_annex_controls')->fetch()['n'] ?? 0);
|
||||||
|
$byStd = $pdo->query('SELECT standard, COUNT(*) AS n FROM iso27001_annex_controls GROUP BY standard')->fetchAll();
|
||||||
|
|
||||||
|
echo "\n[OK] Eseguiti $totalStmts statement.\n";
|
||||||
|
echo "[OK] iso27001_annex_controls: $count controlli totali.\n";
|
||||||
|
foreach ($byStd as $r) {
|
||||||
|
echo " - {$r['standard']}: {$r['n']}\n";
|
||||||
|
}
|
||||||
|
echo "[OK] Tabelle SGSI pronte (isms_models, isms_roles, isms_soa, isms_documents).\n";
|
||||||
Reference in New Issue
Block a user