__( 'Database Security Check', 'agentic-admin' ), 'description' => __( 'Scan the WordPress database for indicators of compromise: injected scripts, base64 payloads, eval() calls, rogue cron jobs, and suspicious users.', 'agentic-admin' ), 'category' => 'sre-tools', 'input_schema' => array( 'type' => 'object', 'default' => array(), 'properties' => array(), 'additionalProperties' => false, ), 'output_schema' => array( 'type' => 'object', 'properties' => array( 'success' => array( 'type' => 'boolean', 'description' => __( 'Whether the scan completed.', 'agentic-admin' ), ), 'message' => array( 'type' => 'string', 'description' => __( 'Status message.', 'agentic-admin' ), ), 'total_issues' => array( 'type' => 'integer', 'description' => __( 'Total suspicious findings.', 'agentic-admin' ), ), 'checks' => array( 'type' => 'array', 'description' => __( 'Results of each individual check.', 'agentic-admin' ), ), ), ), 'execute_callback' => 'agentic_admin_execute_database_check', 'permission_callback' => function () { return current_user_can( 'manage_options' ); }, 'meta' => array( 'show_in_rest' => true, 'annotations' => array( 'readonly' => true, 'destructive' => false, 'idempotent' => true, ), ), ), // JS configuration for chat interface. array( 'keywords' => array( 'database check', 'db security', 'scan database', 'malware database', 'injected code', 'base64', 'eval', 'spam pages', 'rogue cron', 'suspicious users' ), 'initialMessage' => __( 'Scanning the database for indicators of compromise...', 'agentic-admin' ), ) ); } /** * Execute the database-check ability. * * @param array $input Input parameters. * @return array */ function agentic_admin_execute_database_check( array $input = array() ): array { // phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter.Found -- Required parameter for callback signature. $checks = array(); $total_issues = 0; $total_raw = 0; /** * Filters the risk score threshold for the database security check. * * Only findings at or above this threshold are included in results. * * @since 0.9.6 * * @param float $threshold Risk score threshold (default 6.0). */ $threshold = apply_filters( 'agentic_admin_db_check_threshold', 6.0 ); // Run all checks. $check_functions = array( 'agentic_admin_check_options_base64', 'agentic_admin_check_options_eval', 'agentic_admin_check_options_suspicious_urls', 'agentic_admin_check_posts_scripts', 'agentic_admin_check_posts_base64_eval', 'agentic_admin_check_posts_iframes', 'agentic_admin_check_posts_hidden_links', 'agentic_admin_check_usermeta_injections', 'agentic_admin_check_suspicious_admins', 'agentic_admin_check_rogue_cron_jobs', 'agentic_admin_check_siteurl_integrity', 'agentic_admin_check_comments_injections', 'agentic_admin_check_widgets_injections', ); /** * Filters the list of database check functions to run. * * Plugins can add their own check functions or remove built-in ones. * Each function must return an array with keys: name, description, count, findings. * Each finding must include a 'risk_score' key (float 1.0–10.0). * * @since 0.9.6 * * @param array $check_functions Array of callable function names. */ $check_functions = apply_filters( 'agentic_admin_db_check_functions', $check_functions ); foreach ( $check_functions as $func ) { $result = $func(); // Filter findings to only those at or above the risk threshold. $all_findings = $result['findings']; $filtered_findings = array_values( array_filter( $all_findings, function ( $f ) use ( $threshold ) { return isset( $f['risk_score'] ) && $f['risk_score'] >= $threshold; } ) ); $result['findings'] = $filtered_findings; $result['count'] = count( $filtered_findings ); $result['total_raw'] = count( $all_findings ); $result['filtered_below'] = count( $all_findings ) - count( $filtered_findings ); $total_raw += count( $all_findings ); $total_issues += $result['count']; $checks[] = $result; } $filtered_out = $total_raw - $total_issues; if ( 0 === $total_issues ) { $message = 0 === $total_raw ? __( 'Database scan complete. No findings detected.', 'agentic-admin' ) : sprintf( /* translators: %d: number of low-risk findings filtered out */ _n( 'Database scan complete. No high-risk findings. %d low-risk finding was filtered out (below 6.0/10).', 'Database scan complete. No high-risk findings. %d low-risk findings were filtered out (below 6.0/10).', $filtered_out, 'agentic-admin' ), $filtered_out ); } else { $message = sprintf( /* translators: 1: number of high-risk findings, 2: risk threshold, 3: number filtered out */ __( 'Database scan complete. Found %1$d high-risk finding(s) (score >= %2$s/10). %3$d low-risk finding(s) filtered out.', 'agentic-admin' ), $total_issues, number_format( $threshold, 1 ), $filtered_out ); } return array( 'success' => true, 'message' => $message, 'total_issues' => $total_issues, 'total_scanned' => $total_raw, 'filtered_out' => $filtered_out, 'risk_threshold' => $threshold, 'checks' => $checks, ); } // --------------------------------------------------------------------------- // Individual check functions. // Each returns: [ 'name' => string, 'description' => string, 'count' => int, 'findings' => array ] // --------------------------------------------------------------------------- /** * Check options table for base64_decode payloads. * * Malware commonly stores base64-encoded payloads in wp_options to survive updates. * * @return array Check result. */ function agentic_admin_check_options_base64(): array { global $wpdb; // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching $rows = $wpdb->get_results( $wpdb->prepare( "SELECT option_name, LEFT(option_value, 200) AS option_value_preview FROM {$wpdb->options} WHERE option_value LIKE %s LIMIT 50", '%' . $wpdb->esc_like( 'base64_decode' ) . '%' ) ); // Known legitimate options that may contain base64 references. $known_safe = array( 'active_plugins', 'uninstall_plugins', 'rewrite_rules', 'cron' ); $findings = array(); foreach ( $rows as $row ) { $findings[] = array( 'option_name' => $row->option_name, 'preview' => $row->option_value_preview, 'risk_score' => in_array( $row->option_name, $known_safe, true ) ? 2.0 : 8.5, ); } return array( 'name' => __( 'Options: base64_decode', 'agentic-admin' ), 'description' => __( 'Options containing base64_decode() calls — often used to hide malware.', 'agentic-admin' ), 'count' => count( $findings ), 'findings' => $findings, ); } /** * Check options table for eval() injections. * * Eval() in stored options is almost always malicious. * * @return array Check result. */ function agentic_admin_check_options_eval(): array { global $wpdb; // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching $rows = $wpdb->get_results( $wpdb->prepare( "SELECT option_name, LEFT(option_value, 200) AS option_value_preview FROM {$wpdb->options} WHERE option_value LIKE %s LIMIT 50", '%' . $wpdb->esc_like( 'eval(' ) . '%' ) ); $findings = array(); foreach ( $rows as $row ) { $findings[] = array( 'option_name' => $row->option_name, 'preview' => $row->option_value_preview, 'risk_score' => 9.5, // eval() in options is almost always malicious. ); } return array( 'name' => __( 'Options: eval() injections', 'agentic-admin' ), 'description' => __( 'Options containing eval() calls — a strong indicator of injected code.', 'agentic-admin' ), 'count' => count( $findings ), 'findings' => $findings, ); } /** * Check options for suspicious external URLs. * * Attackers inject redirect URLs or SEO spam links into options like * siteurl, home, or custom options. * * @return array Check result. */ function agentic_admin_check_options_suspicious_urls(): array { global $wpdb; // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching $rows = $wpdb->get_results( $wpdb->prepare( "SELECT option_name, LEFT(option_value, 200) AS option_value_preview FROM {$wpdb->options} WHERE option_value LIKE %s OR option_value LIKE %s OR option_value LIKE %s OR option_value LIKE %s LIMIT 50", '%' . $wpdb->esc_like( 'esc_like( 'document.write' ) . '%', '%' . $wpdb->esc_like( 'window.location' ) . '%', '%' . $wpdb->esc_like( 'String.fromCharCode' ) . '%' ) ); $findings = array(); foreach ( $rows as $row ) { $findings[] = array( 'option_name' => $row->option_name, 'preview' => $row->option_value_preview, 'risk_score' => 8.0, // JS injection in options is highly suspicious. ); } return array( 'name' => __( 'Options: suspicious JavaScript', 'agentic-admin' ), 'description' => __( 'Options containing script tags, document.write, redirects, or obfuscated JS.', 'agentic-admin' ), 'count' => count( $findings ), 'findings' => $findings, ); } /** * Check published posts/pages for injected