- 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>
392 lines
16 KiB
PHP
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());
|
|
}
|
|
}
|