diff --git a/docs/EVIX_ANALISI_CONCORRENZA.html b/docs/EVIX_ANALISI_CONCORRENZA.html index b2c9d53..cb60ad2 100644 --- a/docs/EVIX_ANALISI_CONCORRENZA.html +++ b/docs/EVIX_ANALISI_CONCORRENZA.html @@ -100,18 +100,16 @@

Asset & Sistemi Rilevanti

Best-of-breed (IT) - - + +

Gestione Incidenti

Best-of-breed (IT) - - +
-

Risk Management

Competitivo - - +

Risk Management

Best-of-breed (IT) +

Audit & Evidence

Differenziante @@ -134,12 +132,14 @@
-

Continuous Monitoring

Gap rilevante - +

Continuous Monitoring

Competitivo (backend) +
  • NEW: Evidence Automation (POST /services/evidence-ingest) + Continuous Control Monitoring con semafori freschezza healthy/warning/stale/failing
  • UI "Monitoraggio Continuo" in Audit & Report
+
  • Gap residuo: connettori per-vendor live (richiedono OAuth/credenziali) — il framework di ingestion e pronto
-

Integrazioni / Connettori

Gap rilevante -
  • Assente catalogo connettori (M365, Google, AWS/Azure, Jira, IdP, EDR)
  • Presente: Services API + Webhook HMAC (base solida per costruirli)
+

Integrazioni / Connettori

Competitivo (backend) +
  • Services API + Webhook HMAC + NEW endpoint inbound: incidents-ingest, evidence-ingest, assets-ingest (scope dedicati ingest:*)
  • Catalogo connettori predisposto in area provider (M365/Google/AWS/Azure/IdP/EDR/SIEM/Ticketing)
+
  • Gap residuo: client per-vendor con OAuth (le credenziali/app registration sono lato cliente)
@@ -182,13 +182,13 @@ Raro [dv] No Continuous control monitoring + evidence automation - No (gap) + Si (backend) — evidence-ingest + CCM semafori; connettori live in roadmap Sì [dv] Sì — core [dv] No [dv] No Catalogo connettori/integrazioni (cloud, IdP, EDR, ticketing) - Parziale — API/webhook, no connettori pronti + Endpoint inbound ingest:* + catalogo predisposto; client per-vendor in roadmap Sì [dv] Sì — molti [dv] Raro [dv] @@ -235,10 +235,12 @@ Priorità per impatto competitivo. Le voci P1 sono quelle che oggi ci fanno perdere confronti vs Vanta/Drata e GRC enterprise.
-
P1 · Colmare il gap "compliance automation"
- Continuous Control Monitoring + Evidence Automation. Connettori per raccolta automatica evidenze (M365, Google Workspace, AWS/Azure, IdP, EDR). È il core di Vanta/Drata e oggi è il nostro gap più visibile. Base esistente: Services API + Webhook HMAC.
-
P1 · Ingestion incidenti
- Integrazione SIEM/SOC/EDR per apertura automatica incidenti (i mockup analizzati già prevedevano "Alert SIEM/SOC" come fonte). Trasforma il modulo incidenti da reattivo a proattivo.
+
✅ FATTO (2026-05-30) · P1 compliance automation
+ Continuous Control Monitoring + Evidence Automation. Implementato backend: POST /services/evidence-ingest (M365/Google/AWS/Azure/IdP/EDR/SIEM) + ricalcolo semafori freschezza + UI "Monitoraggio Continuo". Restano da attivare i client per-vendor con OAuth (credenziali lato cliente).
+
✅ FATTO (2026-05-30) · P1 ingestion incidenti
+ Integrazione SIEM/SOC/EDR implementata: POST /services/incidents-ingest apre automaticamente incidenti Art.23 con dedup e classificazione AI. Modulo incidenti ora proattivo.
+
✅ FATTO (2026-05-30) · P2 asset import + P2 risk quantitativo
+ Import CMDB/cloud (POST /assets/import) con scoring GV.OC-04 automatico, e Risk quantitativo FAIR (Monte Carlo, ALE EUR) + dashboard KRI. Restano: auto-discovery attiva asset; benchmark settoriale (P2 reporting).
P2 · Asset
Auto-discovery asset + import CMDB/cloud per alimentare automaticamente lo scoring di rilevanza GV.OC-04 appena introdotto.
P2 · Risk quantitativo
diff --git a/public/assets.html b/public/assets.html index 9248513..70e3464 100644 --- a/public/assets.html +++ b/public/assets.html @@ -869,6 +869,60 @@ // ── Initial Load ──────────────────────────────────────── loadAssets(); - + +/* ── Import asset CMDB/CSV (P2) ───────────────────────────── */ +function openImportModal(){ + const body = ` +

