Files
InformatiQ-Toolkit/includes/class-itk-database.php
Malin 742047915f feat: add WAF + Attack Intelligence system
- class-itk-waf.php: WordPress WAF scanning GET/POST/COOKIE/UA
- class-itk-attacks-api.php: queue/flush/history client for Attack API
- config/waf-rules.conf: 9 attack categories, 60+ WP-specific rules
- class-itk-database.php: itk_attack_log table, DB version 2
- class-itk-admin.php: WAF tab (toggles, response settings, API card),
  Attack Logs tab (filterable table), attacks dispatch in AJAX handlers
- informatiq-toolkit.php: wire WAF + Attacks API into plugin bootstrap
- .gitignore: exclude attack-api/ (separate repo)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-10 09:37:31 +02:00

392 lines
16 KiB
PHP

<?php
if (!defined('ABSPATH')) exit;
/**
* Database helper for InformatiQ Toolkit.
* Manages two log tables: bot_log and honeypot_log.
*/
class ITK_Database {
const DB_VERSION = 2;
const DB_VERSION_OPTION = 'itk_db_version';
/* ── Table names ──────────────────────────────────────────── */
public static function bot_table(): string {
global $wpdb;
return $wpdb->prefix . 'itk_bot_log';
}
public static function honeypot_table(): string {
global $wpdb;
return $wpdb->prefix . 'itk_honeypot_log';
}
public static function attack_table(): string {
global $wpdb;
return $wpdb->prefix . 'itk_attack_log';
}
/* ── Install / upgrade ────────────────────────────────────── */
public static function install() {
global $wpdb;
$charset = $wpdb->get_charset_collate();
$sql_bot = "CREATE TABLE " . self::bot_table() . " (
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
logged_at DATETIME NOT NULL,
ip_address VARCHAR(45) NOT NULL DEFAULT '',
user_agent TEXT NOT NULL,
referrer VARCHAR(1000) NOT NULL DEFAULT '',
request_uri VARCHAR(1000) NOT NULL DEFAULT '',
bot_type VARCHAR(100) NOT NULL DEFAULT '',
reason VARCHAR(255) NOT NULL DEFAULT '',
action VARCHAR(20) NOT NULL DEFAULT 'blocked',
PRIMARY KEY (id),
KEY ip_address (ip_address),
KEY logged_at (logged_at),
KEY bot_type (bot_type),
KEY action (action)
) {$charset};";
$sql_hp = "CREATE TABLE " . self::honeypot_table() . " (
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
blocked_at DATETIME NOT NULL,
ip_address VARCHAR(45) NOT NULL DEFAULT '',
form_type VARCHAR(100) NOT NULL DEFAULT '',
reason VARCHAR(255) NOT NULL DEFAULT '',
request_uri VARCHAR(1000) NOT NULL DEFAULT '',
user_agent TEXT NOT NULL,
PRIMARY KEY (id),
KEY ip_address (ip_address),
KEY blocked_at (blocked_at),
KEY form_type (form_type)
) {$charset};";
$sql_atk = "CREATE TABLE " . self::attack_table() . " (
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
logged_at DATETIME NOT NULL,
ip_address VARCHAR(45) NOT NULL DEFAULT '',
attack_type VARCHAR(50) NOT NULL DEFAULT '',
rule_desc VARCHAR(255) NOT NULL DEFAULT '',
source VARCHAR(20) NOT NULL DEFAULT '',
param VARCHAR(200) NOT NULL DEFAULT '',
payload TEXT NOT NULL,
request_uri VARCHAR(1000) NOT NULL DEFAULT '',
method VARCHAR(10) NOT NULL DEFAULT '',
user_agent TEXT NOT NULL,
PRIMARY KEY (id),
KEY ip_address (ip_address),
KEY logged_at (logged_at),
KEY attack_type (attack_type)
) {$charset};";
require_once ABSPATH . 'wp-admin/includes/upgrade.php';
dbDelta($sql_bot);
dbDelta($sql_hp);
dbDelta($sql_atk);
update_option(self::DB_VERSION_OPTION, self::DB_VERSION);
}
/* ── Bot log ──────────────────────────────────────────────── */
public static function log_bot(array $data): void {
global $wpdb;
$wpdb->insert(
self::bot_table(),
[
'logged_at' => current_time('mysql'),
'ip_address' => sanitize_text_field($data['ip'] ?? ''),
'user_agent' => sanitize_textarea_field($data['ua'] ?? ''),
'referrer' => esc_url_raw(substr($data['referrer'] ?? '', 0, 1000)),
'request_uri' => esc_url_raw(substr($data['uri'] ?? '', 0, 1000)),
'bot_type' => sanitize_text_field($data['bot_type'] ?? ''),
'reason' => sanitize_text_field($data['reason'] ?? ''),
'action' => sanitize_text_field($data['action'] ?? 'blocked'),
],
['%s','%s','%s','%s','%s','%s','%s','%s']
);
}
public static function get_bot_rows(array $args = []): array {
global $wpdb;
$table = self::bot_table();
$limit = max(1, (int)($args['per_page'] ?? 25));
$offset = max(0, (int)($args['offset'] ?? 0));
$where = '1=1';
$params = [];
if (!empty($args['action'])) {
$where .= ' AND action = %s';
$params[] = $args['action'];
}
if (!empty($args['bot_type'])) {
$where .= ' AND bot_type = %s';
$params[] = $args['bot_type'];
}
if (!empty($args['ip'])) {
$where .= ' AND ip_address = %s';
$params[] = $args['ip'];
}
if (!empty($args['search'])) {
$like = '%' . $wpdb->esc_like($args['search']) . '%';
$where .= ' AND (ip_address LIKE %s OR user_agent LIKE %s OR reason LIKE %s)';
$params[] = $like; $params[] = $like; $params[] = $like;
}
$params[] = $limit;
$params[] = $offset;
$sql = "SELECT * FROM {$table} WHERE {$where} ORDER BY logged_at DESC LIMIT %d OFFSET %d";
return $wpdb->get_results($wpdb->prepare($sql, $params)) ?: [];
}
public static function count_bot_rows(array $args = []): int {
global $wpdb;
$table = self::bot_table();
$where = '1=1';
$params = [];
if (!empty($args['action'])) {
$where .= ' AND action = %s';
$params[] = $args['action'];
}
if (!empty($args['bot_type'])) {
$where .= ' AND bot_type = %s';
$params[] = $args['bot_type'];
}
if (!empty($args['ip'])) {
$where .= ' AND ip_address = %s';
$params[] = $args['ip'];
}
if (!empty($args['search'])) {
$like = '%' . $wpdb->esc_like($args['search']) . '%';
$where .= ' AND (ip_address LIKE %s OR user_agent LIKE %s OR reason LIKE %s)';
$params[] = $like; $params[] = $like; $params[] = $like;
}
$sql = "SELECT COUNT(*) FROM {$table} WHERE {$where}";
return (int)($params ? $wpdb->get_var($wpdb->prepare($sql, $params)) : $wpdb->get_var($sql));
}
public static function get_bot_stats(): array {
global $wpdb;
$table = self::bot_table();
$today = current_time('Y-m-d');
return [
'total' => (int)$wpdb->get_var("SELECT COUNT(*) FROM {$table}"),
'today' => (int)$wpdb->get_var($wpdb->prepare("SELECT COUNT(*) FROM {$table} WHERE DATE(logged_at)=%s", $today)),
'blocked' => (int)$wpdb->get_var("SELECT COUNT(*) FROM {$table} WHERE action='blocked'"),
'rate_limited' => (int)$wpdb->get_var("SELECT COUNT(*) FROM {$table} WHERE action='rate_limited'"),
'top_bot_types' => $wpdb->get_results("SELECT bot_type, COUNT(*) as cnt FROM {$table} WHERE bot_type != '' GROUP BY bot_type ORDER BY cnt DESC LIMIT 8") ?: [],
'top_ips' => $wpdb->get_results("SELECT ip_address, COUNT(*) as cnt FROM {$table} GROUP BY ip_address ORDER BY cnt DESC LIMIT 5") ?: [],
'last_24h_counts' => $wpdb->get_results("SELECT DATE_FORMAT(logged_at,'%H:00') as hour, COUNT(*) as cnt FROM {$table} WHERE logged_at >= DATE_SUB(NOW(), INTERVAL 24 HOUR) GROUP BY hour ORDER BY hour ASC") ?: [],
];
}
public static function get_bot_types(): array {
global $wpdb;
return $wpdb->get_col("SELECT DISTINCT bot_type FROM " . self::bot_table() . " WHERE bot_type != '' ORDER BY bot_type ASC") ?: [];
}
public static function clear_bot_log(): void {
global $wpdb;
$wpdb->query("TRUNCATE TABLE " . self::bot_table());
}
public static function prune_bot_log(int $days): void {
global $wpdb;
$wpdb->query($wpdb->prepare(
"DELETE FROM " . self::bot_table() . " WHERE logged_at < DATE_SUB(NOW(), INTERVAL %d DAY)",
$days
));
}
/* ── Honeypot log ─────────────────────────────────────────── */
public static function log_honeypot(array $data): void {
global $wpdb;
$wpdb->insert(
self::honeypot_table(),
[
'blocked_at' => current_time('mysql'),
'ip_address' => sanitize_text_field($data['ip'] ?? ''),
'form_type' => sanitize_text_field($data['form'] ?? 'Unknown'),
'reason' => sanitize_text_field($data['reason'] ?? ''),
'request_uri' => esc_url_raw(substr($data['uri'] ?? '', 0, 1000)),
'user_agent' => sanitize_textarea_field($data['ua'] ?? ''),
],
['%s','%s','%s','%s','%s','%s']
);
}
public static function get_honeypot_rows(array $args = []): array {
global $wpdb;
$table = self::honeypot_table();
$limit = max(1, (int)($args['per_page'] ?? 25));
$offset = max(0, (int)($args['offset'] ?? 0));
$where = '1=1';
$params = [];
if (!empty($args['form'])) {
$where .= ' AND form_type = %s';
$params[] = $args['form'];
}
if (!empty($args['ip'])) {
$where .= ' AND ip_address = %s';
$params[] = $args['ip'];
}
if (!empty($args['search'])) {
$like = '%' . $wpdb->esc_like($args['search']) . '%';
$where .= ' AND (ip_address LIKE %s OR user_agent LIKE %s OR reason LIKE %s)';
$params[] = $like; $params[] = $like; $params[] = $like;
}
$params[] = $limit;
$params[] = $offset;
$sql = "SELECT * FROM {$table} WHERE {$where} ORDER BY blocked_at DESC LIMIT %d OFFSET %d";
return $wpdb->get_results($wpdb->prepare($sql, $params)) ?: [];
}
public static function count_honeypot_rows(array $args = []): int {
global $wpdb;
$table = self::honeypot_table();
$where = '1=1';
$params = [];
if (!empty($args['form'])) {
$where .= ' AND form_type = %s';
$params[] = $args['form'];
}
if (!empty($args['ip'])) {
$where .= ' AND ip_address = %s';
$params[] = $args['ip'];
}
if (!empty($args['search'])) {
$like = '%' . $wpdb->esc_like($args['search']) . '%';
$where .= ' AND (ip_address LIKE %s OR user_agent LIKE %s OR reason LIKE %s)';
$params[] = $like; $params[] = $like; $params[] = $like;
}
$sql = "SELECT COUNT(*) FROM {$table} WHERE {$where}";
return (int)($params ? $wpdb->get_var($wpdb->prepare($sql, $params)) : $wpdb->get_var($sql));
}
public static function get_honeypot_stats(): array {
global $wpdb;
$table = self::honeypot_table();
$today = current_time('Y-m-d');
return [
'total' => (int)$wpdb->get_var("SELECT COUNT(*) FROM {$table}"),
'today' => (int)$wpdb->get_var($wpdb->prepare("SELECT COUNT(*) FROM {$table} WHERE DATE(blocked_at)=%s", $today)),
'top_forms' => $wpdb->get_results("SELECT form_type, COUNT(*) as cnt FROM {$table} GROUP BY form_type ORDER BY cnt DESC LIMIT 8") ?: [],
'top_ips' => $wpdb->get_results("SELECT ip_address, COUNT(*) as cnt FROM {$table} GROUP BY ip_address ORDER BY cnt DESC LIMIT 5") ?: [],
];
}
public static function get_honeypot_form_types(): array {
global $wpdb;
return $wpdb->get_col("SELECT DISTINCT form_type FROM " . self::honeypot_table() . " ORDER BY form_type ASC") ?: [];
}
public static function clear_honeypot_log(): void {
global $wpdb;
$wpdb->query("TRUNCATE TABLE " . self::honeypot_table());
}
public static function prune_honeypot_log(int $days): void {
global $wpdb;
$wpdb->query($wpdb->prepare(
"DELETE FROM " . self::honeypot_table() . " WHERE blocked_at < DATE_SUB(NOW(), INTERVAL %d DAY)",
$days
));
}
/* ── Attack log ───────────────────────────────────────────── */
public static function log_attack(array $data): void {
global $wpdb;
$wpdb->insert(
self::attack_table(),
[
'logged_at' => current_time('mysql'),
'ip_address' => sanitize_text_field($data['ip'] ?? ''),
'attack_type' => sanitize_text_field($data['attack_type'] ?? ''),
'rule_desc' => sanitize_text_field($data['rule_desc'] ?? ''),
'source' => sanitize_text_field($data['source'] ?? ''),
'param' => sanitize_text_field($data['param'] ?? ''),
'payload' => sanitize_textarea_field(substr($data['payload'] ?? '', 0, 500)),
'request_uri' => sanitize_text_field(substr($data['uri'] ?? '', 0, 1000)),
'method' => sanitize_text_field($data['method'] ?? ''),
'user_agent' => sanitize_textarea_field($data['ua'] ?? ''),
],
['%s','%s','%s','%s','%s','%s','%s','%s','%s','%s']
);
}
public static function get_attack_rows(array $args = []): array {
global $wpdb;
$table = self::attack_table();
$limit = max(1, (int)($args['per_page'] ?? 25));
$offset = max(0, (int)($args['offset'] ?? 0));
$where = '1=1';
$params = [];
if (!empty($args['attack_type'])) {
$where .= ' AND attack_type = %s';
$params[] = $args['attack_type'];
}
if (!empty($args['ip'])) {
$where .= ' AND ip_address = %s';
$params[] = $args['ip'];
}
if (!empty($args['search'])) {
$like = '%' . $wpdb->esc_like($args['search']) . '%';
$where .= ' AND (ip_address LIKE %s OR param LIKE %s OR payload LIKE %s OR rule_desc LIKE %s)';
$params[] = $like; $params[] = $like; $params[] = $like; $params[] = $like;
}
$params[] = $limit;
$params[] = $offset;
$sql = "SELECT * FROM {$table} WHERE {$where} ORDER BY logged_at DESC LIMIT %d OFFSET %d";
return $wpdb->get_results($wpdb->prepare($sql, $params)) ?: [];
}
public static function count_attack_rows(array $args = []): int {
global $wpdb;
$table = self::attack_table();
$where = '1=1';
$params = [];
if (!empty($args['attack_type'])) {
$where .= ' AND attack_type = %s';
$params[] = $args['attack_type'];
}
if (!empty($args['ip'])) {
$where .= ' AND ip_address = %s';
$params[] = $args['ip'];
}
if (!empty($args['search'])) {
$like = '%' . $wpdb->esc_like($args['search']) . '%';
$where .= ' AND (ip_address LIKE %s OR param LIKE %s OR payload LIKE %s OR rule_desc LIKE %s)';
$params[] = $like; $params[] = $like; $params[] = $like; $params[] = $like;
}
$sql = "SELECT COUNT(*) FROM {$table} WHERE {$where}";
return (int)($params ? $wpdb->get_var($wpdb->prepare($sql, $params)) : $wpdb->get_var($sql));
}
public static function get_attack_types(): array {
global $wpdb;
return $wpdb->get_col("SELECT DISTINCT attack_type FROM " . self::attack_table() . " WHERE attack_type != '' ORDER BY attack_type ASC") ?: [];
}
public static function clear_attack_log(): void {
global $wpdb;
$wpdb->query("TRUNCATE TABLE " . self::attack_table());
}
}