PluginProbe
Defender Security – Malware Scanner, Login Security & Firewall / 6.2.4
Defender Security – Malware Scanner, Login Security & Firewall v6.2.4
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-malware-scan.php

class-malware-scan.php in Defender Security – Malware Scanner, Login Security & Firewall 6.2.4, at src/behavior/scan/class-malware-scan.php

301 lines 8.8 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Handles malware 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 Calotes\Base\Component;
13 use WP_Defender\Traits\Plugin;
14 use WP_Defender\Component\Timer;
15 use WP_Defender\Model\Scan_Item;
16 use WP_Defender\Behavior\WPMUDEV;
17 use WP_Defender\Model\Scan as Model_Scan;
18 use WP_Defender\Model\Setting\Scan as Scan_Settings;
19 use WP_Defender\Helper\Analytics\Scan as Scan_Analytics;
20 use WP_Defender\Controller\Scan as Scan_Controller;
21 use WP_Defender\Component\Scan as Scan_Component;
22
23 /**
24 * It is responsible for performing suspicious checks on files using a quick scan and a deep scan.
25 */
26 class Malware_Scan extends Component {
27
28 use IO;
29 use Plugin;
30
31 public const YARA_RULES = 'defender_yara_rules';
32 public const MALWARE_LOG = 'malware_scan.log';
33
34 /**
35 * Backup memory.
36 *
37 * @var string
38 */
39 private $memory;
40
41 /**
42 * Holds the WPMUDEV object or null if not set.
43 *
44 * @var WPMUDEV
45 */
46 private $wpmudev;
47
48 /**
49 * Holds the Model_Scan model or null if not set.
50 *
51 * @var Model_Scan
52 */
53 private $scan;
54
55 /**
56 * Constructor for the Malware_Scan class.
57 *
58 * @param WPMUDEV $wpmudev The WPMUDEV object.
59 * @param Model_Scan $scan The Model_Scan model.
60 */
61 public function __construct( WPMUDEV $wpmudev, Model_Scan $scan ) {
62 $this->wpmudev = $wpmudev;
63 $this->scan = $scan;
64 }
65
66 /**
67 * Retrieves additional rules based on the provided scan settings.
68 *
69 * @param Scan_Settings $scan_settings The scan settings object.
70 *
71 * @return array Returns an array containing plugin-related rules.
72 */
73 protected function get_additional_rules( Scan_Settings $scan_settings ): array {
74 $plugin_cache = false;
75 $plugin_slugs_changes = array();
76 $plugin_pro_slugs = array();
77 $plugin_all_slugs = array();
78 // Checked Plugin option.
79 if ( $scan_settings->integrity_check && $scan_settings->check_plugins ) {
80 $plugin_cache = true;
81 $arr = get_site_option( Plugin_Integrity::PLUGIN_SLUGS, array() );
82 if ( is_array( $arr ) && array() !== $arr ) {
83 $plugin_slugs_changes = $arr;
84 }
85 $plugin_slugs = get_site_option( Plugin_Integrity::PLUGIN_PREMIUM_SLUGS, array() );
86 if ( is_array( $plugin_slugs ) && array() !== $plugin_slugs ) {
87 $plugin_pro_slugs = $plugin_slugs;
88 }
89 $actioned_plugins = get_site_option( Scan_Component::PLUGINS_ACTIONED );
90 $plugin_all_slugs = Scan_Component::are_actioned_plugins( $actioned_plugins )
91 ? array_keys( $actioned_plugins )
92 : $this->get_plugin_slugs();
93 }
94
95 return array(
96 'plugin_change' => $plugin_cache,
97 'plugin_all_slugs' => $plugin_all_slugs,
98 // List of plugins with modifications.
99 'plugin_slugs_changes' => $plugin_slugs_changes,
100 // List of pro plugins.
101 'plugin_pro_slugs' => $plugin_pro_slugs,
102 );
103 }
104
105 /**
106 * Check if a file has been modified based on the given rules.
107 *
108 * @param string $file_path The path of the file to check.
109 * @param array $rules The rules to determine if a file has been modified.
110 *
111 * @return bool Returns true if the file has been modified, false otherwise.
112 */
113 protected function was_modificated_file( $file_path, $rules ): bool {
114 // Unchecked 'Plugin change file' option, so green light to display Suspicious checks.
115 if ( ! $rules['plugin_change'] ) {
116 return true;
117 }
118
119 $search_on_plugin = WP_PLUGIN_DIR . '/';
120 if ( false !== stripos( $file_path, $search_on_plugin ) ) {
121 /**
122 * Suspicious code in /plugins.
123 * Empty list of plugin slugs because there are premium plugins,
124 * not modifications on Free plugins,
125 * or not plugins on site.
126 * Should check separate custom files/dirs in the root too.
127 */
128 $rev_file = str_replace( $search_on_plugin, '', $file_path );
129 $matches = explode( '/', $rev_file );
130 $base_slug = array_shift( $matches );
131 // Custom files/dirs.
132 if ( ! in_array( $base_slug, $rules['plugin_all_slugs'], true ) ) {
133 return true;
134 }
135 if (
136 ( ! isset( $rules['plugin_slugs_changes'] ) || array() === (array) $rules['plugin_slugs_changes'] )
137 && ( ! isset( $rules['plugin_pro_slugs'] ) || array() === (array) $rules['plugin_pro_slugs'] )
138 ) {
139 // No modifications.
140 return false;
141 }
142 // Is it on premium plugins?
143 if ( in_array( $base_slug, $rules['plugin_pro_slugs'], true ) ) {
144 return true;
145 }
146 if ( in_array( $base_slug, (array) $rules['plugin_slugs_changes'], true ) ) {
147 // Modifications in this plugin.
148 return true;
149 }
150
151 // Modifications are not here.
152 return false;
153 }
154
155 // Other WP places.
156 return true;
157 }
158
159 /**
160 * Perform a suspicious check on files using a quick scan and a deep scan.
161 *
162 * @param Malware_Quick_Scan $quick_scan The quick scan object.
163 * @param Malware_Deep_Scan $deep_scan The deep scan object.
164 *
165 * @return bool Returns true if the check is successful, false otherwise.
166 */
167 public function suspicious_check( Malware_Quick_Scan $quick_scan, Malware_Deep_Scan $deep_scan ): bool {
168 $files = get_site_option( Gather_Fact::CACHE_CONTENT, array() );
169 if ( array() === $files ) {
170 return true;
171 }
172
173 set_time_limit( 0 );
174 $this->prepare_emergency_shutdown();
175 $timer = new Timer();
176 $rules = $this->fetch_yara_rules();
177
178 $model = $this->scan;
179 $pos = (int) $model->task_checkpoint;
180
181 $combinations = $this->get_additional_rules( new Scan_Settings() );
182 $files = new ArrayIterator( $files );
183 $files->seek( $pos );
184 while ( $files->valid() ) {
185 if ( ! $timer->check() ) {
186 $reason = 'Rage quit';
187
188 /**
189 * Retrieves the Scan_Analytics class.
190 *
191 * @var Scan_Analytics $scan_analytics
192 */
193 $scan_analytics = wd_di()->get( Scan_Analytics::class );
194
195 $scan_analytics->track_feature(
196 $scan_analytics::EVENT_SCAN_FAILED,
197 array(
198 $scan_analytics::EVENT_SCAN_FAILED_PROP => $scan_analytics::EVENT_SCAN_FAILED_ERROR,
199 'Error_Reason' => $reason,
200 )
201 );
202
203 $this->log( $reason, self::MALWARE_LOG );
204 $model->save();
205 break;
206 }
207 if ( $model->is_issue_ignored( $files->current() ) ) {
208 $this->log( sprintf( 'skip %s because of file is ignored', $files->current() ), self::MALWARE_LOG );
209 $files->next();
210 continue;
211 }
212
213 [ $result, $qs_detail ] = $quick_scan->do_quick_scan( $files->current(), $rules );
214 if ( $result ) {
215 $this->log( sprintf( 'file %s suspicious', $files->current() ), self::MALWARE_LOG );
216 $result = $deep_scan->do_deep_scan( $files->current(), $qs_detail );
217 /**
218 * Add new item if Suspicious code is found and:
219 * plugins are premium,
220 * plugins are on wp.org but the code doesn't match from the WP repo (there are differences in checksums),
221 * deactivated options of File change detection > Scan plugin file changes.
222 */
223 if ( is_array( $result ) && $this->was_modificated_file( $files->current(), $combinations ) ) {
224 $result['file'] = $files->current();
225 $model->add_item( Scan_Item::TYPE_SUSPICIOUS, $result );
226 }
227 }
228 $files->next();
229 $files_key = $files->key();
230 $model->task_checkpoint = ! is_null( $files_key ) ? $files_key : '';
231 $model->calculate_percent( $files_key * 100 / $files->count(), 6 );
232 if ( 0 === $files_key % 100 ) {
233 // We should update the model percent each 100 files so we have some progress on the screen.
234 $model->save();
235 }
236 }
237
238 if ( ! $files->valid() ) {
239 $last = Model_Scan::get_last();
240 if ( is_object( $last ) ) {
241 $ignored_issues = $last->get_issues( Scan_Item::TYPE_SUSPICIOUS, Scan_Item::STATUS_IGNORE );
242 $this->scan->carry_forward_ignored_issues( Scan_Item::TYPE_SUSPICIOUS, $ignored_issues, 'file' );
243 }
244 $model->task_checkpoint = '';
245 }
246
247 $model->save();
248
249 return ! $files->valid();
250 }
251
252 /**
253 * We will use this for a safe switch when memory out happen.
254 */
255 public function prepare_emergency_shutdown() {
256 $this->memory = str_repeat( '*', 1024 * 1024 );
257
258 register_shutdown_function(
259 function () {
260 if ( Model_Scan::STATUS_FINISH === $this->scan->status ) {
261 return;
262 }
263
264 $this->memory = null;
265 $err = error_get_last();
266 if (
267 ( ! is_null( $err ) )
268 && ( ! in_array( $err['type'], array( E_NOTICE, E_WARNING, E_DEPRECATED ), true ) )
269 ) {
270 $this->log( $err, Scan_Controller::SCAN_LOG );
271 $this->log( 'Something wrong happen, saving and quit.', Scan_Controller::SCAN_LOG );
272 $this->scan->status = Model_Scan::STATUS_ERROR;
273 ++$this->scan->task_checkpoint;
274 $this->scan->save();
275 }
276 }
277 );
278 }
279
280 /**
281 * Fetch yara rules from API.
282 *
283 * @return array
284 */
285 private function fetch_yara_rules(): array {
286 $rules = get_site_option( self::YARA_RULES, false );
287 if ( is_array( $rules ) ) {
288 return $rules;
289 }
290
291 $rules = $this->wpmudev->make_wpmu_request( WPMUDEV::API_SCAN_SIGNATURE );
292 if ( is_array( $rules ) ) {
293 update_site_option( self::YARA_RULES, $rules );
294
295 return $rules;
296 }
297
298 return array();
299 }
300 }
301