diff --git a/application/controllers/AuditController.php b/application/controllers/AuditController.php index 1263ac3..7b3e252 100644 --- a/application/controllers/AuditController.php +++ b/application/controllers/AuditController.php @@ -406,4 +406,68 @@ class AuditController extends BaseController header('Content-Disposition: attachment; filename="nis2_audit_certified_' . date('Y-m-d') . '.json"'); $this->jsonSuccess($result); } + + /** + * GET /api/audit/controlsMonitoring + * Continuous Control Monitoring (P1): stato/freschezza dei controlli + * alimentato dalle evidenze automatiche. Versione JWT per la UI. + */ + public function controlsMonitoring(): void + { + $this->requireOrgAccess(); + $orgId = $this->getCurrentOrgId(); + + try { + Database::query( + "UPDATE compliance_controls cc + LEFT JOIN ( + SELECT t.control_code, t.valid_until + FROM control_evidence_auto t + JOIN (SELECT control_code, MAX(collected_at) mx FROM control_evidence_auto + WHERE organization_id = ? GROUP BY control_code) m + ON m.control_code = t.control_code AND m.mx = t.collected_at + WHERE t.organization_id = ? + ) last ON last.control_code = cc.control_code + SET cc.monitoring_status = 'stale' + WHERE cc.organization_id = ? + AND cc.monitoring_status NOT IN ('not_monitored') + AND last.valid_until IS NOT NULL AND last.valid_until < NOW()", + [$orgId, $orgId, $orgId] + ); + } catch (Throwable $e) { + error_log('[CCM] ' . $e->getMessage()); + } + + $rows = Database::fetchAll( + 'SELECT control_code, title, status, monitoring_status, last_checked_at, freshness_days, implementation_percentage + FROM compliance_controls WHERE organization_id = ? ORDER BY control_code', + [$orgId] + ); + + $summary = ['healthy' => 0, 'warning' => 0, 'stale' => 0, 'failing' => 0, 'not_monitored' => 0]; + foreach ($rows as $r) { + $ms = $r['monitoring_status'] ?? 'not_monitored'; + if (isset($summary[$ms])) $summary[$ms]++; + } + $monitored = count($rows) - $summary['not_monitored']; + + $recent = []; + try { + $recent = Database::fetchAll( + 'SELECT control_code, source, source_system, status, summary, collected_at, valid_until + FROM control_evidence_auto WHERE organization_id = ? + ORDER BY collected_at DESC LIMIT 20', + [$orgId] + ); + } catch (Throwable $e) { /* ignore */ } + + $this->jsonSuccess([ + 'total_controls' => count($rows), + 'monitored' => $monitored, + 'coverage_percent' => count($rows) ? (int) round($monitored * 100 / count($rows)) : 0, + 'summary' => $summary, + 'controls' => $rows, + 'recent_evidence' => $recent, + ]); + } } diff --git a/public/js/help.js b/public/js/help.js index d3c93af..139437f 100644 --- a/public/js/help.js +++ b/public/js/help.js @@ -467,6 +467,15 @@ const HelpSystem = (function () { 'Report disponibili: stato di compliance, registro rischi, incidenti, formazione.', 'I report possono essere personalizzati per periodo e ambito.' ] + }, + { + heading: 'Monitoraggio Continuo dei Controlli (tab "Monitoraggio Continuo")', + items: [ + 'Mostra lo stato di freschezza di ogni controllo alimentato dalle evidenze raccolte automaticamente dai connettori (Evidence Automation).', + 'Semafori: Sano (evidenza valida), Attenzione, Evidenza scaduta (oltre la soglia di freschezza), Non conforme (ultimo check fallito), Non monitorato.', + 'La copertura indica la quota di controlli alimentati da evidenze automatiche: avvicina il modello alla compliance continua (vs assessment puntuale).', + 'Le evidenze arrivano via API POST /api/services/evidence-ingest (scope ingest:evidence) dai collector M365/AWS/EDR/SIEM.' + ] } ], references: [ diff --git a/public/js/i18n.js b/public/js/i18n.js index 99ab212..b0897f0 100644 --- a/public/js/i18n.js +++ b/public/js/i18n.js @@ -61,6 +61,8 @@ const I18n = (function () { // ── Rischi ───────────────────────────────────── 'risks.title': { it: 'Gestione Rischi', en: 'Risk Management' }, + 'risks.fair_tab': { it: 'Quantitativo (FAIR)', en: 'Quantitative (FAIR)' }, + 'risks.kri_tab': { it: 'KRI', en: 'KRI' }, 'risks.new': { it: 'Nuovo Rischio', en: 'New Risk' }, 'risks.matrix': { it: 'Matrice Rischi', en: 'Risk Matrix' }, 'risks.likelihood': { it: 'Probabilita\'', en: 'Likelihood' }, @@ -110,6 +112,7 @@ const I18n = (function () { // ── Asset ────────────────────────────────────── 'assets.title': { it: 'Inventario Asset', en: 'Asset Inventory' }, + 'assets.import_btn': { it: 'Importa', en: 'Import' }, 'assets.new': { it: 'Nuovo Asset', en: 'New Asset' }, 'assets.dependency_map': { it: 'Mappa Dipendenze', en: 'Dependency Map' }, 'assets.hardware': { it: 'Hardware', en: 'Hardware' }, @@ -119,6 +122,7 @@ const I18n = (function () { // ── Audit ────────────────────────────────────── 'audit.title': { it: 'Audit & Report', en: 'Audit & Reports' }, + 'audit.monitoring_tab': { it: 'Monitoraggio Continuo', en: 'Continuous Monitoring' }, 'audit.controls': { it: 'Controlli di Compliance', en: 'Compliance Controls' }, 'audit.evidence': { it: 'Evidenze', en: 'Evidence' }, 'audit.logs': { it: 'Log di Audit', en: 'Audit Logs' },