PluginProbe
Defender Security – Malware Scanner, Login Security & Firewall / 6.3.0
Defender Security – Malware Scanner, Login Security & Firewall v6.3.0
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
← All changes | src/model/class-scan.php +107 -100 6.2.0 → 6.3.0 View file →
@@ -183,28 +183,20 @@
183 183 $count_issues_filtered = 0;
184 184
185 185 $scan_item_group_total = wd_di()->get( Scan_Item::class )->get_types_total( $this->id, Scan_Item::STATUS_ACTIVE );
186 186
187 - $count_issues = isset( $scan_item_group_total['all'] ) ?
188 - $scan_item_group_total['all'] : 0;
189 - $count_core = isset( $scan_item_group_total[ Scan_Item::TYPE_INTEGRITY ] ) ?
190 - $scan_item_group_total[ Scan_Item::TYPE_INTEGRITY ] : 0;
191 - $count_plugin = isset( $scan_item_group_total[ Scan_Item::TYPE_PLUGIN_CHECK ] ) ?
192 - $scan_item_group_total[ Scan_Item::TYPE_PLUGIN_CHECK ] : 0;
193 - $count_malware = isset( $scan_item_group_total[ Scan_Item::TYPE_SUSPICIOUS ] ) ?
194 - $scan_item_group_total[ Scan_Item::TYPE_SUSPICIOUS ] : 0;
195 - $count_vuln = isset( $scan_item_group_total[ Scan_Item::TYPE_VULNERABILITY ] ) ?
196 - $scan_item_group_total[ Scan_Item::TYPE_VULNERABILITY ] : 0;
187 + $count_issues = $scan_item_group_total['all'] ?? 0;
188 + $count_core = $scan_item_group_total[ Scan_Item::TYPE_INTEGRITY ] ?? 0;
189 + $count_plugin = $scan_item_group_total[ Scan_Item::TYPE_PLUGIN_CHECK ] ?? 0;
190 + $count_malware = $scan_item_group_total[ Scan_Item::TYPE_SUSPICIOUS ] ?? 0;
191 + $count_vuln = $scan_item_group_total[ Scan_Item::TYPE_VULNERABILITY ] ?? 0;
197 192 // New counts since v5.5.0.
198 - $count_outdated_plugin = isset( $scan_item_group_total[ Scan_Item::TYPE_PLUGIN_OUTDATED ] ) ?
199 - $scan_item_group_total[ Scan_Item::TYPE_PLUGIN_OUTDATED ] : 0;
200 - $count_closed_plugin = isset( $scan_item_group_total[ Scan_Item::TYPE_PLUGIN_CLOSED ] ) ?
201 - $scan_item_group_total[ Scan_Item::TYPE_PLUGIN_CLOSED ] : 0;
193 + $count_outdated_plugin = $scan_item_group_total[ Scan_Item::TYPE_PLUGIN_OUTDATED ] ?? 0;
194 + $count_closed_plugin = $scan_item_group_total[ Scan_Item::TYPE_PLUGIN_CLOSED ] ?? 0;
202 195
203 196 $scan_item_ignore_total = wd_di()->get( Scan_Item::class )->get_types_total( $this->id, Scan_Item::STATUS_IGNORE );
204 197
205 - $count_ignored = isset( $scan_item_ignore_total['all'] ) ?
206 - $scan_item_ignore_total['all'] : 0;
198 + $count_ignored = $scan_item_ignore_total['all'] ?? 0;
207 199
208 200 foreach ( $ignored_models as $model ) {
209 201 $ignored[] = $model->to_array();
210 202 }
@@ -356,8 +348,9 @@
356 348
357 349 return $builder->count();
358 350 }
359 351
352 +
360 353 /**
361 354 * Allow a specific issue by updating its status and removing it from the global ignore indexer.
362 355 *
363 356 * @param int $id The ID of the issue to Allow.
@@ -364,37 +357,9 @@
364 357 *
365 358 * @return bool
366 359 */
367 360 public function unignore_issue( $id ): bool {
368 - $issue = $this->get_issue( $id );
369 - if ( ! is_object( $issue ) ) {
370 - return false;
371 - }
372 - // Check if the current issue already exists in the Issues list, there is no need to add a duplicate.
373 - $current_issue_arr = $issue->to_array();
374 - foreach ( $this->get_issues( null, Scan_Item::STATUS_ACTIVE ) as $active_issue ) {
375 - $active_issue_arr = $active_issue->to_array();
376 - if (
377 - $current_issue_arr['type'] === $active_issue_arr['type']
378 - && $current_issue_arr['full_path'] === $active_issue_arr['full_path']
379 - ) {
380 - return false;
381 - }
382 - }
383 -
384 - $issue->status = Scan_Item::STATUS_ACTIVE;
385 - $issue->save();
386 -
387 - $ignore_lists = get_site_option( self::IGNORE_INDEXER, array() );
388 - $data = $issue->raw_data;
389 - if ( isset( $data['file'] ) ) {
390 - unset( $ignore_lists[ array_search( $data['file'], $ignore_lists, true ) ] );
391 - } elseif ( isset( $data['slug'] ) ) {
392 - unset( $ignore_lists[ array_search( $data['slug'], $ignore_lists, true ) ] );
393 - }
394 - $this->update_ignore_list( $ignore_lists );
395 -
396 - return true;
361 + return $this->update_issue_status( (int) $id, Scan_Item::STATUS_ACTIVE, Scan_Item::STATUS_ACTIVE );
397 362 }
398 363
399 364 /**
400 365 * Check if a slug is ignored, we use a global indexer, so we can check while
@@ -417,32 +382,9 @@
417 382 *
418 383 * @return bool
419 384 */
420 385 public function ignore_issue( $id ): bool {
421 - $issue = $this->get_issue( $id );
422 - if ( ! is_object( $issue ) ) {
423 - return false;
424 - }
425 - // Check if the current issue already exists in the Ignored list, there is no need to add a duplicate.
426 - $current_issue_arr = $issue->to_array();
427 - foreach ( $this->get_issues( null, Scan_Item::STATUS_IGNORE ) as $ignore_issue ) {
428 - $ignore_issue_arr = $ignore_issue->to_array();
429 - if ( $current_issue_arr['type'] === $ignore_issue_arr['type'] &&
430 - $current_issue_arr['full_path'] === $ignore_issue_arr['full_path']
431 - ) {
432 - return false;
433 - }
434 - }
435 -
436 - $issue->status = Scan_Item::STATUS_IGNORE;
437 - $issue->save();
438 -
439 - // Add this into a global ignored index and update the ignored list.
440 - $ignore_lists = get_site_option( self::IGNORE_INDEXER, array() );
441 - $ignore_lists[] = $current_issue_arr['full_path'];
442 - $this->update_ignore_list( $ignore_lists );
443 -
444 - return true;
386 + return $this->update_issue_status( (int) $id, Scan_Item::STATUS_IGNORE, Scan_Item::STATUS_IGNORE );
445 387 }
446 388
447 389 /**
448 390 * Retrieves a Scan_Item object based on the given ID.
@@ -554,10 +496,9 @@
554 496
555 497 $scan_item_ignore_total = wd_di()->get( Scan_Item::class )
556 498 ->get_types_total( $this->id, Scan_Item::STATUS_IGNORE );
557 499
558 - $count_ignored = isset( $scan_item_ignore_total['all'] ) ?
559 - $scan_item_ignore_total['all'] : 0;
500 + $count_ignored = $scan_item_ignore_total['all'] ?? 0;
560 501 $count_ignored_filtered = (int) $this->count( $type, Scan_Item::STATUS_IGNORE );
561 502
562 503 $total_issue_pages = 1;
563 504 $total_ignored_pages = 1;
@@ -580,27 +521,8 @@
580 521 } else {
581 522 $data = $this->prepare_issues( null, null, $type, $scenario );
582 523 }
583 524
584 - $scan_item_group_total = wd_di()->get( Scan_Item::class )
585 - ->get_types_total( $this->id, Scan_Item::STATUS_ACTIVE );
586 -
587 - $count_issues = isset( $scan_item_group_total['all'] ) ?
588 - $scan_item_group_total['all'] : 0;
589 - $count_core = isset( $scan_item_group_total[ Scan_Item::TYPE_INTEGRITY ] ) ?
590 - $scan_item_group_total[ Scan_Item::TYPE_INTEGRITY ] : 0;
591 - $count_plugin = isset( $scan_item_group_total[ Scan_Item::TYPE_PLUGIN_CHECK ] ) ?
592 - $scan_item_group_total[ Scan_Item::TYPE_PLUGIN_CHECK ] : 0;
593 - $count_malware = isset( $scan_item_group_total[ Scan_Item::TYPE_SUSPICIOUS ] ) ?
594 - $scan_item_group_total[ Scan_Item::TYPE_SUSPICIOUS ] : 0;
595 - $count_vuln = isset( $scan_item_group_total[ Scan_Item::TYPE_VULNERABILITY ] ) ?
596 - $scan_item_group_total[ Scan_Item::TYPE_VULNERABILITY ] : 0;
597 - // New counts since v5.5.0.
598 - $count_outdated_plugin = isset( $scan_item_group_total[ Scan_Item::TYPE_PLUGIN_OUTDATED ] ) ?
599 - $scan_item_group_total[ Scan_Item::TYPE_PLUGIN_OUTDATED ] : 0;
600 - $count_closed_plugin = isset( $scan_item_group_total[ Scan_Item::TYPE_PLUGIN_CLOSED ] ) ?
601 - $scan_item_group_total[ Scan_Item::TYPE_PLUGIN_CLOSED ] : 0;
602 -
603 525 return array(
604 526 'status' => $this->status,
605 527 'issues_items' => $data['issues'],
606 528 'ignored_items' => $data['ignored'],
@@ -613,17 +535,17 @@
613 535 : '',
614 536 'count' => array(
615 537 'total' => is_array( $data['issues'] ) || $data['issues'] instanceof Countable ? count( $data['issues'] ) : 0,
616 538 'total_filtered' => $total_filtered,
617 - 'issues_total' => $count_issues,
539 + 'issues_total' => $data['count_issues'],
618 540 'issues_total_filtered' => $count_issues_filtered,
619 541 'ignored_total' => $count_ignored,
620 542 'ignored_total_filtered' => $count_ignored_filtered,
621 - 'core' => $count_core + $count_plugin,
622 - 'content' => $count_malware,
623 - 'vuln' => $count_vuln,
624 - 'outdated_plugin' => $count_outdated_plugin,
625 - 'closed_plugin' => $count_closed_plugin,
543 + 'core' => $data['count_core'] + $data['count_plugin'],
544 + 'content' => $data['count_malware'],
545 + 'vuln' => $data['count_vuln'],
546 + 'outdated_plugin' => $data['count_outdated_plugin'],
547 + 'closed_plugin' => $data['count_closed_plugin'],
626 548 ),
627 549 'paging' => array(
628 550 'issue' => array(
629 551 'paged' => $paged,
@@ -981,17 +903,32 @@
981 903
982 904 /**
983 905 * Check if a slug is whitelisted.
984 906 *
907 + * File entries are matched only when the slug's base name equals the entry
908 + * (case-insensitive), so paths like "robots.txt.backdoor" or
909 + * "robots.txt/malware.php" are never falsely accepted.
910 + *
911 + * Directory entries are matched as complete path segments, preventing
912 + * substring bypasses like ".git-malware/payload.php" or "not.git/payload.php".
913 + *
985 914 * @param string $slug The path to file.
986 915 *
987 916 * @return bool
988 917 */
989 918 public function is_issue_whitelisted( string $slug ): bool {
990 - $whitelisted_files = $this->whitelisted_files();
991 - foreach ( $whitelisted_files as $file ) {
992 - if ( false !== stristr( $slug, $file ) ) {
993 - return true;
919 + $whitelisted_files = $this->whitelisted_files();
920 + $whitelisted_directories = array( '.well-known', '.idea', '.svn', '.git', '.quarantine', '.tmb', '.vscode' );
921 + $normalized_slug = str_replace( '\\', '/', $slug );
922 + $bounded_slug = '/' . trim( $normalized_slug, '/' ) . '/';
923 +
924 + foreach ( $whitelisted_files as $entry ) {
925 + if ( ! in_array( $entry, $whitelisted_directories, true ) ) {
926 + if ( 0 === strcasecmp( basename( $normalized_slug ), $entry ) ) {
927 + return true;
928 + }
929 + } elseif ( false !== stripos( $bounded_slug, '/' . $entry . '/' ) ) {
930 + return true;
994 931 }
995 932 }
996 933
997 934 return false;
@@ -1083,6 +1020,76 @@
1083 1020 * @since 2.6.1
1084 1021 */
1085 1022 private function is_positive_int( $id ): bool {
1086 1023 return is_int( $id ) && $id > 0;
1024 + }
1025 +
1026 + /**
1027 + * Check if an issue with the given type and full_path already exists under a specific status.
1028 + * Used to prevent adding duplicate entries to the ignored or active lists.
1029 + *
1030 + * @param array $current_issue_arr Associative array representation of the issue to check.
1031 + * @param string $status The status to search within (e.g. Scan_Item::STATUS_ACTIVE or STATUS_IGNORE).
1032 + *
1033 + * @return bool True if a matching issue already exists with that status.
1034 + */
1035 + private function issue_exists_with_status( array $current_issue_arr, string $status ): bool {
1036 + foreach ( $this->get_issues( null, $status ) as $existing_issue ) {
1037 + $existing_arr = $existing_issue->to_array();
1038 + if (
1039 + $current_issue_arr['type'] === $existing_arr['type']
1040 + && $current_issue_arr['full_path'] === $existing_arr['full_path']
1041 + ) {
1042 + return true;
1043 + }
1044 + }
1045 +
1046 + return false;
1047 + }
1048 +
1049 + /**
1050 + * Update the status of an issue and sync the global ignore indexer accordingly.
1051 + *
1052 + * When $new_status is STATUS_IGNORE the issue's full_path is appended to the indexer.
1053 + * When $new_status is STATUS_ACTIVE the issue's file/slug key is removed from the indexer.
1054 + *
1055 + * @param int $id The ID of the issue to update.
1056 + * @param string $new_status The target status (Scan_Item::STATUS_IGNORE or STATUS_ACTIVE).
1057 + * @param string $guard_status The status to check for existing duplicates before updating.
1058 + *
1059 + * @return bool True on success, false if the issue does not exist or a duplicate was found.
1060 + */
1061 + private function update_issue_status( int $id, string $new_status, string $guard_status ): bool {
1062 + $issue = $this->get_issue( $id );
1063 + if ( ! is_object( $issue ) ) {
1064 + return false;
1065 + }
1066 +
1067 + $current_issue_arr = $issue->to_array();
1068 + // Bail out if an identical issue already exists under the target status.
1069 + if ( $this->issue_exists_with_status( $current_issue_arr, $guard_status ) ) {
1070 + return false;
1071 + }
1072 +
1073 + $issue->status = $new_status;
1074 + $issue->save();
1075 +
1076 + $ignore_lists = get_site_option( self::IGNORE_INDEXER, array() );
1077 +
1078 + if ( Scan_Item::STATUS_IGNORE === $new_status ) {
1079 + // Add this into a global ignored index and update the ignored list.
1080 + $ignore_lists[] = $current_issue_arr['full_path'];
1081 + } else {
1082 + // Remove the entry from the global ignored index.
1083 + $data = $issue->raw_data;
1084 + if ( isset( $data['file'] ) ) {
1085 + unset( $ignore_lists[ array_search( $data['file'], $ignore_lists, true ) ] );
1086 + } elseif ( isset( $data['slug'] ) ) {
1087 + unset( $ignore_lists[ array_search( $data['slug'], $ignore_lists, true ) ] );
1088 + }
1089 + }
1090 +
1091 + $this->update_ignore_list( $ignore_lists );
1092 +
1093 + return true;
1087 1094 }
1088 1095 }