PluginProbe
Defender Security – Malware Scanner, Login Security & Firewall / 6.2.3
Defender Security – Malware Scanner, Login Security & Firewall v6.2.3
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 / behavior / scan / class-core-integrity.php

class-core-integrity.php in Defender Security – Malware Scanner, Login Security & Firewall 6.2.3, at src/behavior/scan/class-core-integrity.php

228 lines 6.5 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Handles core integrity scan.
4 *
5 * @package WP_Defender\Behavior\Scan
6 */
7
8 namespace WP_Defender\Behavior\Scan;
9
10 use ArrayIterator;
11 use WP_Defender\Traits\IO;
12 use WP_Defender\Model\Scan;
13 use Calotes\Component\Behavior;
14 use WP_Defender\Model\Scan_Item;
15 use WP_Defender\Component\Timer;
16 use WP_Defender\Helper\Analytics\Scan as Scan_Analytics;
17 use WP_Defender\Controller\Scan as Scan_Controller;
18
19 /**
20 * Handles core integrity scan.
21 */
22 class Core_Integrity extends Behavior {
23
24 use IO;
25
26 public const CACHE_CHECKSUMS = 'wd_cache_checksums', ISSUE_CHECKSUMS = 'wd_issue_checksums';
27
28 /**
29 * Check that the folder is empty.
30 *
31 * @param string $path The path to check.
32 *
33 * @return bool
34 */
35 protected function is_dir_empty( $path ) {
36 $rfiles = scandir( $path );
37 foreach ( $rfiles as $rfile ) {
38 if ( ! in_array( $rfile, array( '.', '..' ), true ) ) {
39 return false;
40 }
41 }
42
43 return true;
44 }
45
46 /**
47 * Check if the core file is on touch.
48 *
49 * @return bool
50 */
51 public function core_integrity_check(): bool {
52 $checksums = $this->get_checksum();
53 if ( false === $checksums ) {
54 return false;
55 }
56 $core_files = get_site_option( Gather_Fact::CACHE_CORE, array() );
57 $core_files = new ArrayIterator( $core_files );
58 $timer = new Timer();
59 $model = $this->owner->scan;
60 $pos = (int) $model->task_checkpoint;
61 if ( $pos > 0 ) {
62 $core_files->seek( $pos );
63 }
64 $this->log( sprintf( 'current pos %s', $pos ), Scan_Controller::SCAN_LOG );
65 while ( $core_files->valid() ) {
66 if ( ! $timer->check() ) {
67
68 $reason = 'break out cause too long';
69
70 /**
71 * Retrieves an instance of the Scan_Analytics class.
72 *
73 * @var Scan_Analytics $scan_analytics
74 */
75 $scan_analytics = wd_di()->get( Scan_Analytics::class );
76
77 $scan_analytics->track_feature(
78 $scan_analytics::EVENT_SCAN_FAILED,
79 array(
80 $scan_analytics::EVENT_SCAN_FAILED_PROP => $scan_analytics::EVENT_SCAN_FAILED_ERROR,
81 'Error_Reason' => $reason,
82 )
83 );
84
85 $this->log( $reason, Scan_Controller::SCAN_LOG );
86 break;
87 }
88
89 if ( $model->is_issue_whitelisted( $core_files->current() ) ) {
90 // This is whitelisted, so do nothing.
91 $this->log( sprintf( 'skip %s because of file is whitelisted', $core_files->current() ), Scan_Controller::SCAN_LOG );
92 $core_files->next();
93 continue;
94 }
95
96 if ( $model->is_issue_ignored( $core_files->current() ) ) {
97 // This is ignored, so do nothing.
98 $this->log( sprintf( 'skip %s because of file is ignored', $core_files->current() ), Scan_Controller::SCAN_LOG );
99 $core_files->next();
100 continue;
101 }
102
103 $file = $core_files->current();
104 // Get relative so we can compare.
105 $abs_path = defender_replace_line( ABSPATH );
106 $rev_file = str_replace( $abs_path, '', $file );
107 // Remove directory separator on the left.
108 $rev_file = ltrim( $rev_file, DIRECTORY_SEPARATOR );
109
110 if ( isset( $checksums[ $rev_file ] ) ) {
111 if ( ! $this->compare_hashes( $file, $checksums[ $rev_file ] ) ) {
112 $this->log( sprintf( 'modified %s', $file ), Scan_Controller::SCAN_LOG );
113 $model->add_item(
114 Scan_Item::TYPE_INTEGRITY,
115 array(
116 'file' => $file,
117 'type' => 'modified',
118 )
119 );
120 }
121 } else {
122 if ( is_dir( $file ) ) {
123 if ( $this->is_dir_empty( $core_files->current() ) ) {
124 $this->log(
125 sprintf( 'skip %s because of non-WP directory is empty', $core_files->current() ),
126 Scan_Controller::SCAN_LOG
127 );
128 $core_files->next();
129 continue;
130 }
131 $item_type = 'dir';
132 } else {
133 // Sometimes plugins create duplicate readme files. If the content is identical, we'll ignore the issue.
134 if ( isset( $checksums['readme.html'] ) && $this->compare_hashes( $file, $checksums['readme.html'] ) ) {
135 $this->log( sprintf( 'skip %s because this is just the same renamed readme file', $file ), Scan_Controller::SCAN_LOG );
136 $core_files->next();
137 continue;
138 }
139 $item_type = 'unversion';
140 }
141
142 $model->add_item(
143 Scan_Item::TYPE_INTEGRITY,
144 array(
145 'file' => $file,
146 'type' => $item_type,
147 )
148 );
149 }
150 $model->calculate_percent( $core_files->key() * 100 / $core_files->count(), 2 );
151 if ( 0 === $core_files->key() % 100 ) {
152 // We should update the model percent each 100 files, so we have some progress ont he screen$pos * 100 / $core_files->count().
153 $model->save();
154 }
155 $core_files->next();
156 }
157 if ( $core_files->valid() ) {
158 // Save the current progress and quit.
159 $model->task_checkpoint = $core_files->key();
160 } else {
161 // We will check if we have any ignore issue from last scan, so we can bring it here.
162 $last = Scan::get_last();
163 if ( is_object( $last ) ) {
164 $ignored_issues = $last->get_issues( Scan_Item::TYPE_INTEGRITY, Scan_Item::STATUS_IGNORE );
165 $model->carry_forward_ignored_issues( Scan_Item::TYPE_INTEGRITY, $ignored_issues, 'file' );
166 }
167 // Done, reset this, so we can use it later.
168 $model->task_checkpoint = '';
169 }
170 $model->save();
171
172 return ! $core_files->valid();
173 }
174
175 /**
176 * Fetch the checksums.
177 *
178 * @return bool|array
179 */
180 protected function get_checksum() {
181 $cache = get_site_option( self::CACHE_CHECKSUMS, false );
182 if ( is_array( $cache ) ) {
183 return $cache;
184 }
185 $this->log( 'Fetch checksums, should only once', Scan_Controller::SCAN_LOG );
186
187 global $wp_version, $wp_local_package;
188
189 if ( ! function_exists( 'get_core_checksums' ) ) {
190 include_once ABSPATH . 'wp-admin/includes/update.php';
191 }
192 $locale = isset( $wp_local_package ) && '' !== $wp_local_package ? $wp_local_package : 'en_US';
193 $checksums = get_core_checksums( $wp_version, $locale );
194 if ( false === $checksums ) {
195 $this->log( 'Error from fetching checksums from wp.org', Scan_Controller::SCAN_LOG );
196 $scan = $this->owner->scan;
197 $scan->status = Scan::STATUS_IDLE;
198 $scan->save();
199
200 update_site_option( self::ISSUE_CHECKSUMS, time() );
201 // We can't send to MP event now because the Scan process continues to ping wp.org. This can cause a event's surge.
202 return false;
203 }
204
205 if ( isset( $checksums[ $wp_version ] ) ) {
206 /**
207 * Sometimes the API returns the format [$wp_version]=>'...' when locale not right.
208 * Use this as fail-safe.
209 */
210 $checksums = $checksums[ $wp_version ];
211 }
212 if ( is_array( $checksums ) ) {
213 foreach ( $checksums as $key => $checksum ) {
214 $formatted_key = defender_replace_line( $key );
215
216 if ( $key !== $formatted_key ) {
217 $checksums[ $formatted_key ] = $checksum;
218 unset( $checksums[ $key ] );
219 }
220 }
221 }
222
223 update_site_option( self::CACHE_CHECKSUMS, $checksums );
224
225 return $checksums;
226 }
227 }
228