From c782aa54fa78303e95fc76fcd0b2280f6031185b Mon Sep 17 00:00:00 2001 From: DevEnv nis2-agile Date: Tue, 16 Jun 2026 20:54:57 +0200 Subject: [PATCH] =?UTF-8?q?[FEAT]=20Epic=20C=20/=20C5.1=20=E2=80=94=20Modu?= =?UTF-8?q?lo=20Stakeholder:=20registro=20+=20matrice=20di=20Mendelow=20(m?= =?UTF-8?q?ig.051)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Registro dedicato degli stakeholder (tabella `stakeholders`, NON una colonna su suppliers: gli interni non sono fornitori; gli esterni si COLLEGANO opzionalmente a un supplier esistente senza duplicare il dato). - mig.051: cfg_stakeholder_types (30 di sistema Stak.01-30 + org-added da Stak.31), cfg_stakeholder_quadrants (4, range corretti: Q4 potere 0-2), stakeholders, stakeholder_procedures (m2m -> policies). Seeder idempotente seed_stakeholders.php. - StakeholderController: list/create/update/delete + types/addType + quadrants + pickers. Quadrante CALCOLATO a read-time dai range config. Anti-IDOR su org_role/supplier/ policy/tipo; coerenza kind (interni->organigramma, esterni->fornitore); punteggi 0-5. - api.js: metodi stk*. stakeholders.html riscritta: registro + matrice di Mendelow in SVG dependency-free + modali (stakeholder, nuovo tipo). Voce sidebar V2 (common-bi.js). - Help aggiornato; cache-buster ?v=20260626 su tutte le pagine; version 1.19.0; sw cache v1.19.0. Ancoraggio GV.SC-02 (obbligo, art. 24 D.Lgs. 138/2024). La matrice di Mendelow e' etichettata come BUONA PRASSI, non obbligo NIS2 (regola fonti-certe). Additivo. Co-Authored-By: Claude Opus 4.8 (1M context) --- application/cli/seed_stakeholders.php | 151 ++++++ .../controllers/StakeholderController.php | 479 ++++++++++++++++++ docs/sql/051_stakeholder_registry.sql | 103 ++++ public/_app-bi-demo.html | 6 +- public/architecture.html | 8 +- public/assessment.html | 10 +- public/assets.html | 10 +- public/companies.html | 10 +- public/competenze.html | 10 +- public/cross-analysis.html | 8 +- public/dashboard.html | 10 +- public/forgot-password.html | 2 +- public/guida.html | 10 +- public/incidents.html | 10 +- public/index.php | 13 + public/integrazioniext.html | 4 +- public/isms.html | 10 +- public/js/api.js | 14 + public/js/common-bi.js | 1 + public/js/help.js | 35 +- public/kb.html | 10 +- public/licenseExt.html | 2 +- public/login.html | 4 +- public/misure-requisiti.html | 12 +- public/mktg-api-doc.html | 2 +- public/normative.html | 10 +- public/onboarding.html | 4 +- public/organigramma.html | 10 +- public/policies.html | 10 +- public/raci.html | 10 +- public/register.html | 4 +- public/reports.html | 10 +- public/review-schedule.html | 10 +- public/risks.html | 10 +- public/settings.html | 10 +- public/setup-org.html | 4 +- public/stakeholders.html | 468 ++++++++++++----- public/supply-chain.html | 10 +- public/sw.js | 2 +- public/training.html | 10 +- public/version.json | 2 +- public/whistleblowing.html | 10 +- public/workflow.html | 6 +- 43 files changed, 1256 insertions(+), 278 deletions(-) create mode 100644 application/cli/seed_stakeholders.php create mode 100644 application/controllers/StakeholderController.php create mode 100644 docs/sql/051_stakeholder_registry.sql diff --git a/application/cli/seed_stakeholders.php b/application/cli/seed_stakeholders.php new file mode 100644 index 0000000..01507e1 --- /dev/null +++ b/application/cli/seed_stakeholders.php @@ -0,0 +1,151 @@ +exec("SET NAMES utf8mb4 COLLATE utf8mb4_unicode_ci"); + +// --- DDL (idempotente, FK dentro il CREATE; allineato a docs/sql/051_*.sql) --- +$ddl = [ +"CREATE TABLE IF NOT EXISTS cfg_stakeholder_types ( + code VARCHAR(16) NOT NULL, organization_id INT NULL, + tipo ENUM('Interno','Esterno') NOT NULL, descr TEXT NOT NULL, ord INT NOT NULL DEFAULT 0, + created_by INT NULL, + created_at DATETIME DEFAULT CURRENT_TIMESTAMP, + updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + PRIMARY KEY (code), KEY idx_cfg_stk_type_org (organization_id), + CONSTRAINT fk_cfg_stk_type_org FOREIGN KEY (organization_id) REFERENCES organizations (id) ON DELETE CASCADE, + CONSTRAINT fk_cfg_stk_type_user FOREIGN KEY (created_by) REFERENCES users (id) ON DELETE SET NULL +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci", + +"CREATE TABLE IF NOT EXISTS cfg_stakeholder_quadrants ( + code VARCHAR(4) NOT NULL, organization_id INT NULL, + label VARCHAR(64) NOT NULL, strategy TEXT NULL, + power_min TINYINT NOT NULL, power_max TINYINT NOT NULL, + interest_min TINYINT NOT NULL, interest_max TINYINT NOT NULL, ord INT NOT NULL DEFAULT 0, + PRIMARY KEY (code), KEY idx_cfg_stk_quad_org (organization_id) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci", + +"CREATE TABLE IF NOT EXISTS stakeholders ( + id INT NOT NULL AUTO_INCREMENT, organization_id INT NOT NULL, + stak_code VARCHAR(16) NOT NULL, name VARCHAR(255) NOT NULL, + org_role_id INT NULL, supplier_id INT NULL, + power TINYINT NULL, interest TINYINT NULL, + contact_name VARCHAR(255) NULL, contact_email VARCHAR(255) NULL, notes TEXT NULL, + created_by INT NULL, + created_at DATETIME DEFAULT CURRENT_TIMESTAMP, + updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + PRIMARY KEY (id), + KEY idx_stk_org (organization_id), KEY idx_stk_org_code (organization_id, stak_code), + KEY idx_stk_role (org_role_id), KEY idx_stk_supplier (supplier_id), + CONSTRAINT fk_stk_org FOREIGN KEY (organization_id) REFERENCES organizations (id) ON DELETE CASCADE, + CONSTRAINT fk_stk_code FOREIGN KEY (stak_code) REFERENCES cfg_stakeholder_types (code), + CONSTRAINT fk_stk_role FOREIGN KEY (org_role_id) REFERENCES org_roles (id) ON DELETE SET NULL, + CONSTRAINT fk_stk_supplier FOREIGN KEY (supplier_id) REFERENCES suppliers (id) ON DELETE SET NULL, + CONSTRAINT fk_stk_user FOREIGN KEY (created_by) REFERENCES users (id) ON DELETE SET NULL +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci", + +"CREATE TABLE IF NOT EXISTS stakeholder_procedures ( + stakeholder_id INT NOT NULL, policy_id INT NOT NULL, + PRIMARY KEY (stakeholder_id, policy_id), KEY idx_stk_proc_policy (policy_id), + CONSTRAINT fk_stk_proc_stk FOREIGN KEY (stakeholder_id) REFERENCES stakeholders (id) ON DELETE CASCADE, + CONSTRAINT fk_stk_proc_policy FOREIGN KEY (policy_id) REFERENCES policies (id) ON DELETE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci", +]; +foreach ($ddl as $stmt) { + try { $pdo->exec($stmt); } + catch (PDOException $e) { if (!in_array($e->errorInfo[1] ?? 0, [1050, 1061], true)) { throw $e; } } +} + +// --- Dati di sistema --- +$seedPath = __DIR__ . '/../data/nis2_framework_seed.json'; +$seed = json_decode(@file_get_contents($seedPath), true); +if (!$seed || empty($seed['stakeholder_types'])) { + fwrite(STDERR, "SEED non leggibile o privo di stakeholder_types: $seedPath\n"); + exit(1); +} + +// 4 quadranti di sistema — matrice di Mendelow (x=interesse, y=potere). +// REFUSO corretto: Q4 (basso-sinistra, Monitorare) ha potere 0-2 (non 3-5). +$quadrants = [ + // code, label, strategy, power_min, power_max, interest_min, interest_max, ord + ['Q1', 'Tenere soddisfatti (Keep Satisfied)', + 'Stakeholder istituzionali: forte potere di blocco, scarso interesse quotidiano. Tenerli soddisfatti ed evitare scontenti improvvisi.', + 3, 5, 0, 2, 1], + ['Q2', 'Gestire attivamente (Manage Closely)', + 'Stakeholder chiave: decisori principali, supporto vitale. Gestione attiva e coinvolgimento costante.', + 3, 5, 3, 5, 2], + ['Q3', 'Tenere informati (Keep Informed)', + 'Stakeholder operativi: molto coinvolti ma scarso peso decisionale. Tenerli informati.', + 0, 2, 3, 5, 3], + ['Q4', 'Monitorare (Monitor)', + 'Stakeholder marginali: basso potere e basso interesse. Monitoraggio costante col minimo sforzo, comunicazioni periodiche non dispendiose.', + 0, 2, 0, 2, 4], +]; + +$pdo->beginTransaction(); +try { + $stQ = $pdo->prepare( + "INSERT INTO cfg_stakeholder_quadrants + (code,label,strategy,power_min,power_max,interest_min,interest_max,ord,organization_id) + VALUES (?,?,?,?,?,?,?,?,NULL) + ON DUPLICATE KEY UPDATE label=VALUES(label),strategy=VALUES(strategy), + power_min=VALUES(power_min),power_max=VALUES(power_max), + interest_min=VALUES(interest_min),interest_max=VALUES(interest_max),ord=VALUES(ord)" + ); + foreach ($quadrants as $q) { $stQ->execute($q); } + + // 30 tipi di sistema: code PK, organization_id NULL, tipo Interno/Esterno, descr verbatim. + $stT = $pdo->prepare( + "INSERT INTO cfg_stakeholder_types (code,organization_id,tipo,descr,ord) + VALUES (?,NULL,?,?,?) + ON DUPLICATE KEY UPDATE tipo=VALUES(tipo),descr=VALUES(descr),ord=VALUES(ord)" + ); + $ord = 0; + foreach ($seed['stakeholder_types'] as $t) { + $ord++; + $tipo = ($t['tipo'] === 'Interno') ? 'Interno' : 'Esterno'; + $stT->execute([$t['code'], $tipo, $t['descr'], $ord]); + } + $pdo->commit(); +} catch (Throwable $e) { + $pdo->rollBack(); + fwrite(STDERR, "SEED FALLITO: " . $e->getMessage() . "\n"); + exit(1); +} + +$counts = [ + 'quadranti' => (int) $pdo->query("SELECT COUNT(*) FROM cfg_stakeholder_quadrants WHERE organization_id IS NULL")->fetchColumn(), + 'tipi_sistema' => (int) $pdo->query("SELECT COUNT(*) FROM cfg_stakeholder_types WHERE organization_id IS NULL")->fetchColumn(), + 'tipi_interni' => (int) $pdo->query("SELECT COUNT(*) FROM cfg_stakeholder_types WHERE organization_id IS NULL AND tipo='Interno'")->fetchColumn(), + 'tipi_esterni' => (int) $pdo->query("SELECT COUNT(*) FROM cfg_stakeholder_types WHERE organization_id IS NULL AND tipo='Esterno'")->fetchColumn(), +]; +echo "OK seed-stakeholders — " . json_encode($counts, JSON_UNESCAPED_UNICODE) . "\n"; diff --git a/application/controllers/StakeholderController.php b/application/controllers/StakeholderController.php new file mode 100644 index 0000000..5ed09f0 --- /dev/null +++ b/application/controllers/StakeholderController.php @@ -0,0 +1,479 @@ + art. 24 D.Lgs. 138/2024). La matrice di Mendelow + * (potere/interesse) e' BUONA PRASSI, NON un obbligo NIS2. + * + * Multi-tenancy: ogni query filtra organization_id. Scritture: org_admin / + * compliance_manager. Anti-IDOR su tutti i link (org_role_id/supplier_id/policy_id/ + * stak_code) verificati appartenere all'org corrente (o di sistema per i tipi). + * + * NOTE strutturali: DB API Database::query/fetchAll/fetchOne/insert/update/delete + * (NON Database::execute). jsonSuccess/jsonError fanno exit. + */ + +require_once __DIR__ . '/BaseController.php'; + +class StakeholderController extends BaseController +{ + private const MANAGE_ROLES = ['org_admin', 'compliance_manager']; + + // ───────────────────────────────────────────────────────────────────────── + // LETTURE + // ───────────────────────────────────────────────────────────────────────── + + /** + * GET /api/stakeholders/list + * Registro dell'org + quadrante calcolato + procedure collegate + i 4 quadranti + * (per disegnare gli assi anche quando non ci sono stakeholder). + */ + public function list(): void + { + $this->requireOrgAccess(); + $orgId = $this->getCurrentOrgId(); + + $quads = $this->loadQuadrants(); + + $rows = Database::fetchAll( + 'SELECT s.id, s.stak_code, t.tipo, t.descr AS type_descr, s.name, + s.org_role_id, r.role_name AS org_role_name, + s.supplier_id, sup.name AS supplier_name, + s.power, s.interest, s.contact_name, s.contact_email, s.notes, + s.created_at, s.updated_at + FROM stakeholders s + JOIN cfg_stakeholder_types t ON t.code = s.stak_code + LEFT JOIN org_roles r ON r.id = s.org_role_id + LEFT JOIN suppliers sup ON sup.id = s.supplier_id + WHERE s.organization_id = ? + ORDER BY t.tipo ASC, s.stak_code ASC, s.name ASC', + [$orgId] + ); + + // Procedure collegate (m2m) per tutti gli stakeholder in un colpo solo + $procMap = []; + if ($rows) { + $ids = array_map(static fn($r) => (int) $r['id'], $rows); + $place = implode(',', array_fill(0, count($ids), '?')); + foreach (Database::fetchAll( + "SELECT sp.stakeholder_id, sp.policy_id, p.title AS policy_title + FROM stakeholder_procedures sp + JOIN policies p ON p.id = sp.policy_id + WHERE sp.stakeholder_id IN ($place)", + $ids + ) as $lp) { + $sid = (int) $lp['stakeholder_id']; + $procMap[$sid][] = ['id' => (int) $lp['policy_id'], 'title' => $lp['policy_title']]; + } + } + + $stakeholders = []; + foreach ($rows as $r) { + $power = $r['power'] !== null ? (int) $r['power'] : null; + $interest = $r['interest'] !== null ? (int) $r['interest'] : null; + $rated = ($power !== null && $interest !== null); + $quad = $rated ? $this->quadrantFor($quads, $power, $interest) : null; + $sid = (int) $r['id']; + $procs = $procMap[$sid] ?? []; + + $stakeholders[] = [ + 'id' => $sid, + 'stak_code' => $r['stak_code'], + 'tipo' => $r['tipo'], + 'kind' => $r['tipo'] === 'Interno' ? 'internal' : 'external', + 'type_descr' => $r['type_descr'], + 'name' => $r['name'], + 'org_role_id' => $r['org_role_id'] !== null ? (int) $r['org_role_id'] : null, + 'org_role_name' => $r['org_role_name'], + 'supplier_id' => $r['supplier_id'] !== null ? (int) $r['supplier_id'] : null, + 'supplier_name' => $r['supplier_name'], + 'power' => $power, + 'interest' => $interest, + 'rated' => $rated, + 'quadrant_code' => $quad['code'] ?? null, + 'quadrant_label' => $quad['label'] ?? null, + 'contact_name' => $r['contact_name'], + 'contact_email' => $r['contact_email'], + 'notes' => $r['notes'], + 'policies' => $procs, + 'policy_ids' => array_map(static fn($p) => $p['id'], $procs), + 'updated_at' => $r['updated_at'], + ]; + } + + $this->jsonSuccess([ + 'stakeholders' => $stakeholders, + 'quadrants' => $quads, + 'total' => count($stakeholders), + 'mendelow_note'=> 'La matrice di Mendelow (potere/interesse) e\' una buona prassi di gestione degli stakeholder, non un obbligo NIS2. L\'obbligo e\' GV.SC-02 (ruoli e responsabilita verso fornitori, clienti e partner).', + ]); + } + + /** + * GET /api/stakeholders/types + * Tipi visibili all'org: 30 di sistema (organization_id NULL) + quelli aggiunti + * dall'org. La codifica di sistema NON e' modificabile dall'utente. + */ + public function types(): void + { + $this->requireOrgAccess(); + $orgId = $this->getCurrentOrgId(); + $rows = Database::fetchAll( + 'SELECT code, tipo, descr, (organization_id IS NULL) AS is_default, ord + FROM cfg_stakeholder_types + WHERE organization_id IS NULL OR organization_id = ? + ORDER BY ord ASC, code ASC', + [$orgId] + ); + $types = array_map(static fn($t) => [ + 'code' => $t['code'], + 'tipo' => $t['tipo'], + 'descr' => $t['descr'], + 'is_default' => ((int) $t['is_default'] === 1), + ], $rows); + $this->jsonSuccess(['types' => $types]); + } + + /** + * GET /api/stakeholders/quadrants — i 4 quadranti di sistema (per gli assi). + */ + public function quadrants(): void + { + $this->requireOrgAccess(); + $this->jsonSuccess(['quadrants' => $this->loadQuadrants()]); + } + + /** + * GET /api/stakeholders/pickers — opzioni leggere per i selettori del form: + * ruoli dell'organigramma (interni) + procedure (m2m) + fornitori (link esterni). + */ + public function pickers(): void + { + $this->requireOrgAccess(); + $orgId = $this->getCurrentOrgId(); + $roles = Database::fetchAll( + 'SELECT id, role_name FROM org_roles WHERE organization_id = ? ORDER BY sort_order ASC, role_name ASC', + [$orgId] + ); + $policies = Database::fetchAll( + 'SELECT id, title FROM policies WHERE organization_id = ? AND deleted_at IS NULL ORDER BY title ASC', + [$orgId] + ); + $suppliers = Database::fetchAll( + 'SELECT id, name, stakeholder_type FROM suppliers WHERE organization_id = ? AND deleted_at IS NULL ORDER BY name ASC', + [$orgId] + ); + $this->jsonSuccess([ + 'org_roles' => array_map(static fn($r) => ['id' => (int) $r['id'], 'role_name' => $r['role_name']], $roles), + 'policies' => array_map(static fn($p) => ['id' => (int) $p['id'], 'title' => $p['title']], $policies), + 'suppliers' => array_map(static fn($s) => ['id' => (int) $s['id'], 'name' => $s['name'], 'stakeholder_type' => $s['stakeholder_type']], $suppliers), + ]); + } + + // ───────────────────────────────────────────────────────────────────────── + // SCRITTURE + // ───────────────────────────────────────────────────────────────────────── + + /** + * POST /api/stakeholders/create + * Body: {stak_code*, name*, org_role_id?, supplier_id?, power?, interest?, + * contact_name?, contact_email?, notes?, policy_ids?:[]} + */ + public function create(): void + { + $this->requireOrgRole(self::MANAGE_ROLES); + $orgId = $this->getCurrentOrgId(); + $body = $this->getJsonBody(); + + $code = trim((string) ($body['stak_code'] ?? '')); + $name = trim((string) ($body['name'] ?? '')); + if ($code === '') { $this->jsonError('Tipo stakeholder (stak_code) obbligatorio', 422, 'MISSING_TYPE'); } + if ($name === '' || mb_strlen($name) > 255) { $this->jsonError('Nome obbligatorio (max 255 caratteri)', 422, 'INVALID_NAME'); } + + $tipo = $this->resolveTypeTipo($code, $orgId); // 422 se non visibile + + $orgRoleId = $this->validateOrgRoleForKind($body['org_role_id'] ?? null, $tipo, $orgId); + $supplierId = $this->validateSupplierForKind($body['supplier_id'] ?? null, $tipo, $orgId); + $power = $this->validateScore($body['power'] ?? null, 'power'); + $interest = $this->validateScore($body['interest'] ?? null, 'interest'); + $policyIds = $this->validatePolicyIds($body['policy_ids'] ?? null, $orgId); + + // dup soft: stesso tipo + stesso nome nell'org + $dup = Database::fetchOne( + 'SELECT id FROM stakeholders WHERE organization_id = ? AND stak_code = ? AND name = ?', + [$orgId, $code, $name] + ); + if ($dup) { $this->jsonError('Stakeholder gia\' presente con questo tipo e nome', 409, 'DUPLICATE'); } + + $id = Database::insert('stakeholders', [ + 'organization_id' => $orgId, + 'stak_code' => $code, + 'name' => $name, + 'org_role_id' => $orgRoleId, + 'supplier_id' => $supplierId, + 'power' => $power, + 'interest' => $interest, + 'contact_name' => $this->nullableStr($body['contact_name'] ?? null, 255), + 'contact_email' => $this->nullableStr($body['contact_email'] ?? null, 255), + 'notes' => $this->nullableStr($body['notes'] ?? null), + 'created_by' => $this->getCurrentUserId(), + ]); + + $this->syncPolicies((int) $id, $policyIds); + $this->logAudit('stakeholder_created', 'stakeholder', (int) $id, ['stak_code' => $code, 'name' => $name]); + + $this->jsonSuccess(['id' => (int) $id], 'Stakeholder creato', 201); + } + + /** + * PUT /api/stakeholders/{id} — update parziale. + */ + public function update(int $id): void + { + $this->requireOrgRole(self::MANAGE_ROLES); + $orgId = $this->getCurrentOrgId(); + $body = $this->getJsonBody(); + + $existing = Database::fetchOne( + 'SELECT id, stak_code FROM stakeholders WHERE id = ? AND organization_id = ?', + [$id, $orgId] + ); + if (!$existing) { $this->jsonError('Stakeholder non trovato', 404, 'NOT_FOUND'); } + + // Il tipo (e quindi il "kind") puo' cambiare: determina quello effettivo + $code = $existing['stak_code']; + if ($this->hasParam('stak_code')) { + $code = trim((string) ($body['stak_code'] ?? '')); + if ($code === '') { $this->jsonError('Tipo stakeholder non valido', 422, 'INVALID_TYPE'); } + } + $tipo = $this->resolveTypeTipo($code, $orgId); + + $updates = []; + if ($this->hasParam('stak_code')) { $updates['stak_code'] = $code; } + if ($this->hasParam('name')) { + $name = trim((string) ($body['name'] ?? '')); + if ($name === '' || mb_strlen($name) > 255) { $this->jsonError('Nome non valido (max 255 caratteri)', 422, 'INVALID_NAME'); } + $updates['name'] = $name; + } + // Link coerenti col kind: se cambia il tipo verso Interno azzero supplier (e viceversa) + if ($this->hasParam('org_role_id') || $tipo === 'Esterno') { + $updates['org_role_id'] = $this->validateOrgRoleForKind($body['org_role_id'] ?? null, $tipo, $orgId); + } + if ($this->hasParam('supplier_id') || $tipo === 'Interno') { + $updates['supplier_id'] = $this->validateSupplierForKind($body['supplier_id'] ?? null, $tipo, $orgId); + } + if ($this->hasParam('power')) { $updates['power'] = $this->validateScore($body['power'] ?? null, 'power'); } + if ($this->hasParam('interest')) { $updates['interest'] = $this->validateScore($body['interest'] ?? null, 'interest'); } + if ($this->hasParam('contact_name')) { $updates['contact_name'] = $this->nullableStr($body['contact_name'] ?? null, 255); } + if ($this->hasParam('contact_email')) { $updates['contact_email'] = $this->nullableStr($body['contact_email'] ?? null, 255); } + if ($this->hasParam('notes')) { $updates['notes'] = $this->nullableStr($body['notes'] ?? null); } + + if (!empty($updates)) { + Database::update('stakeholders', $updates, 'id = ? AND organization_id = ?', [$id, $orgId]); + } + if ($this->hasParam('policy_ids')) { + $this->syncPolicies($id, $this->validatePolicyIds($body['policy_ids'] ?? null, $orgId)); + } + + $this->logAudit('stakeholder_updated', 'stakeholder', $id, array_keys($updates)); + $this->jsonSuccess(['id' => $id, 'updated' => array_keys($updates)], 'Stakeholder aggiornato'); + } + + /** + * DELETE /api/stakeholders/{id} + */ + public function delete(int $id): void + { + $this->requireOrgRole(['org_admin']); + $deleted = Database::delete('stakeholders', 'id = ? AND organization_id = ?', [$id, $this->getCurrentOrgId()]); + if ($deleted === 0) { $this->jsonError('Stakeholder non trovato', 404, 'NOT_FOUND'); } + $this->logAudit('stakeholder_deleted', 'stakeholder', $id); + $this->jsonSuccess(null, 'Stakeholder eliminato'); + } + + /** + * POST /api/stakeholders/types — aggiunge un TIPO org-scoped, codifica + * automatica proseguendo da Stak.31 (codice globalmente univoco: e' PK). + * Body: {tipo*, descr*} + */ + public function addType(): void + { + $this->requireOrgRole(self::MANAGE_ROLES); + $orgId = $this->getCurrentOrgId(); + $body = $this->getJsonBody(); + + $tipo = ($body['tipo'] ?? '') === 'Interno' ? 'Interno' : (($body['tipo'] ?? '') === 'Esterno' ? 'Esterno' : ''); + $descr = trim((string) ($body['descr'] ?? '')); + if ($tipo === '') { $this->jsonError('Tipo deve essere "Interno" o "Esterno"', 422, 'INVALID_TIPO'); } + if ($descr === '') { $this->jsonError('Descrizione obbligatoria', 422, 'INVALID_DESCR'); } + + // Codice successivo: MAX suffisso numerico tra TUTTI i codici Stak.NN (sistema + org). + $next = $this->nextStakCode(); + + try { + Database::insert('cfg_stakeholder_types', [ + 'code' => $next, + 'organization_id' => $orgId, + 'tipo' => $tipo, + 'descr' => $descr, + 'ord' => 1000, // le voci org si ordinano dopo le 30 di sistema + 'created_by' => $this->getCurrentUserId(), + ]); + } catch (PDOException $e) { + // 1062 = race su PK duplicata: ritenta una volta con il codice ricalcolato + if (($e->errorInfo[1] ?? 0) === 1062) { + $next = $this->nextStakCode(); + Database::insert('cfg_stakeholder_types', [ + 'code' => $next, 'organization_id' => $orgId, 'tipo' => $tipo, + 'descr' => $descr, 'ord' => 1000, 'created_by' => $this->getCurrentUserId(), + ]); + } else { throw $e; } + } + + $this->logAudit('stakeholder_type_added', 'cfg_stakeholder_types', null, ['code' => $next, 'tipo' => $tipo]); + $this->jsonSuccess(['code' => $next, 'tipo' => $tipo, 'descr' => $descr], 'Tipo stakeholder aggiunto', 201); + } + + // ───────────────────────────────────────────────────────────────────────── + // HELPER + // ───────────────────────────────────────────────────────────────────────── + + /** Carica i 4 quadranti di sistema (ordinati). */ + private function loadQuadrants(): array + { + $rows = Database::fetchAll( + 'SELECT code, label, strategy, power_min, power_max, interest_min, interest_max, ord + FROM cfg_stakeholder_quadrants WHERE organization_id IS NULL ORDER BY ord ASC, code ASC' + ); + return array_map(static fn($q) => [ + 'code' => $q['code'], + 'label' => $q['label'], + 'strategy' => $q['strategy'], + 'power_min' => (int) $q['power_min'], + 'power_max' => (int) $q['power_max'], + 'interest_min' => (int) $q['interest_min'], + 'interest_max' => (int) $q['interest_max'], + ], $rows); + } + + /** Trova il quadrante che contiene (power, interest) dai range di config. */ + private function quadrantFor(array $quads, int $power, int $interest): ?array + { + foreach ($quads as $q) { + if ($power >= $q['power_min'] && $power <= $q['power_max'] + && $interest >= $q['interest_min'] && $interest <= $q['interest_max']) { + return $q; + } + } + return null; + } + + /** Verifica che il tipo sia visibile all'org (sistema o proprio) e ne ritorna il "tipo". */ + private function resolveTypeTipo(string $code, int $orgId): string + { + $row = Database::fetchOne( + 'SELECT tipo FROM cfg_stakeholder_types WHERE code = ? AND (organization_id IS NULL OR organization_id = ?)', + [$code, $orgId] + ); + if (!$row) { $this->jsonError('Tipo stakeholder inesistente o non accessibile', 422, 'INVALID_TYPE'); } + return $row['tipo']; + } + + /** org_role consentito solo agli INTERNI; deve appartenere all'org (anti-IDOR). */ + private function validateOrgRoleForKind($id, string $tipo, int $orgId): ?int + { + $id = ($id === null || $id === '') ? null : (int) $id; + if ($id === null) { return null; } + if ($tipo !== 'Interno') { + $this->jsonError('Il collegamento all\'organigramma e\' previsto solo per stakeholder interni', 422, 'ROLE_NOT_ALLOWED'); + } + $row = Database::fetchOne('SELECT id FROM org_roles WHERE id = ? AND organization_id = ?', [$id, $orgId]); + if (!$row) { $this->jsonError('Ruolo dell\'organigramma non valido', 422, 'INVALID_ORG_ROLE'); } + return $id; + } + + /** supplier consentito solo agli ESTERNI; deve appartenere all'org (anti-IDOR). */ + private function validateSupplierForKind($id, string $tipo, int $orgId): ?int + { + $id = ($id === null || $id === '') ? null : (int) $id; + if ($id === null) { return null; } + if ($tipo !== 'Esterno') { + $this->jsonError('Il collegamento a un fornitore e\' previsto solo per stakeholder esterni', 422, 'SUPPLIER_NOT_ALLOWED'); + } + $row = Database::fetchOne('SELECT id FROM suppliers WHERE id = ? AND organization_id = ?', [$id, $orgId]); + if (!$row) { $this->jsonError('Fornitore collegato non valido', 422, 'INVALID_SUPPLIER'); } + return $id; + } + + /** Punteggio 0-5 o null (assente / vuoto = non valutato). */ + private function validateScore($v, string $field): ?int + { + if ($v === null || $v === '') { return null; } + if (!is_numeric($v)) { $this->jsonError("Valore $field non numerico", 422, 'INVALID_SCORE'); } + $n = (int) $v; + if ($n < 0 || $n > 5) { $this->jsonError("Il valore $field deve essere tra 0 e 5", 422, 'SCORE_OUT_OF_RANGE'); } + return $n; + } + + /** Normalizza array di policy_id e verifica che TUTTE appartengano all'org (anti-IDOR). */ + private function validatePolicyIds($raw, int $orgId): array + { + if ($raw === null) { return []; } + if (!is_array($raw)) { $this->jsonError('policy_ids deve essere un array', 422, 'INVALID_POLICIES'); } + $ids = array_values(array_unique(array_filter(array_map('intval', $raw), static fn($i) => $i > 0))); + if (!$ids) { return []; } + $place = implode(',', array_fill(0, count($ids), '?')); + $rows = Database::fetchAll( + "SELECT id FROM policies WHERE id IN ($place) AND organization_id = ? AND deleted_at IS NULL", + array_merge($ids, [$orgId]) + ); + if (count($rows) !== count($ids)) { + $this->jsonError('Una o piu\' procedure collegate non sono valide', 422, 'INVALID_POLICIES'); + } + return $ids; + } + + /** Sostituisce le procedure collegate (m2m) per uno stakeholder. */ + private function syncPolicies(int $stakeholderId, array $policyIds): void + { + Database::delete('stakeholder_procedures', 'stakeholder_id = ?', [$stakeholderId]); + foreach ($policyIds as $pid) { + Database::insert('stakeholder_procedures', ['stakeholder_id' => $stakeholderId, 'policy_id' => $pid]); + } + } + + /** Prossimo codice Stak.NN (globale: il codice e' PK). */ + private function nextStakCode(): string + { + $max = Database::fetchOne( + "SELECT MAX(CAST(SUBSTRING(code, 6) AS UNSIGNED)) AS m + FROM cfg_stakeholder_types + WHERE code REGEXP '^Stak\\\\.[0-9]+$'" + ); + $n = ((int) ($max['m'] ?? 0)) + 1; + return 'Stak.' . str_pad((string) $n, 2, '0', STR_PAD_LEFT); + } + + private function nullableStr($v, ?int $max = null): ?string + { + if ($v === null) { return null; } + $s = trim((string) $v); + if ($s === '') { return null; } + if ($max !== null && mb_strlen($s) > $max) { $s = mb_substr($s, 0, $max); } + return $s; + } +} diff --git a/docs/sql/051_stakeholder_registry.sql b/docs/sql/051_stakeholder_registry.sql new file mode 100644 index 0000000..61fae9b --- /dev/null +++ b/docs/sql/051_stakeholder_registry.sql @@ -0,0 +1,103 @@ +-- ===================================================================== +-- 051 — Registro Stakeholder + matrice di Mendelow (Epic C / C5.1) +-- ===================================================================== +-- Simon C5: registro degli stakeholder con tipo configurabile +-- (Stak.01-30 di sistema + voci aggiunte dall'org da Stak.31), doppia +-- valutazione POTERE/INTERESSE 0-5, collegamento all'organigramma +-- (org_roles, per gli stakeholder INTERNI) e alle procedure (policies), +-- e collocazione automatica su una matrice a 4 quadranti. +-- +-- Ancoraggio normativo: GV.SC-02 (ruoli/responsabilita verso fornitori, +-- clienti e partner) -> obblighi art. 24 D.Lgs. 138/2024. La matrice di +-- Mendelow (potere/interesse) e' BUONA PRASSI, NON un obbligo NIS2. +-- +-- Modello: tabella DEDICATA `stakeholders` (NON una colonna su suppliers): +-- gli stakeholder interni (Dipendenti, Management, Azionisti...) non sono +-- fornitori. Gli esterni possono COLLEGARSI a un supplier esistente +-- (supplier_id, nessuna copia di dati): il modulo Supply Chain resta +-- intatto e coesiste. +-- +-- Config-driven: righe di sistema con organization_id NULL; le voci +-- aggiunte dall'org hanno organization_id valorizzato. +-- +-- Additivo, reversibile. Runner-safe: solo CREATE TABLE IF NOT EXISTS con +-- le FK DENTRO il corpo del CREATE (il runner non ha pre-check per +-- ADD CONSTRAINT; con IF NOT EXISTS il CREATE e' nativamente idempotente). +-- Nessun DELIMITER, nessuna stored procedure, nessun ';' nei commenti. +-- +-- I dati (4 quadranti + 30 tipi di sistema) sono seedati dal CLI +-- idempotente application/cli/seed_stakeholders.php (modello seed_framework.php), +-- che crea anche le tabelle se mancanti. Eseguire dentro il container app: +-- docker exec nis2-app php /var/www/nis2-agile/application/cli/seed_stakeholders.php +-- ===================================================================== + +CREATE TABLE IF NOT EXISTS cfg_stakeholder_types ( + code VARCHAR(16) NOT NULL, + organization_id INT NULL, -- NULL = tipo di sistema; valorizzato = tipo dell'org + tipo ENUM('Interno','Esterno') NOT NULL, + descr TEXT NOT NULL, + ord INT NOT NULL DEFAULT 0, + created_by INT NULL, + created_at DATETIME DEFAULT CURRENT_TIMESTAMP, + updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + PRIMARY KEY (code), + KEY idx_cfg_stk_type_org (organization_id), + CONSTRAINT fk_cfg_stk_type_org FOREIGN KEY (organization_id) REFERENCES organizations (id) ON DELETE CASCADE, + CONSTRAINT fk_cfg_stk_type_user FOREIGN KEY (created_by) REFERENCES users (id) ON DELETE SET NULL +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; + +CREATE TABLE IF NOT EXISTS cfg_stakeholder_quadrants ( + code VARCHAR(4) NOT NULL, -- Q1..Q4 + organization_id INT NULL, -- NULL = quadrante di sistema (per C5.1 solo sistema) + label VARCHAR(64) NOT NULL, + strategy TEXT NULL, + power_min TINYINT NOT NULL, + power_max TINYINT NOT NULL, + interest_min TINYINT NOT NULL, + interest_max TINYINT NOT NULL, + ord INT NOT NULL DEFAULT 0, + PRIMARY KEY (code), + KEY idx_cfg_stk_quad_org (organization_id) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; + +CREATE TABLE IF NOT EXISTS stakeholders ( + id INT NOT NULL AUTO_INCREMENT, + organization_id INT NOT NULL, + stak_code VARCHAR(16) NOT NULL, -- FK -> cfg_stakeholder_types.code + name VARCHAR(255) NOT NULL, + org_role_id INT NULL, -- solo INTERNI: nodo dell'organigramma + supplier_id INT NULL, -- solo ESTERNI: link opzionale al modulo Supply Chain + power TINYINT NULL, -- 0..5, NULL = non ancora valutato + interest TINYINT NULL, -- 0..5, NULL = non ancora valutato + contact_name VARCHAR(255) NULL, + contact_email VARCHAR(255) NULL, + notes TEXT NULL, + created_by INT NULL, + created_at DATETIME DEFAULT CURRENT_TIMESTAMP, + updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + PRIMARY KEY (id), + KEY idx_stk_org (organization_id), + KEY idx_stk_org_code (organization_id, stak_code), + KEY idx_stk_role (org_role_id), + KEY idx_stk_supplier (supplier_id), + CONSTRAINT fk_stk_org FOREIGN KEY (organization_id) REFERENCES organizations (id) ON DELETE CASCADE, + CONSTRAINT fk_stk_code FOREIGN KEY (stak_code) REFERENCES cfg_stakeholder_types (code), + CONSTRAINT fk_stk_role FOREIGN KEY (org_role_id) REFERENCES org_roles (id) ON DELETE SET NULL, + CONSTRAINT fk_stk_supplier FOREIGN KEY (supplier_id) REFERENCES suppliers (id) ON DELETE SET NULL, + CONSTRAINT fk_stk_user FOREIGN KEY (created_by) REFERENCES users (id) ON DELETE SET NULL +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; + +CREATE TABLE IF NOT EXISTS stakeholder_procedures ( + stakeholder_id INT NOT NULL, + policy_id INT NOT NULL, + PRIMARY KEY (stakeholder_id, policy_id), + KEY idx_stk_proc_policy (policy_id), + CONSTRAINT fk_stk_proc_stk FOREIGN KEY (stakeholder_id) REFERENCES stakeholders (id) ON DELETE CASCADE, + CONSTRAINT fk_stk_proc_policy FOREIGN KEY (policy_id) REFERENCES policies (id) ON DELETE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; + +-- ROLLBACK (manuale): +-- DROP TABLE IF EXISTS stakeholder_procedures +-- DROP TABLE IF EXISTS stakeholders +-- DROP TABLE IF EXISTS cfg_stakeholder_quadrants +-- DROP TABLE IF EXISTS cfg_stakeholder_types diff --git a/public/_app-bi-demo.html b/public/_app-bi-demo.html index 204e9f4..39c72be 100644 --- a/public/_app-bi-demo.html +++ b/public/_app-bi-demo.html @@ -70,9 +70,9 @@ - - - + + + - - - + + + + - + + @@ -165,9 +165,9 @@ bootstrap.loadFonts('/vendor/bootstrap-italia/dist/fonts'); } - - - + + + - + + @@ -372,9 +372,9 @@ bootstrap.loadFonts('/vendor/bootstrap-italia/dist/fonts'); } - - - + + + - + + - - - + + + - + + - - - + + + diff --git a/public/cross-analysis.html b/public/cross-analysis.html index 6177e80..1025fd4 100644 --- a/public/cross-analysis.html +++ b/public/cross-analysis.html @@ -382,8 +382,8 @@ - - + + @@ -393,8 +393,8 @@ bootstrap.loadFonts('/vendor/bootstrap-italia/dist/fonts'); } - - + + - + + @@ -154,9 +154,9 @@ bootstrap.loadFonts('/vendor/bootstrap-italia/dist/fonts'); } - - - + + + - + - + + @@ -1152,9 +1152,9 @@ bootstrap.loadFonts('/vendor/bootstrap-italia/dist/fonts'); } - - - + + + - + + @@ -362,9 +362,9 @@ bootstrap.loadFonts('/vendor/bootstrap-italia/dist/fonts'); } - - - + + + - + + - + + @@ -195,9 +195,9 @@ bootstrap.loadFonts('/vendor/bootstrap-italia/dist/fonts'); } - - - + + + diff --git a/public/js/api.js b/public/js/api.js index 6e0dd4c..7d29a2c 100644 --- a/public/js/api.js +++ b/public/js/api.js @@ -295,6 +295,20 @@ class NIS2API { // ═══════════════════════════════════════════════════════════════════ stakeholderMap() { return this._acn(this.get('/supply-chain/stakeholder-map')); } + // ═══════════════════════════════════════════════════════════════════ + // Registro Stakeholder + matrice di Mendelow (Epic C / C5). Tutti _acn. + // Tipo configurabile (Stak.01-30 + org-added), potere/interesse 0-5, + // link organigramma (interni) / procedure (m2m). Quadrante calcolato lato API. + // ═══════════════════════════════════════════════════════════════════ + stkList() { return this._acn(this.get('/stakeholders/list')); } + stkTypes() { return this._acn(this.get('/stakeholders/types')); } + stkAddType(d) { return this._acn(this.post('/stakeholders/types', d || {})); } + stkQuadrants() { return this._acn(this.get('/stakeholders/quadrants')); } + stkPickers() { return this._acn(this.get('/stakeholders/pickers')); } + stkCreate(d) { return this._acn(this.post('/stakeholders/create', d || {})); } + stkUpdate(id, d) { return this._acn(this.put(`/stakeholders/${id}`, d || {})); } + stkDelete(id) { return this._acn(this.del(`/stakeholders/${id}`)); } + // ═══════════════════════════════════════════════════════════════════ // Dashboard // ═══════════════════════════════════════════════════════════════════ diff --git a/public/js/common-bi.js b/public/js/common-bi.js index b48079c..bcae99e 100644 --- a/public/js/common-bi.js +++ b/public/js/common-bi.js @@ -77,6 +77,7 @@ { name: 'Incidenti', href: 'incidents.html', icon: iconBell(), i18nKey: 'nav.incidents' }, { name: 'Policy', href: 'policies.html', icon: iconDocumentText(), i18nKey: 'nav.policies' }, { name: 'Supply Chain', href: 'supply-chain.html', icon: iconLink(), i18nKey: 'nav.supply_chain' }, + { name: 'Stakeholder', href: 'stakeholders.html', icon: '', i18nKey: 'nav.stakeholders' }, { name: 'Segnalazioni', href: 'whistleblowing.html', icon: '' }, { name: 'Normative', href: 'normative.html', icon: '' }, { name: 'AI Cross-Analysis', href: 'cross-analysis.html', icon: '' }, diff --git a/public/js/help.js b/public/js/help.js index 58ba675..47bb2d2 100644 --- a/public/js/help.js +++ b/public/js/help.js @@ -364,41 +364,40 @@ const HelpSystem = (function () { // ─── Stakeholder estesi (A4 Fase 4.5) ───────────────────────── 'stakeholders': { title: 'Guida - Stakeholder', - intro: 'La Mappa Stakeholder è una vista di sintesi delle parti interessate dell\'organizzazione, raggruppate in fornitori, clienti e partner. Riusa i dati del modulo Supply Chain (nessuna duplicazione): il dettaglio e la valutazione dei fornitori restano lì, mentre qui puoi avere il quadro d\'insieme e aggiungere o etichettare clienti e partner. È uno strumento di supporto organizzativo, non un parere legale.', + intro: 'Il modulo Stakeholder è un registro delle parti interessate dell\'organizzazione (interne ed esterne). Per ognuna scegli un tipo dall\'elenco configurabile (Stak.01–30 di sistema, puoi aggiungerne altri), la colleghi all\'organigramma (se interna) o a un fornitore della Supply Chain (se esterna) e alle procedure, e ne valuti potere e interesse (0–5). In base a questi due valori il sistema colloca automaticamente lo stakeholder su una matrice a quattro quadranti. È uno strumento di supporto organizzativo, non un parere legale.', sections: [ { - heading: 'I tre gruppi', + heading: 'Tipi di stakeholder', items: [ - 'Fornitori: chi fornisce beni o servizi (inclusi i fornitori ICT). Il loro dettaglio e la valutazione di sicurezza si gestiscono nel modulo Supply Chain.', - 'Clienti: le parti a cui l\'organizzazione eroga servizi rilevanti per la continuità.', - 'Partner: soggetti con cui esistono accordi o collaborazioni rilevanti per la sicurezza.' + 'Interni (Stak.01–06): dipendenti, management, azionisti, collaboratori, sindacalisti, volontari/associati. Si collegano a un nodo dell\'organigramma.', + 'Esterni (Stak.07–30): clienti, fornitori, partner, autorità, banche, assicurazioni, comunità, media… Possono essere collegati a un fornitore già censito in Supply Chain (nessuna duplicazione del dato).', + 'Puoi aggiungere nuovi tipi: ricevono in automatico il codice successivo (da Stak.31). I 30 tipi di sistema non sono modificabili.' ] }, { - heading: 'Come si usa', + heading: 'Valutazione e matrice di Mendelow', items: [ - 'Aggiungi stakeholder: registri nome, tipo (fornitore/cliente/partner), tipo di servizio o relazione, contatto e criticità.', - 'Per i fornitori, usa il modulo Supply Chain per il dettaglio completo, i questionari e la valutazione del rischio (qui niente duplicati).', - 'La vista raggruppata aiuta a documentare ruoli e responsabilità verso tutte le parti interessate, non solo i fornitori.' + 'Assegna potere (capacità di influenzare l\'azienda) e interesse (quanto è coinvolto) con valori da 0 a 5.', + 'La matrice di Mendelow (x = interesse, y = potere) colloca lo stakeholder in un quadrante: Q1 Tenere soddisfatti, Q2 Gestire attivamente, Q3 Tenere informati, Q4 Monitorare.', + 'La matrice è una buona prassi di gestione degli stakeholder, non un obbligo NIS2. Gli stakeholder non ancora valutati restano elencati a parte, da completare.' ] }, { heading: 'Riferimenti normativi', items: [ - 'GV.SC-02: ruoli e responsabilità per la sicurezza della catena di fornitura sono stabiliti verso fornitori, clienti e partner.', - 'GV.SC-04: i fornitori sono noti e prioritizzati in base alla criticità (l\'estensione a clienti e partner è coperta da GV.SC-02).', - 'GV.SC-05: i requisiti di sicurezza sono integrati nei contratti e negli accordi con i fornitori e le altre terze parti rilevanti.', - 'GV.SC-07: i rischi e i rapporti con le parti interessate sono monitorati e rivisti nel tempo.' + 'GV.SC-02 (obbligo): ruoli e responsabilità per la sicurezza sono stabiliti e comunicati verso fornitori, clienti e partner.', + 'GV.OC: comprensione del contesto organizzativo e delle parti interessate.', + 'GV.SC-04/05/07: fornitori prioritizzati per criticità, requisiti di sicurezza negli accordi, monitoraggio e revisione nel tempo.' ] } ], references: [ - 'NIST CSF 2.0 / GV.SC-02 - Ruoli e responsabilità verso fornitori, clienti e partner', - 'NIST CSF 2.0 / GV.SC-04 - Fornitori noti e prioritizzati per criticità (clienti/partner: GV.SC-02)', - 'NIST CSF 2.0 / GV.SC-05 - Requisiti di sicurezza negli accordi', - 'NIST CSF 2.0 / GV.SC-07 - Monitoraggio e revisione delle parti interessate', + 'NIST CSF 2.0 / GV.SC-02 - Ruoli e responsabilità verso fornitori, clienti e partner (obbligo)', + 'NIST CSF 2.0 / GV.OC - Contesto organizzativo e parti interessate', + 'NIST CSF 2.0 / GV.SC-04/05/07 - Prioritizzazione, accordi e monitoraggio delle terze parti', 'Obblighi: la gestione della supply chain è prevista dall\'art. 24 del D.Lgs. 138/2024.', - 'NOTA: gli obblighi normativi in Italia derivano dal D.Lgs. 138/2024; la mappa stakeholder e i framework NIST CSF/ACN sono strumenti di supporto, non un parere legale.' + 'La matrice di Mendelow (potere/interesse) è una buona prassi di gestione, NON un requisito NIS2.', + 'NOTA: gli obblighi normativi in Italia derivano dal D.Lgs. 138/2024; la matrice e i framework NIST CSF/ACN sono strumenti di supporto, non un parere legale.' ] }, diff --git a/public/kb.html b/public/kb.html index 83ebce9..f314963 100644 --- a/public/kb.html +++ b/public/kb.html @@ -151,8 +151,8 @@ - - + + @@ -161,9 +161,9 @@ bootstrap.loadFonts('/vendor/bootstrap-italia/dist/fonts'); } - - - + + + + - - + + - + + - - - + + + + diff --git a/public/normative.html b/public/normative.html index c5719a4..d7d9832 100644 --- a/public/normative.html +++ b/public/normative.html @@ -112,8 +112,8 @@ - - + + @@ -123,9 +123,9 @@ bootstrap.loadFonts('/vendor/bootstrap-italia/dist/fonts'); } - - - + + + - - + + - + + - - - + + + diff --git a/public/policies.html b/public/policies.html index 03cc3f0..501e968 100644 --- a/public/policies.html +++ b/public/policies.html @@ -333,8 +333,8 @@ - - + + @@ -344,9 +344,9 @@ bootstrap.loadFonts('/vendor/bootstrap-italia/dist/fonts'); } - - - + + + - + + - - - + + + diff --git a/public/register.html b/public/register.html index 434581b..23fd9fe 100644 --- a/public/register.html +++ b/public/register.html @@ -268,8 +268,8 @@ - - + + - + + @@ -454,9 +454,9 @@ bootstrap.loadFonts('/vendor/bootstrap-italia/dist/fonts'); } - - - + + + - + + - - - + + + - + + @@ -505,9 +505,9 @@ bootstrap.loadFonts('/vendor/bootstrap-italia/dist/fonts'); } - - - + + + - + + @@ -683,9 +683,9 @@ bootstrap.loadFonts('/vendor/bootstrap-italia/dist/fonts'); } - - - + + + - + + - + + + + + - - - + + + diff --git a/public/supply-chain.html b/public/supply-chain.html index 1029baf..794f4d2 100644 --- a/public/supply-chain.html +++ b/public/supply-chain.html @@ -477,8 +477,8 @@ - - + + @@ -488,9 +488,9 @@ bootstrap.loadFonts('/vendor/bootstrap-italia/dist/fonts'); } - - - + + + - + + @@ -303,9 +303,9 @@ bootstrap.loadFonts('/vendor/bootstrap-italia/dist/fonts'); } - - - + + + - + + @@ -218,9 +218,9 @@ bootstrap.loadFonts('/vendor/bootstrap-italia/dist/fonts'); } - - - + + + - - + + +