Incolla CSV o carica un file. Colonne riconosciute: + name, asset_type, criticality, data_classification, internet_facing, dependencies_count, regulated, external_ref, vendor, location. + Lo scoring di rilevanza NIS2 (GV.OC-04) viene calcolato automaticamente.

+
+
+
+
`; + showModal('Importa Asset', body, { + size:'lg', + footer:` + ` + }); +} +function impFile(e){ + const f=e.target.files[0]; if(!f)return; + const r=new FileReader(); r.onload=()=>{document.getElementById('imp-csv').value=r.result;}; r.readAsText(f); +} +function parseCsv(text){ + const lines=text.trim().split(/\r?\n/).filter(l=>l.trim()); + if(lines.length<2)return []; + const headers=lines[0].split(',').map(h=>h.trim().toLowerCase()); + const truthy=v=>/^(1|true|si|sì|yes|y)$/i.test((v||'').trim()); + return lines.slice(1).map(line=>{ + const cells=line.split(','); const o={}; + headers.forEach((h,i)=>{ o[h]=(cells[i]||'').trim(); }); + const a={name:o.name, asset_type:o.asset_type, criticality:o.criticality, + data_classification:o.data_classification, vendor:o.vendor, location:o.location, + external_ref:o.external_ref}; + if('internet_facing' in o) a.internet_facing=truthy(o.internet_facing); + if('regulated' in o) a.regulated=truthy(o.regulated); + if('dependencies_count' in o) a.dependencies_count=parseInt(o.dependencies_count)||0; + return a; + }).filter(a=>a.name); +} +async function doImport(){ + const assets=parseCsv(document.getElementById('imp-csv').value); + if(!assets.length){showNotification('Nessuna riga valida (serve almeno name)','warning');return;} + try{ + const res=await api.importAssets({source:'csv', assets}); + if(res.success){ + const d=res.data; + closeModal(); + showNotification(`Import: ${d.imported} nuovi, ${d.updated} aggiornati, ${d.relevant} rilevanti NIS2, ${d.skipped} scartati`,'success'); + if(typeof loadAssets==='function')loadAssets(); + } else showNotification(res.message||'Errore import','error'); + }catch(e){showNotification('Errore di connessione','error');} +} + + diff --git a/public/index.php b/public/index.php index 7c19bf3..53dd722 100644 --- a/public/index.php +++ b/public/index.php @@ -298,6 +298,7 @@ $actionMap = [ 'GET:logs' => 'getAuditLogs', 'GET:iso27001Mapping' => 'getIsoMapping', 'GET:nistCsfMapping' => 'getNistCsfMapping', + 'GET:controlsMonitoring' => 'controlsMonitoring', 'GET:executiveReport' => 'executiveReport', 'GET:relevantSystemsRegister' => 'relevantSystemsRegister', 'GET:export' => 'export', diff --git a/public/js/api.js b/public/js/api.js index c24fcf6..51edc52 100644 --- a/public/js/api.js +++ b/public/js/api.js @@ -200,6 +200,12 @@ class NIS2API { deleteRisk(id) { return this.del(`/risks/${id}`); } getRiskMatrix() { return this.get('/risks/matrix'); } aiSuggestRisks() { return this.post('/risks/ai-suggest', {}); } + // FAIR quantitativo + KRI (P2) + computeFair(id, data) { return this.post(`/risks/${id}/fair`, data); } + getFairRegister() { return this.get('/risks/fairRegister'); } + listKri() { return this.get('/risks/kri'); } + createKri(data) { return this.post('/risks/kri', data); } + updateKri(id, data) { return this.put(`/risks/kri/${id}`, data); } // ═══════════════════════════════════════════════════════════════════ // Incidents @@ -260,6 +266,8 @@ class NIS2API { getScoringGrid() { return this.get('/assets/scoringGrid'); } scoreAsset(id, criteria) { return this.post(`/assets/${id}/score`, { criteria }); } listRelevantSystems() { return this.get('/assets/relevantSystems'); } + importAssets(data) { return this.post('/assets/import', data); } // P2 import CMDB/CSV + getControlsMonitoring() { return this.get('/audit/controlsMonitoring'); } // P1 continuous control monitoring (JWT) // ═══════════════════════════════════════════════════════════════════ // Audit diff --git a/public/js/help.js b/public/js/help.js index 3fcdd2a..d3c93af 100644 --- a/public/js/help.js +++ b/public/js/help.js @@ -407,6 +407,16 @@ const HelpSystem = (function () { 'Registrazione di proprietario, ubicazione, fornitore e contratti di supporto.', 'Alert automatici per asset con supporto in scadenza o fine vita.' ] + }, + { + heading: 'Import da CMDB / Cloud (pulsante "Importa")', + items: [ + 'Importa asset in blocco da un file CSV o da un export CMDB/cloud, invece di inserirli manualmente.', + 'Colonne riconosciute: name, asset_type, criticality, data_classification, internet_facing, dependencies_count, regulated, external_ref, vendor, location.', + 'Lo scoring di rilevanza NIS2 (GV.OC-04) viene calcolato automaticamente per ogni asset importato, classificandolo critico/alto/medio/basso.', + 'Il campo external_ref abilita la deduplica: re-importare lo stesso ID aggiorna l\'asset invece di duplicarlo.', + 'Gli stessi dati possono arrivare via connettore automatico (API POST /api/services/assets-ingest, scope ingest:assets).' + ] } ], references: [ diff --git a/public/reports.html b/public/reports.html index 0c2213f..7a98735 100644 --- a/public/reports.html +++ b/public/reports.html @@ -359,6 +359,7 @@ +
@@ -401,6 +402,16 @@
+ + +
+
+
+
+

Caricamento monitoraggio...

+
+
+
@@ -467,6 +478,46 @@ if (tabId === 'controls') loadControls(); if (tabId === 'audit') { auditPage = 1; loadAuditLogs(); } if (tabId === 'iso') loadIsoMapping(); + if (tabId === 'monitoring') loadMonitoring(); + } + + // ── Continuous Control Monitoring (P1) ─────────────────────── + const MON_STATUS = { + healthy: ['#22c55e', 'Sano'], + warning: ['#eab308', 'Attenzione'], + stale: ['#f97316', 'Evidenza scaduta'], + failing: ['#ef4444', 'Non conforme'], + not_monitored: ['#9ca3af', 'Non monitorato'] + }; + async function loadMonitoring() { + const c = document.getElementById('monitoring-container'); + try { + const res = await api.getControlsMonitoring(); + if (!res.success) { c.innerHTML = '

' + escapeHtml(res.message || 'Errore') + '

'; return; } + const d = res.data, s = d.summary || {}; + const cards = ['healthy','warning','stale','failing','not_monitored'].map(k => + `
+
${s[k] || 0}
${MON_STATUS[k][1]}
`).join(''); + const rows = (d.controls || []).map(ct => { + const st = MON_STATUS[ct.monitoring_status] || MON_STATUS.not_monitored; + return ` + ${st[1]} + ${escapeHtml(ct.control_code)} + ${escapeHtml(ct.title || '')} + ${ct.last_checked_at || 'mai'} + ${ct.freshness_days || 30} gg`; + }).join(''); + c.innerHTML = ` +

