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
defender-security / src / behavior / scan / class-core-integrity.php

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

229 lines 6.6 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 'checksum' => $checksums[ $rev_file ],
119 )
120 );
121 }
122 } else {
123 if ( is_dir( $file ) ) {
124 if ( $this->is_dir_empty( $core_files->current() ) ) {
125 $this->log(
126 sprintf( 'skip %s because of non-WP directory is empty', $core_files->current() ),
127 Scan_Controller::SCAN_LOG
128 );
129 $core_files->next();
130 continue;
131 }
132 $item_type = 'dir';
133 } else {
134 // Sometimes plugins create duplicate readme files. If the content is identical, we'll ignore the issue.
135 if ( isset( $checksums['readme.html'] ) && $this->compare_hashes( $file, $checksums['readme.html'] ) ) {
136 $this->log( sprintf( 'skip %s because this is just the same renamed readme file', $file ), Scan_Controller::SCAN_LOG );
137 $core_files->next();
138 continue;
139 }
140 $item_type = 'unversion';
141 }
142
143 $model->add_item(
144 Scan_Item::TYPE_INTEGRITY,
145 array(
146 'file' => $file,
147 'type' => $item_type,
148 )
149 );
150 }
151 $model->calculate_percent( $core_files->key() * 100 / $core_files->count(), 2 );
152 if ( 0 === $core_files->key() % 100 ) {
153 // We should update the model percent each 100 files, so we have some progress ont he screen$pos * 100 / $core_files->count().
154 $model->save();
155 }
156 $core_files->next();
157 }
158 if ( $core_files->valid() ) {
159 // Save the current progress and quit.
160 $model->task_checkpoint = $core_files->key();
161 } else {
162 // We will check if we have any ignore issue from last scan, so we can bring it here.
163 $last = Scan::get_last();
164 if ( is_object( $last ) ) {
165 $ignored_issues = $last->get_issues( Scan_Item::TYPE_INTEGRITY, Scan_Item::STATUS_IGNORE );
166 $model->carry_forward_ignored_issues( Scan_Item::TYPE_INTEGRITY, $ignored_issues, 'file' );
167 }
168 // Done, reset this, so we can use it later.
169 $model->task_checkpoint = '';
170 }
171 $model->save();
172
173 return ! $core_files->valid();
174 }
175
176 /**
177 * Fetch the checksums.
178 *
179 * @return bool|array
180 */
181 protected function get_checksum() {
182 $cache = get_site_option( self::CACHE_CHECKSUMS, false );
183 if ( is_array( $cache ) ) {
184 return $cache;
185 }
186 $this->log( 'Fetch checksums, should only once', Scan_Controller::SCAN_LOG );
187
188 global $wp_version, $wp_local_package;
189
190 if ( ! function_exists( 'get_core_checksums' ) ) {
191 include_once ABSPATH . 'wp-admin/includes/update.php';
192 }
193 $locale = isset( $wp_local_package ) && '' !== $wp_local_package ? $wp_local_package : 'en_US';
194 $checksums = get_core_checksums( $wp_version, $locale );
195 if ( false === $checksums ) {
196 $this->log( 'Error from fetching checksums from wp.org', Scan_Controller::SCAN_LOG );
197 $scan = $this->owner->scan;
198 $scan->status = Scan::STATUS_IDLE;
199 $scan->save();
200
201 update_site_option( self::ISSUE_CHECKSUMS, time() );
202 // We can't send to MP event now because the Scan process continues to ping wp.org. This can cause a event's surge.
203 return false;
204 }
205
206 if ( isset( $checksums[ $wp_version ] ) ) {
207 /**
208 * Sometimes the API returns the format [$wp_version]=>'...' when locale not right.
209 * Use this as fail-safe.
210 */
211 $checksums = $checksums[ $wp_version ];
212 }
213 if ( is_array( $checksums ) ) {
214 foreach ( $checksums as $key => $checksum ) {
215 $formatted_key = defender_replace_line( $key );
216
217 if ( $key !== $formatted_key ) {
218 $checksums[ $formatted_key ] = $checksum;
219 unset( $checksums[ $key ] );
220 }
221 }
222 }
223
224 update_site_option( self::CACHE_CHECKSUMS, $checksums );
225
226 return $checksums;
227 }
228 }
229