prefix . 'honeypot_log'; } public static function install() { global $wpdb; $table = self::table(); $charset_collate = $wpdb->get_charset_collate(); $sql = "CREATE TABLE {$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_collate};"; require_once ABSPATH . 'wp-admin/includes/upgrade.php'; dbDelta($sql); update_option(self::TABLE_VERSION_OPTION, self::TABLE_VERSION); } public static function insert(array $data) { global $wpdb; $wpdb->insert( self::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_rows(array $args = []): array { global $wpdb; $table = self::table(); $limit = max(1, intval($args['per_page'] ?? 25)); $offset = max(0, intval($args['offset'] ?? 0)); $where = '1=1'; $params = []; if (!empty($args['ip'])) { $where .= ' AND ip_address = %s'; $params[] = sanitize_text_field($args['ip']); } if (!empty($args['form'])) { $where .= ' AND form_type = %s'; $params[] = sanitize_text_field($args['form']); } 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(array $args = []): int { global $wpdb; $table = self::table(); $where = '1=1'; $params = []; if (!empty($args['ip'])) { $where .= ' AND ip_address = %s'; $params[] = sanitize_text_field($args['ip']); } if (!empty($args['form'])) { $where .= ' AND form_type = %s'; $params[] = sanitize_text_field($args['form']); } 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) $wpdb->get_var($params ? $wpdb->prepare($sql, $params) : $sql); } public static function get_form_types(): array { global $wpdb; return $wpdb->get_col("SELECT DISTINCT form_type FROM " . self::table() . " ORDER BY form_type ASC") ?: []; } public static function clear(): void { global $wpdb; $wpdb->query("TRUNCATE TABLE " . self::table()); } public static function delete_older_than_days(int $days): void { global $wpdb; $wpdb->query( $wpdb->prepare( "DELETE FROM " . self::table() . " WHERE blocked_at < DATE_SUB(NOW(), INTERVAL %d DAY)", $days ) ); } } /* ====================================================================== * CENTRAL API CLIENT * Queues blocked submissions and batch-sends to a central dashboard. * ====================================================================*/ class SmartHoneypotAPIClient { const OPT_SETTINGS = 'hp_api_settings'; const OPT_QUEUE = 'hp_api_queue'; const QUEUE_MAX = 500; const BATCH_SIZE = 50; public static function defaults(): array { return [ 'enabled' => false, 'api_url' => '', 'last_sync' => 0, 'sent_total' => 0, ]; } public static function settings(): array { return wp_parse_args(get_option(self::OPT_SETTINGS, []), self::defaults()); } /** Called from log_spam() — very fast, just appends to option. */ public static function enqueue(array $data): void { $s = self::settings(); if (!$s['enabled'] || empty($s['api_url'])) { return; } $queue = (array) get_option(self::OPT_QUEUE, []); if (count($queue) >= self::QUEUE_MAX) { array_shift($queue); // drop oldest when full } $queue[] = $data; update_option(self::OPT_QUEUE, $queue, false); // no autoload } /** Called by WP-cron every 5 minutes. Sends pending batch to the API. */ public static function flush(): void { $s = self::settings(); if (!$s['enabled'] || empty($s['api_url'])) { return; } $queue = (array) get_option(self::OPT_QUEUE, []); if (empty($queue)) { return; } $batch = array_splice($queue, 0, self::BATCH_SIZE); $site_hash = hash('sha256', home_url()); $response = wp_remote_post( trailingslashit(esc_url_raw($s['api_url'])) . 'api/v1/submit', [ 'timeout' => 15, 'blocking' => true, 'headers' => ['Content-Type' => 'application/json'], 'body' => wp_json_encode([ 'site_hash' => $site_hash, 'blocks' => $batch, ]), ] ); if (!is_wp_error($response) && wp_remote_retrieve_response_code($response) === 200) { update_option(self::OPT_QUEUE, $queue, false); $s['last_sync'] = time(); $s['sent_total'] = ($s['sent_total'] ?? 0) + count($batch); update_option(self::OPT_SETTINGS, $s); } } /** Number of items currently waiting to be sent. */ public static function queue_size(): int { return count((array) get_option(self::OPT_QUEUE, [])); } } /* ====================================================================== * ADMIN PAGE * ====================================================================*/ class SmartHoneypotAdmin { const MENU_SLUG = 'honeypot-logs'; const NONCE_ACTION = 'hp_admin_action'; const PER_PAGE = 25; public static function register() { add_action('admin_menu', [self::class, 'add_menu']); add_action('admin_init', [self::class, 'handle_actions']); add_action('admin_enqueue_scripts', [self::class, 'enqueue_styles']); add_filter('plugin_action_links_' . plugin_basename(HP_PLUGIN_FILE), [self::class, 'plugin_links']); } public static function plugin_links($links) { array_unshift($links, 'View Logs'); $links[] = 'Documentation'; return $links; } public static function add_menu() { add_menu_page( 'Honeypot Logs', 'Honeypot Logs', 'manage_options', self::MENU_SLUG, [self::class, 'render_page'], 'dashicons-shield-alt', 81 ); } public static function enqueue_styles($hook) { if ($hook !== 'toplevel_page_' . self::MENU_SLUG) { return; } wp_add_inline_style('common', ' #hp-wrap { max-width:1400px; } #hp-wrap .hp-tabs { margin:16px 0 0; } #hp-wrap .hp-stats { display:flex; gap:14px; margin:16px 0; flex-wrap:wrap; } #hp-wrap .hp-stat-card { background:#fff; border:1px solid #c3c4c7; border-radius:4px; padding:14px 22px; min-width:130px; text-align:center; } #hp-wrap .hp-stat-num { font-size:2em; font-weight:700; color:#2271b1; line-height:1.2; } #hp-wrap .hp-stat-lbl { color:#646970; font-size:12px; } #hp-wrap .hp-filters { display:flex; gap:8px; align-items:center; flex-wrap:wrap; margin-bottom:12px; } #hp-wrap .hp-filters input, #hp-wrap .hp-filters select { height:32px; } #hp-wrap table.hp-log { width:100%; border-collapse:collapse; background:#fff; } #hp-wrap table.hp-log th { background:#f0f0f1; padding:8px 12px; text-align:left; border-bottom:2px solid #c3c4c7; white-space:nowrap; } #hp-wrap table.hp-log td { padding:8px 12px; border-bottom:1px solid #f0f0f1; vertical-align:top; } #hp-wrap table.hp-log tr:hover td { background:#f6f7f7; } #hp-wrap .hp-ua { font-size:11px; color:#646970; max-width:300px; word-break:break-all; } #hp-wrap .hp-badge { display:inline-block; padding:2px 8px; border-radius:3px; font-size:11px; font-weight:600; background:#ffecec; color:#b32d2e; border:1px solid #f7c5c5; } #hp-wrap .hp-pager { margin:12px 0; display:flex; align-items:center; gap:8px; } #hp-wrap .hp-pager a, #hp-wrap .hp-pager span { display:inline-block; padding:4px 10px; border:1px solid #c3c4c7; border-radius:3px; background:#fff; text-decoration:none; color:#2271b1; } #hp-wrap .hp-pager span.current { background:#2271b1; color:#fff; border-color:#2271b1; } #hp-wrap .hp-red { color:#b32d2e; } #hp-wrap .hp-api-status { display:inline-flex; align-items:center; gap:6px; font-weight:600; } #hp-wrap .hp-api-status .dot { width:10px; height:10px; border-radius:50%; display:inline-block; } #hp-wrap .dot-on { background:#00a32a; } #hp-wrap .dot-off { background:#646970; } '); } public static function handle_actions() { if (!isset($_POST['hp_action']) || !check_admin_referer(self::NONCE_ACTION)) { return; } if (!current_user_can('manage_options')) { wp_die('Unauthorized'); } if ($_POST['hp_action'] === 'clear_logs') { SmartHoneypotDB::clear(); wp_redirect(add_query_arg(['page' => self::MENU_SLUG, 'tab' => 'logs', 'cleared' => 1], admin_url('admin.php'))); exit; } if ($_POST['hp_action'] === 'save_api_settings') { $current = SmartHoneypotAPIClient::settings(); $new = [ 'enabled' => !empty($_POST['hp_api_enabled']), 'api_url' => esc_url_raw(trim($_POST['hp_api_url'] ?? '')), 'last_sync' => $current['last_sync'], 'sent_total' => $current['sent_total'], ]; update_option(SmartHoneypotAPIClient::OPT_SETTINGS, $new); wp_redirect(add_query_arg(['page' => self::MENU_SLUG, 'tab' => 'settings', 'saved' => 1], admin_url('admin.php'))); exit; } if ($_POST['hp_action'] === 'flush_queue') { SmartHoneypotAPIClient::flush(); wp_redirect(add_query_arg(['page' => self::MENU_SLUG, 'tab' => 'settings', 'flushed' => 1], admin_url('admin.php'))); exit; } } public static function render_page() { if (!current_user_can('manage_options')) { return; } $tab = sanitize_key($_GET['tab'] ?? 'logs'); ?>
Logs cleared.
API settings saved.
Queue flushed to central API.
Showing = number_format($total) ?> result= $total !== 1 ? 's' : '' ?> (page = $paged ?> of = $total_pages ?>)
| # | Date / Time | IP Address | Form Type | Reason | URI | User Agent |
|---|---|---|---|---|---|---|
| No blocked attempts recorded yet. | ||||||
| = esc_html($row->id) ?> | = esc_html($row->blocked_at) ?> |
= esc_html($row->ip_address) ?>filter lookup ↗ |
= esc_html($row->form_type) ?> | = esc_html($row->reason) ?> | = esc_html($row->request_uri) ?> | = esc_html($row->user_agent) ?> |
Submit blocked attempts anonymously to a central dashboard for aggregate threat intelligence. Only anonymised data is sent: masked IPs (first 2 octets only), form type, block reason, and UA family. No site URL, no full IPs.
| Enable Submission | |
|---|---|
| API Endpoint URL |
Base URL of your Honeypot API Docker container. |
| Status | = $s['enabled'] && $s['api_url'] ? 'Active' : 'Inactive' ?> |
|---|---|
| Last Sync | = $s['last_sync'] ? esc_html(date('Y-m-d H:i:s', $s['last_sync'])) : 'Never' ?> |
| Total Sent | = number_format((int)$s['sent_total']) ?> blocks |
| Queue Size | = number_format($queue_size) ?> pending blocks |
| Next Auto-Flush | = $next_run ? esc_html(date('Y-m-d H:i:s', $next_run)) . ' (every 5 min)' : 'Not scheduled' ?> |