PluginProbe
Vigilant – 100% Free Security Suite: Firewall, 2FA, Login, Headers, Scanner… / 3.0.0
Vigilant – 100% Free Security Suite: Firewall, 2FA, Login, Headers, Scanner… v3.0.0
3.0.0 2.11.12 2.11.11 2.11.10 2.11.9 2.11.7 2.11.8 2.11.6 2.11.5 2.11.4 2.11.3 2.11.1 2.11.2 2.11.0 2.10.5 2.10.4 2.10.3 2.10.2 2.10.1 2.10.0 2.9.9 2.9.8 2.9.6 2.9.7 2.9.5 All 88 releases
vigilante / includes / class-self-integrity-guidance.php

class-self-integrity-guidance.php in Vigilant – 100% Free Security Suite: Firewall, 2FA, Login, Headers, Scanner… 3.0.0, at includes/class-self-integrity-guidance.php

476 lines 26.7 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Guidance texts for self-protection (what a finding means, and how to fix it).
4 *
5 * Single source used by the File Integrity box, the Security Check detail, the
6 * Security Audit details, the admin notices and the alert email, so the five
7 * never drift apart. Strings and a mapping only: nothing here reads state or
8 * touches the filesystem.
9 *
10 * Several finding codes share one explanation, so the entry point is a case
11 * key (see case_key()), which is also what the File Integrity box groups by:
12 * ten modified files are one case with ten paths, not ten explanations.
13 *
14 * @package Vigilante
15 * @since 3.0.0
16 */
17
18 // Prevent direct access
19 if ( ! defined( 'ABSPATH' ) ) {
20 exit;
21 }
22
23 /**
24 * Class Vigilante_Self_Integrity_Guidance
25 */
26 class Vigilante_Self_Integrity_Guidance {
27
28 /**
29 * Case key for a finding. Codes with the same answer share a key.
30 *
31 * @param string $code Finding code.
32 * @param string $severity info|warning|critical.
33 * @param string $variant Optional variant set by the finding (unreadable,
34 * exec, link, dir, walk).
35 * @return string
36 */
37 public static function case_key( $code, $severity = 'critical', $variant = '' ) {
38 switch ( (string) $code ) {
39 case 'self_modified':
40 if ( 'unreadable' === $variant ) {
41 return 'unreadable';
42 }
43 return ( 'critical' === $severity ) ? 'modified_code' : 'modified_asset';
44 case 'self_missing':
45 return 'missing';
46 case 'self_symlink':
47 return 'link';
48 case 'self_extra':
49 if ( 'link' === $variant ) {
50 return 'link';
51 }
52 if ( 'dir' === $variant || 'walk' === $variant ) {
53 return 'unlistable';
54 }
55 return ( 'critical' === $severity ) ? 'extra_exec' : 'extra_file';
56 case 'manifest_replaced':
57 case 'manifest_stale':
58 return 'manifest_replaced';
59 case 'manifest_missing':
60 case 'manifest_invalid':
61 return ( 'critical' === $severity ) ? 'manifest_replaced' : 'manifest_soft';
62 case 'manifest_unverified':
63 return 'unverified';
64 case 'distribution_mismatch':
65 return 'distribution';
66 case 'self_downgraded':
67 return 'downgrade';
68 case 'no_anchors':
69 return 'no_anchors';
70 case 'cron_cleared_repeatedly':
71 return 'cron_repeated';
72 case 'self_disabled':
73 return 'disabled';
74 case 'self_hooks_removed':
75 return 'hooks_removed';
76 case 'self_stale':
77 return 'stale';
78 }
79 return 'unknown';
80 }
81
82 /**
83 * Guidance for one finding.
84 *
85 * @param array|string $finding Finding array, or a bare code.
86 * @return array { key, title, meaning, steps, repair }
87 */
88 public static function for_finding( $finding ) {
89 if ( is_array( $finding ) ) {
90 $code = isset( $finding['code'] ) ? (string) $finding['code'] : '';
91 $severity = isset( $finding['severity'] ) ? (string) $finding['severity'] : 'critical';
92 $variant = isset( $finding['variant'] ) ? (string) $finding['variant'] : '';
93 } else {
94 $code = (string) $finding;
95 $severity = 'critical';
96 $variant = '';
97 }
98 return self::case_data( self::case_key( $code, $severity, $variant ) );
99 }
100
101 /**
102 * Guidance for the state itself, when there is nothing wrong to explain
103 * (verified, verified with fewer references, turned off, never run).
104 *
105 * @param array $args {
106 * @type string $status Stored status (ok, degraded, warning, critical).
107 * @type int $files Files checked.
108 * @type array $anchors manifest/wporg/fingerprint booleans.
109 * @type bool $enabled Whether the self-check is on.
110 * @type bool $has_run Whether it has ever run.
111 * }
112 * @return array { key, title, meaning, steps, repair }
113 */
114 public static function for_status( $args = array() ) {
115 $status = isset( $args['status'] ) ? (string) $args['status'] : '';
116 $files = isset( $args['files'] ) ? (int) $args['files'] : 0;
117 $anchors = ( isset( $args['anchors'] ) && is_array( $args['anchors'] ) ) ? $args['anchors'] : array();
118 $enabled = ! isset( $args['enabled'] ) || (bool) $args['enabled'];
119 $has_run = ! isset( $args['has_run'] ) || (bool) $args['has_run'];
120
121 if ( ! $enabled ) {
122 return self::case_data( 'disabled' );
123 }
124 if ( ! $has_run ) {
125 return self::case_data( 'never_run' );
126 }
127 if ( $files < 1 ) {
128 return self::case_data( 'no_anchors' );
129 }
130
131 $present = count( array_filter( $anchors ) );
132 if ( $present >= 3 ) {
133 return self::case_data( 'verified' );
134 }
135 if ( empty( $anchors['wporg'] ) ) {
136 return self::case_data( 'verified_no_wporg' );
137 }
138 return self::case_data( 'verified_partial' );
139 }
140
141 /**
142 * Explanation for one Security Audit entry about self-protection: which
143 * files it was about, what it means and what to do, so the popup of the
144 * log says the same as the File Integrity box.
145 *
146 * @param string $action Event action.
147 * @param string|array $extra_data extra_data column (JSON string or array).
148 * @param string $severity Severity of the log entry.
149 * @return array|null { title, meaning, steps, files }
150 */
151 public static function for_log_event( $action, $extra_data = '', $severity = 'info' ) {
152 $cases = array(
153 'self_integrity_fail' => '',
154 'self_verified_post_update' => 'post_update',
155 'self_integrity_restored' => 'restored',
156 'self_integrity_scan' => 'checked',
157 'self_integrity_captured' => 'captured',
158 'self_rebaselined' => 'captured',
159 'cron_restored' => 'cron_restored',
160 'cron_scheduled' => 'cron_restored',
161 );
162 $action = (string) $action;
163 if ( ! isset( $cases[ $action ] ) ) {
164 return null;
165 }
166
167 $data = is_string( $extra_data ) ? json_decode( $extra_data, true ) : $extra_data;
168 $data = is_array( $data ) ? $data : array();
169
170 $files = array();
171 $codes = array();
172 if ( ! empty( $data['findings'] ) && is_array( $data['findings'] ) ) {
173 foreach ( $data['findings'] as $entry ) {
174 // The log stores each finding as "code:file".
175 $parts = explode( ':', (string) $entry, 2 );
176 $code = $parts[0];
177 $file = isset( $parts[1] ) ? $parts[1] : '';
178 $codes[] = $code;
179 if ( '' !== $file ) {
180 $files[] = $file;
181 }
182 }
183 }
184 foreach ( array( 'restored', 'scheduled' ) as $hook_key ) {
185 if ( ! empty( $data[ $hook_key ] ) && is_array( $data[ $hook_key ] ) ) {
186 foreach ( $data[ $hook_key ] as $hook ) {
187 $files[] = (string) $hook;
188 }
189 }
190 }
191
192 $key = $cases[ $action ];
193 if ( '' === $key ) {
194 $key = $codes ? self::case_key( $codes[0], 'critical' === $severity ? 'critical' : 'warning' ) : 'unknown';
195 }
196 if ( 'cron_restored' === $key && 'critical' === $severity ) {
197 $key = 'cron_repeated';
198 }
199
200 $guidance = self::case_data( $key );
201 $guidance['files'] = array_values( array_unique( $files ) );
202 if ( count( array_unique( $codes ) ) > 1 ) {
203 $guidance['meaning'] .= ' ' . __( 'This entry covers more than one kind of finding: File Integrity lists each one with what it means and what to do.', 'vigilante' );
204 }
205 return $guidance;
206 }
207
208 /**
209 * What to do when the repair button is not available.
210 *
211 * @param string $reason general|no_caps|network|renamed.
212 * @return array List of steps.
213 */
214 public static function manual_steps( $reason = 'general' ) {
215 if ( 'no_caps' === $reason ) {
216 return array(
217 __( 'Plugin files cannot be changed from WordPress on this site, so Vigilant cannot repair itself here. Ask whoever manages your server to reinstall Vigilant from WordPress.org.', 'vigilante' ),
218 );
219 }
220 if ( 'renamed' === $reason ) {
221 return array(
222 __( 'This copy of Vigilant lives in a folder with a different name, and the package WordPress.org distributes always installs as vigilante, so repairing from here would leave a clean copy beside the one that is running.', 'vigilante' ),
223 __( 'Download Vigilant from WordPress.org and replace the contents of the folder it is installed in, keeping the folder name, or rename the folder back to vigilante and repair from here.', 'vigilante' ),
224 );
225 }
226 if ( 'network' === $reason ) {
227 return array(
228 __( 'Only your network administrator can repair Vigilant, because its files are shared by every site in the network. Let them know what this page reports.', 'vigilante' ),
229 );
230 }
231 return array(
232 __( 'Download Vigilant from WordPress.org and install it from Plugins > Add New Plugin > Upload Plugin, choosing Replace current with uploaded. Your settings, tables and log are kept.', 'vigilante' ),
233 __( 'Do not delete Vigilant to reinstall it: deleting a plugin removes its settings, its tables and its log.', 'vigilante' ),
234 );
235 }
236
237 /**
238 * The three steps shared by every case that a reinstall fixes.
239 *
240 * @param bool $tail Whether to add the what-if-it-comes-back step.
241 * @return array
242 */
243 private static function repair_steps( $tail = true ) {
244 $steps = array(
245 __( 'Repair Vigilant: it downloads a clean copy from WordPress.org and replaces only the plugin files, so your settings, tables and log are kept.', 'vigilante' ),
246 __( 'Run a new scan from this tab to confirm the finding is gone.', 'vigilante' ),
247 );
248 if ( $tail ) {
249 $steps[] = __( 'If it comes back after repairing, someone can still write to your server: change your hosting, FTP and administrator passwords, review the administrator accounts and ask your host to check the server.', 'vigilante' );
250 }
251 return $steps;
252 }
253
254 /**
255 * Title, meaning and steps of each case.
256 *
257 * @param string $key Case key.
258 * @return array { key, title, meaning, steps, repair }
259 */
260 public static function case_data( $key ) {
261 $repair = true;
262 switch ( $key ) {
263 case 'modified_code':
264 $title = __( 'Code file modified', 'vigilante' );
265 $meaning = __( 'This Vigilant code file no longer matches the version published on WordPress.org. Something with write access to your server changed it: an attacker, a script, or a manual edit.', 'vigilante' );
266 $steps = self::repair_steps();
267 break;
268 case 'missing':
269 $title = __( 'File missing', 'vigilante' );
270 $meaning = __( 'A file that is part of Vigilant has been deleted. A missing module stops protecting your site without showing any error.', 'vigilante' );
271 $steps = self::repair_steps();
272 break;
273 case 'unreadable':
274 $title = __( 'File cannot be read', 'vigilante' );
275 $meaning = __( 'Vigilant cannot read this file, so it cannot check it. Files distributed by WordPress.org are always readable.', 'vigilante' );
276 $steps = array(
277 __( 'Repair Vigilant, which restores the file with standard permissions.', 'vigilante' ),
278 __( 'If repairing fails, ask your host to fix the permissions of wp-content/plugins/vigilante.', 'vigilante' ),
279 );
280 break;
281 case 'link':
282 $title = __( 'Symbolic link inside Vigilant', 'vigilante' );
283 $meaning = __( 'A Vigilant file is a symbolic link, or points outside the plugin folder. WordPress.org never distributes links, so someone put it there.', 'vigilante' );
284 $steps = self::repair_steps();
285 break;
286 case 'extra_exec':
287 $title = __( 'Executable file added', 'vigilante' );
288 $meaning = __( 'There is an executable file (PHP or similar) in the Vigilant folder that is not part of Vigilant. Hiding files inside a security plugin is a common way to keep a backdoor.', 'vigilante' );
289 $steps = array_merge(
290 array( __( 'If you want to know how it got there, ask your host to check when the file was created before you repair, because repairing removes it.', 'vigilante' ) ),
291 self::repair_steps()
292 );
293 break;
294 case 'unlistable':
295 $title = __( 'Part of the folder cannot be checked', 'vigilante' );
296 $meaning = __( 'Vigilant cannot list one of its folders, or found far more files than it ships, so part of its own folder cannot be checked. A web server can still run files hidden in a folder that cannot be listed.', 'vigilante' );
297 $steps = array(
298 __( 'Repair Vigilant, which replaces the folder with a clean copy.', 'vigilante' ),
299 __( 'If repairing fails, ask your host to check the permissions of wp-content/plugins/vigilante.', 'vigilante' ),
300 );
301 break;
302 case 'extra_file':
303 $title = __( 'Extra file that is not code', 'vigilante' );
304 $meaning = __( 'There is a file in the Vigilant folder that is not part of Vigilant, and it is not executable code. It is often a PHP error log written by your server, or something left behind by a backup or sync tool.', 'vigilante' );
305 $steps = array(
306 __( 'If it is an error_log, your server is writing PHP errors inside Vigilant folder: open it, and if the errors mention Vigilant, report them in the Vigilant support forum.', 'vigilante' ),
307 __( 'Repairing Vigilant removes the file, because the folder is replaced with a clean copy.', 'vigilante' ),
308 );
309 break;
310 case 'manifest_replaced':
311 $title = __( 'Manifest replaced, deleted or damaged', 'vigilante' );
312 $meaning = __( 'MANIFEST.sha256, the list of fingerprints Vigilant uses to check its own files, was replaced, deleted or damaged without a plugin update. Replacing that list is how someone would hide changes to the plugin.', 'vigilante' );
313 $steps = self::repair_steps();
314 break;
315 case 'manifest_soft':
316 $title = __( 'Manifest missing or damaged', 'vigilante' );
317 $meaning = __( 'MANIFEST.sha256 is missing or is not a valid manifest. Vigilant still checks its files against WordPress.org, but one of its three references is gone.', 'vigilante' );
318 $steps = array(
319 __( 'Repair Vigilant to restore the manifest.', 'vigilante' ),
320 );
321 break;
322 case 'distribution':
323 $title = __( 'Different from the WordPress.org copy', 'vigilante' );
324 $meaning = __( 'These files match the manifest inside the plugin, but not the copy WordPress.org distributes for this version. On a development copy that is expected; on a live site it means the whole plugin, manifest included, came from somewhere else.', 'vigilante' );
325 $steps = array(
326 __( 'If you installed Vigilant from WordPress.org, repair it.', 'vigilante' ),
327 __( 'If you are running a development build on purpose, install the published version when you finish testing.', 'vigilante' ),
328 );
329 break;
330 case 'modified_asset':
331 $title = __( 'Stylesheet, script or image modified', 'vigilante' );
332 $meaning = __( 'A stylesheet, script or image of Vigilant no longer matches the distributed version. Usually an optimization or cache plugin, or your host, rewrote it while minifying or combining files.', 'vigilante' );
333 $steps = array(
334 __( 'If you use an optimization or cache plugin, exclude wp-content/plugins/vigilante from it, then repair Vigilant.', 'vigilante' ),
335 __( 'If you do not use one, repair Vigilant and run a new scan.', 'vigilante' ),
336 );
337 break;
338 case 'downgrade':
339 $title = __( 'Older version installed', 'vigilante' );
340 $meaning = __( 'Vigilant was replaced by an older version. Older versions can contain vulnerabilities that are already fixed and public.', 'vigilante' );
341 $steps = array(
342 __( 'If you did not downgrade it on purpose, repair Vigilant: it installs the version WordPress.org distributes now.', 'vigilante' ),
343 );
344 break;
345 case 'unverified':
346 $title = __( 'Version changed, not confirmed yet', 'vigilante' );
347 $meaning = __( 'The Vigilant version changed without the WordPress updater (an FTP upload, a file manager, a deployment), and the new files cannot be confirmed yet because WordPress.org has not published checksums for that version.', 'vigilante' );
348 $steps = array(
349 __( 'If you just updated Vigilant by hand with a copy from WordPress.org, you do not need to do anything: the next checks confirm it once WordPress.org publishes the checksums, usually within a day.', 'vigilante' ),
350 __( 'If you did not update it, or the copy did not come from WordPress.org, repair Vigilant.', 'vigilante' ),
351 );
352 break;
353 case 'no_anchors':
354 $title = __( 'Vigilant could not check its files', 'vigilante' );
355 $meaning = __( 'The manifest is not available and WordPress.org could not be reached, so there was nothing to check the files against.', 'vigilante' );
356 $steps = array(
357 __( 'Open Tools > Site Health. If your server cannot connect to WordPress.org, ask your host to allow it.', 'vigilante' ),
358 __( 'Then repair Vigilant to restore the manifest.', 'vigilante' ),
359 );
360 break;
361 case 'cron_repeated':
362 $title = __( 'Scheduled tasks keep disappearing', 'vigilante' );
363 $meaning = __( 'Vigilant scheduled tasks keep being removed and Vigilant keeps scheduling them again. While they are gone there are no scheduled scans, no daily check and no alerts.', 'vigilante' );
364 $steps = array(
365 __( 'Check whether a cleanup or optimization plugin removes scheduled events, and exclude the ones whose name starts with vigilante_. A plugin such as WP Crontrol lists them.', 'vigilante' ),
366 __( 'If you cannot find the cause, treat it as a possible compromise: review recently installed plugins and the administrator accounts.', 'vigilante' ),
367 );
368 $repair = false;
369 break;
370 case 'cron_restored':
371 $title = __( 'Scheduled task restored', 'vigilante' );
372 $meaning = __( 'One of Vigilant scheduled tasks had disappeared and Vigilant scheduled it again. A cron cleanup plugin, a database restore or a site migration can do that.', 'vigilante' );
373 $steps = array(
374 __( 'Nothing to do if it does not happen again. A plugin such as WP Crontrol lists the scheduled events of your site.', 'vigilante' ),
375 );
376 $repair = false;
377 break;
378 case 'restored':
379 $title = __( 'Integrity restored', 'vigilante' );
380 $meaning = __( 'The files that had been reported match what WordPress.org distributes again, so what was reported before is closed.', 'vigilante' );
381 $steps = array();
382 $repair = false;
383 break;
384 case 'post_update':
385 $title = __( 'Checked right after updating', 'vigilante' );
386 $meaning = __( 'WordPress updated Vigilant and Vigilant checked its own files at the end of that same request, which is when a half copied or replaced file would show up.', 'vigilante' );
387 $steps = array();
388 $repair = false;
389 break;
390 case 'checked':
391 $title = __( 'Files checked', 'vigilante' );
392 $meaning = __( 'Vigilant checked its own files against the references available and found nothing to report.', 'vigilante' );
393 $steps = array();
394 $repair = false;
395 break;
396 case 'captured':
397 $title = __( 'Reference saved', 'vigilante' );
398 $meaning = __( 'Vigilant saved the fingerprint of its manifest in your database. That fingerprint is what makes a swapped or deleted manifest detectable later.', 'vigilante' );
399 $steps = array();
400 $repair = false;
401 break;
402 case 'verified':
403 $title = __( 'Verified', 'vigilante' );
404 $meaning = __( 'Vigilant checked its own files against its three references and they all match: the checksums WordPress.org publishes for this version, the manifest shipped inside the plugin, and the fingerprint kept in your database.', 'vigilante' );
405 $steps = array();
406 $repair = false;
407 break;
408 case 'verified_no_wporg':
409 $title = __( 'Verified with fewer references', 'vigilante' );
410 $meaning = __( 'The files match the manifest shipped inside the plugin and the fingerprint kept in your database, but WordPress.org has not published checksums for this version yet, which is normal for a few hours after a release, or your server could not reach it.', 'vigilante' );
411 $steps = array(
412 __( 'Nothing to do. If this lasts more than two days, open Tools > Site Health and check that your server can connect to WordPress.org.', 'vigilante' ),
413 );
414 $repair = false;
415 break;
416 case 'verified_partial':
417 $title = __( 'Verified with fewer references', 'vigilante' );
418 $meaning = __( 'The files were checked, but one of the three references was not available for this check.', 'vigilante' );
419 $steps = array(
420 __( 'Nothing to do. The next check uses every reference that is available then.', 'vigilante' ),
421 );
422 $repair = false;
423 break;
424 case 'disabled':
425 $title = __( 'Self-protection is switched off by code', 'vigilante' );
426 $meaning = __( 'Nothing is checking that Vigilant own files are intact, so a change to them would go unnoticed. There is no setting that does this: something on this site is using the vigilante_self_integrity_enabled filter, and the files listed here are the ones that hook it.', 'vigilante' );
427 $steps = array(
428 __( 'If you did not ask for this, treat it as a compromise: a plugin or a snippet that switches off the check of a security plugin is doing the one thing an attacker needs first.', 'vigilante' ),
429 __( 'Open the files listed above and remove that filter, or deactivate whatever added it. The check starts again on its own.', 'vigilante' ),
430 __( 'If you turned it off on purpose because this site must not contact WordPress.org, nothing else is wrong: the plugin says so on every screen while it lasts.', 'vigilante' ),
431 );
432 $repair = false;
433 break;
434 case 'hooks_removed':
435 $title = __( 'Something removed the self-protection hooks', 'vigilante' );
436 $meaning = __( 'Code on this site unhooked the checks that run after updates and on each admin page, so Vigilant would keep showing its last result as if it were current. WordPress says nothing when a callback is removed, so the file that did it cannot be named; the plugins loaded in that request are in the entry of the Security Audit.', 'vigilante' );
437 $steps = array(
438 __( 'Look at the Security Audit entry for the list of plugins loaded when it happened, and deactivate them one by one until it stops.', 'vigilante' ),
439 __( 'If none of them explains it, treat it as a compromise: change your hosting, FTP and administrator passwords and ask your host to check the server.', 'vigilante' ),
440 );
441 $repair = false;
442 break;
443 case 'stale':
444 $title = __( 'Not checked recently', 'vigilante' );
445 $meaning = __( 'The last check is more than three days old, so what you see is not a current result. It happens on sites nobody opens and with no cron running, and it also happens when something stopped the check.', 'vigilante' );
446 $steps = array(
447 __( 'Run a new scan from this tab. If the date does not move, your scheduled tasks are not running: check them with a plugin such as WP Crontrol.', 'vigilante' ),
448 );
449 $repair = false;
450 break;
451 case 'never_run':
452 $title = __( 'Not checked yet', 'vigilante' );
453 $meaning = __( 'Vigilant has not checked its own files yet. It checks them in every integrity scan, right after every update, and once a day.', 'vigilante' );
454 $steps = array(
455 __( 'Run a scan from this tab to check them now.', 'vigilante' ),
456 );
457 $repair = false;
458 break;
459 default:
460 $key = 'unknown';
461 $title = __( 'Finding in Vigilant own files', 'vigilante' );
462 $meaning = __( 'Vigilant reported something about its own files that this version has no explanation for.', 'vigilante' );
463 $steps = self::repair_steps();
464 break;
465 }
466
467 return array(
468 'key' => $key,
469 'title' => $title,
470 'meaning' => $meaning,
471 'steps' => $steps,
472 'repair' => $repair,
473 );
474 }
475 }
476