PluginProbe
Defender Security – Malware Scanner, Login Security & Firewall / 6.2.4
Defender Security – Malware Scanner, Login Security & Firewall v6.2.4
6.3.0 6.2.3 6.2.4 6.2.0 6.2.1 6.2.2 6.1.0 5.3.1 5.4.0 5.4.1 5.5.0 5.5.1 5.6.0 5.6.1 5.6.2 5.7.0 5.7.1 5.7.2 5.8.0 5.8.1 5.9.0 6.0.0 6.0.1 3.0.1 3.1.0 All 141 releases
defender-security / src / component / class-audit.php

class-audit.php in Defender Security – Malware Scanner, Login Security & Firewall 6.2.4, at src/component/class-audit.php

354 lines 10.6 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Responsible for handling audit logs.
4 *
5 * @package WP_Defender\Component
6 */
7
8 namespace WP_Defender\Component;
9
10 use WP_Error;
11 use DateTime;
12 use Exception;
13 use Countable;
14 use DateInterval;
15 use WP_Defender\Traits\IO;
16 use WP_Defender\Component;
17 use WP_Defender\Traits\Formats;
18 use Calotes\Helper\Array_Cache;
19 use WP_Defender\Model\Audit_Log;
20 use WP_Defender\Behavior\WPMUDEV;
21 use WP_Defender\Model\Setting\Audit_Logging;
22
23 /**
24 * Provides methods for fetching, querying, and managing audit logs.
25 */
26 class Audit extends Component {
27
28 use IO;
29 use Formats;
30
31 public const AUDIT_LOG = 'audit.log';
32 public const CACHE_LAST_CHECKPOINT = 'wd_audit_fetch_checkpoint';
33
34 /**
35 * Fetches audit logs, either from local storage or via API if not available locally.
36 *
37 * @param int $date_from Start date for fetching logs.
38 * @param int $date_to End date for fetching logs.
39 * @param array $events Specific events to fetch.
40 * @param string $user_id User ID to filter logs.
41 * @param string $ip IP address to filter logs.
42 * @param int $paged Pagination page number.
43 * @param int $per_page Number of logs per page.
44 *
45 * @return Audit_Log[]|WP_Error Returns an array of Audit_Log objects or WP_Error on failure.
46 * @throws Exception Throws exception on failure.
47 */
48 public function fetch( $date_from, $date_to, $events = array(), $user_id = '', $ip = '', $paged = 1, $per_page = 10 ) {
49 $internal = Audit_Log::query( $date_from, $date_to, $events, $user_id, $ip, $paged, $per_page );
50 $this->log( sprintf( 'Found %s from local', count( $internal ) ), self::AUDIT_LOG );
51 if ( ! wd_di()->get( WPMUDEV::class )->is_pro() ) {
52 return $internal;
53 }
54 $checkpoint = get_site_option( self::CACHE_LAST_CHECKPOINT );
55 if ( false === $checkpoint ) {
56 // This case where user install the plugin, have some local data but never reach to Logs page, then check point will be today.
57 $checkpoint = time();
58 }
59 $checkpoint = (int) $checkpoint;
60 $date_from = ! is_int( $date_from ) ? (int) $date_from : $date_from;
61 if ( 0 === count( $internal ) && $checkpoint > $date_from ) {
62 // Have to fetch from API.
63 $this->log( 'fetch from cloud', self::AUDIT_LOG );
64 // Todo:need $paged as 'nopaging'-arg?
65 $cloud = $this->query_from_api( $date_from, $date_to );
66 if ( is_wp_error( $cloud ) ) {
67 $this->log( sprintf( 'Fetch error %s', $cloud->get_error_message() ), self::AUDIT_LOG );
68
69 return $cloud;
70 }
71 if ( count( $cloud ) ) {
72 // No data from cloud too.
73 Audit_Log::mass_insert( $cloud );
74 // Because this is roughly fetch, so we have to filter out again using the local data.
75 $internal = Audit_Log::query( $date_from, $date_to, $events, $user_id, $ip, $paged, $per_page );
76 }
77 // Cache the last time fetch, this will be useful in case of mixed data.
78 update_site_option( self::CACHE_LAST_CHECKPOINT, $date_from );
79 // This case we have the data, however, maybe it can be out of the cached range, so we have to check.
80 // Note that, the out of range only happen with date_from, as the local always have the newest data.
81 } elseif ( $checkpoint > $date_from ) {
82 // We have some data out of range, fetch and cache.
83 $this->log(
84 sprintf(
85 'checkpoint %s - date from %s',
86 wp_date( 'Y-m-d H:i:s', $checkpoint ),
87 wp_date( 'Y-m-d H:i:s', $date_from )
88 ),
89 self::AUDIT_LOG
90 );
91 $cloud = $this->query_from_api( $date_from, $checkpoint );
92 if ( is_wp_error( $cloud ) ) {
93 $this->log( sprintf( 'Fetch error %s', $cloud->get_error_message() ), self::AUDIT_LOG );
94
95 return $cloud;
96 }
97 if ( is_array( $cloud ) ) {
98 // Silence the error here, as we actually have data.
99 Audit_Log::mass_insert( $cloud );
100 $internal = Audit_Log::query( $date_from, $date_to, $events, $user_id, $ip, $paged, $per_page );
101 // Cache the last time fetch, this will be useful in case of mixed data.
102 update_site_option( self::CACHE_LAST_CHECKPOINT, $date_from );
103 }
104 }
105
106 return $internal;
107 }
108
109 /**
110 * Queries audit logs from the API.
111 *
112 * @param int $date_from Start date for the query.
113 * @param int $date_to End date for the query.
114 *
115 * @return array|WP_Error Returns an array of logs or WP_Error on failure.
116 * @throws Exception Throws exception on failure.
117 */
118 public function query_from_api( $date_from, $date_to ) {
119 $date_format = 'Y-m-d H:i:s';
120 $date_to = wp_date( $date_format, $date_to );
121 $date_from = wp_date( $date_format, $date_from );
122 $args = array(
123 'site_url' => network_site_url(),
124 'order_by' => 'timestamp',
125 'order' => 'desc',
126 'nopaging' => true,
127 'timezone' => get_option( 'gmt_offset' ),
128 'date_from' => $date_from,
129 'date_to' => $date_to,
130 );
131
132 $this->attach_behavior( WPMUDEV::class, WPMUDEV::class );
133 $data = $this->make_wpmu_request(
134 WPMUDEV::API_AUDIT,
135 $args,
136 array( 'method' => 'GET' ),
137 true
138 );
139
140 if ( is_wp_error( $data ) ) {
141 $this->log( sprintf( 'Fetch error %s', $data->get_error_message() ), self::AUDIT_LOG );
142
143 return $data;
144 }
145
146 if ( 'success' !== $data['status'] ) {
147 return new WP_Error(
148 Error_Code::API_ERROR,
149 esc_html__( 'Something wrong happen, please try again!', 'defender-security' )
150 );
151 }
152
153 return $data['data'];
154 }
155
156
157 /**
158 * Delete all local audit log entries from database and reset checkpoint.
159 *
160 * @return void
161 */
162 public function reset(): void {
163 Audit_Log::truncate();
164 delete_site_option( self::CACHE_LAST_CHECKPOINT );
165 }
166
167 /**
168 * Cleans up old logs based on storage settings.
169 *
170 * @throws Exception When the $duration cannot be parsed as an interval.
171 */
172 public function audit_clean_up_logs() {
173 $audit_settings = wd_di()->get( Audit_Logging::class );
174 $interval = $this->calculate_date_interval( $audit_settings->storage_days );
175 // Since v5.7.0.
176 $interval = apply_filters( 'wpdef_audit_logs_store_backward', $interval );
177 $interval = is_string( $interval ) ? $interval : (string) $interval;
178
179 try {
180 $interval_obj = new DateInterval( $interval );
181 } catch ( Exception ) {
182 // Fallback if the filter supplied an incorrect value.
183 $interval_obj = new DateInterval( 'P6M' );
184 }
185
186 $date_from = ( new DateTime() )->setTimezone( wp_timezone() )
187 ->sub( new DateInterval( 'P1Y' ) )
188 ->setTime( 0, 0, 0 );
189 $date_to = ( new DateTime() )->setTimezone( wp_timezone() )
190 ->sub( $interval_obj );
191
192 if ( $date_from < $date_to ) {
193 // Count the logs that should be deleted.
194 $logs_count = Audit_Log::count( $date_from->getTimestamp(), $date_to->getTimestamp() );
195 if ( $logs_count > 0 ) {
196 $this->log( 'Cleaning up old logs from ' . $date_from->format( 'Y-m-d H:i:s' ) . ' to ' . $date_to->format( 'Y-m-d H:i:s' ), self::AUDIT_LOG );
197 // Since v5.0.0.
198 $delete_count = apply_filters( 'wpdef_audit_limit_deleted_logs', 50 );
199 Audit_Log::delete_old_logs(
200 $date_from->getTimestamp(),
201 $date_to->getTimestamp(),
202 is_int( $delete_count ) ? $delete_count : (int) $delete_count,
203 );
204 }
205 }
206 }
207
208 /**
209 * Sends data to the API using cURL.
210 *
211 * @param array $data Data to be sent to the API.
212 *
213 * @return mixed Returns the response from the API.
214 */
215 public function curl_to_api( $data ) {
216 $this->attach_behavior( WPMUDEV::class, WPMUDEV::class );
217 $ret = $this->make_wpmu_request(
218 WPMUDEV::API_AUDIT_ADD,
219 $data,
220 array(
221 'method' => 'POST',
222 'timeout' => 3,
223 'headers' => array(
224 'apikey' => $this->get_apikey(),
225 ),
226 )
227 );
228
229 return $ret;
230 }
231
232 /**
233 * Strips the protocol from a URL.
234 *
235 * @param string $url URL to process.
236 *
237 * @return string Returns the URL without the protocol.
238 */
239 private function strip_protocol( $url ) {
240 $parts = wp_parse_url( $url );
241 $host = $parts['host'] . ( $parts['path'] ?? null );
242
243 return rtrim( $host, '/' );
244 }
245
246 /**
247 * Returns the API endpoint.
248 *
249 * @return string Returns the API endpoint URL.
250 */
251 private function get_endpoint(): string {
252 return defined( 'WPMUDEV_CUSTOM_AUDIT_SERVER' )
253 ? constant( 'WPMUDEV_CUSTOM_AUDIT_SERVER' )
254 : 'https://audit.wpmudev.org/';
255 }
256
257 /**
258 * Queue all the events listeners, so we can listen and build log base on user behaviors.
259 * Never catch if it runs from WP CLI or CRON.
260 */
261 public function enqueue_event_listener() {
262 if ( ! wp_doing_cron() && ! defender_is_wp_cli() ) {
263 $events_class = array(
264 new Component\Audit\Comment_Audit(),
265 new Component\Audit\Core_Audit(),
266 new Component\Audit\Media_Audit(),
267 new Component\Audit\Post_Audit(),
268 new Component\Audit\User_Audit(),
269 new Component\Audit\Options_Audit(),
270 new Component\Audit\Menu_Audit(),
271 new Component\Audit\Theme_Audit(),
272 new Component\Audit\Feature_Audit(),
273 new Component\Audit\Password_Audit(),
274 new Component\Audit\Application_Password_Audit(),
275 );
276
277 foreach ( $events_class as $class ) {
278 // since 2.4.7.
279 $hooks = apply_filters( 'wp_defender_audit_hooks', $class->get_hooks() );
280 foreach ( $hooks as $key => $hook ) {
281 $func = function () use ( $key, $hook, $class ) {
282 global $wp_filter;
283 if ( isset( $wp_filter['gettext'] ) ) {
284 $gettext_callbacks = $wp_filter['gettext']->callbacks;
285
286 // Disable all gettext filters.
287 $wp_filter['gettext']->callbacks = array();
288 }
289
290 // This is arguments of the hook.
291 $args = func_get_args();
292 // This is hook data, defined in each event class.
293 $class->build_log_data( $key, $args, $hook );
294
295 // Add all filters back for gettext.
296 if ( isset( $wp_filter['gettext'], $gettext_callbacks ) ) {
297 $wp_filter['gettext']->callbacks = $gettext_callbacks;
298 }
299 };
300 add_action(
301 $key,
302 $func,
303 11,
304 is_array( $hook['args'] ) || $hook['args'] instanceof Countable ? count( $hook['args'] ) : 0
305 );
306 }
307 }
308 }
309 }
310
311 /**
312 * Logs audit events.
313 */
314 public function log_audit_events() {
315 $events = Array_Cache::get( 'logs', 'audit', array() );
316
317 if ( ! ( is_array( $events ) || $events instanceof Countable ? count( $events ) : 0 )
318 || ! class_exists( Audit_Log::class )
319 ) {
320 return;
321 }
322 $model = new Audit_Log();
323
324 if ( ( is_array( $events ) || $events instanceof Countable ? count( $events ) : 0 ) > 1 ) {
325 if ( $model->has_method( 'mass_insert' ) ) {
326 $model->mass_insert( $events, 0 );
327
328 return;
329 }
330 }
331
332 foreach ( $events as $event ) {
333 $model->import( $event );
334 $model->synced = 0;
335 $model->save();
336 }
337 }
338
339 /**
340 * Get audit events per week.
341 */
342 public static function get_audit_events_per_week(): int {
343 $timezone = wp_timezone();
344 $date_to = ( new DateTime( 'now', $timezone ) )
345 ->setTime( 23, 59, 59 )
346 ->getTimestamp();
347 $date_from = ( new DateTime( '-7 days', $timezone ) )
348 ->setTime( 0, 0, 0 )
349 ->getTimestamp();
350
351 return Audit_Log::count( $date_from, $date_to );
352 }
353 }
354