Files
nis2-agile/public/js/isms.js
T
DevEnv nis2-agileandClaude Opus 4.8 e281aa4970 [FEAT] Epic C / C3 — rimosso modulo 'Analisi GAP ACN' (superato da C1)
Simon C3: l'analisi gap ACN non è un artefatto normativo e va eliminata; la conformità
emerge da C1 (elenco Misure/Requisiti + valutazione per requisito in org_requisito_state).

Ricognizione → migrazione → backup → rimozione (sicuro, 0 perdite):
- Tabelle acn_assessments/acn_assessment_responses VUOTE (0 righe) -> droppate (mig.049,
  guard anti-drop se >0 righe). Ricreabili da 036. Nessun dato utente perso.
- Integrazione lg231: /api/services/gap-analysis legge 'assessments' generale, NON il modulo
  ACN -> intatta. Verificato.
- RaciController ripuntato su cfg_nis2_misure (fonte canonica) al posto di acn_measures.json
  (rimosso); validazione measure_code verificata (/raci/objects?type=measure -> 43 misure).
Rimossi: AcnAssessmentController, public/acn-gap.html, application/data/acn_measures.json,
routing acn-gap, voci sidebar (common.js + common-bi.js), metodi api.js acn*, sezione help 'acn',
chiavi i18n acn.*, mapping demo-selectors. Tour demo step 3 RIPUNTATO su misure-requisiti
(no step rotto, 10 step). Sidebar BI ora mostra 'Misure e Requisiti' (gap di common-bi.js sanato).
Verifiche prod: /acn-gap.html 404, /api/acn-gap/* 404, /misure-requisiti 200, RaciController OK.
Cache-buster ?v=20260624, version 1.18.3, SW v1.18.3.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-16 19:24:29 +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 le altre pagine: 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.')); }
}