+ Stato di freschezza dei controlli alimentato dalle evidenze automatiche dai connettori (Evidence Automation). + Copertura: ${d.coverage_percent}% (${d.monitored}/${d.total_controls} controlli monitorati).

+
${cards}
+ + + ${rows || ''}
StatoControlloTitoloUltima evidenzaFreschezza
Nessun controllo. Le evidenze arrivano via POST /api/services/evidence-ingest.
`; + } catch (e) { + c.innerHTML = '

Errore di connessione

'; + } } // ══════════════════════════════════════════════════════════ diff --git a/public/risks.html b/public/risks.html index 9b84a7d..0d9c9d6 100644 --- a/public/risks.html +++ b/public/risks.html @@ -306,6 +306,8 @@
+ +
+ + + +
+

Registro Quantitativo

+ ALE di portafoglio:
+
+ + +
CodiceTitoloCategoriaALE MinALE MedianaALE MaxALE Media
Nessun rischio quantificato
+
+
+ + + + + @@ -566,14 +627,164 @@ currentView = view; document.getElementById('view-table').classList.toggle('hidden', view !== 'table'); document.getElementById('view-matrix').classList.toggle('hidden', view !== 'matrix'); + document.getElementById('view-fair').classList.toggle('hidden', view !== 'fair'); + document.getElementById('view-kri').classList.toggle('hidden', view !== 'kri'); document.getElementById('view-detail').classList.toggle('hidden', true); const btns = document.querySelectorAll('#view-toggle button'); btns.forEach(b => b.classList.remove('active')); - if (view === 'table') btns[0].classList.add('active'); - else btns[1].classList.add('active'); + const idx = { table: 0, matrix: 1, fair: 2, kri: 3 }[view] ?? 0; + if (btns[idx]) btns[idx].classList.add('active'); if (view === 'matrix') loadMatrix(); + if (view === 'fair') loadFair(); + if (view === 'kri') loadKri(); + } + + // ── FAIR quantitativo ──────────────────────────────────────── + const fmtEur = n => '€ ' + Number(n || 0).toLocaleString('it-IT', { maximumFractionDigits: 0 }); + + async function loadFair() { + // popola select rischi + registro + try { + const [risksRes, regRes] = await Promise.all([api.listRisks({ per_page: 200 }), api.getFairRegister()]); + const sel = document.getElementById('fair-risk-select'); + if (risksRes.success) { + const risks = risksRes.data.risks || risksRes.data || []; + sel.innerHTML = '' + + risks.map(r => ``).join(''); + } + if (regRes.success) renderFairRegister(regRes.data); + } catch (e) { showNotification('Errore caricamento FAIR', 'error'); } + } + + function renderFairRegister(data) { + document.getElementById('fair-portfolio').textContent = fmtEur(data.portfolio_ale_mean); + const body = document.getElementById('fair-register-body'); + const risks = data.risks || []; + if (!risks.length) { body.innerHTML = 'Nessun rischio quantificato. Calcola l\'ALE di un rischio qui sopra.'; return; } + body.innerHTML = risks.map(r => ` + ${escapeHtml(r.risk_code || '—')}${escapeHtml(r.title)} + ${escapeHtml(CATEGORY_LABELS[r.category] || r.category || '')} + ${fmtEur(r.ale_min)}${fmtEur(r.ale_ml)} + ${fmtEur(r.ale_max)}${fmtEur(r.ale_mean)}`).join(''); + } + + async function runFair() { + const riskId = document.getElementById('fair-risk-select').value; + if (!riskId) { showNotification('Seleziona un rischio', 'warning'); return; } + const btn = document.getElementById('btn-run-fair'); + setButtonLoading(btn, true); + const payload = { + tef_min: +document.getElementById('fair-tef-min').value, + tef_ml: +document.getElementById('fair-tef-ml').value, + tef_max: +document.getElementById('fair-tef-max').value, + vuln: +document.getElementById('fair-vuln').value, + lm_min: +document.getElementById('fair-lm-min').value, + lm_ml: +document.getElementById('fair-lm-ml').value, + lm_max: +document.getElementById('fair-lm-max').value, + }; + try { + const res = await api.computeFair(riskId, payload); + if (res.success) { + renderFairResult(res.data.result); + showNotification('Analisi FAIR calcolata', 'success'); + api.getFairRegister().then(r => { if (r.success) renderFairRegister(r.data); }); + } else { showNotification(res.message || 'Errore calcolo FAIR', 'error'); } + } catch (e) { showNotification('Errore di connessione', 'error'); } + finally { setButtonLoading(btn, false); } + } + + function renderFairResult(r) { + document.getElementById('fair-result').classList.remove('hidden'); + document.getElementById('fair-p10').textContent = fmtEur(r.ale_min); + document.getElementById('fair-p50').textContent = fmtEur(r.ale_ml); + document.getElementById('fair-p90').textContent = fmtEur(r.ale_max); + document.getElementById('fair-mean').textContent = fmtEur(r.ale_mean); + document.getElementById('fair-lef').textContent = (r.lef_mean ?? 0).toFixed(3); + const bars = document.getElementById('fair-histogram'); + const labels = document.getElementById('fair-histogram-labels'); + const h = r.histogram || []; + const maxCount = Math.max(1, ...h.map(b => b.count)); + bars.innerHTML = h.map(b => { + const pct = Math.round(b.count / maxCount * 100); + return `
`; + }).join(''); + labels.innerHTML = h.map((b, i) => i % 3 === 0 ? `
${fmtEur(b.from)}
` : '').join(''); + } + + // ── KRI dashboard ──────────────────────────────────────────── + const KRI_STATUS = { green: ['#22c55e', 'OK'], amber: ['#eab308', 'Attenzione'], red: ['#ef4444', 'Critico'], unknown: ['#9ca3af', 'N/D'] }; + + async function loadKri() { + try { + const res = await api.listKri(); + if (res.success) renderKri(res.data); + else showNotification(res.message || 'Errore KRI', 'error'); + } catch (e) { showNotification('Errore di connessione', 'error'); } + } + + function renderKri(data) { + const sum = data.summary || {}; + document.getElementById('kri-summary').innerHTML = ['red', 'amber', 'green', 'unknown'].map(s => + `
${sum[s] || 0}
${KRI_STATUS[s][1]}
`).join(''); + const body = document.getElementById('kri-table-body'); + const kris = data.kris || []; + if (!kris.length) { body.innerHTML = 'Nessun KRI definito. Creane uno per monitorare gli indicatori di rischio.'; return; } + body.innerHTML = kris.map(k => { + const st = KRI_STATUS[k.status] || KRI_STATUS.unknown; + const val = k.current_value !== null ? `${(+k.current_value).toLocaleString('it-IT')} ${k.unit || ''}` : '—'; + return ` + + ${escapeHtml(k.name)}${k.description ? `
${escapeHtml(k.description)}` : ''} + ${escapeHtml(CATEGORY_LABELS[k.category] || k.category || '')} + ${escapeHtml(val)} + ${k.threshold_warning ?? '—'}${k.threshold_critical ?? '—'} + `; + }).join(''); + } + + function showKriModal(kri = null) { + const k = kri || {}; + const body = ` +
+
+
+
+
+
+
+
+
+
+
`; + showModal(k.id ? 'Modifica KRI' : 'Nuovo KRI', body, { + footer: ` + ` + }); + } + + async function saveKri(id) { + const data = { + name: document.getElementById('kri-name').value.trim(), + description: document.getElementById('kri-desc').value.trim(), + category: document.getElementById('kri-cat').value, + unit: document.getElementById('kri-unit').value.trim(), + current_value: document.getElementById('kri-cur').value, + target_value: document.getElementById('kri-target').value, + threshold_warning: document.getElementById('kri-warn').value, + threshold_critical: document.getElementById('kri-crit').value, + direction: document.getElementById('kri-dir').value, + }; + if (!data.name) { showNotification('Nome obbligatorio', 'warning'); return; } + try { + const res = id ? await api.updateKri(id, data) : await api.createKri(data); + if (res.success) { closeModal(); showNotification('KRI salvato', 'success'); loadKri(); } + else showNotification(res.message || 'Errore salvataggio', 'error'); + } catch (e) { showNotification('Errore di connessione', 'error'); } } // ── Matrix ───────────────────────────────────────────────────