From e67c1e0e9c5ef8720626781873fbf416ed4fa247 Mon Sep 17 00:00:00 2001 From: colorlink Date: Fri, 16 Oct 2026 02:09:48 +0000 Subject: [PATCH] =?UTF-8?q?feat(batch15):=20Wordfence=20security=20?= =?UTF-8?q?=E2=80=94=20status,=20scan=20issues,=20blocked=20IPs,=20logins,?= =?UTF-8?q?=20block=20IP=20(guarded)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_01Q8t9GEyyLZbUQu26na1JQg --- includes/Extensions/Batch15.php | 127 ++++++++++++++++++++++++++++++++ 1 file changed, 127 insertions(+) create mode 100644 includes/Extensions/Batch15.php diff --git a/includes/Extensions/Batch15.php b/includes/Extensions/Batch15.php new file mode 100644 index 0000000..65ca2c7 --- /dev/null +++ b/includes/Extensions/Batch15.php @@ -0,0 +1,127 @@ + 'object', 'properties' => $props]; + if ($required) $schema['required'] = $required; + return ['name' => $name, 'description' => $desc, 'scope' => $scope, + 'handler' => 'AskTamMCP\\Extensions\\Batch15::' . $method, 'inputSchema' => $schema]; + }; + + $registry->register($def('wordfence_status', 'Wordfence status: version, last scan time, and open issue count by severity.', + [], [], 'read', 'status')); + + $registry->register($def('wordfence_scan_issues', 'List current Wordfence scan issues (type, severity, short description). status = new | all (default new).', + ['status' => ['type' => 'string', 'enum' => ['new', 'all']], 'limit' => ['type' => 'integer', 'default' => 50]], [], 'read', 'scanIssues')); + + $registry->register($def('wordfence_blocked_ips', 'List IPs currently blocked by Wordfence (IP, reason, expiry).', + ['limit' => ['type' => 'integer', 'default' => 100]], [], 'read', 'blockedIps')); + + $registry->register($def('wordfence_recent_logins', 'List recent login attempts recorded by Wordfence (IP, user, success/fail, time).', + ['limit' => ['type' => 'integer', 'default' => 50], 'failed_only' => ['type' => 'boolean']], [], 'read', 'recentLogins')); + + $registry->register($def('wordfence_block_ip', 'Manually block an IP in Wordfence with a reason. Requires the Wordfence block API.', + ['ip' => ['type' => 'string'], 'reason' => ['type' => 'string']], ['ip'], 'write', 'blockIp')); + } + + private static function tbl(string $suffix): string { + global $wpdb; + return $wpdb->base_prefix . 'wf' . $suffix; + } + + public static function status(array $args): array { + $ver = defined('WORDFENCE_VERSION') ? WORDFENCE_VERSION : 'unknown'; + $last = class_exists('wfConfig') ? \wfConfig::get('lastScanCompleted') : null; + global $wpdb; + $t = self::tbl('Issues'); + $bySeverity = []; + $exists = $wpdb->get_var($wpdb->prepare('SHOW TABLES LIKE %s', $t)); + if ($exists) { + $rows = $wpdb->get_results("SELECT severity, COUNT(*) c FROM {$t} WHERE status='new' GROUP BY severity", ARRAY_A); + foreach ((array) $rows as $r) $bySeverity[(string) $r['severity']] = (int) $r['c']; + } + return ['version' => $ver, 'last_scan_completed' => $last, 'open_issues_by_severity' => $bySeverity]; + } + + public static function scanIssues(array $args): array { + global $wpdb; + $t = self::tbl('Issues'); + if (!$wpdb->get_var($wpdb->prepare('SHOW TABLES LIKE %s', $t))) { + throw new \RuntimeException('Wordfence issues table not found.'); + } + $status = ($args['status'] ?? 'new') === 'all' ? '' : "WHERE status='new'"; + $limit = max(1, (int) ($args['limit'] ?? 50)); + $rows = $wpdb->get_results("SELECT type, severity, shortMsg, status FROM {$t} {$status} ORDER BY severity DESC LIMIT {$limit}", ARRAY_A); + return ['count' => count((array) $rows), 'issues' => array_map(fn($r) => [ + 'type' => $r['type'], 'severity' => (int) $r['severity'], 'message' => $r['shortMsg'], 'status' => $r['status'], + ], (array) $rows)]; + } + + public static function blockedIps(array $args): array { + global $wpdb; + $t = self::tbl('Blocks7'); + if (!$wpdb->get_var($wpdb->prepare('SHOW TABLES LIKE %s', $t))) { + throw new \RuntimeException('Wordfence blocks table not found.'); + } + $limit = max(1, (int) ($args['limit'] ?? 100)); + $rows = $wpdb->get_results("SELECT IP, reason, blockedTime, expiration FROM {$t} ORDER BY blockedTime DESC LIMIT {$limit}", ARRAY_A); + $out = []; + foreach ((array) $rows as $r) { + $ip = @inet_ntop($r['IP']) ?: bin2hex((string) $r['IP']); + $out[] = ['ip' => $ip, 'reason' => $r['reason'], 'blocked_time' => (int) $r['blockedTime'], + 'expiration' => (int) $r['expiration'] === 0 ? 'permanent' : (int) $r['expiration']]; + } + return ['count' => count($out), 'blocked' => $out]; + } + + public static function recentLogins(array $args): array { + global $wpdb; + $t = self::tbl('Logins'); + if (!$wpdb->get_var($wpdb->prepare('SHOW TABLES LIKE %s', $t))) { + throw new \RuntimeException('Wordfence logins table not found.'); + } + $limit = max(1, (int) ($args['limit'] ?? 50)); + $where = !empty($args['failed_only']) ? 'WHERE fail=1' : ''; + $rows = $wpdb->get_results("SELECT ctime, fail, username, userID, IP FROM {$t} {$where} ORDER BY ctime DESC LIMIT {$limit}", ARRAY_A); + $out = []; + foreach ((array) $rows as $r) { + $ip = @inet_ntop($r['IP']) ?: ''; + $out[] = ['time' => (float) $r['ctime'], 'failed' => (bool) $r['fail'], + 'username' => $r['username'], 'user_id' => (int) $r['userID'], 'ip' => $ip]; + } + return ['count' => count($out), 'logins' => $out]; + } + + public static function blockIp(array $args): array { + $ip = trim((string) ($args['ip'] ?? '')); + if ($ip === '' || !filter_var($ip, FILTER_VALIDATE_IP)) throw new \RuntimeException('Valid ip required.'); + $reason = (string) ($args['reason'] ?? 'Blocked via Advanced MCP'); + if (class_exists('wfBlock') && method_exists('wfBlock', 'createIP')) { + $block = \wfBlock::createIP($reason, $ip); + if (method_exists($block, 'save')) $block->save(); + return ['ip' => $ip, 'blocked' => true, 'reason' => $reason]; + } + if (class_exists('wfConfig') && method_exists('wfConfig', 'inc')) { + // Older Wordfence fallback is not reliably scriptable. + throw new \RuntimeException('This Wordfence version lacks a scriptable block API (wfBlock::createIP).'); + } + throw new \RuntimeException('Wordfence block API unavailable.'); + } +}