UI data-driven sulle tabelle di config cfg_nis2_* (mig.046): - FrameworkController::catalog (GET /api/framework/catalog): 43 misure / 116 requisiti, procedura+rischio di default per requisito, flag importanti/essenziali, classe org. - public/misure-requisiti.html (Bootstrap Italia/AGID): vista per misura / per area politica, selettore soggetto (auto/importante/essenziale/tutti), righe che si 'spengono' per classe, ricerca, sola lettura (codifica non modificabile). Voce sidebar in sezione Compliance. - api.js frameworkCatalog(); routing index.php; i18n nav.framework IT/EN. - help.js: guida 'misure-requisiti' (struttura, importanti/essenziali, riferimenti art.23/24 D.Lgs.). - KB AI: doc SYSTEM ingestato (uuid 7871c1ea). - Cache-buster ?v=20260621 (35 pagine), version 1.18.0, SW cache nis2-shell-v1.18.0. - docs/simon/: file sorgente autoritativi (provenienza seed, codifica Simon non variata). Smoke prod: /framework/catalog 200 (43/116, 6 misure solo-essenziali, GV.OC-04->Proc.01/Risk.01), pagina 200, USR2 applicato. Additivo, nessuna modifica a dati esistenti. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
238 lines
14 KiB
HTML
238 lines
14 KiB
HTML
<!DOCTYPE html>
|
||
<html lang="it">
|
||
<head>
|
||
<meta charset="UTF-8">
|
||
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
|
||
<title>Misure e Requisiti - NIS2 Agile</title>
|
||
<!-- Bootstrap Italia v2.18.1 self-hostato (regola: MAI CDN). Caricato PRIMA di style.css. -->
|
||
<link rel="stylesheet" href="/vendor/bootstrap-italia/dist/css/bootstrap-italia.min.css">
|
||
<link rel="stylesheet" href="/css/style.css?v=20260621">
|
||
<style>
|
||
.mr-intro { background:#eff6ff; border-left:4px solid var(--primary,#2563eb); padding:14px 18px; border-radius:8px; margin-bottom:14px; font-size:.92rem; line-height:1.6; }
|
||
.mr-note { font-size:.82rem; color:#92400e; background:#fffbeb; border:1px solid #fde68a; border-radius:8px; padding:10px 12px; margin:0 0 16px; }
|
||
.mr-ro { font-size:.8rem; color:#3730a3; background:#eef2ff; border:1px solid #c7d2fe; border-radius:8px; padding:8px 12px; margin:0 0 16px; display:flex; gap:8px; align-items:center; }
|
||
.mr-stats { display:flex; flex-wrap:wrap; gap:10px; margin-bottom:16px; }
|
||
.mr-stat { flex:1 1 140px; min-width:120px; background:#fff; border:1px solid var(--gray-200,#e5e7eb); border-radius:10px; padding:12px 14px; }
|
||
.mr-stat .n { font-size:1.5rem; font-weight:800; line-height:1; }
|
||
.mr-stat .l { font-size:.76rem; color:var(--gray-500,#6b7280); margin-top:4px; }
|
||
.mr-toolbar { display:flex; gap:8px; flex-wrap:wrap; align-items:center; margin-bottom:14px; }
|
||
.mr-toolbar label { font-weight:600; font-size:.82rem; }
|
||
.mr-toolbar select, .mr-toolbar input[type=search] { padding:8px 10px; border:1px solid var(--gray-200,#e5e7eb); border-radius:8px; font-size:.88rem; }
|
||
.mr-toolbar .grow { flex:1 1 auto; }
|
||
.mr-group { border:1px solid var(--gray-200,#e5e7eb); border-radius:10px; margin-bottom:14px; overflow:hidden; }
|
||
.mr-group-head { background:#f8fafc; padding:11px 14px; border-bottom:1px solid var(--gray-100,#f3f4f6); }
|
||
.mr-group-head .code { font-weight:800; color:#1e3a8a; font-size:.95rem; }
|
||
.mr-group-head .area { font-size:.78rem; color:var(--gray-500,#6b7280); }
|
||
.mr-group-head .descr { font-size:.86rem; color:#334155; margin-top:3px; line-height:1.5; }
|
||
.mr-tag { display:inline-block; font-size:.66rem; font-weight:800; padding:2px 8px; border-radius:20px; margin-left:6px; vertical-align:middle; }
|
||
.mr-tag.ess { background:#fef3c7; color:#92400e; }
|
||
.mr-table { width:100%; border-collapse:collapse; font-size:.85rem; }
|
||
.mr-table th, .mr-table td { text-align:left; padding:9px 11px; border-bottom:1px solid var(--gray-100,#f3f4f6); vertical-align:top; }
|
||
.mr-table th { font-size:.72rem; text-transform:uppercase; letter-spacing:.04em; color:var(--gray-500,#6b7280); background:#fff; position:sticky; top:0; }
|
||
.mr-req-code { white-space:nowrap; font-weight:700; color:#475569; }
|
||
.mr-code-pill { display:inline-block; font-size:.7rem; font-weight:700; color:#3730a3; background:#eef2ff; border-radius:6px; padding:1px 6px; }
|
||
.mr-yn { text-align:center; font-weight:800; }
|
||
.mr-yn.y { color:#166534; }
|
||
.mr-yn.n { color:#cbd5e1; }
|
||
.mr-row.spento { opacity:.4; }
|
||
.mr-row.spento td { font-style:italic; }
|
||
.mr-empty { text-align:center; padding:34px 16px; color:var(--gray-500,#6b7280); }
|
||
</style>
|
||
<!-- PWA:start -->
|
||
<link rel="manifest" href="/manifest.webmanifest">
|
||
<meta name="theme-color" content="#0066CC">
|
||
<link rel="icon" type="image/png" sizes="32x32" href="/assets/icons/favicon-32.png">
|
||
<link rel="icon" type="image/png" sizes="16x16" href="/assets/icons/favicon-16.png">
|
||
<link rel="apple-touch-icon" sizes="180x180" href="/assets/icons/apple-touch-icon.png">
|
||
<meta name="apple-mobile-web-app-capable" content="yes">
|
||
<meta name="mobile-web-app-capable" content="yes">
|
||
<meta name="apple-mobile-web-app-status-bar-style" content="default">
|
||
<meta name="apple-mobile-web-app-title" content="NIS2 Agile">
|
||
<meta name="application-name" content="NIS2 Agile">
|
||
<script src="/js/pwa.js?v=20260614" defer></script>
|
||
<!-- PWA:end -->
|
||
</head>
|
||
<body>
|
||
<div class="app-layout">
|
||
<aside class="sidebar" id="sidebar"></aside>
|
||
<main class="main-content">
|
||
<header class="content-header">
|
||
<h2 data-i18n="mr.title">Misure e Requisiti</h2>
|
||
<div class="content-header-actions">
|
||
<span class="savehint" id="mr-hint" role="status" aria-live="polite"></span>
|
||
</div>
|
||
</header>
|
||
|
||
<div class="content-body">
|
||
<div class="mr-intro">
|
||
<strong>L'elenco canonico delle misure di sicurezza di base e dei relativi requisiti.</strong>
|
||
Ogni requisito riporta la <strong>procedura di default</strong> che lo soddisfa e il <strong>rischio di default</strong> associato. È la base su cui si costruiscono procedure, rischi e attività: le righe si "spengono" quando non sono applicabili alla classe del tuo soggetto.
|
||
</div>
|
||
<div class="mr-ro">
|
||
🔒 <span>Elenco di riferimento, <strong>sola lettura</strong>: la codifica (misure, requisiti, ordine) non è modificabile. Procedure e rischi qui elencati vengono proposti come <em>default</em> nei rispettivi moduli, dove potrai personalizzarli.</span>
|
||
</div>
|
||
<div class="mr-note">
|
||
Fonte: <strong>Determinazione ACN n. 164179 del 14 aprile 2025</strong> (misure di base, Allegati 1–2). Le misure applicabili dipendono dalla classe del soggetto: <strong>importanti</strong> 37 misure / 92 requisiti, <strong>essenziali</strong> 43 / 116. Strumento di supporto, non un parere legale.
|
||
</div>
|
||
|
||
<div class="mr-stats" id="mr-stats" style="display:none;">
|
||
<div class="mr-stat"><div class="n" id="mr-stat-mis">0</div><div class="l">Misure</div></div>
|
||
<div class="mr-stat"><div class="n" id="mr-stat-req">0</div><div class="l">Requisiti</div></div>
|
||
<div class="mr-stat"><div class="n" id="mr-stat-active">0</div><div class="l">Applicabili al soggetto</div></div>
|
||
</div>
|
||
|
||
<div class="mr-toolbar">
|
||
<label for="mr-view">Vista:</label>
|
||
<select id="mr-view" onchange="mrRender()">
|
||
<option value="misura">Per misura</option>
|
||
<option value="area">Per area politica</option>
|
||
</select>
|
||
<label for="mr-class">Soggetto:</label>
|
||
<select id="mr-class" onchange="mrRender()">
|
||
<option value="auto">Automatico (dalla mia org)</option>
|
||
<option value="important">Importante</option>
|
||
<option value="essential">Essenziale</option>
|
||
<option value="all">Tutti (mostra tutto)</option>
|
||
</select>
|
||
<label style="font-weight:500;"><input type="checkbox" id="mr-hide" onchange="mrRender()"> nascondi non applicabili</label>
|
||
<span class="grow"></span>
|
||
<input type="search" id="mr-q" placeholder="Cerca codice o testo…" oninput="mrRender()" style="min-width:200px;">
|
||
</div>
|
||
|
||
<div id="mr-list" aria-live="polite"><div class="mr-empty">Caricamento…</div></div>
|
||
</div>
|
||
</main>
|
||
</div>
|
||
|
||
<script src="/js/api.js?v=20260621"></script>
|
||
<script src="/js/common.js?v=20260621"></script>
|
||
<script src="/vendor/bootstrap-italia/dist/js/bootstrap-italia.bundle.min.js"></script>
|
||
<script src="/js/common-bi.js?v=20260618"></script>
|
||
<script src="/js/i18n.js?v=20260621"></script>
|
||
<script src="/js/help.js?v=20260621"></script>
|
||
<script>
|
||
'use strict';
|
||
/*
|
||
* NIS2 Agile - Elenco canonico Misure/Requisiti (Epic C / C1) - SOLA LETTURA.
|
||
* Dati da api.frameworkCatalog() (tabelle cfg_nis2_*). Codifica non modificabile.
|
||
* Le righe si "spengono" per la classe del soggetto (importante/essenziale).
|
||
*/
|
||
let MR = { measures: [], org_class: null };
|
||
|
||
function el(id) { return document.getElementById(id); }
|
||
function esc(s) { const d = document.createElement('div'); d.textContent = (s == null ? '' : String(s)); return d.innerHTML; }
|
||
|
||
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();
|
||
await mrLoad();
|
||
});
|
||
|
||
async function mrLoad() {
|
||
try {
|
||
const data = await api.frameworkCatalog();
|
||
MR.measures = (data && data.measures) || [];
|
||
MR.org_class = (data && data.org_class) || null;
|
||
// se conosco la classe dell'org, parto in modalità "auto"; altrimenti mostro tutto
|
||
if (!MR.org_class) el('mr-class').value = 'all';
|
||
el('mr-stat-mis').textContent = (data && data.totals && data.totals.misure) || MR.measures.length;
|
||
el('mr-stat-req').textContent = (data && data.totals && data.totals.requisiti) || 0;
|
||
el('mr-stats').style.display = 'flex';
|
||
mrRender();
|
||
} catch (e) {
|
||
el('mr-list').innerHTML = '<div class="mr-empty">Impossibile caricare l\'elenco. Riprova più tardi.</div>';
|
||
}
|
||
}
|
||
|
||
// classe effettiva selezionata (risolve "auto" sulla classe dell'org)
|
||
function mrEffectiveClass() {
|
||
const sel = el('mr-class').value;
|
||
if (sel === 'auto') return MR.org_class; // può essere null => tutto attivo
|
||
if (sel === 'all') return null;
|
||
return sel; // important | essential
|
||
}
|
||
// un requisito è "attivo" per la classe effettiva?
|
||
function reqActive(req, cls) {
|
||
if (!cls) return true;
|
||
return cls === 'important' ? !!req.applies_important : !!req.applies_essential;
|
||
}
|
||
function matchQuery(m, req, q) {
|
||
if (!q) return true;
|
||
q = q.toLowerCase();
|
||
return (m.misura_code + ' ' + m.misura_descr + ' ' + m.area_politica + ' ' +
|
||
req.requisito_code + ' ' + req.requisito_descr + ' ' +
|
||
(req.proc_descr || '') + ' ' + (req.risk_descr || '')).toLowerCase().indexOf(q) >= 0;
|
||
}
|
||
|
||
function mrRender() {
|
||
const cls = mrEffectiveClass();
|
||
const hide = el('mr-hide').checked;
|
||
const q = (el('mr-q').value || '').trim();
|
||
const view = el('mr-view').value;
|
||
|
||
// costruisco le righe filtrate (misura -> requisiti visibili)
|
||
let activeCount = 0;
|
||
const groupsByKey = {}; // key -> {label, descr, area, code, essOnly, rows:[]}
|
||
|
||
MR.measures.forEach(function (m) {
|
||
const essOnly = !m.applies_important;
|
||
m.requisiti.forEach(function (req) {
|
||
if (!matchQuery(m, req, q)) return;
|
||
const active = reqActive(req, cls);
|
||
if (active) activeCount++;
|
||
if (hide && !active) return;
|
||
let key, head;
|
||
if (view === 'area') {
|
||
key = m.area_politica || '—';
|
||
head = { code: m.area_politica || '—', descr: '', area: '', essOnly: false };
|
||
} else {
|
||
key = m.misura_code;
|
||
head = { code: m.misura_code + ' — ' + (m.nist_code || ''), descr: m.misura_descr, area: m.area_politica, essOnly: essOnly };
|
||
}
|
||
if (!groupsByKey[key]) groupsByKey[key] = Object.assign({ rows: [] }, head);
|
||
groupsByKey[key].rows.push({ m: m, req: req, active: active });
|
||
});
|
||
});
|
||
|
||
el('mr-stat-active').textContent = activeCount;
|
||
|
||
const keys = Object.keys(groupsByKey);
|
||
if (!keys.length) { el('mr-list').innerHTML = '<div class="mr-empty">Nessun risultato per i filtri selezionati.</div>'; return; }
|
||
|
||
let html = '';
|
||
keys.forEach(function (key) {
|
||
const g = groupsByKey[key];
|
||
html += '<div class="mr-group">';
|
||
html += '<div class="mr-group-head"><div><span class="code">' + esc(g.code) + '</span>' +
|
||
(g.essOnly ? '<span class="mr-tag ess">solo essenziali</span>' : '') + '</div>';
|
||
if (g.area) html += '<div class="area">' + esc(g.area) + '</div>';
|
||
if (g.descr) html += '<div class="descr">' + esc(g.descr) + '</div>';
|
||
html += '</div>';
|
||
html += '<table class="mr-table"><thead><tr>' +
|
||
'<th>Requisito</th><th>Descrizione</th><th>Procedura (default)</th><th>Rischio (default)</th>' +
|
||
'<th style="text-align:center">Imp.</th><th style="text-align:center">Ess.</th></tr></thead><tbody>';
|
||
g.rows.forEach(function (r) {
|
||
const req = r.req, m = r.m;
|
||
html += '<tr class="mr-row' + (r.active ? '' : ' spento') + '">';
|
||
if (view === 'area') {
|
||
html += '<td class="mr-req-code"><span class="mr-code-pill">' + esc(m.misura_code) + '</span> ' + esc(req.requisito_code) + '</td>';
|
||
} else {
|
||
html += '<td class="mr-req-code">' + esc(req.requisito_code) + '</td>';
|
||
}
|
||
html += '<td>' + esc(req.requisito_descr) + '</td>';
|
||
html += '<td>' + (req.proc_code ? '<span class="mr-code-pill">' + esc(req.proc_code) + '</span> ' + esc(req.proc_descr || '') : '—') + '</td>';
|
||
html += '<td>' + (req.risk_code ? '<span class="mr-code-pill">' + esc(req.risk_code) + '</span> ' + esc(req.risk_descr || '') : '—') + '</td>';
|
||
html += '<td class="mr-yn ' + (req.applies_important ? 'y' : 'n') + '">' + (req.applies_important ? '✓' : '—') + '</td>';
|
||
html += '<td class="mr-yn ' + (req.applies_essential ? 'y' : 'n') + '">' + (req.applies_essential ? '✓' : '—') + '</td>';
|
||
html += '</tr>';
|
||
});
|
||
html += '</tbody></table></div>';
|
||
});
|
||
el('mr-list').innerHTML = html;
|
||
}
|
||
</script>
|
||
</body>
|
||
</html>
|