Files
nis2-agile/public/js/isms.js
T
DevEnv nis2-agile 3f74165531 [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>
2026-06-11 11:32:42 +02:00

289 lines
15 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* 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.')); }
}