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>
This commit is contained in:
2026-04-10 09:37:31 +02:00
parent a8d7972ad7
commit 742047915f
7 changed files with 1093 additions and 4 deletions

View File

@@ -7,7 +7,7 @@ if (!defined('ABSPATH')) exit;
*/
class ITK_Database {
const DB_VERSION = 1;
const DB_VERSION = 2;
const DB_VERSION_OPTION = 'itk_db_version';
/* ── Table names ──────────────────────────────────────────── */
@@ -22,6 +22,11 @@ class ITK_Database {
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() {
@@ -59,9 +64,28 @@ class ITK_Database {
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);
}
@@ -280,4 +304,88 @@ class ITK_Database {
$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());
}
}