[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) <noreply@anthropic.com>
This commit is contained in:
DevEnv nis2-agile 2026-05-30 11:40:50 +02:00
parent 5413730b00
commit 43c6a87c5f

View File

@ -2369,7 +2369,40 @@ class ServicesController extends BaseController
$data['final_report_due'] = date('Y-m-d H:i:s', $t + 30 * 86400); $data['final_report_due'] = date('Y-m-d H:i:s', $t + 30 * 86400);
} }
// 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); $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', [ Database::insert('incident_timeline', [
'incident_id' => $incidentId, 'incident_id' => $incidentId,
@ -2626,7 +2659,9 @@ class ServicesController extends BaseController
$this->requireApiKey('read:compliance'); $this->requireApiKey('read:compliance');
$orgId = $this->currentOrgId; $orgId = $this->currentOrgId;
// Auto-stale: controlli la cui ultima evidenza è scaduta vanno marcati stale // 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( Database::query(
"UPDATE compliance_controls cc "UPDATE compliance_controls cc
LEFT JOIN ( LEFT JOIN (
@ -2643,6 +2678,9 @@ class ServicesController extends BaseController
AND last.valid_until IS NOT NULL AND last.valid_until < NOW()", AND last.valid_until IS NOT NULL AND last.valid_until < NOW()",
[$orgId, $orgId, $orgId] [$orgId, $orgId, $orgId]
); );
} catch (Throwable $e) {
error_log('[CCM-services] ' . $e->getMessage());
}
$rows = Database::fetchAll( $rows = Database::fetchAll(
'SELECT control_code, title, status, monitoring_status, last_checked_at, freshness_days, implementation_percentage 'SELECT control_code, title, status, monitoring_status, last_checked_at, freshness_days, implementation_percentage