-- ============================================================ -- NIS2 Agile - Migration 010: Audit Trail Certificato SHA-256 -- Hash chain per immutabilità certificabile (ispirato da lg231 AuditTrailService) -- Estende la tabella audit_logs con prev_hash, entry_hash, severity, performed_by -- ============================================================ -- Eseguire su Hetzner: -- mysql -u nis2_agile_user -p nis2_agile_db < docs/sql/010_audit_hash_chain.sql USE nis2_agile_db; -- ── Aggiunta colonne (idempotente via information_schema) ────────────────── -- prev_hash: hash del record precedente per la stessa organization_id SET @has_prev = ( SELECT COUNT(*) FROM information_schema.COLUMNS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'audit_logs' AND COLUMN_NAME = 'prev_hash' ); SET @sql_prev = IF(@has_prev = 0, 'ALTER TABLE audit_logs ADD COLUMN prev_hash VARCHAR(64) NULL AFTER details', 'SELECT ''prev_hash already exists'' AS note' ); PREPARE s FROM @sql_prev; EXECUTE s; DEALLOCATE PREPARE s; -- entry_hash: SHA-256 di questo record (include prev_hash per la catena) SET @has_entry = ( SELECT COUNT(*) FROM information_schema.COLUMNS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'audit_logs' AND COLUMN_NAME = 'entry_hash' ); SET @sql_entry = IF(@has_entry = 0, 'ALTER TABLE audit_logs ADD COLUMN entry_hash VARCHAR(64) NOT NULL DEFAULT \'\' AFTER prev_hash', 'SELECT ''entry_hash already exists'' AS note' ); PREPARE s FROM @sql_entry; EXECUTE s; DEALLOCATE PREPARE s; -- severity: criticità dell'evento per filtro dashboard SET @has_sev = ( SELECT COUNT(*) FROM information_schema.COLUMNS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'audit_logs' AND COLUMN_NAME = 'severity' ); SET @sql_sev = IF(@has_sev = 0, "ALTER TABLE audit_logs ADD COLUMN severity ENUM('info','warning','critical') NOT NULL DEFAULT 'info' AFTER entry_hash", 'SELECT ''severity already exists'' AS note' ); PREPARE s FROM @sql_sev; EXECUTE s; DEALLOCATE PREPARE s; -- performed_by: email/identificatore utente (denormalizzato per export certificato) SET @has_by = ( SELECT COUNT(*) FROM information_schema.COLUMNS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'audit_logs' AND COLUMN_NAME = 'performed_by' ); SET @sql_by = IF(@has_by = 0, 'ALTER TABLE audit_logs ADD COLUMN performed_by VARCHAR(255) NULL AFTER severity', 'SELECT ''performed_by already exists'' AS note' ); PREPARE s FROM @sql_by; EXECUTE s; DEALLOCATE PREPARE s; -- ── Indici ──────────────────────────────────────────────────────────────── -- Indice su entry_hash per verifica integrità e ricerca SET @has_idx_hash = ( SELECT COUNT(*) FROM information_schema.STATISTICS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'audit_logs' AND INDEX_NAME = 'idx_entry_hash' ); SET @sql_idx = IF(@has_idx_hash = 0, 'ALTER TABLE audit_logs ADD INDEX idx_entry_hash (entry_hash)', 'SELECT ''idx_entry_hash already exists'' AS note' ); PREPARE s FROM @sql_idx; EXECUTE s; DEALLOCATE PREPARE s; -- Indice su severity per filtro dashboard SET @has_idx_sev = ( SELECT COUNT(*) FROM information_schema.STATISTICS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'audit_logs' AND INDEX_NAME = 'idx_severity' ); SET @sql_isev = IF(@has_idx_sev = 0, 'ALTER TABLE audit_logs ADD INDEX idx_severity (severity, organization_id)', 'SELECT ''idx_severity already exists'' AS note' ); PREPARE s FROM @sql_isev; EXECUTE s; DEALLOCATE PREPARE s; -- ── Tabella export certificati audit ───────────────────────────────────── CREATE TABLE IF NOT EXISTS audit_exports ( id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY, organization_id INT NOT NULL, exported_by INT NULL, -- user_id (NULL = sistema) performed_by VARCHAR(255) NULL, -- email utente format ENUM('json','xml','csv') NOT NULL DEFAULT 'json', records_count INT UNSIGNED NOT NULL DEFAULT 0, date_from DATE NULL, date_to DATE NULL, export_hash VARCHAR(64) NOT NULL, -- SHA-256 del contenuto export chain_valid TINYINT(1) NOT NULL DEFAULT 1, -- integrità al momento export purpose VARCHAR(100) NOT NULL DEFAULT 'export_certificato', created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, INDEX idx_org (organization_id), INDEX idx_created (created_at) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; -- ── Tabella violazioni integrità catena ─────────────────────────────────── CREATE TABLE IF NOT EXISTS audit_violations ( id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY, organization_id INT NOT NULL, detected_by VARCHAR(255) NOT NULL DEFAULT 'system', broken_at_id INT NULL, -- audit_logs.id dove la catena si rompe chain_length INT UNSIGNED NOT NULL DEFAULT 0, -- record totali verificati notes TEXT NULL, resolved TINYINT(1) NOT NULL DEFAULT 0, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, INDEX idx_org (organization_id) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; SELECT 'Migration 010 completata: audit_logs con hash chain SHA-256 certificato' AS result;