e67c1e0e9c
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Q8t9GEyyLZbUQu26na1JQg
128 lines
6.6 KiB
PHP
128 lines
6.6 KiB
PHP
<?php
|
|
/**
|
|
* Batch 15 — Wordfence security (Pro-equivalent, net-new).
|
|
*
|
|
* Status, scan issues, blocked IPs, recent logins, and manual IP block.
|
|
* Reads stable Wordfence DB tables (wfIssues / wfBlocks7 / wfLogins) and
|
|
* wfConfig; every handler checks the API is present and throws clearly if not.
|
|
* Guarded by WORDFENCE_VERSION / wfConfig — inert unless Wordfence is active.
|
|
* GPL-2.0-or-later. See NOTICE.md.
|
|
*/
|
|
|
|
namespace AskTamMCP\Extensions;
|
|
|
|
defined('ABSPATH') || exit;
|
|
|
|
class Batch15 {
|
|
|
|
public static function register($registry): void {
|
|
if (!defined('WORDFENCE_VERSION') && !class_exists('wfConfig')) return;
|
|
|
|
$def = function (string $name, string $desc, array $props, array $required, string $scope, string $method) {
|
|
$schema = ['type' => '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.');
|
|
}
|
|
}
|