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' },