/** * 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 += '
'+(i+1)+'
'+esc(t&&t!==STEP_KEYS[i]?t:lbl)+'
'; }); 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 += '
'+(r.raci||'-')+''+ ''+esc(r.role_name)+''+ ''+esc(r.responsibility||'')+''+ '
'; }); el('roles-list').innerHTML = html || '

'+(lang()==='en'?'No roles yet.':'Nessun ruolo definito.')+'

'; } catch(e){ el('roles-list').innerHTML='

'+esc((e&&e.message)||'')+'

'; } } 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 = '

'+(lang()==='en'?'Loading…':'Caricamento…')+'

'; try { const res = await api.ismsGetSoa(); renderSoa(res); ISMS.soaLoaded = true; markDone(); } catch(e){ el('soa-groups').innerHTML = '

'+esc((e&&e.message)||'')+'

'; } } 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 += '
'+esc(THEME_LABELS[th]?THEME_LABELS[th][lang()]:th)+'
'; byTheme[th].forEach(function(c){ inner += renderCtrl(c); }); }); html += '
'+ ''+esc(g.label)+''+(g.controls||[]).length+' '+(lang()==='en'?'controls':'controlli')+'
'+ '
'+inner+'
'; }); el('soa-groups').innerHTML = html || '

'+(lang()==='en'?'No controls.':'Nessun controllo.')+'

'; } 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) ? ''+(lang()==='en'?'from NIS2':'da NIS2')+'' : ''; 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 += ''; }); return '
'+ '
'+esc(c.control_code)+''+esc(title)+' '+derived+'
'+ '
'+ '
'+opts+'
'+ ''+ '
'; } 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) — ciclo di vita ISO 27001 cl.7.5 ── const DOC_STATUS = { draft: { it:'Bozza', en:'Draft', color:'#6b7280' }, review: { it:'In revisione', en:'In review', color:'#f59e0b' }, approved: { it:'Approvato', en:'Approved', color:'#3b82f6' }, published:{ it:'Pubblicato', en:'Published', color:'#16a34a' }, archived: { it:'Archiviato', en:'Archived', color:'#475569' } }; function fmtDate(d){ if(!d) return ''; try{ return new Date((''+d).replace(' ','T')).toLocaleDateString(lang()==='en'?'en-GB':'it-IT'); }catch(e){ return esc(d); } } function docBtn(label, onclick, bg){ return ''; } async function loadDocs(){ try { const res = await api.ismsDocuments(); const docs = res.documents||[]; const isEn = lang()==='en'; if(!docs.length){ el('docs-list').innerHTML = '

'+(isEn?'No documents yet.':'Nessun documento.')+'

'; return; } let html = '
'+docBtn(isEn?'Download all (Word)':'Scarica tutto (Word)','docWordAll()','#0066CC')+'
'; docs.forEach(function(d){ const st = DOC_STATUS[d.status] || { it:d.status, en:d.status, color:'#6b7280' }; const ai = (+d.ai_generated)?' AI':''; let meta = (isEn?'Version ':'Versione ')+esc(d.version||'1.0'); if(d.approved_at) meta += ' · '+(isEn?'approved ':'approvato il ')+fmtDate(d.approved_at)+(d.approved_by_name?' ('+esc(d.approved_by_name)+')':''); if(d.published_at) meta += ' · '+(isEn?'in force from ':'in vigore dal ')+fmtDate(d.published_at); if(d.next_review_date) meta += ' · '+(isEn?'next review ':'prossimo riesame ')+fmtDate(d.next_review_date); let btns=''; if(d.status==='draft'){ btns += docBtn(isEn?'Submit for review':'Invia in revisione','docAction('+d.id+',\'submit\')','#3b82f6'); } else if(d.status==='review'){ btns += docBtn(isEn?'Approve':'Approva','docAction('+d.id+',\'approve\')','#16a34a'); btns += docBtn(isEn?'Send back':'Rimanda in bozza','docReject('+d.id+')','#ef4444'); } else if(d.status==='approved'){ btns += docBtn(isEn?'Publish':'Pubblica','docAction('+d.id+',\'publish\')','#16a34a'); } else if(d.status==='published'){ btns += docBtn(isEn?'New version':'Nuova versione','docNewVersion('+d.id+')'); btns += docBtn(isEn?'Archive':'Archivia','docAction('+d.id+',\'archive\')','#475569'); } else if(d.status==='archived'){ btns += docBtn(isEn?'New version':'Nuova versione','docNewVersion('+d.id+')'); } if(d.status==='draft'||d.status==='review') btns += docBtn(isEn?'Edit':'Modifica','docEdit('+d.id+')','#7c3aed'); btns += docBtn(isEn?'Open':'Apri','docView('+d.id+')'); btns += docBtn('Word','docWord('+d.id+')'); btns += docBtn(isEn?'History':'Storico','docVersions('+d.id+')'); const note = (d.review_note && d.status==='draft') ? ' · '+esc(d.review_note)+'' : ''; html += '
'+ '
'+ '
'+esc(d.title)+''+ai+' ('+esc(d.doc_type)+')
'+ ''+(isEn?st.en:st.it)+''+ '
'+ '
'+meta+note+'
'+ '
'+btns+'
'+ '
'+ '
'; }); el('docs-list').innerHTML = html; } catch(e){ el('docs-list').innerHTML='

'+esc((e&&e.message)||'')+'

'; } } async function docAction(id, action){ const msg = { submit:(lang()==='en'?'Submit for review?':'Inviare in revisione?'), approve:(lang()==='en'?'Approve this document?':'Approvare il documento?'), publish:(lang()==='en'?'Publish (put in force)?':'Pubblicare il documento (metterlo in vigore)?'), archive:(lang()==='en'?'Archive this document?':'Archiviare il documento?') }[action] || 'OK?'; if(!confirm(msg)) return; const r = await api.post('/isms/documents/'+id+'/'+action); if(r && r.success){ loadDocs(); } else { alert((r&&r.message)||(lang()==='en'?'Action failed':'Operazione non riuscita')); } } async function docReject(id){ const note = prompt(lang()==='en'?'Reason for sending back to draft:':'Motivo del rinvio in bozza:'); if(note===null) return; const r = await api.post('/isms/documents/'+id+'/reject', { note:note }); if(r && r.success){ loadDocs(); } else { alert((r&&r.message)||'Errore'); } } async function docNewVersion(id){ if(!confirm(lang()==='en'?'Create a new editable draft version?':'Creare una nuova versione modificabile (bozza)?')) return; const r = await api.post('/isms/documents/'+id+'/newVersion', { major:false }); if(r && r.success){ loadDocs(); } else { alert((r&&r.message)||'Errore'); } } async function docVersions(id){ const box = el('docver-'+id); if(!box) return; if(box.innerHTML){ box.innerHTML=''; return; } box.innerHTML = '…'; const r = await api.get('/isms/documents/'+id+'/versions'); const vs = (r && r.data && r.data.versions) || []; if(!vs.length){ box.innerHTML = ''+(lang()==='en'?'No version history.':'Nessuna versione storicizzata.')+''; return; } let h = ''+ ''+ ''+ ''+ ''; vs.forEach(function(v){ h += ''; }); box.innerHTML = h + '
Vers.'+(lang()==='en'?'Status':'Stato')+''+(lang()==='en'?'Note':'Nota')+''+(lang()==='en'?'By / date':'Da / data')+'
'+esc(v.version)+''+esc(v.status)+''+esc(v.change_note||'')+''+esc(v.by_name||'')+' · '+fmtDate(v.created_at)+'
'; } async function docWord(id){ try{ const headers = { 'Authorization':'Bearer '+api.token }; if(api.orgId) headers['X-Organization-Id']=api.orgId; const resp = await fetch(api.baseUrl+'/isms/documents/'+id+'/word', { headers }); if(!resp.ok) throw new Error((lang()==='en'?'Download failed ':'Download non riuscito ')+'('+resp.status+')'); let fname = 'documento_'+id+'.doc'; const cd = resp.headers.get('Content-Disposition')||''; const mm = cd.match(/filename="?([^"]+)"?/); if(mm) fname = mm[1]; const blob = await resp.blob(); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href=url; a.download=fname; document.body.appendChild(a); a.click(); a.remove(); URL.revokeObjectURL(url); }catch(e){ alert((e&&e.message)||'Errore download'); } } async function docView(id){ try{ const headers = { 'Authorization':'Bearer '+api.token }; if(api.orgId) headers['X-Organization-Id']=api.orgId; const resp = await fetch(api.baseUrl+'/isms/documents/'+id+'/word', { headers }); if(!resp.ok) throw new Error('Apertura non riuscita'); const txt = await resp.text(); const w = window.open('','_blank'); if(!w){ alert(lang()==='en'?'Enable popups to view':'Abilita i popup per visualizzare'); return; } w.document.write(txt); w.document.close(); }catch(e){ alert((e&&e.message)||'Errore'); } } 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; } } // ── Download UNICO dell'intero set ── async function docWordAll(){ try{ const headers = { 'Authorization':'Bearer '+api.token }; if(api.orgId) headers['X-Organization-Id']=api.orgId; const resp = await fetch(api.baseUrl+'/isms/documentsWordAll', { headers }); if(!resp.ok) throw new Error((lang()==='en'?'Download failed ':'Download non riuscito ')+'('+resp.status+')'); let fname='SGSI_completo.doc'; const cd = resp.headers.get('Content-Disposition')||''; const mm=cd.match(/filename="?([^"]+)"?/); if(mm) fname=mm[1]; const blob=await resp.blob(); const url=URL.createObjectURL(blob); const a=document.createElement('a'); a.href=url; a.download=fname; document.body.appendChild(a); a.click(); a.remove(); URL.revokeObjectURL(url); }catch(e){ alert((e&&e.message)||'Errore download'); } } // ── Editor in-app del contenuto (bozza/revisione) ── let _docEditId = null; function _tb(label, onclick, st){ return ''; } function ensureDocEditor(){ if(el('doc-editor-overlay')) return; const isEn=lang()==='en'; const ov=document.createElement('div'); ov.id='doc-editor-overlay'; ov.style.cssText='display:none;position:fixed;inset:0;background:rgba(0,0,0,.5);z-index:9999;'; ov.innerHTML= '
'+ '
'+ ''+(isEn?'Edit document':'Modifica documento')+''+ ''+ ''+ '
'+ '
'+ _tb('B','docFmt(\'bold\')','font-weight:700;')+_tb('I','docFmt(\'italic\')','font-style:italic;')+ _tb('H2','docBlock(\'h2\')','')+_tb('H3','docBlock(\'h3\')','')+_tb('¶','docBlock(\'p\')','')+ _tb('• '+(isEn?'List':'Elenco'),'docFmt(\'insertUnorderedList\')','')+_tb('1.','docFmt(\'insertOrderedList\')','')+ ''+_tb('</> HTML','docToggleHtml()','')+ '
'+ '
'+ ''+ '
'+ ''+ ''+ ''+ '
'+ '
'; document.body.appendChild(ov); } function docFmt(cmd){ try{ document.execCommand(cmd,false,null); }catch(e){} const b=el('doc-edit-body'); if(b) b.focus(); } function docBlock(tag){ try{ document.execCommand('formatBlock',false,tag); }catch(e){} const b=el('doc-edit-body'); if(b) b.focus(); } function docToggleHtml(){ const body=el('doc-edit-body'), ta=el('doc-edit-html'); if(ta.style.display==='none'){ ta.value=body.innerHTML; ta.style.display='block'; body.style.display='none'; } else { body.innerHTML=ta.value; ta.style.display='none'; body.style.display='block'; } } async function docEdit(id){ ensureDocEditor(); const r=await api.get('/isms/documents/'+id); const d=r&&r.data&&r.data.document; if(!d){ alert((r&&r.message)||'Errore'); return; } _docEditId=id; el('doc-edit-title').value=d.title||''; el('doc-edit-body').innerHTML=d.body_html||''; el('doc-edit-html').value=''; el('doc-edit-html').style.display='none'; el('doc-edit-body').style.display='block'; const st=DOC_STATUS[d.status]; el('doc-edit-hint').textContent=(lang()==='en'?'Status: ':'Stato: ')+(st?(lang()==='en'?st.en:st.it):d.status)+' · v'+(d.version||'1.0'); el('doc-editor-overlay').style.display='block'; } function docEditClose(){ const o=el('doc-editor-overlay'); if(o) o.style.display='none'; _docEditId=null; } async function docEditSave(){ if(!_docEditId) return; const ta=el('doc-edit-html'); const body=(ta.style.display!=='none')?ta.value:el('doc-edit-body').innerHTML; const title=el('doc-edit-title').value.trim(); el('doc-edit-hint').textContent=lang()==='en'?'Saving…':'Salvataggio…'; const r=await api.put('/isms/documents/'+_docEditId, { title:title, body_html:body }); if(r&&r.success){ docEditClose(); loadDocs(); } else { el('doc-edit-hint').textContent=''; alert((r&&r.message)||'Errore salvataggio'); } } // ── 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 += '
'+esc(c.clause)+''+esc(c.label)+''+ ''+(c.done?'✓':'—')+'
'; }); el('readiness-checklist').innerHTML = html; } catch(e){ el('readiness-checklist').innerHTML='

'+esc((e&&e.message)||'')+'

'; } } 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 += ''+esc(c.control_code)+''+esc(c.title_it||'')+''+((+c.applicable)?'Sì':'No')+''+esc(c.implementation_status||'')+''+(c.implementation_pct||0)+'%'; }); w.document.write('SGSI - '+esc((data.organization&&data.organization.name)||'')+''+ ''+ '

Modello Organizzativo SGSI (ISO/IEC 27001:2022)

'+ '

'+esc((data.organization&&data.organization.name)||'')+' — generato il '+esc(data.generated_at||'')+'

'+ '
'+esc(data.disclaimer||'')+'
'+ '

Ambito

'+esc(m.scope_statement||'—')+'

'+ '

Metodologia di risk assessment

'+esc(m.risk_methodology||'—')+'

'+ '

Statement of Applicability

'+soa+'
ControlloTitoloApplicabileStato%
'+ ''); w.document.close(); } catch(e){ alert((e&&e.message)||(lang()==='en'?'Start the SGSI first.':'Avvia prima il SGSI.')); } }