Estende l'audit mig.039 ai moduli 040-057 (non coperti). TIER1 additivo (0 dup/0 orfani verificati su DB live): - UNIQUE internal_audits/management_reviews (org,code), kb_uploaded_documents.qdrant_doc_uuid, whistleblowing_reports.anonymous_token (drop idx_token ridondante) - retry-on-1062 in InternalAuditController/ManagementReviewController (allineati a periodic_controls) - 6 FK: consulting_firm_id (organizations/users/kb)->consulting_firms; isms_soa.linked_control_id; isms_documents.linked_policy_id; management_review_decisions.capa_id (tutte SET NULL) TIER2 bonifica (backup pre-DELETE in .backups/): -9 firm_org_assignments orfane (org 126-129, chiude deferred-b mig.039) -7 active_sessions scadute, +2 FK CASCADE. FK totali 196->204. Sonda diagnostica db_integrity_probe.php. audit_logs lasciato by-design. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
138 lines
9.5 KiB
PHP
138 lines
9.5 KiB
PHP
<?php
|
|
/**
|
|
* migrate_058_integrity_keys.php — Hardening integrità chiavi (estende mig.039
|
|
* alle tabelle aggiunte dopo: 040-057). Runner IDEMPOTENTE e GUARDATO.
|
|
*
|
|
* Verificato sul dato reale (db_integrity_probe.php/probe2.php, 2026-06-18):
|
|
* - 0 duplicati (org,code) su internal_audits/management_reviews
|
|
* - 0 valori duplicati su kb_uploaded_documents.qdrant_doc_uuid (16 righe)
|
|
* - whistleblowing_reports vuota; anonymous_token = chiave retrieval
|
|
* - 0 orfani su consulting_firm_id (organizations/users/kb), linked_control_id,
|
|
* linked_policy_id, capa_id
|
|
*
|
|
* TIER 1 (additivo, default): 4 UNIQUE + 6 FK. Nessuna modifica di dato.
|
|
* TIER 2 (--with-cleanup): bonifica righe stale (orfane) + 2 FK. CANCELLA righe.
|
|
*
|
|
* Uso:
|
|
* docker exec nis2-app php .../application/cli/migrate_058_integrity_keys.php # Tier1
|
|
* docker exec nis2-app php .../application/cli/migrate_058_integrity_keys.php --with-cleanup
|
|
* ... --dry-run # mostra solo cosa farebbe, non esegue
|
|
*/
|
|
if (PHP_SAPI !== 'cli') { http_response_code(403); exit("CLI only\n"); }
|
|
require_once __DIR__ . '/../config/env.php';
|
|
require_once __DIR__ . '/../config/database.php';
|
|
|
|
$DRY = in_array('--dry-run', $argv, true);
|
|
$CLEANUP = in_array('--with-cleanup', $argv, true);
|
|
$pdo = Database::getInstance();
|
|
$db = $pdo->query('SELECT DATABASE()')->fetchColumn();
|
|
echo "mig.058 — schema=$db | dry-run=" . ($DRY?'SI':'no') . " | cleanup=" . ($CLEANUP?'SI':'no') . "\n\n";
|
|
|
|
function col1(PDO $p,$s,$a=[]){ $st=$p->prepare($s); $st->execute($a); return $st->fetchColumn(); }
|
|
function idxExists(PDO $p,$db,$t,$idx){ return (int)col1($p,"SELECT COUNT(*) FROM information_schema.STATISTICS WHERE TABLE_SCHEMA=? AND TABLE_NAME=? AND INDEX_NAME=?",[$db,$t,$idx])>0; }
|
|
function fkExists(PDO $p,$db,$name){ return (int)col1($p,"SELECT COUNT(*) FROM information_schema.TABLE_CONSTRAINTS WHERE TABLE_SCHEMA=? AND CONSTRAINT_TYPE='FOREIGN KEY' AND CONSTRAINT_NAME=?",[$db,$name])>0; }
|
|
function tblExists(PDO $p,$db,$t){ return (int)col1($p,"SELECT COUNT(*) FROM information_schema.TABLES WHERE TABLE_SCHEMA=? AND TABLE_NAME=?",[$db,$t])>0; }
|
|
|
|
$applied=0; $skipped=0; $failed=0;
|
|
function run(PDO $pdo,bool $DRY,string $label,string $sql){
|
|
global $applied,$skipped,$failed;
|
|
if($DRY){ echo " [DRY] $label\n $sql\n"; return; }
|
|
try { $pdo->exec($sql); echo " [OK ] $label\n"; $applied++; }
|
|
catch(\PDOException $e){
|
|
$c=(int)($e->errorInfo[1]??0);
|
|
// 1061 dup key, 1826/1022 dup FK, 1062 dup entry (già unico) → idempotente
|
|
if(in_array($c,[1061,1826,1022,1062],true)){ echo " [SKIP] $label (già presente, code=$c)\n"; $skipped++; }
|
|
else { echo " [FAIL] $label → ".$e->getMessage()."\n"; $failed++; }
|
|
}
|
|
}
|
|
|
|
/* ───────── TIER 1: UNIQUE su chiavi naturali (pre-check duplicati) ───────── */
|
|
echo "── TIER 1: UNIQUE ──\n";
|
|
|
|
// U1 internal_audits (organization_id, code)
|
|
if(!idxExists($pdo,$db,'internal_audits','uk_intaud_code')){
|
|
$dup=(int)col1($pdo,"SELECT COUNT(*) FROM (SELECT 1 FROM internal_audits WHERE code IS NOT NULL GROUP BY organization_id,code HAVING COUNT(*)>1) x");
|
|
if($dup>0){ echo " [FAIL] internal_audits: $dup duplicati (org,code) — bonificare prima\n"; $failed++; }
|
|
else run($pdo,$DRY,'internal_audits ADD UNIQUE uk_intaud_code(organization_id,code)',
|
|
"ALTER TABLE internal_audits ADD UNIQUE KEY uk_intaud_code (organization_id, code)");
|
|
} else { echo " [SKIP] internal_audits.uk_intaud_code già presente\n"; $skipped++; }
|
|
|
|
// U2 management_reviews (organization_id, code)
|
|
if(!idxExists($pdo,$db,'management_reviews','uk_mgr_code')){
|
|
$dup=(int)col1($pdo,"SELECT COUNT(*) FROM (SELECT 1 FROM management_reviews WHERE code IS NOT NULL GROUP BY organization_id,code HAVING COUNT(*)>1) x");
|
|
if($dup>0){ echo " [FAIL] management_reviews: $dup duplicati (org,code)\n"; $failed++; }
|
|
else run($pdo,$DRY,'management_reviews ADD UNIQUE uk_mgr_code(organization_id,code)',
|
|
"ALTER TABLE management_reviews ADD UNIQUE KEY uk_mgr_code (organization_id, code)");
|
|
} else { echo " [SKIP] management_reviews.uk_mgr_code già presente\n"; $skipped++; }
|
|
|
|
// U3 kb_uploaded_documents.qdrant_doc_uuid
|
|
if(!idxExists($pdo,$db,'kb_uploaded_documents','uk_kbdoc_qdrant')){
|
|
$dup=(int)col1($pdo,"SELECT COUNT(*) FROM (SELECT 1 FROM kb_uploaded_documents WHERE qdrant_doc_uuid IS NOT NULL AND qdrant_doc_uuid<>'' GROUP BY qdrant_doc_uuid HAVING COUNT(*)>1) x");
|
|
if($dup>0){ echo " [FAIL] kb_uploaded_documents: $dup uuid duplicati\n"; $failed++; }
|
|
else run($pdo,$DRY,'kb_uploaded_documents ADD UNIQUE uk_kbdoc_qdrant(qdrant_doc_uuid)',
|
|
"ALTER TABLE kb_uploaded_documents ADD UNIQUE KEY uk_kbdoc_qdrant (qdrant_doc_uuid)");
|
|
} else { echo " [SKIP] kb_uploaded_documents.uk_kbdoc_qdrant già presente\n"; $skipped++; }
|
|
|
|
// U4 whistleblowing_reports.anonymous_token (DROP idx_token ridondante + UNIQUE)
|
|
if(!idxExists($pdo,$db,'whistleblowing_reports','uq_wb_anon_token')){
|
|
$dup=(int)col1($pdo,"SELECT COUNT(*) FROM (SELECT 1 FROM whistleblowing_reports WHERE anonymous_token IS NOT NULL AND anonymous_token<>'' GROUP BY anonymous_token HAVING COUNT(*)>1) x");
|
|
if($dup>0){ echo " [FAIL] whistleblowing_reports: $dup token duplicati\n"; $failed++; }
|
|
else {
|
|
if(idxExists($pdo,$db,'whistleblowing_reports','idx_token'))
|
|
run($pdo,$DRY,'whistleblowing_reports DROP INDEX idx_token (ridondante)',
|
|
"ALTER TABLE whistleblowing_reports DROP INDEX idx_token");
|
|
run($pdo,$DRY,'whistleblowing_reports ADD UNIQUE uq_wb_anon_token(anonymous_token)',
|
|
"ALTER TABLE whistleblowing_reports ADD UNIQUE KEY uq_wb_anon_token (anonymous_token)");
|
|
}
|
|
} else { echo " [SKIP] whistleblowing_reports.uq_wb_anon_token già presente\n"; $skipped++; }
|
|
|
|
/* ───────── TIER 1: FOREIGN KEY mancanti (pre-check orfani) ───────── */
|
|
echo "\n── TIER 1: FOREIGN KEY ──\n";
|
|
// [constraint, tabella, colonna, ref_table, on_delete]
|
|
$fks = [
|
|
['fk_org_consulting_firm','organizations','consulting_firm_id','consulting_firms','SET NULL'],
|
|
['fk_users_consulting_firm','users','consulting_firm_id','consulting_firms','SET NULL'],
|
|
['fk_kbdoc_consulting_firm','kb_uploaded_documents','consulting_firm_id','consulting_firms','SET NULL'],
|
|
['fk_isms_soa_control','isms_soa','linked_control_id','compliance_controls','SET NULL'],
|
|
['fk_isms_doc_policy','isms_documents','linked_policy_id','policies','SET NULL'],
|
|
['fk_mrd_capa','management_review_decisions','capa_id','capa_actions','SET NULL'],
|
|
];
|
|
foreach($fks as [$name,$t,$c,$ref,$od]){
|
|
if(!tblExists($pdo,$db,$t) || !tblExists($pdo,$db,$ref)){ echo " [SKIP] $name (tabella assente)\n"; $skipped++; continue; }
|
|
if(fkExists($pdo,$db,$name)){ echo " [SKIP] $name già presente\n"; $skipped++; continue; }
|
|
$orf=(int)col1($pdo,"SELECT COUNT(*) FROM `$t` x WHERE x.`$c` IS NOT NULL AND x.`$c`<>0 AND NOT EXISTS(SELECT 1 FROM `$ref` r WHERE r.id=x.`$c`)");
|
|
if($orf>0){ echo " [FAIL] $name: $orf righe orfane su $t.$c → bonificare prima\n"; $failed++; continue; }
|
|
run($pdo,$DRY,"$t ADD FK $name($c)->$ref ON DELETE $od",
|
|
"ALTER TABLE `$t` ADD CONSTRAINT `$name` FOREIGN KEY (`$c`) REFERENCES `$ref`(id) ON DELETE $od ON UPDATE CASCADE");
|
|
}
|
|
|
|
/* ───────── TIER 2: bonifica righe stale + FK (solo con --with-cleanup) ───────── */
|
|
echo "\n── TIER 2: cleanup orfani (".($CLEANUP?'ATTIVO':'saltato, usa --with-cleanup').") ──\n";
|
|
if($CLEANUP){
|
|
// C1 firm_org_assignments: 9 righe verso org 126-129 inesistenti (dogfooding, deferred-b 039)
|
|
$n=(int)col1($pdo,"SELECT COUNT(*) FROM firm_org_assignments x WHERE NOT EXISTS(SELECT 1 FROM organizations o WHERE o.id=x.organization_id)");
|
|
if($n>0){
|
|
if($DRY) echo " [DRY] DELETE $n righe stale firm_org_assignments (org inesistente)\n";
|
|
else { $pdo->exec("DELETE FROM firm_org_assignments WHERE organization_id NOT IN (SELECT id FROM organizations)"); echo " [OK ] cancellate $n righe stale firm_org_assignments\n"; $applied++; }
|
|
} else echo " [SKIP] firm_org_assignments: 0 orfani\n";
|
|
if(!fkExists($pdo,$db,'fk_firm_org_assignments_organization_id'))
|
|
run($pdo,$DRY,'firm_org_assignments ADD FK organization_id->organizations CASCADE',
|
|
"ALTER TABLE firm_org_assignments ADD CONSTRAINT fk_firm_org_assignments_organization_id FOREIGN KEY (organization_id) REFERENCES organizations(id) ON DELETE CASCADE ON UPDATE CASCADE");
|
|
else { echo " [SKIP] fk_firm_org_assignments_organization_id già presente\n"; $skipped++; }
|
|
|
|
// C2 active_sessions: 7 sessioni verso org inesistenti
|
|
$n2=(int)col1($pdo,"SELECT COUNT(*) FROM active_sessions x WHERE x.organization_id IS NOT NULL AND x.organization_id<>0 AND NOT EXISTS(SELECT 1 FROM organizations o WHERE o.id=x.organization_id)");
|
|
if($n2>0){
|
|
if($DRY) echo " [DRY] DELETE $n2 sessioni stale active_sessions\n";
|
|
else { $pdo->exec("DELETE FROM active_sessions WHERE organization_id IS NOT NULL AND organization_id<>0 AND organization_id NOT IN (SELECT id FROM organizations)"); echo " [OK ] cancellate $n2 sessioni stale active_sessions\n"; $applied++; }
|
|
} else echo " [SKIP] active_sessions: 0 orfani\n";
|
|
if(!fkExists($pdo,$db,'fk_active_sessions_organization_id'))
|
|
run($pdo,$DRY,'active_sessions ADD FK organization_id->organizations CASCADE',
|
|
"ALTER TABLE active_sessions ADD CONSTRAINT fk_active_sessions_organization_id FOREIGN KEY (organization_id) REFERENCES organizations(id) ON DELETE CASCADE ON UPDATE CASCADE");
|
|
else { echo " [SKIP] fk_active_sessions_organization_id già presente\n"; $skipped++; }
|
|
}
|
|
|
|
echo "\n── RIEPILOGO ── applicati=$applied skip=$skipped falliti=$failed\n";
|
|
echo ($failed===0 ? "ESITO OK\n" : "ESITO: rivedere i [FAIL]\n");
|
|
exit($failed===0?0:1);
|