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 / bin / verify-manifest.php

verify-manifest.php in Vigilant – 100% Free Security Suite: Firewall, 2FA, Login, Headers, Scanner… 3.0.0, at bin/verify-manifest.php

468 lines 19.9 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Vigilant: verify MANIFEST.sha256 from the command line.
4 *
5 * Plain PHP >= 7.4, no WordPress required. Verifies the plugin tree against
6 * MANIFEST.sha256 (modified, missing and extra files, symbolic links, and
7 * folders that cannot be listed),
8 * and optionally cross-checks the manifest against the SHA-256 checksums
9 * that WordPress.org publishes for the released version (the manifest must
10 * describe exactly what WordPress.org distributes).
11 *
12 * Usage:
13 * php bin/verify-manifest.php [/path/to/plugin/root]
14 * php bin/verify-manifest.php --wporg (version read from vigilante.php)
15 * php bin/verify-manifest.php --wporg=3.0.0
16 *
17 * Exit codes:
18 * 0 everything verified clean
19 * 1 local tree does not match the manifest (modified/missing/extra files, links,
20 * or folders that cannot be listed)
21 * 2 manifest missing, unreadable or not valid, wp.org checksums unavailable, or
22 * the plugin folder cannot be listed
23 * 3 manifest disagrees with what wp.org distributes
24 *
25 * The tree is the same the plugin checks at runtime
26 * (includes/class-self-integrity.php): readme.txt, changelog.txt and the
27 * manifest itself at the root, the files svn writes in .svn/ at the root, and
28 * .DS_Store and Thumbs.db anywhere are not in the manifest. A link, or a file
29 * a web server may run (PHP extensions, .htaccess, .user.ini, php.ini), is
30 * never skipped, and an extension anywhere in the name counts (x.php.jpg).
31 * Text files are also compared with a UTF-8 BOM removed and CRLF line endings
32 * turned into LF, as the plugin does, so a host rewriting them is not reported
33 * (sha256sum -c reports them).
34 *
35 * @package Vigilante
36 * @since 3.0.0
37 */
38
39 // Outside WordPress only the command line may run this tool (from the web it
40 // answers 404); loaded inside WordPress it does nothing.
41 if ( ! defined( 'ABSPATH' ) ) {
42 if ( 'cli' !== PHP_SAPI ) {
43 http_response_code( 404 );
44 exit;
45 }
46 } else {
47 return;
48 }
49
50 // phpcs:disable WordPress.WP.AlternativeFunctions -- Command line tool that runs outside WordPress, where WP_Filesystem and the HTTP API do not exist: fwrite() to STDOUT/STDERR, file_get_contents() and fopen() are the only way. The guard above makes the file inert inside WordPress and unreachable from the web. Closed at the end of the file.
51
52 /*
53 * Exclusions. The same three lists live in includes/class-self-integrity.php
54 * and in the release tool generate-manifest.php, and the release checks
55 * compare them.
56 */
57 $vigilante_root_excluded_files = array( 'MANIFEST.sha256', 'readme.txt', 'changelog.txt' );
58 $vigilante_svn_metadata = '#^\.svn/(?:wc\.db|wc\.db-journal|format|entries|pristine/[0-9a-f]{2}/[0-9a-f]{40}\.svn-base)$#';
59 $vigilante_junk_file_names = array( '.DS_Store', 'Thumbs.db' );
60 $vigilante_executable_ext = array( 'php', 'phtml', 'php3', 'php4', 'php5', 'php7', 'php8', 'phps', 'pht', 'phar', 'inc' );
61 $vigilante_executable_names = array( '.htaccess', '.user.ini', 'php.ini' );
62
63 $vigilante_is_excluded = function ( $relative ) use ( $vigilante_root_excluded_files, $vigilante_svn_metadata, $vigilante_junk_file_names ) {
64 $relative = (string) $relative;
65 $segments = explode( '/', $relative );
66 if ( 1 === count( $segments ) && in_array( $segments[0], $vigilante_root_excluded_files, true ) ) {
67 return true;
68 }
69 if ( preg_match( $vigilante_svn_metadata, $relative ) ) {
70 return true;
71 }
72 return in_array( end( $segments ), $vigilante_junk_file_names, true );
73 };
74
75 $vigilante_is_executable = function ( $relative ) use ( $vigilante_executable_ext, $vigilante_executable_names ) {
76 $name = strtolower( basename( (string) $relative ) );
77 if ( in_array( $name, $vigilante_executable_names, true ) ) {
78 return true;
79 }
80 // Every extension counts, not only the last one (x.php.jpg runs as PHP with AddHandler).
81 $parts = explode( '.', $name );
82 array_shift( $parts );
83 foreach ( $parts as $part ) {
84 if ( in_array( $part, $vigilante_executable_ext, true ) ) {
85 return true;
86 }
87 }
88 return false;
89 };
90
91 // Text files a host may rewrite (UTF-8 BOM, CRLF): compared normalized too, as the plugin does.
92 // Line endings count in every text file, a UTF-8 BOM only where it changes nothing (not in PHP,
93 // where it is output, nor in JSON, which json_decode() then rejects), and nothing above 5 MB.
94 $vigilante_normalized_hash = function ( $path ) use ( $vigilante_is_executable ) {
95 $extension = strtolower( pathinfo( (string) $path, PATHINFO_EXTENSION ) );
96 if ( ! in_array( $extension, array( 'php', 'js', 'css', 'json', 'txt', 'md', 'html', 'htm', 'xml', 'svg', 'po', 'pot', 'ini' ), true ) ) {
97 return null;
98 }
99 $size = filesize( $path );
100 if ( false === $size || $size > 5242880 ) {
101 return null;
102 }
103 // The read stops past the limit too: the size can change between the check and the read.
104 $content = file_get_contents( $path, false, null, 0, 5242881 );
105 if ( false === $content || strlen( $content ) > 5242880 ) {
106 return null;
107 }
108 // Not in an executable name either: x.php.svg runs as PHP where a handler matches any extension.
109 if ( "\xEF\xBB\xBF" === substr( $content, 0, 3 ) && in_array( $extension, array( 'js', 'css', 'txt', 'md', 'html', 'htm', 'xml', 'svg', 'po', 'pot' ), true ) && ! $vigilante_is_executable( $path ) ) {
110 $content = substr( $content, 3 );
111 }
112 return hash( 'sha256', str_replace( array( "\r\n", "\r" ), "\n", $content ) );
113 };
114
115 // Paths come from the disk and from wp.org: printed with control characters
116 // escaped, so a crafted file name cannot rewrite what the terminal shows.
117 $vigilante_printable = function ( $text ) {
118 $text = addcslashes( (string) $text, "\0..\37\177" );
119 if ( ! preg_match( '//u', $text ) ) {
120 // Not valid UTF-8: every byte above ASCII is shown as \xNN.
121 return preg_replace_callback(
122 '/[\x80-\xFF]/',
123 function ( $m ) {
124 return sprintf( '\\x%02X', ord( $m[0] ) );
125 },
126 $text
127 );
128 }
129 // C1 controls, the bidirectional marks that reorder what a terminal shows (a name
130 // that reads informe-php.png and is informe-<U+202E>gnp.php), and the invisible or blank
131 // characters that let one name pass for another (zero width, line separators, BOM, soft
132 // hyphen, grapheme joiner, Hangul and Mongolian fillers).
133 return preg_replace_callback(
134 '/[\x{0080}-\x{009F}\x{00AD}\x{034F}\x{061C}\x{115F}\x{1160}\x{180E}\x{200B}-\x{200F}\x{2028}-\x{202E}\x{2060}-\x{2069}\x{3164}\x{FEFF}\x{FFA0}]/u',
135 function ( $m ) {
136 // Two or three byte UTF-8 sequences only; decoded by hand (mbstring is optional).
137 $b = array_values( unpack( 'C*', $m[0] ) );
138 $code = 2 === count( $b ) ? ( ( $b[0] & 0x1F ) << 6 ) | ( $b[1] & 0x3F ) : ( ( $b[0] & 0x0F ) << 12 ) | ( ( $b[1] & 0x3F ) << 6 ) | ( $b[2] & 0x3F );
139 return sprintf( '\\u{%04X}', $code );
140 },
141 $text
142 );
143 };
144
145 $vigilante_is_safe_path = function ( $path ) {
146 if ( ! is_string( $path ) || '' === $path || strlen( $path ) > 255 ) {
147 return false;
148 }
149 if ( ! preg_match( '#^[A-Za-z0-9._@+-]+(?:/[A-Za-z0-9._@+-]+)*$#', $path ) ) {
150 return false;
151 }
152 foreach ( explode( '/', $path ) as $segment ) {
153 if ( '.' === $segment || '..' === $segment ) {
154 return false;
155 }
156 }
157 return true;
158 };
159
160 // ---------------------------------------------------------------------------
161 // Arguments.
162 // ---------------------------------------------------------------------------
163 $vigilante_root = dirname( __DIR__ );
164 $vigilante_wporg = null; // null = off, '' = auto version, 'X.Y.Z' = explicit.
165
166 $vigilante_argv = array_slice( isset( $argv ) ? $argv : array(), 1 );
167 foreach ( $vigilante_argv as $vigilante_arg ) {
168 if ( '--wporg' === $vigilante_arg ) {
169 $vigilante_wporg = '';
170 } elseif ( 0 === strpos( $vigilante_arg, '--wporg=' ) ) {
171 $vigilante_wporg = substr( $vigilante_arg, 8 );
172 } elseif ( '-' !== substr( $vigilante_arg, 0, 1 ) ) {
173 $vigilante_root = rtrim( $vigilante_arg, '/' );
174 } else {
175 fwrite( STDERR, 'Unknown option: ' . $vigilante_arg . "\n" );
176 exit( 2 );
177 }
178 }
179
180 if ( ! is_dir( $vigilante_root ) ) {
181 fwrite( STDERR, 'ERROR: not a directory: ' . $vigilante_root . "\n" );
182 exit( 2 );
183 }
184
185 // ---------------------------------------------------------------------------
186 // Read the manifest.
187 // ---------------------------------------------------------------------------
188 $vigilante_manifest_path = $vigilante_root . '/MANIFEST.sha256';
189 if ( is_link( $vigilante_manifest_path ) || ! is_readable( $vigilante_manifest_path ) ) {
190 fwrite( STDERR, 'ERROR: MANIFEST.sha256 not found, unreadable or a link at ' . $vigilante_manifest_path . "\n" );
191 exit( 2 );
192 }
193 // 2000 lines take well under a megabyte: a bigger file is not a valid manifest and is not read,
194 // and the read stops past the limit too, since the size can change between the check and the read.
195 $vigilante_manifest_size = filesize( $vigilante_manifest_path );
196 if ( false === $vigilante_manifest_size ) {
197 fwrite( STDERR, "ERROR: could not read the size of MANIFEST.sha256\n" );
198 exit( 2 );
199 }
200 if ( $vigilante_manifest_size > 1048576 ) {
201 fwrite( STDERR, "ERROR: MANIFEST.sha256 is larger than 1 MB, so it is not a valid manifest and was not read\n" );
202 exit( 2 );
203 }
204
205 $vigilante_manifest_raw = file_get_contents( $vigilante_manifest_path, false, null, 0, 1048577 );
206 if ( false !== $vigilante_manifest_raw && strlen( $vigilante_manifest_raw ) > 1048576 ) {
207 fwrite( STDERR, "ERROR: MANIFEST.sha256 is larger than 1 MB, so it is not a valid manifest and was not read\n" );
208 exit( 2 );
209 }
210 if ( false !== $vigilante_manifest_raw ) {
211 // A rewrite of the line endings reaches the manifest too, and gives nobody anything.
212 if ( "\xEF\xBB\xBF" === substr( $vigilante_manifest_raw, 0, 3 ) ) {
213 $vigilante_manifest_raw = substr( $vigilante_manifest_raw, 3 );
214 }
215 $vigilante_manifest_raw = str_replace( array( "\r\n", "\r" ), "\n", $vigilante_manifest_raw );
216 }
217 if ( false === $vigilante_manifest_raw ) {
218 fwrite( STDERR, "ERROR: could not read MANIFEST.sha256\n" );
219 exit( 2 );
220 }
221
222 $vigilante_manifest = array();
223 $vigilante_lines = 0;
224 $vigilante_length = strlen( $vigilante_manifest_raw );
225 $vigilante_offset = 0;
226 // Line by line over the string, not explode(): a manifest of line breaks would build an array of
227 // millions of empty strings and exhaust memory. Blank lines count towards the limit too.
228 while ( $vigilante_offset < $vigilante_length ) {
229 $vigilante_end = strpos( $vigilante_manifest_raw, "\n", $vigilante_offset );
230 $vigilante_end = false === $vigilante_end ? $vigilante_length : $vigilante_end;
231 $vigilante_line = substr( $vigilante_manifest_raw, $vigilante_offset, $vigilante_end - $vigilante_offset );
232 $vigilante_offset = $vigilante_end + 1;
233 $vigilante_lines++;
234 if ( $vigilante_lines > 2000 ) {
235 fwrite( STDERR, "ERROR: MANIFEST.sha256 has more than 2000 lines\n" );
236 exit( 2 );
237 }
238 if ( '' === trim( $vigilante_line ) ) {
239 continue;
240 }
241 if ( ! preg_match( '/^([0-9a-f]{64}) (.+)$/', $vigilante_line, $vigilante_m ) || ! $vigilante_is_safe_path( $vigilante_m[2] ) || isset( $vigilante_manifest[ $vigilante_m[2] ] ) ) {
242 fwrite( STDERR, 'ERROR: not a valid manifest line (format, unsafe path or repeated path): ' . $vigilante_printable( $vigilante_line ) . "\n" );
243 exit( 2 );
244 }
245 if ( $vigilante_is_excluded( $vigilante_m[2] ) ) {
246 continue;
247 }
248 $vigilante_manifest[ $vigilante_m[2] ] = $vigilante_m[1];
249 }
250
251 if ( empty( $vigilante_manifest ) ) {
252 fwrite( STDERR, "ERROR: MANIFEST.sha256 is empty\n" );
253 exit( 2 );
254 }
255
256 // ---------------------------------------------------------------------------
257 // Local verification: mismatches, missing files, links, extra files.
258 // ---------------------------------------------------------------------------
259 $vigilante_mismatch = array();
260 $vigilante_missing = array();
261 $vigilante_links = array();
262 $vigilante_extra = array();
263 $vigilante_hidden = array();
264 $vigilante_realroot = realpath( $vigilante_root );
265
266 foreach ( $vigilante_manifest as $vigilante_relative => $vigilante_expected ) {
267 $vigilante_path = $vigilante_root . '/' . $vigilante_relative;
268 $vigilante_real = realpath( $vigilante_path );
269 if ( is_link( $vigilante_path ) || ( false !== $vigilante_real && false !== $vigilante_realroot && 0 !== strpos( $vigilante_real, $vigilante_realroot . DIRECTORY_SEPARATOR ) ) ) {
270 $vigilante_links[] = $vigilante_relative;
271 continue;
272 }
273 if ( ! is_file( $vigilante_path ) ) {
274 $vigilante_missing[] = $vigilante_relative;
275 continue;
276 }
277 if ( ! is_readable( $vigilante_path ) ) {
278 $vigilante_mismatch[] = $vigilante_relative;
279 continue;
280 }
281 if ( hash_file( 'sha256', $vigilante_path ) !== $vigilante_expected && $vigilante_normalized_hash( $vigilante_path ) !== $vigilante_expected ) {
282 $vigilante_mismatch[] = $vigilante_relative;
283 }
284 }
285
286 // A folder that cannot be listed hides what is inside it, and a web server can
287 // still run a file in it by name: it is reported, and the walk goes on past it
288 // instead of stopping with an uncaught exception.
289 try {
290 $vigilante_iterator = new RecursiveIteratorIterator(
291 new RecursiveDirectoryIterator( $vigilante_root, RecursiveDirectoryIterator::SKIP_DOTS ),
292 RecursiveIteratorIterator::SELF_FIRST,
293 RecursiveIteratorIterator::CATCH_GET_CHILD
294 );
295 } catch ( Exception $vigilante_e ) {
296 fwrite( STDERR, 'ERROR: cannot list ' . $vigilante_root . "\n" );
297 exit( 2 );
298 }
299 foreach ( $vigilante_iterator as $vigilante_file ) {
300 $vigilante_relative = str_replace( '\\', '/', substr( $vigilante_file->getPathname(), strlen( $vigilante_root ) + 1 ) );
301 $vigilante_is_link = $vigilante_file->isLink();
302 if ( ! $vigilante_is_link && $vigilante_file->isDir() ) {
303 // On Windows is_executable() of a folder is always false: only readability counts there.
304 if ( ! $vigilante_file->isReadable() || ( '\\' !== DIRECTORY_SEPARATOR && ! $vigilante_file->isExecutable() ) ) {
305 $vigilante_hidden[] = $vigilante_relative . '/';
306 }
307 continue;
308 }
309 if ( ! $vigilante_is_link && ! $vigilante_file->isFile() ) {
310 continue;
311 }
312 if ( isset( $vigilante_manifest[ $vigilante_relative ] ) ) {
313 continue;
314 }
315 $vigilante_executable = $vigilante_is_executable( $vigilante_relative );
316 if ( $vigilante_is_excluded( $vigilante_relative ) && ! $vigilante_is_link && ! $vigilante_executable ) {
317 continue;
318 }
319 if ( $vigilante_is_link ) {
320 $vigilante_links[] = $vigilante_relative;
321 } else {
322 $vigilante_extra[] = $vigilante_relative;
323 }
324 }
325
326 sort( $vigilante_mismatch, SORT_STRING );
327 sort( $vigilante_missing, SORT_STRING );
328 sort( $vigilante_links, SORT_STRING );
329 sort( $vigilante_extra, SORT_STRING );
330 sort( $vigilante_hidden, SORT_STRING );
331
332 foreach ( $vigilante_mismatch as $vigilante_relative ) {
333 fwrite( STDOUT, 'MODIFIED: ' . $vigilante_printable( $vigilante_relative ) . "\n" );
334 }
335 foreach ( $vigilante_missing as $vigilante_relative ) {
336 fwrite( STDOUT, 'MISSING: ' . $vigilante_printable( $vigilante_relative ) . "\n" );
337 }
338 foreach ( $vigilante_links as $vigilante_relative ) {
339 fwrite( STDOUT, 'LINK: ' . $vigilante_printable( $vigilante_relative ) . "\n" );
340 }
341 foreach ( $vigilante_extra as $vigilante_relative ) {
342 fwrite( STDOUT, 'EXTRA: ' . $vigilante_printable( $vigilante_relative ) . "\n" );
343 }
344 foreach ( $vigilante_hidden as $vigilante_relative ) {
345 fwrite( STDOUT, 'UNLISTED: ' . $vigilante_printable( $vigilante_relative ) . "\n" );
346 }
347
348 $vigilante_local_clean = empty( $vigilante_mismatch ) && empty( $vigilante_missing ) && empty( $vigilante_links ) && empty( $vigilante_extra ) && empty( $vigilante_hidden );
349 if ( $vigilante_local_clean ) {
350 fwrite( STDOUT, 'Local tree OK: ' . count( $vigilante_manifest ) . " files match MANIFEST.sha256, no extras.\n" );
351 }
352
353 // ---------------------------------------------------------------------------
354 // Optional wp.org cross-check.
355 // ---------------------------------------------------------------------------
356 $vigilante_wporg_clean = true;
357 if ( null !== $vigilante_wporg ) {
358 $vigilante_version = $vigilante_wporg;
359 if ( '' === $vigilante_version ) {
360 $vigilante_main = (string) file_get_contents( $vigilante_root . '/vigilante.php' );
361 if ( preg_match( '/^\s*\*\s*Version:\s*([0-9][0-9a-zA-Z.\-]*)\s*$/m', $vigilante_main, $vigilante_m ) ) {
362 $vigilante_version = $vigilante_m[1];
363 } else {
364 fwrite( STDERR, "ERROR: could not parse Version: header from vigilante.php; pass --wporg=X.Y.Z\n" );
365 exit( 2 );
366 }
367 }
368
369 $vigilante_url = 'https://downloads.wordpress.org/plugin-checksums/vigilante/' . rawurlencode( $vigilante_version ) . '.json';
370
371 // TLS verification stays ON. Some PHP builds ship without a default CA
372 // bundle; fall back to the system one if available.
373 $vigilante_ssl = array(
374 'verify_peer' => true,
375 'verify_peer_name' => true,
376 );
377 if ( '' === (string) ini_get( 'openssl.cafile' ) && is_readable( '/etc/ssl/cert.pem' ) ) {
378 $vigilante_ssl['cafile'] = '/etc/ssl/cert.pem';
379 }
380 $vigilante_context = stream_context_create(
381 array(
382 'http' => array(
383 'timeout' => 15,
384 'ignore_errors' => true,
385 'user_agent' => 'Vigilant verify-manifest (https://wordpress.org/plugins/vigilante/)',
386 ),
387 'ssl' => $vigilante_ssl,
388 )
389 );
390 $vigilante_stream = fopen( $vigilante_url, 'r', false, $vigilante_context );
391 $vigilante_body = false;
392 $vigilante_status = 0;
393 if ( false !== $vigilante_stream ) {
394 $vigilante_body = stream_get_contents( $vigilante_stream, 2097152 );
395 $vigilante_meta = stream_get_meta_data( $vigilante_stream );
396 fclose( $vigilante_stream );
397 if ( ! empty( $vigilante_meta['wrapper_data'] ) && is_array( $vigilante_meta['wrapper_data'] ) ) {
398 foreach ( $vigilante_meta['wrapper_data'] as $vigilante_header ) {
399 if ( is_string( $vigilante_header ) && preg_match( '#^HTTP/\S+\s+(\d{3})#', $vigilante_header, $vigilante_m ) ) {
400 $vigilante_status = (int) $vigilante_m[1];
401 }
402 }
403 }
404 }
405
406 if ( false === $vigilante_body || 200 !== $vigilante_status ) {
407 fwrite( STDERR, 'ERROR: could not fetch wp.org checksums for ' . $vigilante_printable( $vigilante_version ) . ' (HTTP ' . $vigilante_status . ").\n" );
408 fwrite( STDERR, "Right after a release wp.org may not have generated them yet: retry later.\n" );
409 exit( 2 );
410 }
411
412 $vigilante_json = json_decode( $vigilante_body, true );
413 if ( ! is_array( $vigilante_json ) || empty( $vigilante_json['files'] ) || ! is_array( $vigilante_json['files'] ) ) {
414 fwrite( STDERR, "ERROR: unexpected wp.org checksums payload.\n" );
415 exit( 2 );
416 }
417
418 $vigilante_wporg_files = array();
419 foreach ( $vigilante_json['files'] as $vigilante_relative => $vigilante_sums ) {
420 if ( ! is_array( $vigilante_sums ) || ! isset( $vigilante_sums['sha256'] ) ) {
421 continue;
422 }
423 // wp.org publishes a string, or an array when the file changed on the same tag.
424 $vigilante_wporg_files[ str_replace( '\\', '/', (string) $vigilante_relative ) ] = array_map( 'strval', (array) $vigilante_sums['sha256'] );
425 }
426
427 // Direction 1: every manifest entry must be distributed with the same hash.
428 foreach ( $vigilante_manifest as $vigilante_relative => $vigilante_expected ) {
429 if ( ! isset( $vigilante_wporg_files[ $vigilante_relative ] ) ) {
430 fwrite( STDOUT, 'WPORG-MISSING: ' . $vigilante_printable( $vigilante_relative ) . " (in manifest, not distributed by wp.org)\n" );
431 $vigilante_wporg_clean = false;
432 continue;
433 }
434 if ( ! in_array( $vigilante_expected, $vigilante_wporg_files[ $vigilante_relative ], true ) ) {
435 fwrite( STDOUT, 'WPORG-MISMATCH: ' . $vigilante_printable( $vigilante_relative ) . " (manifest hash differs from what wp.org distributes)\n" );
436 $vigilante_wporg_clean = false;
437 }
438 }
439
440 // Direction 2: everything wp.org distributes must be in the manifest,
441 // except what is never in it (the manifest, readme.txt, changelog.txt).
442 foreach ( $vigilante_wporg_files as $vigilante_relative => $vigilante_hashes ) {
443 if ( $vigilante_is_excluded( $vigilante_relative ) ) {
444 continue;
445 }
446 if ( ! isset( $vigilante_manifest[ $vigilante_relative ] ) ) {
447 fwrite( STDOUT, 'WPORG-EXTRA: ' . $vigilante_printable( $vigilante_relative ) . " (distributed by wp.org, not in manifest)\n" );
448 $vigilante_wporg_clean = false;
449 }
450 }
451
452 if ( $vigilante_wporg_clean ) {
453 fwrite( STDOUT, 'wp.org cross-check OK for ' . $vigilante_printable( $vigilante_version ) . ': manifest and distributed checksums agree.' . "\n" );
454 } else {
455 fwrite( STDOUT, "wp.org cross-check FAILED: the manifest does not describe what wp.org distributes (see SECURITY.md).\n" );
456 }
457 }
458
459 // phpcs:enable WordPress.WP.AlternativeFunctions
460
461 if ( ! $vigilante_wporg_clean ) {
462 exit( 3 );
463 }
464 if ( ! $vigilante_local_clean ) {
465 exit( 1 );
466 }
467 exit( 0 );
468