PluginProbe
Defender Security – Malware Scanner, Login Security & Firewall / 5.3.1
Defender Security – Malware Scanner, Login Security & Firewall v5.3.1
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 / audit / class-core-audit.php

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

525 lines 18.0 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Represents an audit system for WordPress core, themes, and plugins.
4 *
5 * @package WP_Defender\Component\Audit
6 */
7
8 namespace WP_Defender\Component\Audit;
9
10 use WP_Upgrader;
11 use WP_Defender\Model\Audit_Log;
12 use WP_Defender\Component\Security_Tweaks\WP_Version;
13
14 /**
15 * Handles the auditing of WordPress core, themes, and plugins such as plugin activation, deactivation, and
16 * installation, and theme activation, deactivation, and installation.
17 */
18 class Core_Audit extends Audit_Event {
19
20 public const ACTION_ACTIVATED = 'activated', ACTION_DEACTIVATED = 'deactivated', ACTION_INSTALLED = 'installed', ACTION_UPGRADED = 'upgraded';
21 public const FILE_ADDED = 'file_added', FILE_MODIFIED = 'file_modified';
22 public const CONTEXT_THEME = 'ct_theme', CONTEXT_PLUGIN = 'ct_plugin', CONTEXT_CORE = 'ct_core';
23
24 /**
25 * Retrieves a list of hooks that the audit system will respond to.
26 *
27 * @return array Associative array of hooks and their corresponding callback information.
28 */
29 public function get_hooks(): array {
30 $data = array(
31 'switch_theme' => array(
32 'args' => array( 'new_name', 'new_theme' ),
33 'callback' => array( self::class, 'process_activate_theme' ),
34 'event_type' => Audit_Log::EVENT_TYPE_SYSTEM,
35 'action_type' => self::ACTION_ACTIVATED,
36 ),
37 'activated_plugin' => array(
38 'args' => array( 'plugin' ),
39 'text' => sprintf(
40 /* translators: 1: Blog name, 2: Source of action. For e.g. Hub or a logged-in user, 3: Plugin name, 4: Plugin version. */
41 esc_html__( '%1$s %2$s activated plugin: %3$s, version %4$s', 'defender-security' ),
42 '{{blog_name}}',
43 '{{wp_user}}',
44 '{{plugin_name}}',
45 '{{plugin_version}}'
46 ),
47 'event_type' => Audit_Log::EVENT_TYPE_SYSTEM,
48 'action_type' => self::ACTION_ACTIVATED,
49 'context' => self::CONTEXT_PLUGIN,
50 'program_args' => array(
51 'plugin_abs_path' => array(
52 'callable' => array( self::class, 'get_plugin_abs_path' ),
53 'params' => array(
54 '{{plugin}}',
55 ),
56 ),
57 'plugin_name' => array(
58 'callable' => 'get_plugin_data',
59 'params' => array(
60 '{{plugin_abs_path}}',
61 ),
62 'result_property' => 'Name',
63 ),
64 'plugin_version' => array(
65 'callable' => 'get_plugin_data',
66 'params' => array(
67 '{{plugin_abs_path}}',
68 ),
69 'result_property' => 'Version',
70 ),
71 ),
72 ),
73 'deleted_plugin' => array(
74 'args' => array( 'plugin_file', 'deleted' ),
75 'callback' => array( self::class, 'process_delete_plugin' ),
76 'action_type' => self::ACTION_DEACTIVATED,
77 'event_type' => Audit_Log::EVENT_TYPE_SYSTEM,
78 ),
79 'deactivated_plugin' => array(
80 'args' => array( 'plugin' ),
81 'text' => sprintf(
82 /* translators: 1: Blog name, 2: Source of action. For e.g. Hub or a logged-in user, 3: Plugin name, 4: Plugin version. */
83 esc_html__( '%1$s %2$s deactivated plugin: %3$s, version %4$s', 'defender-security' ),
84 '{{blog_name}}',
85 '{{wp_user}}',
86 '{{plugin_name}}',
87 '{{plugin_version}}'
88 ),
89 'action_type' => self::ACTION_DEACTIVATED,
90 'event_type' => Audit_Log::EVENT_TYPE_SYSTEM,
91 'context' => self::CONTEXT_PLUGIN,
92 'program_args' => array(
93 'plugin_abs_path' => array(
94 'callable' => array( self::class, 'get_plugin_abs_path' ),
95 'params' => array(
96 '{{plugin}}',
97 ),
98 ),
99 'plugin_name' => array(
100 'callable' => 'get_plugin_data',
101 'params' => array(
102 '{{plugin_abs_path}}',
103 ),
104 'result_property' => 'Name',
105 ),
106 'plugin_version' => array(
107 'callable' => 'get_plugin_data',
108 'params' => array(
109 '{{plugin_abs_path}}',
110 ),
111 'result_property' => 'Version',
112 ),
113 ),
114 ),
115 'upgrader_process_complete' => array(
116 'args' => array( 'upgrader', 'options' ),
117 'callback' => array( self::class, 'process_installer' ),
118 'action_type' => self::ACTION_UPGRADED,
119 'event_type' => Audit_Log::EVENT_TYPE_SYSTEM,
120 ),
121 );
122
123 global $wp_version;
124 // @since 2.7.0 Add hook for deleted theme. Use 'deleted_theme' hook that was added since WP v5.8.0.
125 if ( version_compare( $wp_version, '5.8.0', '>=' ) ) {
126 $data['deleted_theme'] = array(
127 'args' => array( 'stylesheet', 'deleted' ),
128 'text' => sprintf(
129 /* translators: 1: Blog name, 2: Source of action. For e.g. Hub or a logged-in user, 3: Stylesheet of the theme. */
130 esc_html__( '%1$s %2$s deleted theme: %3$s', 'defender-security' ),
131 '{{blog_name}}',
132 '{{wp_user}}',
133 '{{stylesheet}}'
134 ),
135 'action_type' => self::ACTION_DEACTIVATED,
136 'event_type' => Audit_Log::EVENT_TYPE_SYSTEM,
137 'context' => self::CONTEXT_THEME,
138 );
139 }
140
141 return $data;
142 }
143
144 /**
145 * Prepares the data for recording a core upgrade event.
146 *
147 * @param string $wp_version The new WordPress version.
148 *
149 * @return array Array containing the formatted message and the context.
150 */
151 private function get_upgraded_core_data_for_record( $wp_version ) {
152 return array(
153 sprintf(
154 /* translators: 1: Blog name, 2: Source of action. For e.g. Hub or a logged-in user, 3: Wordpress version. */
155 esc_html__( '%1$s %2$s updated WordPress to %3$s', 'defender-security' ),
156 is_multisite() ? '[' . get_bloginfo( 'name' ) . ']' : '',
157 $this->get_source_of_action(),
158 $wp_version
159 ),
160 self::CONTEXT_CORE,
161 );
162 }
163
164 /**
165 * Handles the upgrade process for WordPress core and prepares the audit log entry.
166 *
167 * @return array|bool Returns the log entry data or false if no update is needed.
168 */
169 public function upgrade_core() {
170 $update_core = get_site_transient( 'update_core' );
171 if ( is_object( $update_core ) ) {
172 $updates = $update_core->updates;
173 $updates = array_shift( $updates );
174 if ( is_object( $updates ) && property_exists( $updates, 'version' ) ) {
175 return $this->get_upgraded_core_data_for_record( $updates->version );
176 }
177 } elseif ( empty( $update_core ) && $this->is_hub_request() ) {
178 // Hub has already updated WP core to the latest version.
179 $wp_version = ( new WP_Version() )->get_latest_version();
180
181 return $wp_version ? $this->get_upgraded_core_data_for_record( $wp_version ) : false;
182 }
183
184 return false;
185 }
186
187 /**
188 * Processes bulk upgrade actions for themes or plugins and generates the audit log entry.
189 *
190 * @param WP_Upgrader $upgrader The upgrade instance.
191 * @param array $options Options containing type and items being upgraded.
192 *
193 * @return array|bool Returns the log entry data or false if unable to generate log data.
194 */
195 public function bulk_upgrade( WP_Upgrader $upgrader, array $options ) {
196 if ( ! is_object( $upgrader->skin ) ) {
197 return false;
198 }
199 $blog_name = is_multisite() ? '[' . get_bloginfo( 'name' ) . ']' : '';
200 if ( isset( $upgrader->skin->result ) && is_wp_error( $upgrader->skin->result ) ) {
201 // Todo: extend Audit table with a new column for the result of the process and divide it into success and failure.
202 $failed_result = true;
203 } else {
204 $failed_result = false;
205 }
206
207 if ( 'theme' === $options['type'] ) {
208 $texts = array();
209 foreach ( $options['themes'] as $slug ) {
210 $theme = wp_get_theme( $slug );
211 if ( is_object( $theme ) ) {
212 $texts[] = sprintf(
213 /* translators: 1: Theme name, 2: Theme version. */
214 $failed_result ? esc_html( '%1$s version %2$s' ) : esc_html( '%1$s to %2$s' ),
215 $theme->Name, // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase
216 $theme->get( 'Version' )
217 );
218 }
219 }
220 if ( count( $texts ) ) {
221 return array(
222 sprintf(
223 $failed_result ?
224 /* translators: 1: Blog name, 2: Source of action. For e.g. Hub or a logged-in user, 3: Translated status for themes. */
225 esc_html__( '%1$s %2$s was unable to update themes: %3$s', 'defender-security' ) :
226 /* translators: 1: Blog name, 2: Source of action. For e.g. Hub or a logged-in user, 3: Translated status for themes. */
227 esc_html__( '%1$s %2$s updated themes: %3$s', 'defender-security' ),
228 $blog_name,
229 $this->get_source_of_action(),
230 implode( ', ', $texts )
231 ),
232 self::CONTEXT_THEME,
233 );
234 } else {
235 return false;
236 }
237 } elseif ( 'plugin' === $options['type'] ) {
238 $texts = array();
239 foreach ( $options['plugins'] as $slug ) {
240 $plugin = get_plugin_data( WP_PLUGIN_DIR . DIRECTORY_SEPARATOR . $slug );
241 if ( is_array( $plugin ) && isset( $plugin['Name'] ) && ! empty( $plugin['Name'] ) ) {
242 $texts[] = sprintf(
243 /* translators: 1: Plugin name, 2: Plugin version. */
244 $failed_result ? esc_html( '%1$s version %2$s' ) : esc_html( '%1$s to %2$s' ),
245 $plugin['Name'],
246 $plugin['Version']
247 );
248 }
249 }
250 if ( count( $texts ) ) {
251 return array(
252 sprintf(
253 $failed_result ?
254 /* translators: 1: Blog name, 2: Source of action. For e.g. Hub or a logged-in user, 3: Translated status for plugins. */
255 esc_html__( '%1$s %2$s was unable to update plugins: %3$s', 'defender-security' ) :
256 /* translators: 1: Blog name, 2: Source of action. For e.g. Hub or a logged-in user, 3: Translated status for plugins. */
257 esc_html__( '%1$s %2$s updated plugins: %3$s', 'defender-security' ),
258 $blog_name,
259 $this->get_source_of_action(),
260 implode( ', ', $texts )
261 ),
262 self::CONTEXT_PLUGIN,
263 );
264 } else {
265 return false;
266 }
267 }
268 }
269
270 /**
271 * Handles single item upgrade for themes or plugins and prepares the audit log entry.
272 *
273 * @param WP_Upgrader $upgrader The upgrade instance.
274 * @param array $options Options containing type and the item being upgraded.
275 *
276 * @return array|bool Returns the log entry data or false if unable to generate log data.
277 */
278 public function single_upgrade( WP_Upgrader $upgrader, array $options ) {
279 $blog_name = is_multisite() ? '[' . get_bloginfo( 'name' ) . ']' : '';
280
281 if ( 'theme' === $options['type'] ) {
282 $theme = wp_get_theme( $options['theme'] );
283 if ( is_object( $theme ) ) {
284 $name = $theme->Name; // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase
285 $version = $theme->get( 'Version' );
286
287 return array(
288 sprintf(
289 /* translators: 1: Blog name, 2: Source of action. For e.g. Hub or a logged-in user, 3: Theme name, 4: Theme version. */
290 esc_html__( '%1$s %2$s updated theme: %3$s, version %4$s', 'defender-security' ),
291 $blog_name,
292 $this->get_source_of_action(),
293 $name,
294 $version
295 ),
296 self::CONTEXT_THEME,
297 );
298 } else {
299 return false;
300 }
301 } elseif ( 'plugin' === $options['type'] ) {
302 $slug = $options['plugin'];
303 $data = get_plugin_data( WP_PLUGIN_DIR . DIRECTORY_SEPARATOR . $slug );
304 if ( is_array( $data ) ) {
305 $name = $data['Name'];
306 $version = $data['Version'];
307
308 return array(
309 sprintf(
310 /* translators: 1: Blog name, 2: Source of action. For e.g. Hub or a logged-in user, 3: Plugin name, 4: Plugin version. */
311 esc_html__( '%1$s %2$s updated plugin: %3$s, version %4$s', 'defender-security' ),
312 $blog_name,
313 $this->get_source_of_action(),
314 $name,
315 $version
316 ),
317 self::CONTEXT_PLUGIN,
318 );
319 } else {
320 return false;
321 }
322 }
323 }
324
325 /**
326 * Install process.
327 * Log in the format: {BLOG_NAME} {USERNAME} installed {theme/plugin}: {theme/plugin name}, version {VERSION}.
328 *
329 * @param WP_Upgrader $upgrade The upgrade instance.
330 * @param array $options Options containing type and the item being installed.
331 *
332 * @return bool|array
333 */
334 private function single_install( WP_Upgrader $upgrade, array $options ) {
335 if ( ! is_object( $upgrade->skin ) ) {
336 return false;
337 }
338 // Only for plugins, themes. No for translation, core.
339 if ( ! in_array( $options['type'], array( 'theme', 'plugin' ), true ) ) {
340 return false;
341 }
342
343 $name = '';
344 if ( ! empty( $upgrade->skin->api ) ) {
345 $name = $upgrade->skin->api->name;
346 } elseif ( ! empty( $upgrade->skin->upgrader ) ) {
347 $type_data = ( 'theme' === $options['type'] && isset( $upgrade->skin->upgrader->new_theme_data ) )
348 ? $upgrade->skin->upgrader->new_theme_data
349 : $upgrade->skin->upgrader->new_plugin_data;
350 if ( ! empty( $type_data['Name'] ) && ! empty( $type_data['Version'] ) ) {
351 $name = $type_data['Name'] . ', version ' . $type_data['Version'];
352 }
353 } elseif ( ! empty( $upgrade->skin->result ) ) {
354 if ( is_array( $upgrade->skin->result ) && isset( $upgrade->skin->result['destination_name'] ) ) {
355 $name = $upgrade->skin->result['destination_name'];
356 } elseif ( is_object( $upgrade->skin->result ) && property_exists(
357 $upgrade->skin->result,
358 'destination_name'
359 ) ) {
360 $name = $upgrade->skin->result->destination_name;
361 }
362 }
363
364 if ( empty( $name ) ) {
365 return false;
366 }
367
368 $blog_name = is_multisite() ? '[' . get_bloginfo( 'name' ) . ']' : '';
369 if ( 'theme' === $options['type'] ) {
370 return array(
371 sprintf(
372 /* translators: %s - blog name, %s - username, %s - theme name */
373 esc_html__( '%1$s %2$s installed theme: %3$s', 'defender-security' ),
374 $blog_name,
375 $this->get_source_of_action(),
376 $name
377 ),
378 self::CONTEXT_THEME,
379 self::ACTION_INSTALLED,
380 );
381 } else {
382 return array(
383 sprintf(
384 /* translators: %s - blog name, %s - username, %s - plugin name */
385 esc_html__( '%1$s %2$s installed plugin: %3$s', 'defender-security' ),
386 $blog_name,
387 $this->get_source_of_action(),
388 $name
389 ),
390 self::CONTEXT_PLUGIN,
391 self::ACTION_INSTALLED,
392 );
393 }
394
395 return false;
396 }
397
398 /**
399 * Param 'upgrader' is WP_Upgrader instance. It might be a Theme_Upgrader, Plugin_Upgrader, Core_Upgrade, or
400 * Language_Pack_Upgrader instance. Param 'options' is array of bulk item update data. Keys:
401 * 'action' (string) Type of action. Default 'update'.
402 * 'type' (string) Type of update process. Accepts 'plugin', 'theme', 'translation', or 'core'.
403 * 'bulk' (bool) Whether the update process is a bulk update. Default true.
404 * 'plugins' (array) Array of the basename paths of the plugins' main files.
405 * 'themes' (array) The theme slugs.
406 * 'translations' (array) Array of translations update data: 'language', 'type', 'slug', 'version'.
407 *
408 * @return mixed
409 */
410 public function process_installer() {
411 $args = func_get_args();
412 $upgrader = $args[1]['upgrader'];
413 $options = $args[1]['options'];
414 if ( 'core' === $options['type'] ) {
415 return $this->upgrade_core();
416 // If this is core, we just create text and return.
417 } elseif ( isset( $options['bulk'] ) && true === $options['bulk'] ) {
418 // Case: local install/update work with 'bulk' => true (for mass action and no).
419 return $this->bulk_upgrade( $upgrader, $options );
420 } elseif ( 'install' === $options['action'] ) {
421 // Case: actions from Hub.
422 return $this->single_install( $upgrader, $options );
423 } elseif ( 'update' === $options['action'] ) {
424 // Case: actions from Hub.
425 return $this->single_upgrade( $upgrader, $options );
426 }
427 }
428
429 /**
430 * Fires after the theme is switched.
431 *
432 * @return array|bool
433 */
434 public function process_activate_theme() {
435 $args = func_get_args();
436 $new_theme = $args[1]['new_theme'];
437 if ( ! is_object( $new_theme ) ) {
438 return false;
439 }
440 $new_name = $args[1]['new_name'];
441 $version = $new_theme->get( 'Version' );
442 $blog_name = is_multisite() ? '[' . get_bloginfo( 'name' ) . ']' : '';
443
444 return array(
445 sprintf(
446 /* translators: 1: Blog name, 2: Source of action. For e.g. Hub or a logged-in user, 3: Theme name, 4: Theme version. */
447 esc_html__( '%1$s %2$s activated theme: %3$s, version %4$s', 'defender-security' ),
448 $blog_name,
449 $this->get_source_of_action(),
450 $new_name,
451 $version
452 ),
453 self::CONTEXT_THEME,
454 );
455 }
456
457 /**
458 * Fires immediately after a plugin deletion.
459 * There is no way to get the plugin data (name, version) because it has been removed. Only slug.
460 *
461 * @return array
462 */
463 public function process_delete_plugin(): array {
464 $args = func_get_args();
465
466 // If 'deleted'-arg is false then the plugin deletion wasn't successful.
467 if ( empty( $args[1]['deleted'] ) ) {
468 // Todo: extend Audit table with a new column for the result of the process and divide it into success and failure.
469 $failed_result = true;
470 } else {
471 $failed_result = false;
472 }
473 $plugin_file = $args[1]['plugin_file'];
474 $blog_name = is_multisite() ? '[' . get_bloginfo( 'name' ) . ']' : '';
475
476 return array(
477 sprintf(
478 $failed_result ?
479 /* translators: 1: Blog name, 2: Source of action. For e.g. Hub or a logged-in user, 3: Plugin file. */
480 esc_html__( '%1$s %2$s was unable to delete plugin: %3$s', 'defender-security' ) :
481 /* translators: 1: Blog name, 2: Source of action. For e.g. Hub or a logged-in user, 3: Plugin file. */
482 esc_html__( '%1$s %2$s deleted plugin: %3$s', 'defender-security' ),
483 $blog_name,
484 $this->get_source_of_action(),
485 $plugin_file
486 ),
487 self::CONTEXT_PLUGIN,
488 );
489 }
490
491 /**
492 * Provides a dictionary of audit event types and their descriptions.
493 *
494 * @return array Associative array of event keys and their descriptions.
495 */
496 public function dictionary(): array {
497 return array(
498 self::ACTION_DEACTIVATED => esc_html__( 'deactivated', 'defender-security' ),
499 self::ACTION_UPGRADED => esc_html__( 'upgraded', 'defender-security' ),
500 self::ACTION_ACTIVATED => esc_html__( 'activated', 'defender-security' ),
501 self::ACTION_INSTALLED => esc_html__( 'installed', 'defender-security' ),
502 self::CONTEXT_THEME => esc_html__( 'theme', 'defender-security' ),
503 self::CONTEXT_PLUGIN => esc_html__( 'plugin', 'defender-security' ),
504 self::CONTEXT_CORE => esc_html__( 'WordPress', 'defender-security' ),
505 self::FILE_ADDED => esc_html__( 'File Added', 'defender-security' ),
506 self::FILE_MODIFIED => esc_html__( 'File Modified', 'defender-security' ),
507 );
508 }
509
510 /**
511 * Retrieves the absolute path of a plugin.
512 *
513 * @param string $slug The plugin slug or relative path.
514 *
515 * @return string The absolute path of the plugin.
516 */
517 public function get_plugin_abs_path( string $slug ): string {
518 if ( ! is_file( $slug ) ) {
519 $slug = WP_PLUGIN_DIR . DIRECTORY_SEPARATOR . $slug;
520 }
521
522 return $slug;
523 }
524 }
525