Files
nis2-agile/application/config/database.php
T
DevEnv nis2-agileandClaude Opus 4.8 4548cc6ff2 [FIX] TLS-DB nis2: guard anti-outage — salta SSL su connessione via UNIX socket
Lezione 2026-06-10 (3 outage): forzare SSL su una connessione via socket (DB_HOST
'localhost'/vuoto/path) da' PDOException [2002] "Cannot connect to MySQL using SSL"
su ogni richiesta -> app giu'. sslOptions() ora ritorna [] (resta in chiaro + log) se
DB_HOST indica un socket, anche col flag/env attivo. SSL si applica solo alle
connessioni TCP. Il socket e' IPC locale: nessun transito di rete da cifrare.
Cosi' riaccendere .db_ssl_on mentre l'app e' su socket NON puo' piu' causare outage.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-11 06:45:19 +02:00

223 lines
7.2 KiB
PHP

<?php
/**
* NIS2 Agile - Configurazione Database
*
* Database dedicato: nis2_agile_db
* Utente dedicato: nis2_user
* COMPLETAMENTE ISOLATO dagli altri applicativi
*/
require_once __DIR__ . '/env.php';
define('DB_HOST', Env::get('DB_HOST', 'localhost'));
define('DB_PORT', Env::get('DB_PORT', '3306'));
define('DB_NAME', Env::get('DB_NAME', 'nis2_agile_db'));
define('DB_USER', Env::get('DB_USER', 'nis2_user'));
define('DB_PASS', Env::getRequired('DB_PASS'));
define('DB_CHARSET', 'utf8mb4');
class Database
{
private static ?PDO $instance = null;
/**
* Ottiene l'istanza PDO (singleton)
*/
public static function getInstance(): PDO
{
if (self::$instance === null) {
$dsn = sprintf(
'mysql:host=%s;port=%s;dbname=%s;charset=%s',
DB_HOST,
DB_PORT,
DB_NAME,
DB_CHARSET
);
$options = [
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
PDO::ATTR_EMULATE_PREPARES => false,
PDO::MYSQL_ATTR_INIT_COMMAND => "SET NAMES utf8mb4 COLLATE utf8mb4_unicode_ci",
];
// TLS-in-transito (VIGILE 2026-06-10) — pre-equip, GATED, DEFAULT OFF.
// Unione con '+' (NON array_merge): preserva le chiavi-intere delle
// opzioni PDO (array_merge le re-indicizzerebbe). Vedi doc VIGILE §14.
$options = $options + self::sslOptions();
try {
self::$instance = new PDO($dsn, DB_USER, DB_PASS, $options);
} catch (PDOException $e) {
if (defined('APP_DEBUG') && APP_DEBUG) {
throw new Exception('Database connection failed: ' . $e->getMessage());
} else {
throw new Exception('Database connection failed');
}
}
}
return self::$instance;
}
/**
* Opzioni PDO per TLS-in-transito verso MySQL (VIGILE 2026-06-10, pre-equip).
*
* GATED, DEFAULT OFF: ritorna [] (nessuna modifica al comportamento attuale)
* a meno che NON sia attivato esplicitamente tramite:
* - env DB_SSL=true (canonico)
* - OPPURE flag-file application/config/.db_ssl_on (robusto anche in
* PHP-FPM dove getenv() puo' non propagare; nessun recreate necessario)
*
* IMPORTANTE (doc VIGILE §11): PHP PDO/mysqlnd NON cifra senza un CA file.
* MYSQL_ATTR_SSL_VERIFY_SERVER_CERT=false DA SOLO connette IN CHIARO. Quindi
* il TLS si attiva solo se il CA e' presente e leggibile; altrimenti si
* resta in chiaro (come oggi) e si logga — MAI una connessione "a meta'".
* CA host self-signed => VERIFY_SERVER_CERT=false (cifra il canale; la
* verifica della CA e' hardening successivo, coordinato con VIGILE).
*
* @return array opzioni PDO SSL (vuoto se disattivato o CA mancante)
*/
private static function sslOptions(): array
{
$enabled = Env::bool('DB_SSL', false) || is_file(__DIR__ . '/.db_ssl_on');
if (!$enabled) {
return [];
}
// GUARD anti-outage (lezione 2026-06-10, 3 outage): il TLS NON si applica a
// una connessione via UNIX socket (DB_HOST 'localhost', vuoto o path '/...').
// Forzarlo da' PDOException [2002] "Cannot connect to MySQL using SSL" su OGNI
// richiesta. Il socket e' IPC locale (nessun transito di rete da cifrare):
// restiamo in chiaro + log, anche se il flag/env e' attivo. SSL si applica solo
// alle connessioni TCP (es. 127.0.0.1 o un host di rete).
$host = defined('DB_HOST') ? (string) DB_HOST : '';
if ($host === '' || strcasecmp($host, 'localhost') === 0 || $host[0] === '/') {
error_log('[Database] DB_SSL attivo ma connessione via UNIX socket (DB_HOST=' . $host . '): SSL non applicabile, resto in chiaro (socket = IPC locale, nessun transito da cifrare).');
return [];
}
$ca = Env::get('DB_SSL_CA', __DIR__ . '/db-ca.pem');
if (!$ca || !is_file($ca) || !is_readable($ca)) {
error_log('[Database] DB_SSL attivo ma CA non trovato/leggibile (' . $ca . '): connessione NON cifrata (serve il CA file).');
return [];
}
return [
PDO::MYSQL_ATTR_SSL_CA => $ca,
PDO::MYSQL_ATTR_SSL_VERIFY_SERVER_CERT => false,
];
}
/**
* Esegue una query con parametri
*/
public static function query(string $sql, array $params = []): PDOStatement
{
$stmt = self::getInstance()->prepare($sql);
$stmt->execute($params);
return $stmt;
}
/**
* Ottiene una singola riga
*/
public static function fetchOne(string $sql, array $params = []): ?array
{
$result = self::query($sql, $params)->fetch();
return $result ?: null;
}
/**
* Ottiene tutte le righe
*/
public static function fetchAll(string $sql, array $params = []): array
{
return self::query($sql, $params)->fetchAll();
}
/**
* Inserisce e restituisce l'ID
*/
public static function insert(string $table, array $data): int
{
$columns = implode(', ', array_keys($data));
$placeholders = implode(', ', array_fill(0, count($data), '?'));
$sql = "INSERT INTO {$table} ({$columns}) VALUES ({$placeholders})";
self::query($sql, array_values($data));
return (int) self::getInstance()->lastInsertId();
}
/**
* Aggiorna righe
*/
public static function update(string $table, array $data, string $where, array $whereParams = []): int
{
$setParts = [];
foreach (array_keys($data) as $column) {
$setParts[] = "{$column} = ?";
}
$setClause = implode(', ', $setParts);
$sql = "UPDATE {$table} SET {$setClause} WHERE {$where}";
$params = array_merge(array_values($data), $whereParams);
return self::query($sql, $params)->rowCount();
}
/**
* Elimina righe
*/
public static function delete(string $table, string $where, array $params = []): int
{
$sql = "DELETE FROM {$table} WHERE {$where}";
return self::query($sql, $params)->rowCount();
}
/**
* Conta righe
*/
public static function count(string $table, string $where = '1=1', array $params = []): int
{
$sql = "SELECT COUNT(*) as cnt FROM {$table} WHERE {$where}";
$result = self::fetchOne($sql, $params);
return (int) ($result['cnt'] ?? 0);
}
/**
* Inizia una transazione
*/
public static function beginTransaction(): bool
{
return self::getInstance()->beginTransaction();
}
/**
* Commit transazione
*/
public static function commit(): bool
{
return self::getInstance()->commit();
}
/**
* Rollback transazione
*/
public static function rollback(): bool
{
return self::getInstance()->rollBack();
}
/**
* Restituisce l'ultimo ID inserito
*/
public static function lastInsertId(): int
{
return (int) self::getInstance()->lastInsertId();
}
private function __clone() {}
}