/**
* Patch virtuali: regole del feed applicate solo se il plugin/tema è installato in una versione vulnerabile.
* Ogni regola descrive un controllo GENERICO su un parametro (niente firme di exploit):
* {"id":"TWP-0001","titolo":"...","tipo":"plugin","slug":"nome-plugin","versione_da":"1.0","versione_a":"2.3.4",
* "metodo":"POST","path":"/wp-admin/admin-ajax.php","azione_ajax":"nome_azione","param":"id",
* "controllo":"solo_numeri" | "solo_alfanumerico" | "lunghezza_max" | "vietato" | "solo_valori", "valore":"...", "punti":10}
* $installati = ['slug' => 'versione', ...] (plugin e temi WordPress rilevati)
*/
function tw_patch_colpite(string $path, array $installati): array
{
$feed = tw_feed();
$colpi = [];
foreach (($feed['regole'] ?? []) as $r) {
$slug = (string)($r['slug'] ?? '');
if ($slug !== '') {
if (!isset($installati[$slug])) continue;
$v = (string)$installati[$slug];
if (!empty($r['versione_da']) && version_compare($v, (string)$r['versione_da'], '<')) continue;
if (!empty($r['versione_a']) && version_compare($v, (string)$r['versione_a'], '>')) continue;
}
if (!empty($r['metodo']) && strtoupper((string)$r['metodo']) !== ($_SERVER['REQUEST_METHOD'] ?? 'GET')) continue;
$p = (string)($r['path'] ?? '*');
if ($p !== '*' && $p !== '' && strpos($path, $p) === false) continue;
if (!empty($r['azione_ajax']) && (string)($_REQUEST['action'] ?? '') !== (string)$r['azione_ajax']) continue;
$param = (string)($r['param'] ?? '');
if ($param === '' || !isset($_REQUEST[$param])) continue;
$val = $_REQUEST[$param];
$val = is_scalar($val) ? (string)$val : json_encode($val);
$viola = false;
switch ((string)($r['controllo'] ?? '')) {
case 'solo_numeri': $viola = !preg_match('/^\d*$/', $val); break;
case 'solo_alfanumerico': $viola = !preg_match('/^[A-Za-z0-9_\-]*$/', $val); break;
case 'lunghezza_max': $viola = strlen($val) > (int)($r['valore'] ?? 100); break;
case 'vietato': $viola = true; break;
case 'solo_valori': $viola = !in_array($val, array_map('strval', (array)($r['valore'] ?? [])), true); break;
}
if ($viola) {
$colpi[] = ['id' => (string)($r['id'] ?? 'PATCH'), 'cat' => 'patch', 'd' => (string)($r['titolo'] ?? 'falla nota di un plugin'), 'dove' => 'patch:' . ($slug ?: 'sito') . ':' . $param, 'campione' => tw_tronca($val, 120), 'p' => (int)($r['punti'] ?? 10)];
}
}
return $colpi;
}
/* ============================================================ RETE CONDIVISA IP */
function tw_rete_aggiorna(): array
{
if (!tw_on('rete')) return ['ok' => false, 'msg' => 'Rete condivisa disattivata.'];
$url = (string)tw_conf('rete_url', '');
if ($url === '') return ['ok' => false, 'msg' => 'URL rete non impostato.'];
$raw = tw_http_get($url, 20);
if (!$raw) return ['ok' => false, 'msg' => 'Lista IP non raggiungibile.'];
$dati = json_decode($raw, true);
$ips = is_array($dati) ? ($dati['ip'] ?? $dati) : [];
if (!is_array($ips)) return ['ok' => false, 'msg' => 'Lista IP non valida.'];
$puliti = [];
foreach ($ips as $x) {
$ip = is_array($x) ? ($x['ip'] ?? '') : $x;
if (tw_valida_ip_o_cidr((string)$ip)) $puliti[] = (string)$ip;
}
tw_store()->kv_set('rete_ip', $puliti);
tw_store()->kv_set('rete_quando', time());
return ['ok' => true, 'msg' => 'Rete condivisa aggiornata: ' . count($puliti) . ' IP.', 'n' => count($puliti)];
}
function tw_rete_ip(): array
{
$v = tw_store()->kv_get('rete_ip');
return is_array($v) ? $v : [];
}
/** Invia al VPS gli IP che abbiamo bannato (facoltativo). */
function tw_rete_invia(): array
{
if (!tw_conf('rete_invia')) return ['ok' => false, 'msg' => 'Invio disattivato.'];
$url = (string)tw_conf('rete_invia_url', '');
$da = tw_store()->kv_prefisso('rete_da_inviare:');
if (!$da) return ['ok' => true, 'msg' => 'Niente da inviare.', 'n' => 0];
$lista = [];
foreach ($da as $k => $motivo) $lista[] = ['ip' => substr($k, strlen('rete_da_inviare:')), 'motivo' => $motivo];
$corpo = json_encode(['sito' => tw_conf('sito_nome'), 'ip' => $lista], JSON_UNESCAPED_UNICODE);
$r = tw_http_post($url, $corpo);
foreach ($da as $k => $v) tw_store()->kv_del($k);
return ['ok' => (bool)$r, 'msg' => $r ? ('Inviati ' . count($lista) . ' IP alla rete CGFA.') : 'Invio non riuscito.', 'n' => count($lista)];
}
function tw_http_post(string $url, string $corpo): ?string
{
$h = ['Content-Type: application/json'];
if (function_exists('curl_init')) {
$ch = curl_init($url);
curl_setopt_array($ch, [CURLOPT_RETURNTRANSFER => true, CURLOPT_TIMEOUT => 15, CURLOPT_POST => true, CURLOPT_POSTFIELDS => $corpo, CURLOPT_HTTPHEADER => $h, CURLOPT_USERAGENT => 'TheWall-CGFA/1.0']);
$r = curl_exec($ch); $code = curl_getinfo($ch, CURLINFO_HTTP_CODE); curl_close($ch);
return ($r !== false && $code < 400) ? $r : null;
}
$ctx = stream_context_create(['http' => ['method' => 'POST', 'header' => implode("\r\n", $h), 'content' => $corpo, 'timeout' => 15]]);
$r = @file_get_contents($url, false, $ctx);
return $r === false ? null : $r;
}
/* ============================================================ INTEGRITÀ FILE */
function tw_integrita_radice(): string
{
$r = (string)tw_conf('integrita_radice', '');
if ($r !== '' && is_dir($r)) return rtrim(str_replace('\\', '/', $r), '/');
$doc = $_SERVER['DOCUMENT_ROOT'] ?? dirname(TW_DIR);
return rtrim(str_replace('\\', '/', $doc), '/');
}
function tw_integrita_scansiona(bool $salva = false): array
{
$radice = tw_integrita_radice();
$vecchie = (array)tw_store()->kv_get('integrita');
$nuove = [];
$modificati = []; $nuovi = []; $spariti = [];
$n = 0;
$it = new RecursiveIteratorIterator(new RecursiveDirectoryIterator($radice, FilesystemIterator::SKIP_DOTS), RecursiveIteratorIterator::LEAVES_ONLY);
foreach ($it as $f) {
if ($n > 20000) break;
$path = str_replace('\\', '/', $f->getPathname());
if (strpos($path, '/data/') !== false && strpos($path, TW_DIR) !== false) continue;
if (!preg_match('/\.(php|phtml|php5|php7|inc|js)$/i', $path)) continue;
if ($f->getSize() > 8000000) continue;
$rel = ltrim(substr($path, strlen($radice)), '/');
$h = @hash_file('sha256', $path);
if ($h === false) continue;
$nuove[$rel] = $h;
$n++;
if (!empty($vecchie)) {
if (!isset($vecchie[$rel])) $nuovi[] = $rel;
elseif ($vecchie[$rel] !== $h) $modificati[] = $rel;
}
}
if (!empty($vecchie)) {
foreach ($vecchie as $rel => $h) if (!isset($nuove[$rel])) $spariti[] = $rel;
}
if ($salva || empty($vecchie)) {
tw_store()->kv_set('integrita', $nuove);
tw_store()->kv_set('integrita_quando', time());
}
return ['totale' => $n, 'modificati' => $modificati, 'nuovi' => $nuovi, 'spariti' => $spariti, 'prima_volta' => empty($vecchie)];
}
/* ============================================================ SCANNER MALWARE */
function tw_malware_firme(): array
{
return [
'webshell-eval' => '/\b(eval|assert)\s*\(\s*(base64_decode|gzinflate|gzuncompress|str_rot13|rawurldecode)\s*\(/i',
'post-eval' => '/\b(eval|assert|system|passthru|shell_exec)\s*\(\s*\$_(POST|GET|REQUEST|COOKIE)\b/i',
'preg-e' => '/preg_replace\s*\(\s*[\'"][^\'"]*\/e[\'"]/i',
'create-func' => '/create_function\s*\(/i',
'offuscato' => '/(\\\\x[0-9a-f]{2}){8,}|chr\(\d+\)\s*\.\s*chr\(\d+\)\s*\.\s*chr\(\d+\)\s*\.\s*chr\(/i',
'callback' => '/\b(call_user_func(_array)?|array_map|array_filter|usort)\s*\(\s*[\'"]?(assert|system|eval|exec|passthru|shell_exec)/i',
'uploader' => '/move_uploaded_file\s*\([^)]*\$_(FILES|POST|GET)/i',
'base64-lungo' => '/[\'"][A-Za-z0-9+\/]{260,}={0,2}[\'"]/',
'global-var-func' => '/\$\{?\$?_(GET|POST|REQUEST)\}?\[[^\]]+\]\s*\(/i',
'c99-wso' => '/(c99|wso|r57|b374k|FilesMan|\bphpshell\b|WSO\s|Shell by)/i',
];
}
function tw_malware_scansiona(): array
{
$radice = tw_integrita_radice();
$trovati = []; $n = 0;
$firme = tw_malware_firme();
$feed_firme = tw_feed()['malware'] ?? [];
$it = new RecursiveIteratorIterator(new RecursiveDirectoryIterator($radice, FilesystemIterator::SKIP_DOTS), RecursiveIteratorIterator::LEAVES_ONLY);
foreach ($it as $f) {
if ($n > 15000) break;
$path = str_replace('\\', '/', $f->getPathname());
if (!preg_match('/\.(php|phtml|php5|php7|inc)$/i', $path)) continue;
if (strpos($path, TW_DIR) === 0) continue;
if ($f->getSize() > 3000000) continue;
$n++;
$c = (string)@file_get_contents($path);
if ($c === '') continue;
$rel = ltrim(substr($path, strlen($radice)), '/');
foreach ($firme as $nome => $re) {
if (@preg_match($re, $c)) { $trovati[] = ['file' => $rel, 'firma' => $nome]; break; }
}
foreach ($feed_firme as $ff) {
if (!empty($ff['re']) && @preg_match($ff['re'], $c)) { $trovati[] = ['file' => $rel, 'firma' => $ff['id'] ?? 'feed']; break; }
}
}
tw_store()->kv_set('malware', $trovati);
tw_store()->kv_set('malware_quando', time());
return ['totale' => $n, 'trovati' => $trovati];
}
/* ============================================================ 2FA TOTP (RFC 6238) */
function tw_base32_decode(string $b32): string
{
$alf = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567';
$b32 = strtoupper(preg_replace('/[^A-Z2-7]/', '', $b32));
$bin = ''; $buf = 0; $bits = 0;
for ($i = 0, $l = strlen($b32); $i < $l; $i++) {
$buf = ($buf << 5) | strpos($alf, $b32[$i]);
$bits += 5;
if ($bits >= 8) { $bits -= 8; $bin .= chr(($buf >> $bits) & 0xFF); }
}
return $bin;
}
function tw_base32_encode(string $bin): string
{
$alf = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567';
$out = ''; $buf = 0; $bits = 0;
for ($i = 0, $l = strlen($bin); $i < $l; $i++) {
$buf = ($buf << 8) | ord($bin[$i]);
$bits += 8;
while ($bits >= 5) { $bits -= 5; $out .= $alf[($buf >> $bits) & 0x1F]; }
}
if ($bits > 0) $out .= $alf[($buf << (5 - $bits)) & 0x1F];
return $out;
}
function tw_totp_segreto(): string
{
return tw_base32_encode(random_bytes(20));
}
function tw_totp_codice(string $segreto, ?int $t = null): string
{
$t = $t ?? time();
$key = tw_base32_decode($segreto);
$c = intdiv($t, 30);
$bin = pack('N*', 0) . pack('N*', $c);
$hash = hash_hmac('sha1', $bin, $key, true);
$off = ord($hash[19]) & 0xF;
$num = ((ord($hash[$off]) & 0x7F) << 24) | ((ord($hash[$off + 1]) & 0xFF) << 16) | ((ord($hash[$off + 2]) & 0xFF) << 8) | (ord($hash[$off + 3]) & 0xFF);
return str_pad((string)($num % 1000000), 6, '0', STR_PAD_LEFT);
}
function tw_totp_verifica(string $segreto, string $codice): bool
{
$codice = preg_replace('/\D/', '', $codice);
if (strlen($codice) !== 6) return false;
for ($d = -1; $d <= 1; $d++) {
if (hash_equals(tw_totp_codice($segreto, time() + $d * 30), $codice)) return true;
}
return false;
}
function tw_totp_uri(string $segreto, string $utente, string $emittente): string
{
return 'otpauth://totp/' . rawurlencode($emittente . ':' . $utente) . '?secret=' . $segreto . '&issuer=' . rawurlencode($emittente) . '&period=30&digits=6';
}
/**
* QR in SVG puro per il 2FA.
* NOTA: il generatore in lib/qr.php (finder/timing/formato corretti) ha ancora un residuo
* nella posa dei dati e non è garantito leggibile da tutti i lettori; per questo il pannello
* usa come metodo principale la CHIAVE manuale + il link otpauth://. Quando qr.php sarà
* completato basterà togliere il "return ''" qui sotto.
*/
function tw_qr_svg(string $testo): string
{
return '';
}
/* ============================================================ HAVE I BEEN PWNED (k-anonymity) */
function tw_password_violata(string $password): bool
{
if ($password === '') return false;
$sha = strtoupper(sha1($password));
$pre = substr($sha, 0, 5); $suf = substr($sha, 5);
$r = tw_http_get('https://api.pwnedpasswords.com/range/' . $pre, 6, ['Add-Padding: true']);
if (!$r) return false; // in dubbio, non blocca
foreach (explode("\n", $r) as $riga) {
$p = explode(':', trim($riga));
if (isset($p[0]) && $p[0] === $suf && (int)($p[1] ?? 0) > 0) return true;
}
return false;
}
/* ============================================================ UPLOAD */
function tw_controlla_upload(): ?array
{
if (empty($_FILES) || !tw_conf('uploads_no_php', true)) return null;
$cattivi = '/\.(php\d?|phtml|phar|phps|pht|php[0-9]|cgi|pl|asp|aspx|jsp|exe|sh|htaccess)(\.|$)/i';
foreach ($_FILES as $campo => $f) {
$nomi = (array)($f['name'] ?? []);
$tmp = (array)($f['tmp_name'] ?? []);
if (!is_array($f['name'])) { $nomi = [$f['name']]; $tmp = [$f['tmp_name']]; }
foreach ($nomi as $i => $nome) {
$nome = (string)$nome;
if ($nome === '') continue;
// doppia estensione o estensione pericolosa
if (preg_match($cattivi, $nome)) {
return ['cat' => 'upload', 'd' => 'file non permesso: ' . tw_tronca($nome, 60), 'campione' => $nome, 'dove' => 'FILE:' . $campo];
}
// contenuto PHP dentro il file (anche se chiamato .jpg)
$t = $tmp[$i] ?? '';
if ($t && is_uploaded_file($t) || ($t && is_file($t))) {
$testa = (string)@file_get_contents($t, false, null, 0, 4096);
if (preg_match('/<\?php|<\?=|