PluginProbe
Atomic Edge Security – Firewall, Malware Scan and Login Security / 2.2.2
Atomic Edge Security – Firewall, Malware Scan and Login Security v2.2.2
trunk 2.0.0 2.1.0 2.2.0 2.2.1 2.2.2 2.3.0 2.4.0 2.4.1 2.4.2 2.4.3 2.4.4 2.4.5 2.4.6 2.4.7 2.4.8 2.4.9 2.5.0 2.5.1 2.5.2 2.5.3 2.5.4 2.5.5 2.5.6 2.5.7 All 29 releases
atomic-edge-security / includes / class-atomicedge-api.php

class-atomicedge-api.php in Atomic Edge Security – Firewall, Malware Scan and Login Security 2.2.2, at includes/class-atomicedge-api.php

864 lines 21.6 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * AtomicEdge API Client
4 *
5 * Handles all communication with the AtomicEdge API.
6 *
7 * @package AtomicEdge
8 */
9
10 // Prevent direct access.
11 if ( ! defined( 'ABSPATH' ) ) {
12 exit;
13 }
14
15 /**
16 * Class AtomicEdge_API
17 *
18 * API client for AtomicEdge service.
19 */
20 class AtomicEdge_API {
21
22 /**
23 * API base URL.
24 *
25 * @var string
26 */
27 private $api_url;
28
29 /**
30 * Request timeout in seconds.
31 *
32 * @var int
33 */
34 private $timeout = 30;
35
36 /**
37 * Constructor.
38 */
39 public function __construct() {
40 $this->api_url = get_option( 'atomicedge_api_url', 'https://dashboard.atomicedge.io/api/v1' );
41 $this->timeout = apply_filters( 'atomicedge_api_timeout', 30 );
42 }
43
44 /**
45 * Check if the site is connected to AtomicEdge.
46 *
47 * @return bool
48 */
49 public function is_connected() {
50 return (bool) get_option( 'atomicedge_connected', false );
51 }
52
53 /**
54 * Get the decrypted API key.
55 *
56 * @return string|false API key or false if not set.
57 */
58 public function get_api_key() {
59 $encrypted = get_option( 'atomicedge_api_key', '' );
60 if ( empty( $encrypted ) ) {
61 return false;
62 }
63 return $this->decrypt_api_key( $encrypted );
64 }
65
66 /**
67 * Encrypt the API key for storage.
68 *
69 * @param string $key Plain text API key.
70 * @return string Encrypted API key.
71 */
72 private function encrypt_api_key( $key ) {
73 $iv = substr( NONCE_KEY, 0, 16 );
74 $encrypted = openssl_encrypt(
75 $key,
76 'AES-256-CBC',
77 hash( 'sha256', AUTH_KEY . SECURE_AUTH_KEY ),
78 0,
79 $iv
80 );
81 return base64_encode( $encrypted ); // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_encode
82 }
83
84 /**
85 * Decrypt the API key.
86 *
87 * @param string $encrypted Encrypted API key.
88 * @return string|false Decrypted API key or false on failure.
89 */
90 private function decrypt_api_key( $encrypted ) {
91 $iv = substr( NONCE_KEY, 0, 16 );
92 return openssl_decrypt(
93 base64_decode( $encrypted ), // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_decode
94 'AES-256-CBC',
95 hash( 'sha256', AUTH_KEY . SECURE_AUTH_KEY ),
96 0,
97 $iv
98 );
99 }
100
101 /**
102 * Connect to AtomicEdge with an API key.
103 *
104 * @param string $api_key The API key to validate.
105 * @return array Result with success status and message/data.
106 */
107 public function connect( $api_key ) {
108 // Get the site URL without protocol and www.
109 $site_url = $this->get_normalized_site_url();
110
111 // Prepare request data.
112 $data = apply_filters(
113 'atomicedge_before_api_request',
114 array(
115 'api_key' => $api_key,
116 'site_url' => $site_url,
117 ),
118 'connect'
119 );
120
121 // Make the API request.
122 $response = $this->request( 'POST', '/connect', $data, $api_key );
123
124 // Handle response.
125 if ( ! $response['success'] ) {
126 return $response;
127 }
128
129 // Store the encrypted API key.
130 update_option( 'atomicedge_api_key', $this->encrypt_api_key( $api_key ) );
131 update_option( 'atomicedge_connected', true );
132 update_option( 'atomicedge_site_data', $response['data'] );
133
134 // Clear any cached data.
135 $this->clear_cache();
136
137 // Fire action hook.
138 do_action( 'atomicedge_connected', $response['data'] );
139
140 AtomicEdge::log( 'Successfully connected to Atomic Edge', array( 'site_url' => $site_url ) );
141
142 return array(
143 'success' => true,
144 'message' => __( 'Successfully connected to Atomic Edge!', 'atomic-edge-security' ),
145 'data' => $response['data'],
146 );
147 }
148
149 /**
150 * Disconnect from AtomicEdge.
151 *
152 * @return array Result with success status.
153 */
154 public function disconnect() {
155 // Clear stored data.
156 delete_option( 'atomicedge_api_key' );
157 update_option( 'atomicedge_connected', false );
158 delete_option( 'atomicedge_site_data' );
159
160 // Clear all cached data.
161 $this->clear_cache();
162
163 // Fire action hook.
164 do_action( 'atomicedge_disconnected' );
165
166 AtomicEdge::log( 'Disconnected from Atomic Edge' );
167
168 return array(
169 'success' => true,
170 'message' => __( 'Successfully disconnected from Atomic Edge.', 'atomic-edge-security' ),
171 );
172 }
173
174 /**
175 * Get analytics summary.
176 *
177 * @param string $period Time period (24h, 7d, 30d).
178 * @return array Analytics data or error.
179 */
180 public function get_analytics( $period = '24h' ) {
181 $cache_key = 'atomicedge_analytics_' . $period;
182 $cached = get_transient( $cache_key );
183
184 if ( false !== $cached ) {
185 return $cached;
186 }
187
188 $response = $this->request( 'GET', '/analytics', array( 'period' => $period ) );
189
190 if ( $response['success'] ) {
191 $cache_duration = apply_filters( 'atomicedge_analytics_cache_duration', 15 * MINUTE_IN_SECONDS );
192 set_transient( $cache_key, $response, $cache_duration );
193 }
194
195 return $response;
196 }
197
198 /**
199 * Get WAF logs.
200 *
201 * @param array $args Query arguments (page, per_page, search, etc.).
202 * @return array WAF logs or error.
203 */
204 public function get_waf_logs( $args = array() ) {
205 $defaults = array(
206 'page' => 1,
207 'per_page' => 50,
208 );
209 $args = wp_parse_args( $args, $defaults );
210
211 // Only include search if it has a value.
212 if ( isset( $args['search'] ) && '' === $args['search'] ) {
213 unset( $args['search'] );
214 }
215
216 $cache_key = 'atomicedge_waf_logs_' . hash( 'sha256', (string) wp_json_encode( $args ) );
217 $cached = get_transient( $cache_key );
218
219 if ( false !== $cached ) {
220 return $cached;
221 }
222
223 $response = $this->request( 'GET', '/waf-logs', $args );
224
225 if ( $response['success'] ) {
226 $cache_duration = apply_filters( 'atomicedge_waf_cache_duration', 5 * MINUTE_IN_SECONDS );
227 set_transient( $cache_key, $response, $cache_duration );
228 }
229
230 return $response;
231 }
232
233 /**
234 * Get IP access rules.
235 *
236 * @return array IP rules or error.
237 */
238 public function get_ip_rules() {
239 $cache_key = 'atomicedge_ip_rules';
240 $cached = get_transient( $cache_key );
241
242 if ( false !== $cached ) {
243 return $cached;
244 }
245
246 $response = $this->request( 'GET', '/ip-rules' );
247
248 if ( $response['success'] ) {
249 $cache_duration = apply_filters( 'atomicedge_ip_rules_cache_duration', 5 * MINUTE_IN_SECONDS );
250 set_transient( $cache_key, $response, $cache_duration );
251 }
252
253 return $response;
254 }
255
256 /**
257 * Add IP to whitelist.
258 *
259 * @param string $ip IP address or CIDR.
260 * @param string $description Optional description.
261 * @return array Result.
262 */
263 public function add_ip_whitelist( $ip, $description = '' ) {
264 $data = array(
265 'ip' => $ip,
266 'description' => $description,
267 );
268
269 $response = $this->request( 'POST', '/ip-rules/whitelist', $data );
270
271 if ( $response['success'] ) {
272 delete_transient( 'atomicedge_ip_rules' );
273 do_action( 'atomicedge_ip_added', $ip, 'whitelist' );
274 }
275
276 return $response;
277 }
278
279 /**
280 * Add IP to blacklist.
281 *
282 * @param string $ip IP address or CIDR.
283 * @param string $description Optional description.
284 * @return array Result.
285 */
286 public function add_ip_blacklist( $ip, $description = '' ) {
287 $data = array(
288 'ip' => $ip,
289 'description' => $description,
290 );
291
292 $response = $this->request( 'POST', '/ip-rules/blacklist', $data );
293
294 if ( $response['success'] ) {
295 delete_transient( 'atomicedge_ip_rules' );
296 do_action( 'atomicedge_ip_added', $ip, 'blacklist' );
297 }
298
299 return $response;
300 }
301
302 /**
303 * Remove IP from whitelist or blacklist.
304 *
305 * @param string $ip IP address or CIDR.
306 * @param string $type 'whitelist' or 'blacklist'.
307 * @return array Result.
308 */
309 public function remove_ip( $ip, $type ) {
310 $endpoint = '/access/ip/' . sanitize_key( $type ) . '/' . rawurlencode( $ip );
311 $response = $this->request( 'DELETE', $endpoint );
312
313 if ( $response['success'] ) {
314 delete_transient( 'atomicedge_ip_rules' );
315 do_action( 'atomicedge_ip_removed', $ip, $type );
316 }
317
318 return $response;
319 }
320
321 /**
322 * Get geographic access rules.
323 *
324 * @return array Geo rules or error.
325 */
326 public function get_geo_rules() {
327 $cache_key = 'atomicedge_geo_rules';
328 $cached = get_transient( $cache_key );
329
330 if ( false !== $cached ) {
331 return $cached;
332 }
333
334 $response = $this->request( 'GET', '/geo-rules' );
335
336 if ( $response['success'] ) {
337 $cache_duration = apply_filters( 'atomicedge_geo_rules_cache_duration', 5 * MINUTE_IN_SECONDS );
338 set_transient( $cache_key, $response, $cache_duration );
339 }
340
341 return $response;
342 }
343
344 /**
345 * Update geographic access rules.
346 *
347 * @param array $rules Geo rules configuration.
348 * @return array Result.
349 */
350 public function update_geo_rules( $rules ) {
351 $response = $this->request( 'PUT', '/geo-rules', $rules );
352
353 if ( $response['success'] ) {
354 delete_transient( 'atomicedge_geo_rules' );
355 }
356
357 return $response;
358 }
359
360 /**
361 * Get site information from AtomicEdge.
362 *
363 * @return array Site info or error.
364 */
365 public function get_site_info() {
366 $cache_key = 'atomicedge_site_info';
367 $cached = get_transient( $cache_key );
368
369 if ( false !== $cached ) {
370 return $cached;
371 }
372
373 $response = $this->request( 'GET', '/connect' );
374
375 if ( $response['success'] ) {
376 set_transient( $cache_key, $response, HOUR_IN_SECONDS );
377 }
378
379 return $response;
380 }
381
382 /**
383 * Check vulnerabilities for WordPress installation.
384 *
385 * Sends WordPress core version, plugins, and themes to AtomicEdge API
386 * for vulnerability checking against the Wordfence vulnerability database.
387 *
388 * @param array $installation_data Installation data with wordpress_version, plugins, themes.
389 * @return array Response with success status and vulnerability data.
390 */
391 public function check_vulnerabilities( $installation_data ) {
392 $response = $this->request( 'POST', '/wp/vulnerabilities/check', $installation_data );
393
394 return $response;
395 }
396
397 /**
398 * Get CDN status and configuration.
399 *
400 * @return array CDN status or error.
401 */
402 public function get_cdn_status() {
403 $cache_key = 'atomicedge_cdn_status';
404 $cached = get_transient( $cache_key );
405
406 if ( false !== $cached ) {
407 return $cached;
408 }
409
410 $response = $this->request( 'GET', '/cdn/status' );
411
412 if ( $response['success'] ) {
413 // Cache for 5 minutes since CDN status can change.
414 set_transient( $cache_key, $response, 5 * MINUTE_IN_SECONDS );
415 }
416
417 return $response;
418 }
419
420 /**
421 * Purge CDN cache.
422 *
423 * @return array Response with success status.
424 */
425 public function purge_cdn_cache() {
426 $response = $this->request( 'POST', '/cdn/purge' );
427
428 if ( $response['success'] ) {
429 // Clear cached CDN status since purge time changed.
430 delete_transient( 'atomicedge_cdn_status' );
431 }
432
433 return $response;
434 }
435
436 /**
437 * Update CDN optimization settings.
438 *
439 * @param array $settings Settings to update (brotli, js_minification, css_minification, image_optimization).
440 * @return array Response with success status.
441 */
442 public function update_cdn_settings( $settings ) {
443 $response = $this->request( 'PUT', '/cdn/settings', $settings );
444
445 if ( $response['success'] ) {
446 // Clear cached CDN status.
447 delete_transient( 'atomicedge_cdn_status' );
448 }
449
450 return $response;
451 }
452
453 // =========================================================================
454 // Adaptive Defense API Methods
455 // =========================================================================
456
457 /**
458 * Get Adaptive Defense status and overview data.
459 *
460 * @return array Status data or error.
461 */
462 public function get_adaptive_defense() {
463 $cache_key = 'atomicedge_adaptive_defense';
464 $cached = get_transient( $cache_key );
465
466 if ( false !== $cached ) {
467 return $cached;
468 }
469
470 $response = $this->request( 'GET', '/adaptive-defense' );
471
472 if ( $response['success'] ) {
473 // Cache for 2 minutes since this data can change frequently.
474 set_transient( $cache_key, $response, 2 * MINUTE_IN_SECONDS );
475 }
476
477 return $response;
478 }
479
480 /**
481 * Get paginated list of actor profiles.
482 *
483 * @param array $args Query arguments (page, per_page, filter, search).
484 * @return array Actor profiles or error.
485 */
486 public function get_actor_profiles( $args = array() ) {
487 $defaults = array(
488 'page' => 1,
489 'per_page' => 25,
490 'filter' => 'all',
491 );
492 $args = wp_parse_args( $args, $defaults );
493
494 $cache_key = 'atomicedge_actors_' . hash( 'sha256', (string) wp_json_encode( $args ) );
495 $cached = get_transient( $cache_key );
496
497 if ( false !== $cached ) {
498 return $cached;
499 }
500
501 $response = $this->request( 'GET', '/adaptive-defense/actors', $args );
502
503 if ( $response['success'] ) {
504 set_transient( $cache_key, $response, 2 * MINUTE_IN_SECONDS );
505 }
506
507 return $response;
508 }
509
510 /**
511 * Get paginated list of threat detections.
512 *
513 * @param array $args Query arguments (page, per_page, status).
514 * @return array Threat detections or error.
515 */
516 public function get_threat_detections( $args = array() ) {
517 $defaults = array(
518 'page' => 1,
519 'per_page' => 25,
520 'status' => 'all',
521 );
522 $args = wp_parse_args( $args, $defaults );
523
524 $cache_key = 'atomicedge_detections_' . hash( 'sha256', (string) wp_json_encode( $args ) );
525 $cached = get_transient( $cache_key );
526
527 if ( false !== $cached ) {
528 return $cached;
529 }
530
531 $response = $this->request( 'GET', '/adaptive-defense/detections', $args );
532
533 if ( $response['success'] ) {
534 set_transient( $cache_key, $response, 2 * MINUTE_IN_SECONDS );
535 }
536
537 return $response;
538 }
539
540 /**
541 * Get detailed information about a specific threat detection.
542 *
543 * @param int $detection_id The detection ID.
544 * @return array Detection details or error.
545 */
546 public function get_threat_detection_detail( $detection_id ) {
547 $response = $this->request( 'GET', '/adaptive-defense/detections/' . intval( $detection_id ) );
548 return $response;
549 }
550
551 /**
552 * Block an IP address via Adaptive Defense.
553 *
554 * @param string $ip IP address to block.
555 * @param int $duration_hours Duration in hours (default 24).
556 * @param bool $permanent Whether the block is permanent.
557 * @return array Result.
558 */
559 public function block_ip( $ip, $duration_hours = 24, $permanent = false ) {
560 $data = array(
561 'ip' => $ip,
562 'duration_hours' => $duration_hours,
563 'permanent' => $permanent,
564 );
565
566 $response = $this->request( 'POST', '/adaptive-defense/block', $data );
567
568 if ( $response['success'] ) {
569 // Clear caches.
570 $this->clear_adaptive_defense_cache();
571 }
572
573 return $response;
574 }
575
576 /**
577 * Unblock an IP address via Adaptive Defense.
578 *
579 * @param string $ip IP address to unblock.
580 * @return array Result.
581 */
582 public function unblock_ip( $ip ) {
583 $response = $this->request( 'POST', '/adaptive-defense/unblock', array( 'ip' => $ip ) );
584
585 if ( $response['success'] ) {
586 $this->clear_adaptive_defense_cache();
587 }
588
589 return $response;
590 }
591
592 /**
593 * Delete an actor profile.
594 *
595 * @param int $actor_id The actor profile ID.
596 * @return array Result.
597 */
598 public function delete_actor_profile( $actor_id ) {
599 $response = $this->request( 'DELETE', '/adaptive-defense/actors/' . intval( $actor_id ) );
600
601 if ( $response['success'] ) {
602 $this->clear_adaptive_defense_cache();
603 }
604
605 return $response;
606 }
607
608 /**
609 * Dismiss a threat detection.
610 *
611 * @param int $detection_id The detection ID.
612 * @return array Result.
613 */
614 public function dismiss_threat_detection( $detection_id ) {
615 $response = $this->request( 'POST', '/adaptive-defense/detections/' . intval( $detection_id ) . '/dismiss' );
616
617 if ( $response['success'] ) {
618 $this->clear_adaptive_defense_cache();
619 }
620
621 return $response;
622 }
623
624 /**
625 * Clear Adaptive Defense related caches.
626 *
627 * @return void
628 */
629 private function clear_adaptive_defense_cache() {
630 global $wpdb;
631
632 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching
633 $wpdb->query(
634 $wpdb->prepare(
635 "DELETE FROM {$wpdb->options} WHERE option_name LIKE %s OR option_name LIKE %s OR option_name LIKE %s OR option_name LIKE %s",
636 '_transient_atomicedge_adaptive%',
637 '_transient_timeout_atomicedge_adaptive%',
638 '_transient_atomicedge_actors%',
639 '_transient_atomicedge_detections%'
640 )
641 );
642 }
643
644 /**
645 * Make an API request.
646 *
647 * @param string $method HTTP method (GET, POST, PUT, DELETE).
648 * @param string $endpoint API endpoint.
649 * @param array $data Request data.
650 * @param string|null $api_key Optional API key override.
651 * @return array Response with success status and data/error.
652 */
653 private function request( $method, $endpoint, $data = array(), $api_key = null ) {
654 $api_key = $api_key ?? $this->get_api_key();
655
656 if ( ! $api_key && '/connect' !== $endpoint ) {
657 return array(
658 'success' => false,
659 'error' => __( 'Not connected to Atomic Edge.', 'atomic-edge-security' ),
660 );
661 }
662
663 $url = $this->api_url . $endpoint;
664
665 // Add query params for GET requests.
666 if ( 'GET' === $method && ! empty( $data ) ) {
667 $url = add_query_arg( $data, $url );
668 }
669
670 $args = array(
671 'method' => $method,
672 'timeout' => $this->timeout,
673 'headers' => array(
674 'Content-Type' => 'application/json',
675 'Accept' => 'application/json',
676 'X-AtomicEdge-Key' => $api_key,
677 ),
678 );
679
680 // Add body for non-GET requests.
681 if ( 'GET' !== $method && ! empty( $data ) ) {
682 $args['body'] = wp_json_encode( $data );
683 }
684
685 AtomicEdge::log( "API Request: {$method} {$endpoint}" );
686
687 $response = wp_remote_request( $url, $args );
688
689 // Check for WP error.
690 if ( is_wp_error( $response ) ) {
691 AtomicEdge::log( 'API Error', $response->get_error_message() );
692 return array(
693 'success' => false,
694 'error' => $response->get_error_message(),
695 );
696 }
697
698 $code = wp_remote_retrieve_response_code( $response );
699 $body = wp_remote_retrieve_body( $response );
700 $data = json_decode( $body, true );
701
702 // Apply response filter.
703 $data = apply_filters( 'atomicedge_after_api_response', $data, $endpoint );
704
705 // Handle HTTP errors.
706 if ( $code >= 400 ) {
707 $error_message = isset( $data['error'] ) ? $data['error'] : __( 'An error occurred.', 'atomic-edge-security' );
708 if ( isset( $data['message'] ) ) {
709 $error_message = $data['message'];
710 }
711 AtomicEdge::log( "API Error ({$code})", $error_message );
712 return array(
713 'success' => false,
714 'error' => $error_message,
715 'code' => $code,
716 );
717 }
718
719 // Extract nested data if API returns standard response format.
720 // The API returns {"success": true, "data": {...}}, so we extract the inner data.
721 if ( isset( $data['success'] ) && true === $data['success'] && isset( $data['data'] ) ) {
722 return array(
723 'success' => true,
724 'data' => $data['data'],
725 );
726 }
727
728 // Handle API-level errors.
729 if ( isset( $data['success'] ) && false === $data['success'] ) {
730 $error_message = isset( $data['message'] ) ? $data['message'] : __( 'An error occurred.', 'atomic-edge-security' );
731 if ( isset( $data['error'] ) ) {
732 $error_message = $data['error'];
733 }
734 return array(
735 'success' => false,
736 'error' => $error_message,
737 );
738 }
739
740 // Fallback for non-standard responses.
741 return array(
742 'success' => true,
743 'data' => $data,
744 );
745 }
746
747 /**
748 * Get normalized site URL (without protocol and www).
749 *
750 * @return string Normalized URL.
751 */
752 private function get_normalized_site_url() {
753 $url = home_url();
754 $url = preg_replace( '#^https?://#', '', $url );
755 $url = preg_replace( '#^www\.#', '', $url );
756 $url = rtrim( $url, '/' );
757 return $url;
758 }
759
760 /**
761 * Clear all cached API data.
762 *
763 * @return void
764 */
765 public function clear_cache() {
766 global $wpdb;
767
768 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching
769 $wpdb->query(
770 $wpdb->prepare(
771 "DELETE FROM {$wpdb->options} WHERE option_name LIKE %s OR option_name LIKE %s",
772 '_transient_atomicedge_%',
773 '_transient_timeout_atomicedge_%'
774 )
775 );
776 }
777
778 /**
779 * Validate an IP address or CIDR range.
780 *
781 * @param string $ip IP address or CIDR.
782 * @return bool True if valid.
783 */
784 public function is_valid_ip( $ip ) {
785 // Check for CIDR notation.
786 if ( strpos( $ip, '/' ) !== false ) {
787 list( $ip_part, $mask ) = explode( '/', $ip );
788
789 // Validate IP part.
790 if ( ! filter_var( $ip_part, FILTER_VALIDATE_IP ) ) {
791 return false;
792 }
793
794 // Validate mask.
795 $mask = (int) $mask;
796 if ( filter_var( $ip_part, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4 ) ) {
797 return $mask >= 0 && $mask <= 32;
798 } else {
799 return $mask >= 0 && $mask <= 128;
800 }
801 }
802
803 return (bool) filter_var( $ip, FILTER_VALIDATE_IP );
804 }
805
806 /**
807 * Get malware detection signatures from the Atomic Edge API.
808 *
809 * Signatures are cached locally to avoid repeated API calls.
810 * The cache is refreshed every 24 hours or when manually cleared.
811 *
812 * @param bool $force_refresh Force a refresh from the API.
813 * @return array|false Signature data or false on error.
814 */
815 public function get_malware_signatures( $force_refresh = false ) {
816 $cache_key = 'atomicedge_malware_signatures';
817
818 // Check for cached signatures unless force refresh.
819 if ( ! $force_refresh ) {
820 $cached = get_transient( $cache_key );
821 if ( false !== $cached && is_array( $cached ) && ! empty( $cached['patterns'] ) ) {
822 return $cached;
823 }
824 }
825
826 // Fetch from API.
827 $response = $this->request( 'GET', '/wp/malware-signatures' );
828
829 if ( ! $response['success'] || empty( $response['data'] ) ) {
830 // On API failure, try to use stale cache.
831 $stale = get_option( 'atomicedge_malware_signatures_backup' );
832 if ( ! empty( $stale ) && is_array( $stale ) ) {
833 AtomicEdge::log( 'Using stale malware signature cache due to API failure' );
834 return $stale;
835 }
836
837 AtomicEdge::log( 'Failed to fetch malware signatures', $response );
838 return false;
839 }
840
841 $signatures = $response['data'];
842
843 // Cache for 24 hours.
844 $cache_duration = apply_filters( 'atomicedge_malware_signatures_cache_duration', DAY_IN_SECONDS );
845 set_transient( $cache_key, $signatures, $cache_duration );
846
847 // Also save as backup for API failures.
848 update_option( 'atomicedge_malware_signatures_backup', $signatures, false );
849
850 AtomicEdge::log( 'Malware signatures updated', array( 'version' => $signatures['version'] ?? 'unknown' ) );
851
852 return $signatures;
853 }
854
855 /**
856 * Clear malware signature cache.
857 *
858 * @return void
859 */
860 public function clear_malware_signature_cache() {
861 delete_transient( 'atomicedge_malware_signatures' );
862 }
863 }
864