PluginProbe ʕ •ᴥ•ʔ
Really Simple Security – Simple and Performant Security (formerly Really Simple SSL) / 9.5.0.1
Really Simple Security – Simple and Performant Security (formerly Really Simple SSL) v9.5.0.1
9.5.11 9.5.10.1 9.5.10 trunk 9.4.0 9.4.1 9.4.2 9.4.3 9.5.0 9.5.0.1 9.5.0.2 9.5.1 9.5.2 9.5.2.2 9.5.2.3 9.5.3 9.5.3.1 9.5.3.2 9.5.4 9.5.5 9.5.6 9.5.7 9.5.8 9.5.9
really-simple-ssl / security / wordpress / vulnerabilities.php
really-simple-ssl / security / wordpress Last commit date
two-fa 9 months ago vulnerabilities 1 year ago block-code-execution-uploads.php 2 years ago disable-xmlrpc.php 2 years ago display-name-is-login-name.php 2 years ago file-editing.php 1 year ago hide-wp-version.php 2 years ago index.php 2 years ago prevent-login-info-leakage.php 2 years ago rename-admin-user.php 1 year ago rest-api.php 2 years ago user-enumeration.php 1 year ago user-registration.php 2 years ago vulnerabilities.php 9 months ago
vulnerabilities.php
1678 lines
1 <?php
2
3 use security\wordpress\vulnerabilities\Rsssl_File_Storage;
4
5 defined('ABSPATH') or die();
6 //including the file storage class
7 require_once(rsssl_path . 'security/wordpress/vulnerabilities/class-rsssl-file-storage.php');
8
9 /**
10 * @package Really Simple Security
11 * @subpackage RSSSL_VULNERABILITIES
12 */
13 if (!class_exists("rsssl_vulnerabilities")) {
14 /**
15 *
16 * Class rsssl_vulnerabilities
17 * Checks for vulnerabilities in the core, plugins and themes.
18 *
19 * @property $notices
20 * @author Marcel Santing
21 * this class handles import of vulnerabilities, notifying and informing the user.
22 *
23 */
24 class rsssl_vulnerabilities
25 {
26 const RSSSL_SECURITY_API = 'https://downloads.really-simple-security.com/rsssl/vulnerabilities/V1/';
27 public $workable_plugins = [];
28
29 /**
30 * interval to download new jsons
31 */
32 public $interval = 12 * HOUR_IN_SECONDS;
33 public $update_count = 0;
34
35 protected $risk_naming = [];
36
37 /**
38 * @var array|int[]
39 */
40 public $risk_levels = [
41 'l' => 1,
42 'm' => 2,
43 'h' => 3,
44 'c' => 4,
45 ];
46 public $jsons_files_updated = false;
47
48 public function __construct()
49 {
50 $this->init();
51 $this->load_translations_just_in_time();
52
53 add_filter('rsssl_vulnerability_data', array($this, 'get_stats'));
54
55 //now we add the action to the cron.
56 add_action('rsssl_three_hours_cron', array($this, 'run_cron'));
57 add_filter('rsssl_notices', [$this, 'show_help_notices'], 10, 1);
58 add_action( 'rsssl_after_save_field', array( $this, 'maybe_delete_local_files' ), 10, 4 );
59 add_action( 'rsssl_upgrade', array( $this, 'upgrade_encrypted_files') );
60 }
61
62 /**
63 * As this class is not only instantiated by requiring this file
64 * but also in other class instances, we are not 100% sure of the
65 * current filter or action. So we check if we are in the init action
66 * or if the init action has already been executed. If so, we load the
67 * translations immediately and just in time.
68 */
69 public function load_translations_just_in_time(): void
70 {
71 add_action('init', [$this, 'load_translations']);
72
73 if (current_filter() === 'init' || did_action('init') > 0) {
74 $this->load_translations();
75 }
76 }
77
78 /**
79 * Load the translations for the risk levels
80 */
81 public function load_translations(): void
82 {
83 $this->risk_naming = [
84 'l' => __('low-risk', 'really-simple-ssl'),
85 'm' => __('medium-risk', 'really-simple-ssl'),
86 'h' => __('high-risk', 'really-simple-ssl'),
87 'c' => __('critical', 'really-simple-ssl'),
88 ];
89 }
90
91 /**
92 * Upgrade to the new encryption system by deleting all files en re-downloading
93 *
94 * @return void
95 */
96 public function upgrade_encrypted_files($prev_version) : void {
97 if ( $prev_version && version_compare( $prev_version, '8.3.0', '<' ) ) {
98 //delete all files and reload
99 Rsssl_File_Storage::DeleteAll();
100 $this->force_reload_files();
101 delete_option( 'rsssl_hashkey' );
102 }
103
104 }
105
106 // /**
107 // * @param $field_id
108 // * @param $field_value
109 // * @param $prev_value
110 // * @param $field_type
111 // *
112 // * @return void
113 // *
114 // *
115 // */
116 // public function maybe_enable_vulnerability_scanner( $field_id, $field_value, $prev_value, $field_type ) {
117 // if ( $field_id==='enable_vulnerability_scanner' && $field_value !== $prev_value && rsssl_user_can_manage() ) {
118 // if ( $field_value !== false ) {
119 // // Already enabled
120 // rsssl_update_option('enable_vulnerability_scanner', 1);
121 // }
122 // }
123 // }
124
125 /**
126 * Deletes local files if the vulnerability scanner is disabled
127 *
128 * @param $field_id
129 * @param $field_value
130 * @param $prev_value
131 * @param $field_type
132 *
133 * @return void
134 */
135 public static function maybe_delete_local_files($field_id, $field_value, $prev_value, $field_type): void {
136 if ( $field_id==='enable_vulnerability_scanner' && $field_value !== $prev_value && rsssl_user_can_manage() ) {
137 if ( $field_value == false ) {
138 // Already disabled
139 require_once(rsssl_path . 'security/wordpress/vulnerabilities/class-rsssl-file-storage.php');
140 \security\wordpress\vulnerabilities\Rsssl_File_Storage::DeleteAll();
141
142 }
143 }
144 }
145
146 public function riskNaming($risk = null)
147 {
148 if (is_null($risk)) {
149 return $this->risk_naming;
150 }
151 return $this->risk_naming[$risk];
152 }
153
154 /* Public Section 1: Class Build-up initialization and instancing */
155
156 public function run_cron(): void {
157 $this->check_files();
158 $this->cache_installed_plugins(true);
159 if ( $this->jsons_files_updated ) {
160 if ($this->should_send_mail()) {
161 $this->send_vulnerability_mail();
162 }
163
164 $this->check_notice_reset();
165 }
166 }
167
168 /**
169 * Check if dismissed notices have to be reset
170 * @return void
171 */
172 private function check_notice_reset(): void {
173 $this->cache_installed_plugins();
174 $clear_admin_notices_cache = false;
175 foreach ( $this->risk_levels as $level => $int_level ) {
176 if ( $this->should_reset_notification($level) ) {
177 delete_option("rsssl_" . 'risk_level_' . $level . "_dismissed");
178 $clear_admin_notices_cache = true;
179 }
180 }
181 if ( $clear_admin_notices_cache ) {
182 RSSSL()->admin->clear_admin_notices_cache();
183 }
184 }
185
186 /**
187 * Allow users to manually force a re-check, e.g. in case of manually updating plugins
188 * @return void
189 */
190 public function force_reload_files(): void {
191 if ( ! rsssl_admin_logged_in() ) {
192 return;
193 }
194
195 \security\wordpress\vulnerabilities\Rsssl_File_Storage::DeleteOldFiles();
196
197 if ( get_option('rsssl_reload_vulnerability_files') ) {
198
199 delete_option('rsssl_reload_vulnerability_files');
200 $this->reload_files_on_update();
201 update_option('rsssl_clear_vulnerability_notices', true, false);
202 set_transient('rsssl_delay_clear', true, 1 * MINUTE_IN_SECONDS );
203 }
204
205 if ( get_option('rsssl_clear_vulnerability_notices') && !get_transient('rsssl_delay_clear')) {
206 RSSSL()->admin->clear_admin_notices_cache();
207 delete_option('rsssl_clear_vulnerability_notices');
208 }
209 }
210
211 /**
212 * Checks the files on age and downloads if needed.
213 * @return void
214 */
215 public function reload_files_on_update(): void {
216 if ( ! rsssl_admin_logged_in() ) {
217 return;
218 }
219 //if the manifest is not older than 4 hours, we don't download it again.
220 if ( $this->get_file_stored_info(false, true) < time() - 14400) {
221 $this->download_manifest();
222 }
223 $this->download_plugin_vulnerabilities();
224 $this->download_core_vulnerabilities();
225 $this->check_notice_reset();
226
227 }
228
229 public function init(): void {
230 if ( ! rsssl_admin_logged_in() ) {
231 return;
232 }
233 //we check the rsssl options if the enable_feedback_in_plugin is set to true
234 if ( rsssl_get_option('enable_feedback_in_plugin') ) {
235 // we enable the feedback in the plugin
236 $this->enable_feedback_in_plugin();
237 $this->enable_feedback_in_theme();
238 }
239
240 //we check if upgrader_process_complete is called, so we can reload the files.
241 add_action('upgrader_process_complete', array($this, 'reload_files_on_update'), 10, 2);
242 add_action('_core_updated_successfully', array($this, 'prepare_reloading_of_files'), 10, 2);
243 //After activation, we need to reload the files.
244 add_action( 'activate_plugin', array($this, 'reload_files_on_update'), 10, 2);
245 //we can also force it
246 add_action( 'admin_init', array($this, 'force_reload_files'));
247
248 //same goes for themes.
249 add_action('after_switch_theme', array($this, 'reload_files_on_update'), 10, 2);
250 add_action('current_screen', array($this, 'show_inline_code'));
251 }
252
253 /**
254 * Directly hooking into the core upgrader hook doesn't work, so is too early.
255 * To force this, we save an option we can check later
256 *
257 * @return void
258 */
259 public function prepare_reloading_of_files(): void {
260 update_option("rsssl_reload_vulnerability_files", true, false);
261 }
262
263 /**
264 * Function used for first run of the plugin.
265 *
266 * @return array
267 */
268 public static function firstRun(): array
269 {
270 if ( ! rsssl_user_can_manage() ) {
271 return [];
272 }
273 $self = new self();
274 $self->check_files();
275 $self->cache_installed_plugins(true);
276
277 return [
278 'request_success' => true,
279 'data' => $self->workable_plugins
280 ];
281 }
282
283 /**
284 * Get site health notice for vulnerabilities
285 * @return array
286 */
287 public function get_site_health_notice(): array {
288 if (!rsssl_admin_logged_in()){
289 return [];
290 }
291
292 $this->cache_installed_plugins();
293 $risks = $this->count_risk_levels();
294 if (count($risks) === 0) {
295 return array(
296 'label' => __( 'No known vulnerabilities detected', 'really-simple-ssl' ),
297 'status' => 'good',
298 'badge' => array(
299 'label' => __('Security'),
300 'color' => 'blue',
301 ),
302 'description' => sprintf(
303 '<p>%s</p>',
304 __( 'No known vulnerabilities detected', 'really-simple-ssl' )
305 ),
306 'actions' => '',
307 'test' => 'health_test',
308 );
309 }
310 $total = 0;
311 foreach ($this->risk_levels as $risk_level => $value) {
312 $total += $risks[ $risk_level ] ?? 0;
313 }
314
315 return array(
316 'label' => __( 'Vulnerabilities detected','really-simple-ssl' ),
317 'status' => 'critical',
318 'badge' => array(
319 'label' => __( 'Security' ),
320 'color' => 'blue',
321 ),
322 'description' => sprintf(
323 '<p>%s</p>',
324 sprintf(_n( '%s vulnerability has been detected.', '%s vulnerabilities have been detected.', $total, 'really-simple-ssl' ), number_format_i18n( $total )) . ' '.
325 __( 'Please check the vulnerabilities overview for more information and take appropriate action.' ,'really-simple-ssl' )
326 ),
327 'actions' => sprintf(
328 '<p><a href="%s" target="_blank" rel="noopener noreferrer">%s</a></p>',
329 esc_url( rsssl_admin_url([], '#settings/vulnerabilities/vulnerabilities-overview') ),
330 __( 'View vulnerabilities', 'really-simple-ssl' )
331 ),
332 'test' => 'rsssl_vulnerabilities',
333 );
334 }
335
336 public function show_help_notices($notices)
337 {
338 $this->cache_installed_plugins();
339 $risks = $this->count_risk_levels();
340 $level_to_show_on_dashboard = rsssl_get_option('vulnerability_notification_dashboard');
341 $level_to_show_sitewide = rsssl_get_option('vulnerability_notification_sitewide');
342 foreach ($this->risk_levels as $risk_level => $value) {
343 if ( !isset($risks[$risk_level]) ) {
344 continue;
345 }
346 //this is shown bases on the config of vulnerability_notification_dashboard
347 $siteWide = false;
348 $dashboardNotice = false;
349 if ( $level_to_show_on_dashboard && $level_to_show_on_dashboard !== '*') {
350 if ($value >= $this->risk_levels[$level_to_show_on_dashboard]) {
351 $dashboardNotice = true;
352 }
353 }
354 if ($level_to_show_sitewide && $level_to_show_sitewide !== '*') {
355 if ($value >= $this->risk_levels[$level_to_show_sitewide]) {
356 $siteWide = true;
357 }
358 }
359 if ( !$dashboardNotice && !$siteWide ) {
360 continue;
361 }
362
363 $count = $risks[$risk_level];
364 $title = $this->get_warning_string($risk_level, $count);
365 $notice = [
366 'callback' => '_true_',
367 'score' => 1,
368 'show_with_options' => ['enable_vulnerability_scanner'],
369 'output' => [
370 'true' => [
371 'title' => $title,
372 'msg' => $title.' '.__('Please take appropriate action.','really-simple-ssl'),
373 'icon' => ($risk_level === 'c' || $risk_level==='h') ? 'warning' : 'open',
374 'type' => 'warning',
375 'dismissible' => true,
376 'admin_notice' => $siteWide,
377 'plusone' => true,
378 'highlight_field_id' => 'vulnerabilities-overview',
379 ]
380 ],
381 ];
382 $notices['risk_level_' . $risk_level] = $notice;
383
384 }
385 //now we add the test notices for admin and dahboard.
386
387 //if the option is filled, we add the test notice.
388 $test_id = get_option('test_vulnerability_tester');
389 if($test_id) {
390 $dashboard = rsssl_get_option('vulnerability_notification_dashboard');
391 $side_wide = rsssl_get_option('vulnerability_notification_sitewide');
392
393 $site_wide_icon = $side_wide === 'l' || $side_wide === 'm' ? 'open' : 'warning';
394 if ( $side_wide === 'l' || $side_wide === 'm' || $side_wide === 'h' || $side_wide === 'c') {
395 $notices[ 'test_vulnerability_sitewide_' .$test_id ] = [
396 'callback' => '_true_',
397 'score' => 1,
398 'show_with_options' => [ 'enable_vulnerability_scanner' ],
399 'output' => [
400 'true' => [
401 'title' => __( 'Site wide - Test Notification', 'really-simple-ssl' ),
402 'msg' => __( 'This is a test notification from Really Simple Security. You can safely dismiss this message.', 'really-simple-ssl' ),
403 'url' => rsssl_admin_url([], '#settings/vulnerabilities/vulnerabilities-overview'),
404 'icon' => $site_wide_icon,
405 'dismissible' => true,
406 'admin_notice' => true,
407 'plusone' => true,
408 ]
409 ]
410 ];
411 }
412
413 //don't add this one if the same level
414 $dashboard_icon = $dashboard === 'l' || $dashboard === 'm' ? 'open' : 'warning';
415 if ($dashboard_icon !== $site_wide_icon) {
416 if ( $dashboard === 'l' || $dashboard === 'm' || $dashboard === 'h' || $dashboard === 'c' ) {
417 $notices[ 'test_vulnerability_dashboard_' .$test_id ] = [
418 'callback' => '_true_',
419 'score' => 1,
420 'show_with_options' => [ 'enable_vulnerability_scanner' ],
421 'output' => [
422 'true' => [
423 'title' => __( 'Dashboard - Test Notification', 'really-simple-ssl' ),
424 'msg' => __( 'This is a test notification from Really Simple Security. You can safely dismiss this message.', 'really-simple-ssl' ),
425 'icon' => $dashboard_icon,
426 'dismissible' => true,
427 'admin_notice' => false,
428 'plusone' => true,
429 ]
430 ]
431 ];
432 }
433 }
434 }
435
436 return $notices;
437 }
438
439 /**
440 * Generate plugin files for testing purposes.
441 *
442 * @return array
443 */
444 public static function testGenerator(): array
445 {
446 $mail_notification = rsssl_get_option('vulnerability_notification_email_admin');
447 if ( $mail_notification === 'l' || $mail_notification === 'm' || $mail_notification === 'h' || $mail_notification === 'c' ) {
448 $mailer = new rsssl_mailer();
449 $mailer->send_test_mail();
450 }
451 return [];
452 }
453
454
455 /* Public Section 2: DataGathering */
456
457 /**
458 * @param $stats
459 *
460 * @return array
461 */
462 public function get_stats($stats): array
463 {
464 if ( ! rsssl_user_can_manage() ) {
465 return $stats;
466 }
467
468 $this->cache_installed_plugins();
469 //now we only get the data we need.
470 $vulnerabilities = array_filter($this->workable_plugins, static function ($plugin) {
471 if (isset($plugin['vulnerable']) && $plugin['vulnerable']) {
472 return $plugin;
473 }
474 return false;
475 });
476
477 $time = $this->get_file_stored_info(true);
478 $stats['vulnerabilities'] = count($vulnerabilities);
479 $stats['vulList'] = $vulnerabilities;
480 $riskData = $this->measures_data();
481 $stats['riskData'] = $riskData['data'];
482 $stats['lastChecked'] = $time;
483 return $stats;
484 }
485
486
487 /**
488 * This combines the vulnerabilities with the installed plugins
489 *
490 * And loads it into a memory cache on page load
491 *
492 */
493 public function cache_installed_plugins($force_update=false): void
494 {
495 if ( ! rsssl_admin_logged_in() ) {
496 return;
497 }
498
499 if ( !$force_update && !empty($this->workable_plugins) ) {
500 return;
501 }
502
503 //first we get all installed plugins
504 $installed_plugins = get_plugins();
505
506 $installed_themes = wp_get_themes();
507 //we flatten the array
508 $update = get_site_transient('update_themes');
509 //we make the installed_themes look like the installed_plugins
510 $installed_themes = array_map( static function ($theme) use ($update) {
511 return [
512 'Name' => $theme->get('Name'),
513 'Slug' => $theme->get('TextDomain'),
514 'description' => $theme->get('Description'),
515 'Version' => $theme->get('Version'),
516 'Author' => $theme->get('Author'),
517 'AuthorURI' => $theme->get('AuthorURI'),
518 'PluginURI' => $theme->get('ThemeURI'),
519 'TextDomain' => $theme->get('TextDomain'),
520 'RequiresWP' => $theme->get('RequiresWP'),
521 'RequiresPHP' => $theme->get('RequiresPHP'),
522 'update_available' => isset($update->response[$theme->get('TextDomain')]),
523 ];
524 }, $installed_themes);
525
526 //we add a column type to all values in the array
527 $installed_themes = array_map( static function ($theme) {
528 $theme['type'] = 'theme';
529 return $theme;
530 }, $installed_themes);
531
532 //we add a column type to all values in the array
533 //this resets the array keys (currently slugs) so we preserve them in the 'Slug' column.
534 $update = get_site_transient('update_plugins');
535 $installed_plugins = array_map( static function ($plugin, $slug) use ($update) {
536 $plugin['type'] = 'plugin';
537 $plugin['update_available'] = isset($update->response[$slug]);
538 $plugin['Slug'] = dirname($slug);
539 $plugin['File'] = $slug;
540 return $plugin;
541 }, $installed_plugins, array_keys($installed_plugins) );
542
543 //we merge the two arrays
544 $installed_plugins = array_merge($installed_plugins, $installed_themes);
545
546 //now we get the components from the file
547 $components = $this->get_components();
548 //We loop through plugins and check if they are in the components array
549 foreach ($installed_plugins as $plugin) {
550 $slug = $plugin['Slug'];
551 $plugin['vulnerable'] = false;
552 if( $plugin['type'] === 'theme' ) {
553 // we check if the theme exists as a directory
554 $plugin['folder_exists'] = file_exists(get_theme_root() . '/' . $slug );
555 }
556
557 if( $plugin['type'] === 'plugin' ) {
558 //also we check if the folder exists for the plugin we added this check for later purposes
559 $plugin['folder_exists'] = file_exists(WP_PLUGIN_DIR . '/' . dirname($slug) );
560 }
561
562 //if there are no components, we return
563 if ( !empty($components) ) {
564 foreach ($components as $component) {
565 if ($plugin['Slug'] === $component->slug) {
566 if (!empty($component->vulnerabilities) && $plugin['folder_exists'] === true) {
567 $plugin['vulnerable'] = true;
568 $plugin['risk_level'] = $this->get_highest_vulnerability($component->vulnerabilities);
569 $plugin['rss_identifier'] = $this->getLinkedUUID($component->vulnerabilities, $plugin['risk_level']);
570 $plugin['risk_name'] = $this->risk_naming[$plugin['risk_level']];
571 $plugin['date'] = $this->getLinkedDate($component->vulnerabilities, $plugin['risk_level']);
572 }
573 }
574 }
575 }
576
577 //we walk through the components array
578 $this->workable_plugins[$slug] = $plugin;
579 }
580
581
582 //now we get the core information
583 $core = $this->get_core();
584
585 //we create a plugin like entry for core to add to the workable_plugins array
586 $core_plugin = [
587 'Name' => 'WordPress',
588 'Slug' => 'wordpress',
589 'Version' => $core->version?? '',
590 'Author' => 'WordPress',
591 'AuthorURI' => 'https://wordpress.org/',
592 'PluginURI' => 'https://wordpress.org/',
593 'TextDomain' => 'wordpress',
594 'type' => 'core',
595 ];
596 $core_plugin['vulnerable'] = false;
597 //we check if there is an update available
598 $update = get_site_transient('update_core');
599 if (isset($update->updates[0]->response) && $update->updates[0]->response === 'upgrade') {
600 $core_plugin['update_available'] = true;
601 } else {
602 $core_plugin['update_available'] = false;
603 }
604 //if there are no components, we return
605 if ( !empty($core->vulnerabilities) ) {
606 $core_plugin['vulnerable'] = true;
607 $core_plugin['risk_level'] = $this->get_highest_vulnerability($core->vulnerabilities);
608 $core_plugin['rss_identifier'] = $this->getLinkedUUID($core->vulnerabilities, $core_plugin['risk_level']);
609 $core_plugin['risk_name'] = $this->risk_naming[$core_plugin['risk_level']];
610 $core_plugin['date'] = $this->getLinkedDate($core->vulnerabilities, $core_plugin['risk_level']);
611 }
612 //we add the core plugin to the workable_plugins array
613 $this->workable_plugins['wordpress'] = $core_plugin;
614 }
615
616
617 /* Public Section 3: The plugin page add-on */
618 /**
619 * Callback for the manage_plugins_columns hook to add the vulnerability column
620 *
621 * @param $columns
622 */
623 public function add_vulnerability_column($columns)
624 {
625 $columns['vulnerability'] = __('Vulnerabilities', 'really-simple-ssl');
626
627 return $columns;
628 }
629
630 /**
631 * Get the data for the risk vulnerabilities table
632 * @param $data
633 * @return array
634 */
635 public function measures_data(): array
636 {
637 $measures = [];
638 $measures[] = [
639 'id' => 'force_update',
640 'name' => __('Force update', 'really-simple-ssl'),
641 'value' => get_option('rsssl_force_update'),
642 'description' => sprintf(__('Will run a frequent update process on vulnerable components.', 'really-simple-ssl'), $this->riskNaming('l')),
643 ];
644 $measures[] = [
645 'id' => 'quarantine',
646 'name' => __('Quarantine', 'really-simple-ssl'),
647 'value' => get_option('rsssl_quarantine'),
648 'description' => sprintf(__('Components will be quarantined if the update process fails.', 'really-simple-ssl'), $this->riskNaming('m')),
649 ];
650
651 return [
652 "request_success" => true,
653 'data' => $measures
654 ];
655 }
656
657 /**
658 * Store the mesures from the api
659 * @param $measures
660 *
661 * @return array
662 */
663 public function measures_set($measures): array {
664 if (!rsssl_user_can_manage()) {
665 return [];
666 }
667
668 $risk_data = $measures['riskData'] ?? [];
669 foreach ( $risk_data as $risk ) {
670 if ( !isset($risk['value']) ) {
671 continue;
672 }
673 update_option('rsssl_'.sanitize_title($risk['id']), $this->sanitize_measure($risk['value']), false );
674 }
675 return [];
676 }
677
678 /**
679 * Sanitize a measure
680 *
681 * @param string $measure
682 *
683 * @return mixed|string
684 */
685 public function sanitize_measure($measure) {
686 return isset($this->risk_levels[$measure]) ? $measure : '*';
687 }
688
689 /**
690 * Callback for the manage_plugins_custom_column hook to add the vulnerability field
691 *
692 * @param string $column_name
693 * @param string $plugin_file
694 */
695 public function add_vulnerability_field( string $column_name, string $plugin_file): void {
696 if ( ( $column_name === 'vulnerability' ) ) {
697 $this->cache_installed_plugins();
698 if ($this->check_vulnerability( $plugin_file ) ) {
699 switch ( $this->check_severity( $plugin_file ) ) {
700 case 'c':
701 printf(
702 '<a class="rsssl-btn-vulnerable rsssl-critical" target="_blank" rel="noopener noreferrer" href="%s">%s</a>',
703 rsssl_link('vulnerability/' . $this->getIdentifier( $plugin_file ) ),
704 ucfirst( $this->risk_naming['c'] )
705 );
706 break;
707 case 'h':
708 printf(
709 '<a class="rsssl-btn-vulnerable rsssl-high" target="_blank" rel="noopener noreferrer" href="%s">%s</a>',
710 rsssl_link('vulnerability/' . $this->getIdentifier( $plugin_file ) ),
711 ucfirst( $this->risk_naming['h'] )
712 );
713 break;
714 case 'm':
715 printf(
716 '<a class="rsssl-btn-vulnerable rsssl-medium" target="_blank" rel="noopener noreferrer" href="%s">%s</a>',
717 rsssl_link('vulnerability/' . $this->getIdentifier( $plugin_file ) ),
718 ucfirst( $this->risk_naming['m'] )
719 );
720 break;
721 default:
722 echo sprintf(
723 '<a class="rsssl-btn-vulnerable rsssl-low" target="_blank" rel="noopener noreferrer" href="%s">%s</a>',
724 rsssl_link('vulnerability/' . $this->getIdentifier( $plugin_file ) ),
725 ucfirst( $this->risk_naming['l'] )
726 );
727 break;
728 }
729 }
730 if ( $this->is_quarantined($plugin_file)) {
731 echo sprintf( '<a class="rsssl-btn-vulnerable rsssl-critical" target="_blank" rel="noopener noreferrer" href="%s">%s</a>',
732 'https://really-simple-ssl.com/instructions/about-vulnerabilities/#quarantine' , __("Quarantined","really-simple-ssl") );
733 }
734 }
735 }
736
737 /**
738 * Callback for the admin_enqueue_scripts hook to add the vulnerability styles
739 *
740 * @param $hook
741 *
742 * @return void
743 */
744 public function add_vulnerability_styles($hook)
745 {
746 if ('plugins.php' !== $hook) {
747 return;
748 }
749 //only on settings page
750 $rtl = is_rtl() ? 'rtl/' : '';
751 $url = trailingslashit(rsssl_url) . "assets/css/{$rtl}rsssl-plugin.min.css";
752 $path = trailingslashit(rsssl_path) . "assets/css/{$rtl}rsssl-plugin.min.css";
753 if (file_exists($path)) {
754 wp_enqueue_style('rsssl-plugin', $url, array(), rsssl_version);
755 }
756 }
757
758 /**
759 * checks if the plugin is vulnerable
760 *
761 * @param $plugin_file
762 *
763 * @return mixed
764 */
765 private function check_vulnerability($plugin_file)
766 {
767 return $this->workable_plugins[ dirname($plugin_file) ]['vulnerable'] ?? false;
768 }
769
770 /**
771 * Check if a plugin is quarantined
772 *
773 * @param string $plugin_file
774 *
775 * @return bool
776 */
777 private function is_quarantined(string $plugin_file): bool {
778 return strpos($plugin_file, '-rsssl-q-')!==false;
779 }
780
781 /**
782 * checks if the plugin's severity closed
783 *
784 * @param $plugin_file
785 *
786 * @return mixed
787 */
788 private function check_severity($plugin_file)
789 {
790 return $this->workable_plugins[dirname($plugin_file)]['risk_level'];
791 }
792
793 private function getIdentifier($plugin_file)
794 {
795 return $this->workable_plugins[dirname($plugin_file)]['rss_identifier'];
796 }
797 /* End of plug-in page add-on */
798
799
800 /* Public and private functions | Files and storage */
801
802 /**
803 * Checks the files on age and downloads if needed.
804 *
805 * @return void
806 */
807 public function check_files(): void
808 {
809 if ( ! rsssl_admin_logged_in() ) {
810 return;
811 }
812
813 //we download the manifest file if it doesn't exist or is older than 12 hours
814 if ($this->validate_local_file(false, true)) {
815 if ( $this->get_file_stored_info(false, true) < time() - $this->interval ) {
816 $this->download_manifest();
817 }
818 } else {
819 $this->download_manifest();
820 }
821 //We check the core vulnerabilities and validate age and existence
822 if ($this->validate_local_file(true, false)) {
823 //if the file is younger than 12 hours, we don't download it again.
824 if ($this->get_file_stored_info(true) < time() - $this->interval ) {
825 $this->download_core_vulnerabilities();
826 }
827
828 } else {
829 $this->download_core_vulnerabilities();
830 }
831
832 //We check the plugin vulnerabilities and validate age and existence
833 if ($this->validate_local_file()) {
834 if ($this->get_file_stored_info() < time() - $this->interval ) {
835 $this->download_plugin_vulnerabilities();
836 }
837 } else {
838 $this->download_plugin_vulnerabilities();
839 }
840 }
841
842
843 /**
844 * Checks if the file is valid and exists. It checks three files: the manifest, the core vulnerabilities and the plugin vulnerabilities.
845 *
846 * @param bool $isCore
847 * @param bool $manifest
848 *
849 * @return bool
850 */
851 private function validate_local_file(bool $isCore = false, bool $manifest = false): bool
852 {
853 if ( ! rsssl_admin_logged_in() ) {
854 return false;
855 }
856 if (!$manifest) {
857 //if we don't check for the manifest, we check the other files.
858 $isCore ? $file = 'core.json' : $file = 'components.json';
859 } else {
860 $file = 'manifest.json';
861 }
862
863 $upload_dir = Rsssl_File_Storage::get_upload_dir();
864 $file = $upload_dir . '/' . $file;
865 if (file_exists($file)) {
866 //now we check if the file is older than 3 days, if so, we download it again
867 $file_time = filemtime($file);
868 $now = time();
869 $diff = $now - $file_time;
870 $days = floor($diff / (60 * 60 * 24));
871 if ($days < 1) {
872 return true;
873 }
874 }
875
876 return false;
877 }
878
879
880 /**
881 * Downloads bases on given url
882 *
883 * @param string $url
884 *
885 * @return mixed|null
886 */
887 private function download(string $url)
888 {
889 if ( ! rsssl_admin_logged_in() ) {
890 return null;
891 }
892
893 //now we check if the file remotely exists and then log an error if it does not.
894 $response = wp_remote_get( $url );
895 if ( is_wp_error( $response ) ) {
896 return null;
897 }
898
899 if ( wp_remote_retrieve_response_code($response) !== 200 ) {
900 return null;
901 }
902
903 $json = wp_remote_retrieve_body($response);
904 return json_decode($json);
905 }
906
907 private function remote_file_exists($url): bool {
908 try {
909 $headers = @get_headers($url);
910 if ($headers === false) {
911 // URL is not accessible or some error occurred
912 return false;
913 }
914
915 // Check if the HTTP status code starts with "200" (indicating success)
916 return strpos($headers[0], '200') !== false;
917 // Rest of your code handling $headers goes here
918 } catch (Exception $e) {
919 return false;
920 }
921
922 }
923
924 /**
925 * Stores a full core or component file in the upload folder
926 *
927 * @param $data
928 * @param bool $isCore
929 * @param bool $manifest
930 *
931 * @return void
932 */
933 private function store_file($data, bool $isCore = false, bool $manifest = false): void
934 {
935 if ( ! rsssl_admin_logged_in() ) {
936 return;
937 }
938 //we get the upload directory
939 $upload_dir = Rsssl_File_Storage::get_upload_dir();
940
941 if ( !$manifest ) {
942 $file = $upload_dir . '/' . ($isCore ? 'core.json' : 'components.json');
943 } else {
944 $file = $upload_dir . '/manifest.json';
945 }
946
947 //we delete the old file if it exists
948 if ( file_exists($file) ) {
949 wp_delete_file($file);
950 }
951
952 //if the data is empty, we return null
953 if ( empty($data) ) {
954 return;
955 }
956
957 Rsssl_File_Storage::StoreFile($file, $data);
958 $this->jsons_files_updated = true;
959 }
960
961 public function get_file_stored_info($isCore = false, $manifest = false)
962 {
963 if ( ! rsssl_admin_logged_in() ) {
964 return false;
965 }
966
967 $upload_dir = Rsssl_File_Storage::get_upload_dir();
968
969 if ($manifest) {
970 $file = $upload_dir . '/manifest.json';
971 if (!file_exists($file)) {
972 return false;
973 }
974
975 return Rsssl_File_Storage::GetDate($file);
976 }
977 $file = $upload_dir . '/' . ($isCore ? 'core.json' : 'components.json');
978 if (!file_exists($file)) {
979 return false;
980 }
981
982 return Rsssl_File_Storage::GetDate($file);
983 }
984
985 /* End of files and Storage */
986
987 /* Section for the core files Note: No manifest is needed */
988
989 /**
990 * Downloads the vulnerabilities for the current core version.
991 *
992 * @return void
993 */
994 protected function download_core_vulnerabilities(): void
995 {
996 if ( ! rsssl_admin_logged_in() ) {
997 return;
998 }
999 global $wp_version;
1000 $url = self::RSSSL_SECURITY_API . 'core/WordPress.json';
1001 $data = $this->download($url);
1002 if (!$data) {
1003 return;
1004 }
1005
1006 $data->vulnerabilities = $this->filter_vulnerabilities($data->vulnerabilities, $wp_version, true);
1007
1008 //first we store this as a json file in the uploads folder
1009 $this->store_file($data, true);
1010 }
1011
1012 /* End of core files section */
1013
1014
1015 /* Section for the plug-in files */
1016 /**
1017 * Downloads the vulnerabilities for the current plugins.
1018 *
1019 * @return void
1020 */
1021 protected function download_plugin_vulnerabilities(): void
1022 {
1023 if ( ! rsssl_admin_logged_in() ) {
1024 return;
1025 }
1026 //we get all the installed plugins
1027 $installed_plugins = get_plugins();
1028 //first we get the manifest file
1029 $manifest = $this->getManifest();
1030 $vulnerabilities = [];
1031 foreach ($installed_plugins as $file => $plugin) {
1032 $slug = dirname($file);
1033 $installed_plugins[ $file ]['Slug'] = $slug;
1034 $url = self::RSSSL_SECURITY_API . 'plugin/' . $slug . '.json';
1035 //if the plugin is not in the manifest, we skip it
1036 if (!in_array($slug, (array)$manifest)) {
1037 continue;
1038 }
1039 $data = $this->download($url);
1040 if ($data !== null) {
1041 $vulnerabilities[] = $data;
1042 }
1043 }
1044 //we also do it for all the installed themes
1045 $installed_themes = wp_get_themes();
1046 foreach ($installed_themes as $theme) {
1047 $theme = $theme->get('TextDomain');
1048 $url = self::RSSSL_SECURITY_API . 'theme/' . $theme . '.json';
1049
1050 //if the plugin is not in the manifest, we skip it
1051 if (!in_array($theme, (array)$manifest)) {
1052 continue;
1053 }
1054
1055 $data = $this->download($url);
1056
1057 if ($data !== null) {
1058 $vulnerabilities[] = $data;
1059 }
1060 }
1061
1062 //we make the installed_themes look like the installed_plugins
1063 $installed_themes = array_map( static function ($theme) {
1064 return [
1065 'Name' => $theme->get('Name'),
1066 'Slug' => $theme->get('TextDomain'),
1067 'description' => $theme->get('Description'),
1068 'Version' => $theme->get('Version'),
1069 'Author' => $theme->get('Author'),
1070 'AuthorURI' => $theme->get('AuthorURI'),
1071 'PluginURI' => $theme->get('ThemeURI'),
1072 'TextDomain' => $theme->get('TextDomain'),
1073 'RequiresWP' => $theme->get('RequiresWP'),
1074 'RequiresPHP' => $theme->get('RequiresPHP'),
1075 ];
1076 }, $installed_themes);
1077
1078 //we merge $installed_plugins and $installed_themes
1079 $installed_plugins = array_merge($installed_plugins, $installed_themes);
1080 //we filter the vulnerabilities
1081 $vulnerabilities = $this->filter_active_components($vulnerabilities, $installed_plugins);
1082 $this->store_file($vulnerabilities);
1083 }
1084
1085 /**
1086 * Loads the info from the files Note this is also being used for the themes.
1087 *
1088 * @return mixed|null
1089 */
1090 private function get_components()
1091 {
1092 if ( ! rsssl_admin_logged_in() ) {
1093 return [];
1094 }
1095 $upload_dir = Rsssl_File_Storage::get_upload_dir();
1096
1097 $file = $upload_dir . '/components.json';
1098 if (!file_exists($file)) {
1099 return [];
1100 }
1101
1102 $components = Rsssl_File_Storage::GetFile($file);
1103 if (!is_array($components)) $components = [];
1104 return $components;
1105 }
1106
1107 /* End of plug-in files section */
1108
1109 /* Section for the core files Note: No manifest is needed */
1110 private function get_core()
1111 {
1112 if ( ! rsssl_admin_logged_in() ) {
1113 return null;
1114 }
1115
1116 $upload_dir = Rsssl_File_Storage::get_upload_dir();
1117
1118 $file = $upload_dir . '/core.json';
1119 if (!file_exists($file)) {
1120 return false;
1121 }
1122
1123 return Rsssl_File_Storage::GetFile($file);
1124 }
1125
1126 /* Section for the theme files */
1127
1128 public function enable_feedback_in_theme(): void {
1129 //Logic here for theme warning Create Callback and functions for these steps
1130 //we only display the warning for the theme page
1131 add_action('current_screen', [$this, 'show_theme_warning']);
1132 }
1133
1134 public function show_theme_warning($hook)
1135 {
1136 $screen = get_current_screen();
1137
1138 if ($screen && $screen->id !== 'themes') {
1139 return;
1140 }
1141
1142 //we add warning scripts to themes
1143 add_action('admin_enqueue_scripts', [$this, 'enqueue_theme_warning_scripts']);
1144
1145 }
1146
1147 public function show_inline_code($hook): void {
1148 if ($hook !== 'themes.php') {
1149 return;
1150 }
1151 //we add warning scripts to themes
1152 add_action('admin_footer', [$this, 'enqueue_theme_warning_scripts']);
1153 }
1154
1155 public function enqueue_theme_warning_scripts(): void {
1156 //we get all components with vulnerabilities
1157 $components = $this->get_components();
1158 ob_start();?>
1159 <script>
1160 window.addEventListener("load", () => {
1161 let style = document.createElement('style');
1162 let vulnerable_components = [<?php echo implode(',', array_map(function ($component) {
1163 return "{slug: '" . esc_attr($component->slug) . "', risk: '" . esc_attr($this->get_highest_vulnerability($component->vulnerabilities)) . "'}";
1164 }, $components)) ?>];
1165
1166 //we create the style for warning
1167 style.innerHTML = '.rsssl-theme-notice {box-shadow: 0 1px 1px 0 rgba(0,0,0,.1); position:relative; z-index:50; margin-bottom: -35px; padding: 8px 12px;}';
1168 style.innerHTML += '.rsssl-theme-notice-warning {background-color: #FFF6CE; border-left: 4px solid #ffb900;}';
1169 //we create the style for danger
1170 style.innerHTML += '.rsssl-theme-notice-danger {background-color: #FFCECE; border-left: 4px solid #dc3232;}';
1171 style.innerHTML += '.rsssl-theme-notice-below-notice{margin-top: 41px;}';
1172 style.innerHTML += '.rsssl-theme-notice-warning .dashicons, .rsssl-theme-notice-danger .dashicons{margin-right: 12px;}';
1173 let levels = <?php echo json_encode($this->risk_naming)?>;
1174
1175 //we add the style to the head
1176 document.head.appendChild(style);
1177 //we loop through the components
1178 vulnerable_components.forEach(function(component) {
1179 //we get the theme element
1180 let theme_element = document.querySelector(".theme[data-slug='"+component.slug+"']");
1181 //if the theme exists
1182 if (theme_element) {
1183 //check if theme element contains notice. if so, push this notice down with class rsssl-theme-notice-below-notice
1184 let hasNotice = theme_element.querySelector('.update-message.notice');
1185 //we check the risk
1186 let level = levels[component.risk];
1187 let text = '<?php echo esc_attr(__('Vulnerability: %s', 'really-simple-ssl')) ?>';
1188 text = text.replace('%s', level);
1189 let divClass = ' rsssl-theme-notice ';
1190 divClass += component.risk === 'h' || component.risk === 'c' ? 'rsssl-theme-notice-danger' : 'rsssl-theme-notice-warning';
1191 if (hasNotice) divClass += ' rsssl-theme-notice-below-notice';
1192 theme_element.insertAdjacentHTML('afterbegin', `
1193 <div class="${divClass}">
1194 <div><span class="dashicons dashicons-info"></span>${text}</div>
1195 </div>
1196 `);
1197 }
1198 });
1199 //find quarantined themes, find all themes where the data-slug contains '-rsssl-q'
1200 document.querySelectorAll(".theme[data-slug*='-rsssl-q']").forEach(function(theme_element) {
1201 //if the theme exists
1202 if ( theme_element ) {
1203 //we check the risk
1204 let text = '<?php echo esc_attr(__('Quarantined', 'really-simple-ssl')) ?>';
1205 let divClass = 'rsssl-theme-notice rsssl-theme-notice-danger';
1206 theme_element.insertAdjacentHTML('afterbegin', `
1207 <div class="${divClass}">
1208 <div><span class="dashicons dashicons-info"></span>
1209 <a href="https://really-simple-ssl.com/instructions/about-vulnerabilities/#quarantine" target="_blank" rel="noopener noreferrer">${text}</a>
1210 </div>
1211 </div>
1212 `);
1213 }
1214 });
1215 });
1216 </script>
1217 <?php
1218 echo ob_get_clean();
1219 }
1220
1221 /* End of theme files section */
1222
1223
1224 /* Private functions | Filtering and walks */
1225
1226 /**
1227 * Filters the components based on the active plugins
1228 *
1229 * @param $components
1230 * @param array $active_plugins
1231 *
1232 * @return array
1233 */
1234 private function filter_active_components($components, array $active_plugins): array
1235 {
1236 $active_components = [];
1237 foreach ($components as $component) {
1238 foreach ($active_plugins as $active_plugin) {
1239 if (isset($component->slug) && $component->slug === $active_plugin['Slug']) {
1240 //now we filter out the relevant vulnerabilities
1241 $component->vulnerabilities = $this->filter_vulnerabilities($component->vulnerabilities, $active_plugin['Version']);
1242 //if we have vulnerabilities, we add the component to the active components or when the plugin is closed
1243 if (count($component->vulnerabilities) > 0 || $component->status === 'closed') {
1244 $active_components[] = $component;
1245 }
1246 }
1247 }
1248 }
1249
1250 return $active_components;
1251 }
1252
1253 /**
1254 * This function adds the vulnerability with the highest risk to the plugins page
1255 *
1256 * @param $vulnerabilities
1257 *
1258 * @return string
1259 */
1260 private function get_highest_vulnerability($vulnerabilities): string
1261 {
1262 //we loop through the vulnerabilities and get the highest risk level
1263 $highest_risk_level = 0;
1264
1265 foreach ($vulnerabilities as $vulnerability) {
1266 if ($vulnerability->severity === null) {
1267 continue;
1268 }
1269 if (!isset($this->risk_levels[$vulnerability->severity])) {
1270 continue;
1271 }
1272 if ($this->risk_levels[$vulnerability->severity] > $highest_risk_level) {
1273 $highest_risk_level = $this->risk_levels[$vulnerability->severity];
1274 }
1275 }
1276 //we now loop through the risk levels and return the highest one
1277 foreach ($this->risk_levels as $key => $value) {
1278 if ($value === $highest_risk_level) {
1279 return $key;
1280 }
1281 }
1282
1283 return 'l';
1284 }
1285
1286 /* End of private functions | Filtering and walks */
1287
1288
1289 /* Private functions | End of Filtering and walks */
1290
1291
1292 /* Private functions | Feedback, Styles and scripts */
1293
1294 /**
1295 * This function shows the feedback in the plugin
1296 *
1297 * @return void
1298 */
1299 private function enable_feedback_in_plugin(): void {
1300 //we add some styling to this page
1301 add_action('admin_enqueue_scripts', array($this, 'add_vulnerability_styles'));
1302 //we add an extra column to the plugins page
1303 add_filter('manage_plugins_columns', array($this, 'add_vulnerability_column'));
1304 add_filter('manage_plugins-network_columns', array($this, 'add_vulnerability_column'));
1305 //now we add the field to the plugins page
1306 add_action('manage_plugins_custom_column', array($this, 'add_vulnerability_field'), 10, 3);
1307 add_action('manage_plugins-network_custom_column', array($this, 'add_vulnerability_field'), 10, 3);
1308 }
1309
1310 /* End of private functions | Feedback, Styles and scripts */
1311
1312 /**
1313 * This function downloads the manifest file from the api server
1314 *
1315 * @return void
1316 */
1317 private function download_manifest(): void {
1318 if ( ! rsssl_admin_logged_in() ) {
1319 return;
1320 }
1321 $url = self::RSSSL_SECURITY_API . 'manifest.json';
1322 $data = $this->download($url);
1323
1324 //we convert the data to an array
1325 $data = json_decode(json_encode($data), true);
1326
1327 //first we store this as a json file in the uploads folder
1328 $this->store_file($data, true, true);
1329 }
1330
1331 /**
1332 * This function downloads the created file from the uploads
1333 *
1334 * @return false|void
1335 */
1336 private function getManifest()
1337 {
1338 if ( ! rsssl_admin_logged_in() ) {
1339 return false;
1340 }
1341
1342 $upload_dir = Rsssl_File_Storage::get_upload_dir();
1343
1344 $file = $upload_dir . '/manifest.json';
1345 if (!file_exists($file)) {
1346 return false;
1347 }
1348
1349 return Rsssl_File_Storage::GetFile($file);
1350 }
1351
1352 private function filter_vulnerabilities($vulnerabilities, $Version, $core = false): array {
1353 $filtered_vulnerabilities = array();
1354 foreach ( $vulnerabilities as $vulnerability ) {
1355 //if fixed_in contains a version, and the current version is higher than the fixed_in version, we skip it as fixed.
1356 //This needs to be a positive check only, as the fixed_in value is less accurate than the version_from and version_to values
1357 if ( function_exists( 'rsssl_version_compare' ) ) {
1358 if ( $vulnerability->fixed_in !== 'not fixed' && rsssl_version_compare( $Version, $vulnerability->fixed_in, '>=' ) ) {
1359 continue;
1360 }
1361 } else {
1362 # fallback
1363 if ( $vulnerability->fixed_in !== 'not fixed' && version_compare( $Version, $vulnerability->fixed_in, '>=' ) ) {
1364 continue;
1365 }
1366 }
1367
1368 //we have the fields version_from and version_to and their needed operators
1369 $version_from = $vulnerability->version_from;
1370 $version_to = $vulnerability->version_to;
1371 $operator_from = $vulnerability->operator_from;
1372 $operator_to = $vulnerability->operator_to;
1373 //we now check if the version is between the two versions
1374 if ( function_exists( 'rsssl_version_compare' ) ) {
1375 if ( rsssl_version_compare( $Version, $version_from, $operator_from ) && rsssl_version_compare( $Version, $version_to, $operator_to ) ) {
1376 $filtered_vulnerabilities[] = $vulnerability;
1377 }
1378 } else {
1379 if ( version_compare( $Version, $version_from, $operator_from ) && version_compare( $Version, $version_to, $operator_to ) ) {
1380 $filtered_vulnerabilities[] = $vulnerability;
1381 }
1382 }
1383
1384 }
1385 return $filtered_vulnerabilities;
1386 }
1387
1388 /**
1389 * Get count of risk occurrence for each risk level
1390 * @return array
1391 */
1392 public function count_risk_levels(): array {
1393 $plugins = $this->workable_plugins;
1394 $risk_levels = array();
1395 foreach ($plugins as $plugin) {
1396 if (isset($plugin['risk_level'])) {
1397 if (isset($risk_levels[$plugin['risk_level']])) {
1398 $risk_levels[$plugin['risk_level']]++;
1399 } else {
1400 $risk_levels[$plugin['risk_level']] = 1;
1401 }
1402 }
1403 }
1404
1405 return $risk_levels;
1406 }
1407
1408 /**
1409 * check if a a dismissed notice should be reset
1410 *
1411 * @param string $risk_level
1412 *
1413 * @return bool
1414 */
1415 private function should_reset_notification(string $risk_level): bool {
1416 $plugins = $this->workable_plugins;
1417 $vulnerable_plugins = array();
1418 foreach ($plugins as $plugin) {
1419 if (isset($plugin['risk_level']) && $plugin['risk_level'] === $risk_level) {
1420 $vulnerable_plugins[] = $plugin['rss_identifier'];
1421 }
1422 }
1423 $dismissed_for = get_option("rsssl_{$risk_level}_notification_dismissed_for",[]);
1424 //cleanup. Check if plugins in mail_sent_for exist in the $plugins array
1425 foreach ($dismissed_for as $key => $rss_identifier) {
1426 if ( ! in_array($rss_identifier, $vulnerable_plugins) ) {
1427 unset($dismissed_for[$key]);
1428 }
1429 }
1430
1431 $diff = array_diff($vulnerable_plugins, $dismissed_for);
1432 foreach ($diff as $rss_identifier) {
1433 if (!in_array($rss_identifier, $dismissed_for)){
1434 $dismissed_for[] = $rss_identifier;
1435 }
1436 }
1437 //add the new plugins to the $dismissed_for array
1438 update_option("rsssl_{$risk_level}_notification_dismissed_for", $dismissed_for, false );
1439 return !empty($diff);
1440 }
1441
1442 /**
1443 * check if a new mail should be sent about vulnerabilities
1444 * @return bool
1445 */
1446 private function should_send_mail(): bool {
1447 $plugins = $this->workable_plugins;
1448 $vulnerable_plugins = array();
1449 foreach ($plugins as $plugin) {
1450 if (isset($plugin['risk_level'])) {
1451 $vulnerable_plugins[] = $plugin['rss_identifier'];
1452 }
1453 }
1454
1455 $mail_sent_for = get_option('rsssl_vulnerability_mail_sent_for',[]);
1456 //cleanup. Check if plugins in mail_sent_for exist in the $plugins array
1457 foreach ($mail_sent_for as $key => $rss_identifier) {
1458 if ( ! in_array($rss_identifier, $vulnerable_plugins) ) {
1459 unset($mail_sent_for[$key]);
1460 }
1461 }
1462
1463 $diff = array_diff($vulnerable_plugins, $mail_sent_for);
1464 foreach ($diff as $rss_identifier) {
1465 if (!in_array($rss_identifier, $mail_sent_for)){
1466 $mail_sent_for[] = $rss_identifier;
1467 }
1468 }
1469
1470 //add the new plugins to the mail_sent_for array
1471 update_option('rsssl_vulnerability_mail_sent_for',$mail_sent_for, false );
1472 return !empty($diff);
1473 }
1474
1475 /**
1476 * Get id by risk level
1477 * @param array $vulnerabilities
1478 * @param string $risk_level
1479 *
1480 * @return mixed|void
1481 */
1482 private function getLinkedUUID( array $vulnerabilities, string $risk_level)
1483 {
1484 foreach ($vulnerabilities as $vulnerability) {
1485 if ($vulnerability->severity === $risk_level) {
1486 return $vulnerability->rss_identifier;
1487 }
1488 }
1489 }
1490
1491 private function getLinkedDate($vulnerabilities, string $risk_level)
1492 {
1493 foreach ($vulnerabilities as $vulnerability) {
1494 if ($vulnerability->severity === $risk_level) {
1495 //we return the date in a readable format
1496 return date(get_option('date_format'), strtotime($vulnerability->published_date));
1497 }
1498 }
1499 }
1500
1501 /**
1502 * Send email warning
1503 * @return void
1504 */
1505 public function send_vulnerability_mail(): void {
1506 if ( ! rsssl_admin_logged_in() ) {
1507 return;
1508 }
1509
1510 //first we check if the user wants to receive emails
1511 if ( !rsssl_get_option('send_notifications_email') ) {
1512 return;
1513 }
1514
1515 $level_for_email = rsssl_get_option('vulnerability_notification_email_admin');
1516 if ( !$level_for_email || $level_for_email === '*' ) {
1517 return;
1518 }
1519
1520 //now based on the risk level we send a different email
1521 $risk_levels = $this->count_risk_levels();
1522 $total = 0;
1523 $blocks = [];
1524 foreach ($risk_levels as $risk_level => $count) {
1525 if ( $this->risk_levels[$risk_level] >= $this->risk_levels[$level_for_email] ) {
1526 $blocks[] = $this->createBlock($risk_level, $count);
1527 $total += $count;
1528 }
1529 }
1530
1531 //date format is named month day year
1532 $mailer = new rsssl_mailer();
1533 $mailer->subject = sprintf(__("Vulnerability Alert: %s", "really-simple-ssl"), $this->site_url() );
1534 $mailer->title = sprintf(_n("%s: %s vulnerability found", "%s: %s vulnerabilities found", $total, "really-simple-ssl"), $this->date(), $total);
1535 $message = sprintf(__("This is a vulnerability alert from Really Simple Security for %s. ","really-simple-ssl"), $this->domain() );
1536 $mailer->message = $message;
1537 $mailer->warning_blocks = $blocks;
1538 if ($total > 0) {
1539 //if for some reason the total is 0, we don't send an email
1540 $mailer->send_mail();
1541 }
1542 }
1543
1544 /**
1545 * Create an email block by risk level
1546 *
1547 * @param string $risk_level
1548 * @param int $count
1549 *
1550 * @return array
1551 */
1552 protected function createBlock(string $risk_level, int $count): array
1553 {
1554 $plugin_name = '';
1555 //if we have only one plugin with this risk level, we can show the plugin name
1556 //we search it in the list
1557 if ( $count===1 ){
1558 $plugins = $this->workable_plugins;
1559 foreach ($plugins as $plugin) {
1560 if (isset($plugin['risk_level']) && $plugin['risk_level'] === $risk_level) {
1561 $plugin_name = $plugin['Name'];
1562 }
1563 }
1564 }
1565
1566 $risk = $this->risk_naming[$risk_level];
1567 $title = $this->get_warning_string($risk_level, $count);
1568 $message = $count === 1 ? sprintf(__("A %s vulnerability is found in %s.", "really-simple-ssl"),$risk, $plugin_name) : sprintf(__("Multiple %s vulnerabilities have been found.", "really-simple-ssl"), $risk);
1569
1570 return [
1571 'title' => $title,
1572 'message' => $message . ' ' .
1573 __('Based on your settings, Really Simple Security will take appropriate action, or you will need to solve it manually.','really-simple-ssl') .' '.
1574 sprintf(__('Get more information from the Really Simple Security dashboard on %s'), $this->domain() ),
1575 'url' => rsssl_admin_url( [], '#settings/vulnerabilities/vulnerabilities-overview'),
1576 ];
1577 }
1578
1579 /**
1580 * @param string $risk_level
1581 * @param int $count
1582 *
1583 * @return string
1584 */
1585 public function get_warning_string( string $risk_level, int $count): string {
1586 switch ($risk_level){
1587 case 'c':
1588 $warning = sprintf(_n('You have %s critical vulnerability', 'You have %s critical vulnerabilities', $count, 'really-simple-ssl'), $count);
1589 break;
1590 case 'h':
1591 $warning = sprintf(_n('You have %s high-risk vulnerability', 'You have %s high-risk vulnerabilities', $count, 'really-simple-ssl'), $count);
1592 break;
1593 case 'm':
1594 $warning = sprintf(_n('You have %s medium-risk vulnerability', 'You have %s medium-risk vulnerabilities', $count, 'really-simple-ssl'), $count);
1595 break;
1596 default:
1597 $warning = sprintf(_n('You have %s low-risk vulnerability', 'You have %s low-risk vulnerabilities', $count, 'really-simple-ssl'), $count);
1598 break;
1599 }
1600 return $warning;
1601 }
1602
1603 /**
1604 * Get a nicely formatted date for today's date
1605 *
1606 * @return string
1607 */
1608 public function date(): string {
1609 return date_i18n( get_option( 'date_format' ));
1610 }
1611
1612 /**
1613 * Get the domain name in a clickable format
1614 *
1615 * @return string
1616 */
1617 public function domain(): string {
1618 return '<a href="'.$this->site_url().'" target="_blank" rel="noopener noreferrer">'.$this->site_url().'</a>';
1619 }
1620
1621 /**
1622 * Cron triggers may sometimes result in http URL's, even though SSL is enabled in Really Simple Security.
1623 * We ensure that the URL is returned with https if SSL is enabled.
1624 *
1625 * @return string
1626 */
1627 public function site_url(): string {
1628 $ssl_enabled = rsssl_get_option('ssl_enabled') || is_ssl();
1629 $scheme = $ssl_enabled ? 'https' : 'http';
1630 return get_site_url(null, '', $scheme);
1631 }
1632
1633 }
1634
1635 //we initialize the class
1636 //add_action('init', array(rsssl_vulnerabilities::class, 'instance'));
1637 if ( !defined('rsssl_pro') ) {
1638 $vulnerabilities = new rsssl_vulnerabilities();
1639 }
1640 }
1641
1642 #########################################################################################
1643 # Functions for the vulnerability scanner #
1644 # These functions are used in the vulnerability scanner like the notices and the api's #
1645 #########################################################################################
1646 //we clear all the cache when the vulnerability scanner is enabled
1647
1648
1649
1650 function rsssl_vulnerabilities_api( array $response, string $action, $data ): array {
1651 if ( ! rsssl_user_can_manage() ) {
1652 return $response;
1653 }
1654 switch ($action) {
1655 case 'vulnerabilities_test_notification':
1656 //creating a random string based on time.
1657 $random_string = md5( time() );
1658 update_option( 'test_vulnerability_tester', $random_string, false );
1659 //clear admin notices cache
1660 delete_option('rsssl_admin_notices');
1661 $response = rsssl_vulnerabilities::testGenerator();
1662 break;
1663 case 'vulnerabilities_scan_files':
1664 $response = rsssl_vulnerabilities::firstRun();
1665 break;
1666 case 'vulnerabilities_measures_get':
1667 $response = ( new rsssl_vulnerabilities )->measures_data();
1668 break;
1669 case 'vulnerabilities_measures_set':
1670 $response = ( new rsssl_vulnerabilities )->measures_set($data);
1671 break;
1672 }
1673
1674 return $response;
1675 }
1676 add_filter( 'rsssl_do_action', 'rsssl_vulnerabilities_api', 10, 3 );
1677
1678 /* End of Routing and API's */