Changeset 3460160
- Timestamp:
- 02/12/2026 04:40:35 PM (3 weeks ago)
- Location:
- atomic-edge-security/trunk
- Files:
-
- 4 edited
-
atomicedge.php (modified) (2 diffs)
-
includes/class-atomicedge-api.php (modified) (4 diffs)
-
includes/class-atomicedge-scanner.php (modified) (10 diffs)
-
readme.txt (modified) (2 diffs)
Legend:
- Unmodified
- Added
- Removed
-
atomic-edge-security/trunk/atomicedge.php
r3459867 r3460160 4 4 * Plugin URI: https://atomicedge.io/wordpress 5 5 * Description: Connect your WordPress site to Atomic Edge WAF/CDN for advanced security protection, analytics, and access control management. 6 * Version: 2. 2.26 * Version: 2.3.0 7 7 * Requires at least: 5.8 8 8 * Requires PHP: 7.4 … … 26 26 27 27 // Plugin constants. 28 define( 'ATOMICEDGE_VERSION', '2. 2.2' );28 define( 'ATOMICEDGE_VERSION', '2.3.0' ); 29 29 define( 'ATOMICEDGE_PLUGIN_FILE', __FILE__ ); 30 30 define( 'ATOMICEDGE_PLUGIN_DIR', plugin_dir_path( __FILE__ ) ); -
atomic-edge-security/trunk/includes/class-atomicedge-api.php
r3459867 r3460160 54 54 * Get the decrypted API key. 55 55 * 56 * Handles migration from unencrypted to encrypted storage: 57 * - If decryption fails and the stored value looks like a raw 64-char hex key, 58 * it will be re-encrypted and stored properly. 59 * 56 60 * @return string|false API key or false if not set. 57 61 */ 58 62 public function get_api_key() { 59 $ encrypted = get_option( 'atomicedge_api_key', '' );60 if ( empty( $ encrypted ) ) {63 $stored = get_option( 'atomicedge_api_key', '' ); 64 if ( empty( $stored ) ) { 61 65 return false; 62 66 } 63 return $this->decrypt_api_key( $encrypted ); 67 68 // Try normal decryption first. 69 $decrypted = $this->decrypt_api_key( $stored ); 70 71 if ( false !== $decrypted && ! empty( $decrypted ) ) { 72 return $decrypted; 73 } 74 75 // Decryption failed - check if this is a raw hex key that wasn't encrypted. 76 // Valid API keys are 64-character alphanumeric strings. 77 if ( preg_match( '/^[a-f0-9]{64}$/i', $stored ) ) { 78 // This is a raw key - re-encrypt it properly for future use. 79 AtomicEdge::log( 'Migrating unencrypted API key to encrypted storage' ); 80 update_option( 'atomicedge_api_key', $this->encrypt_api_key( $stored ) ); 81 return $stored; 82 } 83 84 // Neither valid encryption nor valid raw key format. 85 return false; 64 86 } 65 87 … … 746 768 747 769 /** 770 * Make an unauthenticated request to a public API endpoint. 771 * 772 * This is used for endpoints that don't require an API key, such as 773 * malware signatures, which should be available before site registration. 774 * 775 * @param string $method HTTP method (GET, POST, etc.). 776 * @param string $endpoint API endpoint (e.g., '/wp/public/malware-signatures'). 777 * @param array $data Request data. 778 * @return array Response with success status and data/error. 779 */ 780 private function public_request( $method, $endpoint, $data = array() ) { 781 // Public endpoints use the base API URL (not under /v1 which requires auth). 782 $base_url = preg_replace( '#/api/v1/?$#', '/api/v1', $this->api_url ); 783 $url = $base_url . $endpoint; 784 785 // Add query params for GET requests. 786 if ( 'GET' === $method && ! empty( $data ) ) { 787 $url = add_query_arg( $data, $url ); 788 } 789 790 $args = array( 791 'method' => $method, 792 'timeout' => $this->timeout, 793 'headers' => array( 794 'Content-Type' => 'application/json', 795 'Accept' => 'application/json', 796 ), 797 ); 798 799 // Add body for non-GET requests. 800 if ( 'GET' !== $method && ! empty( $data ) ) { 801 $args['body'] = wp_json_encode( $data ); 802 } 803 804 AtomicEdge::log( "Public API Request: {$method} {$endpoint}" ); 805 806 $response = wp_remote_request( $url, $args ); 807 808 // Check for WP error. 809 if ( is_wp_error( $response ) ) { 810 AtomicEdge::log( 'Public API Error', $response->get_error_message() ); 811 return array( 812 'success' => false, 813 'error' => $response->get_error_message(), 814 ); 815 } 816 817 $code = wp_remote_retrieve_response_code( $response ); 818 $body = wp_remote_retrieve_body( $response ); 819 $data = json_decode( $body, true ); 820 821 // Handle HTTP errors. 822 if ( $code >= 400 ) { 823 $error_message = isset( $data['error'] ) ? $data['error'] : __( 'An error occurred.', 'atomic-edge-security' ); 824 if ( isset( $data['message'] ) ) { 825 $error_message = $data['message']; 826 } 827 AtomicEdge::log( "Public API Error ({$code})", $error_message ); 828 return array( 829 'success' => false, 830 'error' => $error_message, 831 'code' => $code, 832 ); 833 } 834 835 // Extract nested data if API returns standard response format. 836 if ( isset( $data['success'] ) && true === $data['success'] && isset( $data['data'] ) ) { 837 return array( 838 'success' => true, 839 'data' => $data['data'], 840 ); 841 } 842 843 // Handle API-level errors. 844 if ( isset( $data['success'] ) && false === $data['success'] ) { 845 $error_message = isset( $data['message'] ) ? $data['message'] : __( 'An error occurred.', 'atomic-edge-security' ); 846 if ( isset( $data['error'] ) ) { 847 $error_message = $data['error']; 848 } 849 return array( 850 'success' => false, 851 'error' => $error_message, 852 ); 853 } 854 855 // Fallback for non-standard responses. 856 return array( 857 'success' => true, 858 'data' => $data, 859 ); 860 } 861 862 /** 748 863 * Get normalized site URL (without protocol and www). 749 864 * … … 810 925 * The cache is refreshed every 24 hours or when manually cleared. 811 926 * 927 * This endpoint is PUBLIC and does not require an API key, allowing 928 * users to scan their site before registering with AtomicEdge. 929 * 812 930 * @param bool $force_refresh Force a refresh from the API. 813 931 * @return array|false Signature data or false on error. … … 824 942 } 825 943 826 // Fetch from API.827 $response = $this-> request( 'GET', '/wp/malware-signatures' );944 // Fetch from public API endpoint (no authentication required). 945 $response = $this->public_request( 'GET', '/wp/public/malware-signatures' ); 828 946 829 947 if ( ! $response['success'] || empty( $response['data'] ) ) { -
atomic-edge-security/trunk/includes/class-atomicedge-scanner.php
r3459867 r3460160 326 326 'wp-admin' => array( 'php_files_found' => 0, 'php_files_scanned' => 0 ), 327 327 'wp-includes' => array( 'php_files_found' => 0, 'php_files_scanned' => 0 ), 328 'wp-content' => array( 'php_files_found' => 0, 'php_files_scanned' => 0 ), 328 329 'uploads' => array( 'php_files_found' => 0, 'php_files_scanned' => 0 ), 329 330 'themes' => array( 'php_files_found' => 0, 'php_files_scanned' => 0 ), 330 331 'plugins' => array( 'php_files_found' => 0, 'php_files_scanned' => 0 ), 332 'unknown' => array( 'php_files_found' => 0, 'php_files_scanned' => 0 ), 331 333 ), 332 334 // Performance timing (milliseconds). … … 1552 1554 $this->enqueue_queue_item( $run_id, 'dir', 'plugins', $mu_plugins_dir, array() ); 1553 1555 1554 // Thorough scan: include core directories and uploads checks. 1556 // Thorough scan: discover ALL directories under WordPress root. 1557 // This catches malware hidden in arbitrary folders like /hidden-backdoor/ or /cache-evil/. 1555 1558 if ( 'all' === $scan_mode ) { 1556 $this->enqueue_queue_item( $run_id, 'dir', 'wp-admin', $admin_dir, array() ); 1557 $this->enqueue_queue_item( $run_id, 'dir', 'wp-includes', $includes_dir, array() ); 1558 $this->enqueue_queue_item( $run_id, 'dir', 'uploads', $uploads_dir, array() ); 1559 } 1559 $this->discover_all_directories( $run_id, $root_path, $plugins_dir, $themes_dir, $mu_plugins_dir ); 1560 } 1561 } 1562 1563 /** 1564 * Discover and enqueue all directories under WordPress root for thorough scanning. 1565 * 1566 * This prevents malware from hiding in arbitrary folders that aren't part of 1567 * the standard WordPress directory structure. Every PHP file anywhere in the 1568 * WordPress installation will be scanned. 1569 * 1570 * @param string $run_id Run ID. 1571 * @param string $root_path WordPress root path. 1572 * @param string $plugins_dir Plugins directory (already scanned in quick mode). 1573 * @param string $themes_dir Themes directory (already scanned in quick mode). 1574 * @param string $mu_plugins_dir MU-plugins directory (already scanned in quick mode). 1575 * @return void 1576 */ 1577 private function discover_all_directories( $run_id, $root_path, $plugins_dir, $themes_dir, $mu_plugins_dir ) { 1578 // Directories that were already enqueued in quick scan or should be skipped entirely. 1579 $already_queued = array( 1580 rtrim( $plugins_dir, '/' ), 1581 rtrim( $themes_dir, '/' ), 1582 rtrim( $mu_plugins_dir, '/' ), 1583 ); 1584 1585 // Directories to skip completely (non-PHP or handled separately). 1586 $skip_dirs = array( 1587 '.git', 1588 '.svn', 1589 '.hg', 1590 'node_modules', 1591 'vendor', 1592 ); 1593 1594 // Known WordPress directories that get specific area tags. 1595 $known_areas = array( 1596 'wp-admin' => 'wp-admin', 1597 'wp-includes' => 'wp-includes', 1598 'wp-content' => 'wp-content', // Generic wp-content subdirs. 1599 ); 1600 1601 // phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged 1602 $handle = @opendir( $root_path ); 1603 if ( ! $handle ) { 1604 return; 1605 } 1606 1607 while ( false !== ( $entry = readdir( $handle ) ) ) { 1608 if ( '.' === $entry || '..' === $entry ) { 1609 continue; 1610 } 1611 1612 $full_path = rtrim( $root_path, '/' ) . '/' . $entry; 1613 1614 // Skip if not a directory or is a symlink. 1615 if ( ! is_dir( $full_path ) || is_link( $full_path ) ) { 1616 continue; 1617 } 1618 1619 // Skip known-excluded directories. 1620 if ( in_array( $entry, $skip_dirs, true ) ) { 1621 continue; 1622 } 1623 1624 // Skip if already queued (plugins, themes, mu-plugins). 1625 if ( in_array( rtrim( $full_path, '/' ), $already_queued, true ) ) { 1626 continue; 1627 } 1628 1629 // Determine the area tag based on directory name. 1630 $area = 'unknown'; 1631 if ( isset( $known_areas[ $entry ] ) ) { 1632 $area = $known_areas[ $entry ]; 1633 } 1634 1635 // For wp-content, recursively discover subdirectories. 1636 if ( 'wp-content' === $entry ) { 1637 $this->discover_wp_content_directories( $run_id, $full_path, $already_queued ); 1638 } else { 1639 // Enqueue this top-level directory for scanning. 1640 $this->enqueue_queue_item( $run_id, 'dir', $area, $full_path, array() ); 1641 } 1642 } 1643 1644 closedir( $handle ); 1645 } 1646 1647 /** 1648 * Discover and enqueue directories within wp-content that weren't already queued. 1649 * 1650 * This catches non-standard directories like: 1651 * - wp-content/cache-malware/ 1652 * - wp-content/hidden-folder/ 1653 * - wp-content/languages/ (if contains PHP, which is suspicious) 1654 * 1655 * @param string $run_id Run ID. 1656 * @param string $wp_content_path Path to wp-content. 1657 * @param array $already_queued Directories already queued (plugins, themes, mu-plugins). 1658 * @return void 1659 */ 1660 private function discover_wp_content_directories( $run_id, $wp_content_path, $already_queued ) { 1661 // phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged 1662 $handle = @opendir( $wp_content_path ); 1663 if ( ! $handle ) { 1664 return; 1665 } 1666 1667 // Known wp-content subdirectories and their area tags. 1668 $known_wp_content_areas = array( 1669 'plugins' => 'plugins', // Already handled, will be skipped. 1670 'themes' => 'themes', // Already handled, will be skipped. 1671 'mu-plugins' => 'plugins', // Already handled, will be skipped. 1672 'uploads' => 'uploads', 1673 'upgrade' => 'wp-content', 1674 'cache' => 'wp-content', // Could contain PHP from caching plugins. 1675 'languages' => 'wp-content', // Normally only .mo/.po files, but PHP here is suspicious. 1676 ); 1677 1678 while ( false !== ( $entry = readdir( $handle ) ) ) { 1679 if ( '.' === $entry || '..' === $entry ) { 1680 continue; 1681 } 1682 1683 $full_path = rtrim( $wp_content_path, '/' ) . '/' . $entry; 1684 1685 // Skip if not a directory or is a symlink. 1686 if ( ! is_dir( $full_path ) || is_link( $full_path ) ) { 1687 continue; 1688 } 1689 1690 // Skip if already queued (plugins, themes, mu-plugins). 1691 if ( in_array( rtrim( $full_path, '/' ), $already_queued, true ) ) { 1692 continue; 1693 } 1694 1695 // Determine the area tag. 1696 $area = isset( $known_wp_content_areas[ $entry ] ) ? $known_wp_content_areas[ $entry ] : 'unknown'; 1697 1698 // Enqueue for scanning. 1699 $this->enqueue_queue_item( $run_id, 'dir', $area, $full_path, array() ); 1700 } 1701 1702 closedir( $handle ); 1560 1703 } 1561 1704 … … 1969 2112 1970 2113 $is_uploads = ( 'uploads' === $area ); 2114 $is_unknown_area = in_array( $area, array( 'unknown', 'wp-content' ), true ); 1971 2115 $pattern_groups = $this->get_malware_patterns(); 1972 2116 if ( in_array( $area, array( 'wp-admin', 'wp-includes' ), true ) ) { … … 1975 2119 $pattern_groups = $this->get_refined_patterns_for_plugins(); 1976 2120 } 2121 // Unknown areas and wp-content subdirs get full pattern matching (highest suspicion). 1977 2122 1978 2123 $result = $this->scan_file_for_patterns( $filepath, $pattern_groups, $is_uploads ); … … 1984 2129 } elseif ( 'wp-includes' === $area ) { 1985 2130 $result['location_note'] = __( 'Suspicious pattern in wp-includes', 'atomic-edge-security' ); 2131 } elseif ( 'unknown' === $area ) { 2132 $result['location_note'] = __( 'Suspicious file in non-standard directory (possible hidden malware)', 'atomic-edge-security' ); 2133 $result['severity'] = 'critical'; // Elevate severity for unknown directories. 2134 } elseif ( 'wp-content' === $area ) { 2135 $result['location_note'] = __( 'Suspicious pattern in non-standard wp-content subdirectory', 'atomic-edge-security' ); 1986 2136 } 1987 2137 $state['results']['suspicious'][] = $result; … … 1993 2143 'severity' => 'high', 1994 2144 'reason' => __( 'PHP file found in uploads directory', 'atomic-edge-security' ), 2145 ); 2146 } elseif ( $is_unknown_area ) { 2147 // Any PHP file in an unknown directory is suspicious, even without pattern matches. 2148 $state['results']['suspicious'][] = array( 2149 'file' => $relative_path, 2150 'file_path' => $filepath, 2151 'type' => 'php_in_unknown_dir', 2152 'severity' => 'high', 2153 'reason' => __( 'PHP file found in non-standard directory (possible hidden malware)', 'atomic-edge-security' ), 1995 2154 ); 1996 2155 } … … 2205 2364 2206 2365 $is_uploads = ( 'uploads' === $area ); 2366 $is_unknown_area = in_array( $area, array( 'unknown', 'wp-content' ), true ); 2207 2367 $pattern_groups = $this->get_malware_patterns(); 2208 2368 if ( in_array( $area, array( 'wp-admin', 'wp-includes' ), true ) ) { … … 2211 2371 $pattern_groups = $this->get_refined_patterns_for_plugins(); 2212 2372 } 2373 // Unknown areas and wp-content subdirs get full pattern matching (highest suspicion). 2213 2374 2214 2375 $result = $this->scan_file_for_patterns( $filepath, $pattern_groups, $is_uploads ); … … 2220 2381 } elseif ( 'wp-includes' === $area ) { 2221 2382 $result['location_note'] = __( 'Suspicious pattern in wp-includes', 'atomic-edge-security' ); 2383 } elseif ( 'unknown' === $area ) { 2384 $result['location_note'] = __( 'Suspicious file in non-standard directory (possible hidden malware)', 'atomic-edge-security' ); 2385 $result['severity'] = 'critical'; // Elevate severity for unknown directories. 2386 } elseif ( 'wp-content' === $area ) { 2387 $result['location_note'] = __( 'Suspicious pattern in non-standard wp-content subdirectory', 'atomic-edge-security' ); 2222 2388 } 2223 2389 $state['results']['suspicious'][] = $result; … … 2229 2395 'severity' => 'high', 2230 2396 'reason' => __( 'PHP file found in uploads directory', 'atomic-edge-security' ), 2397 ); 2398 } elseif ( $is_unknown_area ) { 2399 // Any PHP file in an unknown directory is suspicious, even without pattern matches. 2400 $state['results']['suspicious'][] = array( 2401 'file' => $relative_path, 2402 'file_path' => $filepath, 2403 'type' => 'php_in_unknown_dir', 2404 'severity' => 'high', 2405 'reason' => __( 'PHP file found in non-standard directory (possible hidden malware)', 'atomic-edge-security' ), 2231 2406 ); 2232 2407 } -
atomic-edge-security/trunk/readme.txt
r3459867 r3460160 5 5 Tested up to: 6.9 6 6 Requires PHP: 7.4 7 Stable tag: 2. 2.27 Stable tag: 2.3.0 8 8 License: GPLv2 or later 9 9 License URI: https://www.gnu.org/licenses/gpl-2.0.html … … 110 110 111 111 == Changelog == 112 113 = 2.3.0 = 114 * NEW: Malware scanner signatures now fetched from public API (no API key required) 115 * This allows users to scan their site before registering with Atomic Edge 116 * FIX: API key migration for users who had raw keys stored (automatic re-encryption on load) 117 * IMPROVED: Test coverage for scanner with mocked API signatures 112 118 113 119 = 2.2.2 =
Note: See TracChangeset
for help on using the changeset viewer.