database.php: Database::sslOptions() — TLS verso MySQL gated (env DB_SSL=true o flag-file application/config/.db_ssl_on), DEFAULT OFF. PDO::MYSQL_ATTR_SSL_CA + VERIFY_SERVER_CERT=false; fail-safe se CA assente (resta in chiaro + log: un VERIFY=false senza CA connetterebbe in chiaro silenziosamente). Merge con '+' (preserva chiavi-intere PDO). Default-OFF provato inerte (base + [] === base). Validato da 2 agenti: runtime app coperto 100% da Database::getInstance (0 raw PDO in application/); review adversariale = SICURO default-OFF, nessun bug. .gitignore: flag-file + db-ca.pem. Runbook: docs/TLS_DB_PREEQUIP_NIS2.md. NB topologia verificata: l'app usa il DB CONTAINER (db->172.21.0.4, 8.0.45), non il MySQL host -> CA + enforce vanno sul container db (a cura VIGILE). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
211 lines
6.4 KiB
PHP
211 lines
6.4 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 [];
|
|
}
|
|
|
|
$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() {}
|
|
}
|