From 43c6a87c5f4da8407e7cbad9feb059d869bdf331 Mon Sep 17 00:00:00 2001 From: DevEnv nis2-agile Date: Sat, 30 May 2026 11:40:50 +0200 Subject: [PATCH] [FIX] P1 ingestion: retry su collisione incident_code + dedup race graceful + try/catch CCM (findings review) - ingestIncident: insert in loop (max 5) -> rigenera incident_code su collisione UNIQUE (sotto carico SIEM il random a 6 cifre poteva collidere -> 500 = alert perso). Inoltre la race su external_ref (due alert simultanei) ora ritorna 200 dedup invece di 500. - controlsMonitoring (services): UPDATE auto-stale avvolto in try/catch come la gemella in AuditController (degrada con grazia se control_evidence_auto manca). Verificato E2E: ingest 201, dedup 200. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../controllers/ServicesController.php | 74 ++++++++++++++----- 1 file changed, 56 insertions(+), 18 deletions(-) diff --git a/application/controllers/ServicesController.php b/application/controllers/ServicesController.php index c2378f7..56c05ff 100644 --- a/application/controllers/ServicesController.php +++ b/application/controllers/ServicesController.php @@ -2369,7 +2369,40 @@ class ServicesController extends BaseController $data['final_report_due'] = date('Y-m-d H:i:s', $t + 30 * 86400); } - $incidentId = Database::insert('incidents', $data); + // Insert resiliente: retry su collisione incident_code (random 6 cifre), + // e gestione graceful della race su external_ref (dedup concorrente → 200). + $incidentId = null; + for ($attempt = 0; $attempt < 5; $attempt++) { + try { + $incidentId = Database::insert('incidents', $data); + break; + } catch (Throwable $e) { + $msg = $e->getMessage(); + if (stripos($msg, 'Duplicate') === false && stripos($msg, '1062') === false) { + throw $e; // errore non-duplicate: propaga + } + // Race su external_ref: un altro processo ha gia ingerito lo stesso alert + if ($externalRef !== null && stripos($msg, 'uq_incident_external_ref') !== false) { + $dup = Database::fetchOne( + 'SELECT id, incident_code FROM incidents WHERE organization_id = ? AND external_ref = ?', + [$orgId, $externalRef] + ); + if ($dup) { + $this->jsonSuccess([ + 'id' => (int) $dup['id'], + 'incident_code' => $dup['incident_code'], + 'deduplicated' => true, + ], 'Alert già ingerito (dedup concorrente)', 200); + return; + } + } + // Altrimenti: collisione su incident_code → rigenera e riprova + $data['incident_code'] = $this->generateCode('INC'); + } + } + if ($incidentId === null) { + $this->jsonError('Impossibile registrare l\'incidente (codice duplicato)', 500, 'INSERT_FAILED'); + } Database::insert('incident_timeline', [ 'incident_id' => $incidentId, @@ -2626,23 +2659,28 @@ class ServicesController extends BaseController $this->requireApiKey('read:compliance'); $orgId = $this->currentOrgId; - // Auto-stale: controlli la cui ultima evidenza è scaduta vanno marcati stale - Database::query( - "UPDATE compliance_controls cc - LEFT JOIN ( - SELECT t.control_code, t.valid_until, t.status - 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] - ); + // Auto-stale: controlli la cui ultima evidenza è scaduta vanno marcati stale. + // Degrada con grazia se la tabella evidenze non esiste (coerente con AuditController). + try { + Database::query( + "UPDATE compliance_controls cc + LEFT JOIN ( + SELECT t.control_code, t.valid_until, t.status + 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-services] ' . $e->getMessage()); + } $rows = Database::fetchAll( 'SELECT control_code, title, status, monitoring_status, last_checked_at, freshness_days, implementation_percentage