├── .wordpress-org
├── banner-1544x500.jpg
├── banner-1544x500.png
├── banner-772x250.png
├── icon-128x128.png
├── icon-256x256.jpg
├── icon-256x256.png
└── icon.svg
├── composer.json
├── includes
├── class-vuln-patchstack-service.php
├── class-vuln-service.php
├── class-vuln-wordfence-service.php
├── class-vuln-wpscan-service.php
├── class-vulnerability-cli.php
└── vuln.sh
└── wpcli-vulnerability-scanner.php
/.wordpress-org/banner-1544x500.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/10up/wpcli-vulnerability-scanner/36526528da8bec8a84b80b0f383f1058a34e49d8/.wordpress-org/banner-1544x500.jpg
--------------------------------------------------------------------------------
/.wordpress-org/banner-1544x500.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/10up/wpcli-vulnerability-scanner/36526528da8bec8a84b80b0f383f1058a34e49d8/.wordpress-org/banner-1544x500.png
--------------------------------------------------------------------------------
/.wordpress-org/banner-772x250.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/10up/wpcli-vulnerability-scanner/36526528da8bec8a84b80b0f383f1058a34e49d8/.wordpress-org/banner-772x250.png
--------------------------------------------------------------------------------
/.wordpress-org/icon-128x128.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/10up/wpcli-vulnerability-scanner/36526528da8bec8a84b80b0f383f1058a34e49d8/.wordpress-org/icon-128x128.png
--------------------------------------------------------------------------------
/.wordpress-org/icon-256x256.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/10up/wpcli-vulnerability-scanner/36526528da8bec8a84b80b0f383f1058a34e49d8/.wordpress-org/icon-256x256.jpg
--------------------------------------------------------------------------------
/.wordpress-org/icon-256x256.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/10up/wpcli-vulnerability-scanner/36526528da8bec8a84b80b0f383f1058a34e49d8/.wordpress-org/icon-256x256.png
--------------------------------------------------------------------------------
/.wordpress-org/icon.svg:
--------------------------------------------------------------------------------
1 |
9 |
--------------------------------------------------------------------------------
/composer.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "10up/wpcli-vulnerability-scanner",
3 | "description": "Check installed plugins and themes for vulnerabilities",
4 | "type": "wp-cli-package",
5 | "homepage": "https://github.com/10up/wpcli-vulnerability-scanner",
6 | "license": "MIT",
7 | "require": {
8 | "php": ">=7.0",
9 | "halaxa/json-machine": "^1.1"
10 | },
11 | "autoload": {
12 | "classmap": [ "includes/" ],
13 | "files": [ "wpcli-vulnerability-scanner.php" ]
14 | },
15 | "autoload-dev": {
16 | "psr-4": {
17 | "WP_CLI\\Tests\\": "features"
18 | },
19 | "classmap": [ "includes/" ],
20 | "files": [ "wpcli-vulnerability-scanner.php" ]
21 | },
22 | "require-dev": {
23 | "wp-cli/wp-cli-tests": "^3.1",
24 | "wp-cli/extension-command": "^2.1"
25 | },
26 | "authors": [
27 | {
28 | "name": "10up",
29 | "homepage": "https://10up.com"
30 | }
31 | ],
32 | "config": {
33 | "allow-plugins": {
34 | "dealerdirect/phpcodesniffer-composer-installer": true
35 | },
36 | "process-timeout": 1800
37 | },
38 | "scripts": {
39 | "behat": "run-behat-tests",
40 | "prepare-tests": "install-package-tests",
41 | "phpcs": "run-phpcs-tests",
42 | "build-zip": "composer archive --file=wpcli-vulnerability-scanner --format=zip"
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/includes/class-vuln-patchstack-service.php:
--------------------------------------------------------------------------------
1 | call( $endpoint );
41 |
42 | $args = array(
43 | 'slug' => $slug,
44 | 'version' => $wp_version,
45 | );
46 |
47 | // Prepare and return report data.
48 | return $this->prepare_report_data( $response, $args );
49 | }
50 |
51 | /**
52 | * Worker for Plugin/Theme vulnerability checks.
53 | *
54 | * @param string $slug Installed plugin/theme slug.
55 | * @param string $version Installed plugin/theme version.
56 | * @param string|array $type The "thing" we're checking, "plugin" or "theme".
57 | * If string, it's pluralized with an "s".
58 | * If array, should be [ single, plural ].
59 | *
60 | * @return array Data array
61 | */
62 | public function check_status( $slug, $version, $type ) {
63 | list( $singular_type ) = $this->get_slugs( $type );
64 | // Get data from API.
65 | $endpoint = "product/$singular_type/$slug/$version";
66 | $response = $this->call( $endpoint );
67 |
68 | $args = array(
69 | 'slug' => $slug,
70 | 'version' => $version,
71 | );
72 |
73 | // Prepare and return report data.
74 | return $this->prepare_report_data( $response, $args );
75 | }
76 |
77 | /**
78 | * Worker, checks vulnerability in batch for themes/plugins.
79 | * If fail, It tun through checking the status of a each plugin/theme.
80 | *
81 | * @param string|array $type The "thing" we're checking.
82 | * If string, it's pluralized with an "s"
83 | * If array, should be [ single, plural ].
84 | *
85 | * @return array Statuses for all themes/plugins.
86 | */
87 | public function check_thing( $type ) {
88 | list( $singular_type ) = $this->get_slugs( $type );
89 |
90 | $list = WP_CLI::launch_self(
91 | $singular_type,
92 | array( 'list' ),
93 | array(
94 | 'format' => 'csv',
95 | 'fields' => 'name,version',
96 | ),
97 | true,
98 | true
99 | );
100 |
101 | $list = $this->parse_list( $list );
102 |
103 | // test list.
104 | if ( isset( $this->assoc_args['test'] ) && $this->assoc_args['test'] ) {
105 | $list = $this->get_test_list( $type );
106 | }
107 |
108 | $request_data = array();
109 | foreach ( $list as $thing ) {
110 | $request_data[] = array(
111 | 'name' => $thing['name'],
112 | 'version' => isset( $thing['version'] ) ? $thing['version'] : '0',
113 | 'type' => $singular_type,
114 | 'exists' => false,
115 | );
116 | }
117 |
118 | $result = array();
119 | $endpoint = 'batch';
120 | // Batch API has limit of 50 items.
121 | $request_data = array_chunk( $request_data, 50 );
122 |
123 | $retry = false;
124 | foreach ( $request_data as $data ) {
125 | $response = $this->call( $endpoint, $data );
126 | $report = $this->prepare_batch_report_data( $response, $data );
127 |
128 | if ( false === $report ) {
129 | WP_CLI::debug( 'Unable to get vulnerabilities in batch.' );
130 | $retry = true;
131 | break;
132 | }
133 |
134 | if ( ! empty( $report ) ) {
135 | $result = array_merge( $result, $report );
136 | }
137 | }
138 |
139 | /*
140 | * Try getting vulnerabilities with separate request for each plugin/theme.
141 | *
142 | * Batch API can fail in 2 cases.
143 | * 1. Some errors from Patchstack API.
144 | * 2. User has Free API key (Free API don't have supports batch operation).
145 | */
146 | if ( $retry ) {
147 | $result = array();
148 | foreach ( $list as $thing ) {
149 | $status = $this->check_status( $thing['name'], $thing['version'], $singular_type );
150 | $result = array_merge( $result, $status );
151 | }
152 | }
153 |
154 | return $result;
155 | }
156 |
157 | /**
158 | * Prepare data for output.
159 | *
160 | * @param array|mixed|WP_Error $response API response object.
161 | * @param array $args Arguments related to current scan.
162 | */
163 | private function prepare_report_data( $response, $args ) {
164 | $slug = $args['slug'];
165 | $version = $args['version'];
166 | $is_wp = false;
167 | $display_slug = $slug;
168 | // phpcs:ignore WordPress.WP.CapitalPDangit.Misspelled
169 | if ( 'wordpress' === $slug ) {
170 | $is_wp = true;
171 | $display_slug = "WordPress $version";
172 | }
173 |
174 | $code = wp_remote_retrieve_response_code( $response );
175 | $body = wp_remote_retrieve_body( $response );
176 |
177 | $report = array();
178 |
179 | if ( 404 === $code ) {
180 | $report[] = array(
181 | 'title' => "Error generating report for $display_slug",
182 | );
183 | } elseif ( 429 === $code ) {
184 | $report[] = array(
185 | 'title' => 'ERROR_API_QUOTA_FULL: exceed daily rate limit hit.',
186 | );
187 | } else {
188 |
189 | // Let's analyse the report!
190 | $vulndb = json_decode( $body );
191 |
192 | if ( ! isset( $vulndb->vulnerabilities ) ) {
193 | $report[] = array(
194 | 'title' => "Error generating report for $display_slug",
195 | );
196 | } else {
197 | $vulnerabilities = $vulndb->vulnerabilities;
198 |
199 | if ( is_array( $vulnerabilities ) && ! empty( $vulnerabilities ) ) {
200 | $report = $this->format_vulnerability_data( $vulnerabilities, $version );
201 | }
202 |
203 | $total = count( $report );
204 | if ( $total <= 0 ) {
205 | $report[] = array(
206 | 'title' => 'No vulnerabilities reported for this version of ' . ( $is_wp ? 'WordPress' : $slug ),
207 | );
208 | }
209 | }
210 | }
211 |
212 | $table_format = false;
213 | if ( ! isset( $this->assoc_args['format'] ) || 'table' === $this->assoc_args['format'] ) {
214 | $table_format = true;
215 | }
216 |
217 | $data = array();
218 | $last_item = '';
219 | foreach ( $report as $index => $stat ) {
220 |
221 | $stat = wp_parse_args(
222 | $stat,
223 | array(
224 | 'id' => '',
225 | 'action' => '',
226 | 'fixed in' => 'n/a',
227 | 'severity' => 'n/a',
228 | 'affected_in' => 'n/a',
229 | 'reference' => 'n/a',
230 | )
231 | );
232 |
233 | if ( $is_wp ) {
234 | $name = ( $table_format && 0 !== $index ? '' : 'WordPress' );
235 | } else {
236 | $name = ( $table_format && $slug === $last_item ? '' : $slug );
237 | }
238 |
239 | if ( $table_format ) {
240 | switch ( $stat['action'] ) {
241 | case 'update':
242 | $name = \WP_CLI::colorize( "%r$name%n" );
243 | break;
244 | case 'watch':
245 | $name = \WP_CLI::colorize( "%y$name%n" );
246 | break;
247 | default:
248 | break;
249 | }
250 | }
251 |
252 | // These keys must match the column headings in the formatter (extras ok).
253 | $data[] = array(
254 | 'name' => $name,
255 | 'slug' => $slug,
256 | 'installed version' => $version,
257 | 'id' => $stat['id'],
258 | 'status' => $stat['title'],
259 | 'fixed in' => $stat['fixed in'],
260 | 'severity' => $stat['severity'],
261 | 'introduced in' => $stat['affected_in'],
262 | 'reference' => $stat['reference'],
263 | 'action' => $stat['action'],
264 | );
265 |
266 | $last_item = $slug;
267 | }
268 |
269 | return $data;
270 | }
271 |
272 | /**
273 | * Prepare data for output.
274 | *
275 | * @param array|mixed|WP_Error $response API response object.
276 | * @param array $data vulnerability request data.
277 | */
278 | private function prepare_batch_report_data( $response, $data ) {
279 | $code = wp_remote_retrieve_response_code( $response );
280 | $body = wp_remote_retrieve_body( $response );
281 |
282 | if ( 200 !== $code ) {
283 | return false;
284 | }
285 |
286 | $result = array();
287 | $plugin_versions = array();
288 | foreach ( $data as $thing ) {
289 | $plugin_versions[ $thing['name'] ] = $thing['version'];
290 | }
291 |
292 | // Let's analyse the report!
293 | $vulndb = json_decode( $body );
294 |
295 | if ( ! isset( $vulndb->vulnerabilities ) ) {
296 | return false;
297 | } else {
298 | $vuln_data = (array) $vulndb->vulnerabilities;
299 |
300 | if ( is_array( $vuln_data ) && ! empty( $vuln_data ) ) {
301 | foreach ( $vuln_data as $slug => $vulnerabilities ) {
302 | $report = array();
303 | $version = ! empty( $plugin_versions[ $slug ] ) ? $plugin_versions[ $slug ] : '0';
304 |
305 | if ( ! empty( $vulnerabilities ) ) {
306 | $formatted_data = $this->format_vulnerability_data( $vulnerabilities, $version );
307 | if ( ! empty( $formatted_data ) ) {
308 | $report = array_merge( $report, $formatted_data );
309 | } else {
310 | $report[] = array(
311 | 'title' => 'No vulnerabilities reported for this version of ' . $slug,
312 | );
313 | }
314 | } else {
315 | $report[] = array(
316 | 'title' => 'No vulnerabilities reported for this version of ' . $slug,
317 | );
318 | }
319 |
320 | $table_format = false;
321 | if ( ! isset( $this->assoc_args['format'] ) || 'table' === $this->assoc_args['format'] ) {
322 | $table_format = true;
323 | }
324 |
325 | $data = array();
326 | $last_item = '';
327 | foreach ( $report as $index => $stat ) {
328 |
329 | $stat = wp_parse_args(
330 | $stat,
331 | array(
332 | 'id' => '',
333 | 'action' => '',
334 | 'fixed in' => 'n/a',
335 | 'severity' => 'n/a',
336 | 'affected_in' => 'n/a',
337 | 'reference' => 'n/a',
338 | )
339 | );
340 |
341 | $name = ( $table_format && $slug === $last_item ? '' : $slug );
342 |
343 | if ( $table_format ) {
344 | switch ( $stat['action'] ) {
345 | case 'update':
346 | $name = \WP_CLI::colorize( "%r$name%n" );
347 | break;
348 | case 'watch':
349 | $name = \WP_CLI::colorize( "%y$name%n" );
350 | break;
351 | default:
352 | break;
353 | }
354 | }
355 |
356 | // These keys must match the column headings in the formatter (extras ok).
357 | $data[] = array(
358 | 'name' => $name,
359 | 'slug' => $slug,
360 | 'installed version' => $version,
361 | 'id' => $stat['id'],
362 | 'status' => $stat['title'],
363 | 'fixed in' => $stat['fixed in'],
364 | 'severity' => $stat['severity'],
365 | 'introduced in' => $stat['affected_in'],
366 | 'reference' => $stat['reference'],
367 | 'action' => $stat['action'],
368 | );
369 |
370 | $last_item = $slug;
371 | }
372 | $result = array_merge( $result, $data );
373 | }
374 | } else {
375 | return false;
376 | }
377 | }
378 |
379 | return $result;
380 | }
381 |
382 | /**
383 | * Format Vulnerability data.
384 | *
385 | * @param array $vulnerabilities Array of Vulnerability.
386 | * @param string $version plugin or theme version.
387 | * @return array Formatted array of Vulnerability.
388 | */
389 | private function format_vulnerability_data( $vulnerabilities, $version ) {
390 | $report = array();
391 |
392 | foreach ( $vulnerabilities as $vuln ) {
393 | /**
394 | * Filter whether to skip the vulnerability check.
395 | *
396 | * @since 1.2.1
397 | * @hook vuln_skip_vulnerability_check
398 | * @param {bool} $skip True to skip.
399 | * @param {object} $vuln Vulnerability object.
400 | */
401 | if ( apply_filters( 'vuln_skip_vulnerability_check', false, $vuln ) ) {
402 | continue;
403 | }
404 |
405 | // API has records for affected_in ?
406 | $affected_in = $this->obj_has_non_empty_prop( 'affected_in', $vuln );
407 | // Check for fix version.
408 | $fixed_since = $this->obj_has_non_empty_prop( 'fixed_in', $vuln );
409 | $reference = $this->obj_has_non_empty_prop( 'direct_url', $vuln );
410 |
411 | // vulnerability that hasn't been fixed :(.
412 | if ( ! $fixed_since ) {
413 |
414 | $report[] = array(
415 | 'id' => $vuln->id,
416 | 'title' => $vuln->title,
417 | 'fixed in' => 'Not fixed',
418 | 'severity' => $this->get_severity_value( $vuln->cvss_score ),
419 | 'affected_in' => $affected_in ? $vuln->affected_in : 'n/a',
420 | 'reference' => $reference ? $vuln->direct_url : 'n/a',
421 | 'action' => 'watch',
422 | );
423 |
424 | // Vuln version, fix available.
425 | } elseif ( version_compare( $version, $vuln->fixed_in, '<' ) ) {
426 |
427 | $report[] = array(
428 | 'id' => $vuln->id,
429 | 'title' => $vuln->title,
430 | 'fixed in' => $vuln->fixed_in,
431 | 'severity' => $this->get_severity_value( $vuln->cvss_score ),
432 | 'affected_in' => $affected_in ? $vuln->affected_in : 'n/a',
433 | 'reference' => $reference ? $vuln->direct_url : 'n/a',
434 | 'action' => 'update',
435 | );
436 | }
437 | }
438 |
439 | return $report;
440 | }
441 |
442 | /**
443 | * Call the Patchstack API.
444 | *
445 | * @param string $endpoint The endpoint.
446 | * @param array $data Request data (optional).
447 | *
448 | * @return array|mixed|WP_Error
449 | */
450 | protected function call( $endpoint, $data = array() ) {
451 |
452 | if ( ! defined( 'VULN_API_TOKEN' ) ) {
453 | WP_CLI::error( 'VULN_API_TOKEN is not set.' );
454 | die();
455 | }
456 |
457 | $url = $this->api_url . $endpoint;
458 |
459 | $key = 'vuln_check-' . md5( $url );
460 |
461 | $args = array(
462 | 'headers' => array(
463 | 'PSKey' => VULN_API_TOKEN,
464 | ),
465 | 'method' => 'GET',
466 | );
467 | if ( ! empty( $data ) ) {
468 | $request_data = wp_json_encode( $data );
469 | $args['method'] = 'POST';
470 | $args['body'] = $request_data;
471 |
472 | // Set content type.
473 | $args['headers']['Content-Type'] = 'application/json';
474 |
475 | $key = 'vuln_check-' . md5( $url . $request_data );
476 | }
477 |
478 | $response = get_transient( $key );
479 | if ( ! $response ) {
480 | $response = wp_remote_request( $url, $args );
481 | set_transient( $key, $response, HOUR_IN_SECONDS );
482 | } else {
483 | WP_CLI::debug( "Use response cache for $url" );
484 | }
485 |
486 | return $response;
487 | }
488 | }
489 |
--------------------------------------------------------------------------------
/includes/class-vuln-service.php:
--------------------------------------------------------------------------------
1 | assoc_args = $assoc_args;
29 | }
30 | /**
31 | * Helper function to check for a given property in an object
32 | *
33 | * @param string $field Property name.
34 | * @param object $object Object having vulnerability details.
35 | * @return bool Flag indicating the API
36 | */
37 | protected function obj_has_non_empty_prop( $field, $object ) {
38 | return isset( $object->{$field} ) && ! ( empty( $object->{$field} ) );
39 | }
40 |
41 | /**
42 | * Get singular and plural slugs for given string or array.
43 | *
44 | * @param string|array $singular_or_array If string, it's pluralized with an "s".
45 | * If array, should be [ single, plural ].
46 | *
47 | * @return array
48 | */
49 | protected function get_slugs( $singular_or_array ) {
50 | if ( is_array( $singular_or_array ) ) {
51 | $singular_type = $singular_or_array[0];
52 | $plural_type = $singular_or_array[1];
53 | } else {
54 | $singular_type = $singular_or_array;
55 | $plural_type = "{$singular_or_array}s";
56 | }
57 |
58 | return array( $singular_type, $plural_type );
59 | }
60 |
61 | /**
62 | * Parse list string into item array.
63 | *
64 | * @param string $list plugin or theme list.
65 | *
66 | * @return array
67 | */
68 | protected function parse_list( $list ) {
69 |
70 | $list = explode( "\n", $list );
71 | $newlist = array();
72 |
73 | foreach ( $list as $line ) {
74 | // Skip printed command.
75 | if ( '$ ' === substr( $line, 0, 2 ) ) {
76 | continue;
77 | }
78 | // Skip output header.
79 | if ( 'name,version' === $line ) {
80 | continue;
81 | }
82 | // Skip output footer.
83 | if ( empty( $line ) ) {
84 | break;
85 | }
86 |
87 | $newlist[] = array_combine(
88 | array( 'name', 'version' ),
89 | explode( ',', $line )
90 | );
91 |
92 | }
93 |
94 | return $newlist;
95 | }
96 |
97 | /**
98 | * Get plugins/themes list to run test.
99 | *
100 | * @param string $type plugin or theme.
101 | * @return array array of test plugins or themes.
102 | */
103 | protected function get_test_list( $type ) {
104 | $list = array();
105 |
106 | switch ( $type ) {
107 | case 'plugin':
108 | $list = array(
109 | // fixed vulns.
110 | array(
111 | 'name' => 'relevant',
112 | 'version' => '1.0.2',
113 | ),
114 | );
115 | break;
116 | case 'theme':
117 | $list = array(
118 | // fixed vulns.
119 | array(
120 | 'name' => 'digital-store',
121 | 'version' => '1.3',
122 | ),
123 | );
124 | break;
125 | }
126 |
127 | return $list;
128 | }
129 |
130 | /**
131 | * Get formatted Serverity column value based on score.
132 | *
133 | * @param float $csvv_score
134 | * @return string
135 | */
136 | protected function get_severity_value( $csvv_score ) {
137 | if ( empty( $csvv_score ) ) {
138 | return 'n/a';
139 | }
140 |
141 | if ( ! is_float( $csvv_score ) ) {
142 | $csvv_score = (float) $csvv_score;
143 | }
144 |
145 | $severity_ratting = '';
146 |
147 | if ( $csvv_score >= 9 ) {
148 | $severity_ratting = 'Critical';
149 | } elseif ( $csvv_score >= 7 ) {
150 | $severity_ratting = 'High';
151 | } elseif ( $csvv_score >= 4 ) {
152 | $severity_ratting = 'Medium';
153 | } elseif ( $csvv_score > 0 ) {
154 | $severity_ratting = 'Low';
155 | } elseif ( (float) 0 === $csvv_score ) {
156 | $severity_ratting = 'None';
157 | }
158 |
159 | return sprintf( '%s %.1f/10', $severity_ratting, $csvv_score );
160 | }
161 | }
162 |
--------------------------------------------------------------------------------
/includes/class-vuln-wordfence-service.php:
--------------------------------------------------------------------------------
1 | 'core',
41 | 'items' => array( $slug => $wp_version ),
42 | );
43 |
44 | $response = $this->get_vulnerabilities( $args );
45 |
46 | // Prepare and return report data.
47 | return $this->prepare_report_data( $response, $args );
48 | }
49 |
50 | /**
51 | * Worker for Plugin/Theme vulnerability checks.
52 | *
53 | * @param string $slug Installed plugin/theme slug.
54 | * @param string $version Installed plugin/theme version.
55 | * @param string|array $type The "thing" we're checking, "plugin" or "theme".
56 | * If string, it's pluralized with an "s".
57 | * If array, should be [ single, plural ].
58 | *
59 | * @return array Data array
60 | */
61 | public function check_status( $slug, $version, $type ) {
62 | list( $singular_type ) = $this->get_slugs( $type );
63 |
64 | // Get vulnerability data from API.
65 | $args = array(
66 | 'type' => $singular_type,
67 | 'items' => array(
68 | $slug => $version,
69 | ),
70 | );
71 |
72 | $response = $this->get_vulnerabilities( $args );
73 |
74 | // Prepare and return report data.
75 | return $this->prepare_report_data( $response, $args );
76 | }
77 |
78 | /**
79 | * Worker, checks vulnerability in batch for themes/plugins.
80 | * If fail, It tun through checking the status of a each plugin/theme.
81 | *
82 | * @param string|array $type The "thing" we're checking.
83 | * If string, it's pluralized with an "s"
84 | * If array, should be [ single, plural ].
85 | *
86 | * @return array Statuses for all themes/plugins.
87 | */
88 | public function check_thing( $type ) {
89 | list( $singular_type ) = $this->get_slugs( $type );
90 |
91 | // Prepare list of installed plugins/themes.
92 | $list = WP_CLI::launch_self(
93 | $singular_type,
94 | array( 'list' ),
95 | array(
96 | 'format' => 'csv',
97 | 'fields' => 'name,version',
98 | ),
99 | true,
100 | true
101 | );
102 |
103 | $list = $this->parse_list( $list );
104 |
105 | // Test list.
106 | if ( isset( $this->assoc_args['test'] ) && $this->assoc_args['test'] ) {
107 | $list = $this->get_test_list( $type );
108 | }
109 |
110 | // If no plugins/themes installed, return.
111 | if ( empty( $list ) ) {
112 | return array();
113 | }
114 |
115 | $items = array();
116 | foreach ( $list as $item ) {
117 | $items[ $item['name'] ] = isset( $item['version'] ) ? $item['version'] : '0';
118 | }
119 |
120 | $args = array(
121 | 'type' => $singular_type,
122 | 'items' => $items,
123 | );
124 |
125 | // Get vulnerabilities.
126 | $response = $this->get_vulnerabilities( $args );
127 |
128 | // Prepare and return report data.
129 | return $this->prepare_report_data( $response, $args );
130 | }
131 |
132 |
133 | /**
134 | * Prepare report data.
135 | *
136 | * @param array|mixed|WP_Error $response Response from API.
137 | * @param array $args Arguments.
138 | *
139 | * @return array.
140 | */
141 | public function prepare_report_data( $response, $args ) {
142 | if ( ! isset( $args['items'] ) || empty( $args['items'] ) ) {
143 | return array();
144 | }
145 |
146 | $data = array();
147 | foreach ( $args['items'] as $slug => $version ) {
148 | $is_wp = false;
149 | $display_slug = $slug;
150 | // phpcs:ignore WordPress.WP.CapitalPDangit.Misspelled
151 | if ( 'wordpress' === $slug ) {
152 | $is_wp = true;
153 | $display_slug = "WordPress $version";
154 | }
155 |
156 | $report = array();
157 | if ( is_wp_error( $response ) ) {
158 | $error_message = "Error generating report for $display_slug";
159 | if ( strpos( $response->get_error_message(), 'ERROR_API_QUOTA_FULL' ) !== false ) {
160 | $error_message .= ' (' . $response->get_error_message() . ')';
161 | }
162 | $report[] = array(
163 | 'title' => $error_message,
164 | );
165 | } else {
166 | // Let's analyze the report!
167 | if ( isset( $response[ $slug ] ) && ! empty( $response[ $slug ] ) && is_array( $response[ $slug ] ) ) {
168 | $report = $this->format_vulnerability_data( $response[ $slug ], $slug, $version );
169 | }
170 |
171 | if ( count( $report ) <= 0 ) {
172 | $report[] = array(
173 | 'title' => 'No vulnerabilities reported for this version of ' . ( $is_wp ? 'WordPress' : $slug ),
174 | );
175 | }
176 | }
177 |
178 | $table_format = false;
179 | if ( ! isset( $this->assoc_args['format'] ) || 'table' === $this->assoc_args['format'] ) {
180 | $table_format = true;
181 | }
182 |
183 | $last_item = '';
184 | foreach ( $report as $index => $stat ) {
185 |
186 | $stat = wp_parse_args(
187 | $stat,
188 | array(
189 | 'id' => '',
190 | 'action' => '',
191 | 'fixed in' => 'n/a',
192 | 'affected_in' => 'n/a',
193 | 'severity' => 'n/a',
194 | 'reference' => 'n/a',
195 | 'copyrights' => '',
196 | )
197 | );
198 |
199 | if ( $is_wp ) {
200 | $name = ( $table_format && 0 !== $index ? '' : 'WordPress' );
201 | } else {
202 | $name = ( $table_format && $slug === $last_item ? '' : $slug );
203 | }
204 |
205 | if ( $table_format ) {
206 | switch ( $stat['action'] ) {
207 | case 'update':
208 | $name = \WP_CLI::colorize( "%r$name%n" );
209 | break;
210 | case 'watch':
211 | $name = \WP_CLI::colorize( "%y$name%n" );
212 | break;
213 | default:
214 | break;
215 | }
216 | }
217 |
218 | // These keys must match the column headings in the formatter (extras ok).
219 | $data[] = array(
220 | 'name' => $name,
221 | 'slug' => $slug,
222 | 'installed version' => $version,
223 | 'id' => $stat['id'],
224 | 'status' => $stat['title'],
225 | 'fixed in' => $stat['fixed in'],
226 | 'introduced in' => $stat['affected_in'],
227 | 'action' => $stat['action'],
228 | 'severity' => $stat['severity'],
229 | 'reference' => $stat['reference'],
230 | 'copyrights' => $stat['copyrights'],
231 | );
232 |
233 | $last_item = $slug;
234 | }
235 | }
236 | return $data;
237 | }
238 |
239 | /**
240 | * Format Vulnerability data.
241 | *
242 | * @param array $vulnerabilities Array of Vulnerability.
243 | * @param string $slug plugin or theme slug.
244 | * @param string $version plugin or theme version.
245 | * @return array Formatted array of Vulnerability.
246 | */
247 | private function format_vulnerability_data( $vulnerabilities, $slug, $version ) {
248 | $report = array();
249 |
250 | foreach ( $vulnerabilities as $vuln ) {
251 | /**
252 | * Filter whether to skip the vulnerability check.
253 | *
254 | * @since 1.2.1
255 | * @hook vuln_skip_vulnerability_check
256 | * @param {bool} $skip True to skip.
257 | * @param {object} $vuln Vulnerability object.
258 | */
259 | if ( apply_filters( 'vuln_skip_vulnerability_check', false, $vuln ) ) {
260 | continue;
261 | }
262 |
263 | $fixed = false;
264 | $fixed_version = '';
265 | $severity = 'n/a';
266 | if ( ! empty( $vuln->cvss ) ) {
267 | $severity = sprintf( '%s %.1f/10', $vuln->cvss->rating, $vuln->cvss->score );
268 | }
269 |
270 | foreach ( $vuln->software as $software ) {
271 | if ( $software->slug === $slug ) {
272 | $fixed = $software->patched;
273 | if ( $software->patched && ! empty( $software->patched_versions ) ) {
274 | $fixed_versions = array_filter(
275 | $software->patched_versions,
276 | function( $patched_version ) use ( $version ) {
277 | return version_compare( $version, $patched_version, '<' );
278 | }
279 | );
280 | if ( ! empty( $fixed_versions ) ) {
281 | $fixed_version = implode( ', ', $fixed_versions );
282 | }
283 | }
284 | }
285 | }
286 |
287 | // vulnerability that hasn't been fixed :(.
288 | if ( ! $fixed ) {
289 | $report[] = array(
290 | 'id' => $vuln->id,
291 | 'title' => $vuln->title,
292 | 'fixed in' => 'Not fixed',
293 | 'affected_in' => 'n/a',
294 | 'action' => 'watch',
295 | 'severity' => $severity,
296 | 'reference' => $vuln->references ? $vuln->references[0] : 'n/a',
297 | 'copyrights' => $vuln->copyrights ? (array) $vuln->copyrights : array(),
298 | );
299 |
300 | // Vuln version, fix available.
301 | } elseif ( ! empty( $fixed_version ) ) {
302 | $report[] = array(
303 | 'id' => $vuln->id,
304 | 'title' => $vuln->title,
305 | 'fixed in' => $fixed_version,
306 | 'affected_in' => 'n/a',
307 | 'action' => 'update',
308 | 'severity' => $severity,
309 | 'reference' => $vuln->references ? $vuln->references[0] : 'n/a',
310 | 'copyrights' => $vuln->copyrights ? (array) $vuln->copyrights : array(),
311 | );
312 | }
313 | }
314 |
315 | return $report;
316 | }
317 |
318 | /**
319 | * Get vulnerabilities from Wordfence API.
320 | *
321 | * @param array $args Arguments array to request data related to specific plugin or theme.
322 | *
323 | * @return array|mixed|WP_Error
324 | */
325 | protected function get_vulnerabilities( $args ) {
326 | // 1. Get vulnerability data.
327 | $vuln_data = $this->get_vulnerability_data();
328 |
329 | // Return error if unable to retrieve vulnerability data.
330 | if ( is_wp_error( $vuln_data ) ) {
331 | return $vuln_data;
332 | }
333 |
334 | if ( empty( $vuln_data ) ) {
335 | return new WP_Error( 'vuln_db_error', 'Unable to retrieve vulnerability data.' );
336 | }
337 |
338 | $type = $args['type'];
339 | $response = array();
340 | $items = $args['items'];
341 | $item_slugs = array_keys( $args['items'] );
342 |
343 | // 2. Filter vulnerability data for the requested plugins, themes or core.
344 | foreach ( $vuln_data as $vuln ) {
345 | foreach ( $vuln->software as $software ) {
346 | // Filter out vulnerabilities that don't match the requested type and slug.
347 | if ( $software->type === $type && in_array( $software->slug, $item_slugs, true ) ) {
348 | // Filter out vulnerabilities that don't match the requested version.
349 | if ( $items[ $software->slug ] && $software->affected_versions ) {
350 | foreach ( (array) $software->affected_versions as $version ) {
351 | $from_compare = $version->from_inclusive ? '>=' : '>';
352 | $to_compare = $version->to_inclusive ? '<=' : '<';
353 | if (
354 | ( version_compare( $items[ $software->slug ], $version->from_version, $from_compare ) ||
355 | '*' === $version->from_version
356 | ) &&
357 | version_compare( $items[ $software->slug ], $version->to_version, $to_compare )
358 | ) {
359 | $response[ $software->slug ][] = $vuln;
360 | }
361 | }
362 | } else {
363 | $response[ $software->slug ][] = $vuln;
364 | }
365 | }
366 | }
367 | }
368 |
369 | return $response;
370 | }
371 |
372 | /**
373 | * Get vulnerability data from Wordfence API.
374 | *
375 | * If local cache file exists, use it.
376 | * Otherwise, get data from Wordfence API and cache it locally for 1 hour.
377 | *
378 | * @return JsonMachine\Items|WP_Error $vuln_data Array of Vulnerability.
379 | */
380 | private function get_vulnerability_data() {
381 | $wp_filesystem = $this->init_wp_filesystem();
382 |
383 | $key = 'vuln_check-' . md5( $this->api_url );
384 | $file_path = get_transient( $key );
385 |
386 | if ( $file_path && $wp_filesystem->exists( $file_path ) ) {
387 | WP_CLI::debug( 'Using local cached vuln db json' );
388 | } else {
389 | WP_CLI::debug( 'Request Wordfence API for vuln db json' );
390 | $vuln_db_file = download_url( $this->api_url );
391 | if ( is_wp_error( $vuln_db_file ) ) {
392 | return $vuln_db_file;
393 | }
394 |
395 | $file_path = $vuln_db_file;
396 | set_transient( $key, $file_path, HOUR_IN_SECONDS );
397 | }
398 |
399 | return JsonMachine\Items::fromFile( $file_path );
400 | }
401 |
402 | /**
403 | * Initializes WP_Filesystem.
404 | */
405 | private function init_wp_filesystem() {
406 | global $wp_filesystem;
407 | WP_Filesystem();
408 |
409 | return $wp_filesystem;
410 | }
411 | }
412 |
--------------------------------------------------------------------------------
/includes/class-vuln-wpscan-service.php:
--------------------------------------------------------------------------------
1 | call( $endpoint );
43 |
44 | $code = wp_remote_retrieve_response_code( $response );
45 | $body = wp_remote_retrieve_body( $response );
46 |
47 | $table_format = false;
48 | if ( ! isset( $this->assoc_args['format'] ) || 'table' === $this->assoc_args['format'] ) {
49 | $table_format = true;
50 | }
51 |
52 | $report = array();
53 |
54 | if ( 404 === $code ) {
55 | $report[] = array(
56 | 'title' => "Error generating report for WordPress $wp_version",
57 | );
58 | } elseif ( 429 === $code ) {
59 | $report[] = array(
60 | 'title' => 'ERROR_API_QUOTA_FULL: exceed daily rate limit hit.',
61 | );
62 | } else {
63 |
64 | // Let's analyse the report!
65 | $vulndb = json_decode( $body );
66 |
67 | if ( ! isset( $vulndb->$wp_version ) || isset( $vulndb->error ) ) {
68 | if ( isset( $vulndb->error ) ) {
69 | $report[] = array(
70 | 'title' => $vulndb->error,
71 | );
72 | } else {
73 | $report[] = array(
74 | 'title' => "NVF Error generating report for WordPress $wp_version",
75 | );
76 | }
77 | } else {
78 |
79 | $vulnerabilities = $vulndb->$wp_version->vulnerabilities;
80 |
81 | if ( is_array( $vulnerabilities ) ) {
82 | foreach ( $vulnerabilities as $k => $vuln ) {
83 | /**
84 | * Filter whether to skip the vulnerability check.
85 | *
86 | * @since 1.2.1
87 | * @hook vuln_skip_vulnerability_check
88 | * @param {bool} $skip True to skip.
89 | * @param {object} $vuln Vulnerability object.
90 | */
91 | if ( apply_filters( 'vuln_skip_vulnerability_check', false, $vuln ) ) {
92 | continue;
93 | }
94 |
95 | // API has records for when was introduced ?
96 | $reported_since = $this->obj_has_non_empty_prop( 'introduced_in', $vuln );
97 | // Check for fix version.
98 | $fixed_since = $this->obj_has_non_empty_prop( 'fixed_in', $vuln );
99 |
100 | // vulnerability that hasn't been fixed :(.
101 | if ( ! $fixed_since ) {
102 |
103 | $report[] = array(
104 | 'id' => $vuln->id,
105 | 'title' => $vuln->title,
106 | 'fixed in' => 'Not fixed',
107 | 'severity' => $this->get_severity_value( $vuln->cvss ),
108 | 'introduced_in' => $reported_since ? $vuln->introduced_in : 'n/a',
109 | 'reference' => 'https://wpscan.com/vulnerability/' . $vuln->id,
110 | 'action' => 'watch',
111 | );
112 |
113 | // vuln version, fix available.
114 | } elseif (
115 | // If no records for when it was introduced, compare fixed version against current .
116 | (
117 | ! $reported_since
118 | && version_compare( $version, $vuln->fixed_in, '<' )
119 | )
120 | ||
121 | (
122 | // If have records for when it was introduced.
123 | $reported_since
124 | // Check if using version with introduced vulnerablity.
125 | && version_compare( $version, $vuln->introduced_in, '>=' )
126 | // Check if using version with vulnerablity fixed.
127 | && version_compare( $version, $vuln->fixed_in, '<' )
128 | )
129 | ) {
130 |
131 | $report[] = array(
132 | 'id' => $vuln->id,
133 | 'title' => $vuln->title,
134 | 'fixed in' => $vuln->fixed_in,
135 | 'severity' => $this->get_severity_value( $vuln->cvss ),
136 | 'introduced_in' => $reported_since ? $vuln->introduced_in : 'n/a',
137 | 'reference' => 'https://wpscan.com/vulnerability/' . $vuln->id,
138 | 'action' => 'update',
139 | );
140 |
141 | // if installed plugin version is greater than a fixed version,
142 | // unset that vuln entry, we don't need it.
143 | } else {
144 |
145 | // This leaves us with an array of relevant vulns
146 | // not currently used :/.
147 | unset( $vulnerabilities[ $k ] );
148 | }
149 | }
150 | }
151 |
152 | $total = count( $report );
153 | if ( $total <= 0 ) {
154 | $report[] = array(
155 | 'title' => 'No vulnerabilities reported for this version of WordPress',
156 | );
157 | }
158 | }
159 | }
160 |
161 | $data = array();
162 |
163 | foreach ( $report as $index => $stat ) {
164 |
165 | $stat = wp_parse_args(
166 | $stat,
167 | array(
168 | 'id' => '',
169 | 'action' => '',
170 | 'fixed in' => 'n/a',
171 | 'severity' => 'n/a',
172 | 'introduced_in' => 'n/a',
173 | 'reference' => 'n/a',
174 | )
175 | );
176 |
177 | $name = ( $table_format && 0 !== $index ? '' : 'WordPress' );
178 |
179 | if ( $table_format ) {
180 | switch ( $stat['action'] ) {
181 | case 'update':
182 | $name = \WP_CLI::colorize( "%r$name%n" );
183 | break;
184 | case 'watch':
185 | $name = \WP_CLI::colorize( "%y$name%n" );
186 | break;
187 | default:
188 | break;
189 | }
190 | }
191 |
192 | // these keys must match the column headings in the formatter (extras ok).
193 | $data[] = array(
194 | 'name' => $name,
195 | 'slug' => 'wordpress',
196 | 'installed version' => $wp_version,
197 | 'id' => $stat['id'],
198 | 'status' => $stat['title'],
199 | 'fixed in' => $stat['fixed in'],
200 | 'severity' => $stat['severity'],
201 | 'introduced in' => $stat['introduced_in'],
202 | 'reference' => $stat['reference'],
203 | 'action' => $stat['action'],
204 | );
205 | }
206 |
207 | return $data;
208 | }
209 |
210 | /**
211 | * Check wpscan.com for reports
212 | * Total how many are relevant
213 | *
214 | * @param string $slug Installed plugin/theme slug.
215 | * @param string $version Installed plugin/theme version.
216 | * @param string|array $type The "thing" we're checking, "plugin" or "theme".
217 | * If string, it's pluralized with an "s".
218 | * If array, should be [ single, plural ].
219 | *
220 | * @return array Data array
221 | */
222 | public function check_status( $slug, $version, $type ) {
223 | list( $singular_type, $plural_type ) = $this->get_slugs( $type );
224 |
225 | $endpoint = $plural_type . '/' . $slug;
226 | $response = $this->call( $endpoint );
227 |
228 | $code = wp_remote_retrieve_response_code( $response );
229 | $body = wp_remote_retrieve_body( $response );
230 |
231 | $table_format = false;
232 | if ( ! isset( $this->assoc_args['format'] ) || 'table' === $this->assoc_args['format'] ) {
233 | $table_format = true;
234 | }
235 |
236 | $report = array();
237 |
238 | if ( 404 === $code ) {
239 | $report[] = array(
240 | 'title' => "Error generating report for $slug",
241 | );
242 | } elseif ( 429 === $code ) {
243 | $report[] = array(
244 | 'title' => 'ERROR_API_QUOTA_FULL: exceed daily rate limit hit.',
245 | );
246 | } else {
247 |
248 | // let's analyse the report!
249 | $vulndb = json_decode( $body );
250 |
251 | if ( isset( $vulndb->error ) ) {
252 | $report[] = array(
253 | 'title' => "Error generating report for $slug",
254 | );
255 | }
256 |
257 | $vulnerabilities = array();
258 |
259 | if ( isset( $vulndb->$slug ) && isset( $vulndb->$slug->vulnerabilities ) ) {
260 | $vulnerabilities = $vulndb->$slug->vulnerabilities;
261 | }
262 |
263 | if ( is_array( $vulnerabilities ) && ! empty( $vulnerabilities ) ) {
264 | foreach ( $vulnerabilities as $k => $vuln ) {
265 | /**
266 | * Filter whether to skip the vulnerability check.
267 | *
268 | * @since 1.2.1
269 | * @hook vuln_skip_vulnerability_check
270 | * @param {bool} $skip True to skip.
271 | * @param {object} $vuln Vulnerability object.
272 | */
273 | if ( apply_filters( 'vuln_skip_vulnerability_check', false, $vuln ) ) {
274 | continue;
275 | }
276 |
277 | // API has records for when was introduced ?
278 | $reported_since = $this->obj_has_non_empty_prop( 'introduced_in', $vuln );
279 | // Check for fix version.
280 | $fixed_since = $this->obj_has_non_empty_prop( 'fixed_in', $vuln );
281 |
282 | // vulnerability that hasn't been fixed :(.
283 | if ( ! $fixed_since ) {
284 |
285 | $report[] = array(
286 | 'id' => $vuln->id,
287 | 'title' => $vuln->title,
288 | 'fixed in' => 'Not fixed',
289 | 'severity' => $this->get_severity_value( $vuln->cvss ),
290 | 'introduced_in' => $reported_since ? $vuln->introduced_in : 'n/a',
291 | 'reference' => 'https://wpscan.com/vulnerability/' . $vuln->id,
292 | 'action' => 'watch',
293 | );
294 |
295 | // vuln version, fix available.
296 | } elseif (
297 | // If no records for when it was introduced, compare fixed version against current .
298 | (
299 | ! $reported_since
300 | && version_compare( $version, $vuln->fixed_in, '<' )
301 | )
302 | ||
303 | (
304 | // If have records for when it was introduced.
305 | $reported_since
306 | // Check if using version with introduced vulnerablity.
307 | && version_compare( $version, $vuln->introduced_in, '>=' )
308 | // Check if using version with vulnerablity fixed.
309 | && version_compare( $version, $vuln->fixed_in, '<' )
310 | )
311 | ) {
312 |
313 | $report[] = array(
314 | 'id' => $vuln->id,
315 | 'title' => $vuln->title,
316 | 'fixed in' => $vuln->fixed_in,
317 | 'severity' => $this->get_severity_value( $vuln->cvss ),
318 | 'introduced_in' => $reported_since ? $vuln->introduced_in : 'n/a',
319 | 'reference' => 'https://wpscan.com/vulnerability/' . $vuln->id,
320 | 'action' => 'update',
321 | );
322 |
323 | // if installed plugin version is greater than a fixed version,
324 | // unset that vuln entry, we don't need it.
325 | } else {
326 |
327 | // This leaves us with an array of relevant vulns
328 | // not currently used :/.
329 | unset( $vulnerabilities[ $k ] );
330 | }
331 | }
332 | }
333 |
334 | $total = count( $report );
335 | if ( $total <= 0 ) {
336 | $report[] = array(
337 | 'title' => "No vulnerabilities reported for this version of $slug",
338 | );
339 | }
340 | }
341 |
342 | $data = array();
343 |
344 | $last_item = '';
345 | foreach ( $report as $stat ) {
346 |
347 | $stat = wp_parse_args(
348 | $stat,
349 | array(
350 | 'id' => '',
351 | 'action' => '',
352 | 'fixed in' => 'n/a',
353 | 'severity' => 'n/a',
354 | 'introduced_in' => 'n/a',
355 | 'reference' => 'n/a',
356 | )
357 | );
358 | $name = ( $table_format && $slug === $last_item ? '' : $slug );
359 |
360 | if ( $table_format ) {
361 | switch ( $stat['action'] ) {
362 | case 'update':
363 | $name = WP_CLI::colorize( "%r$name%n" );
364 | break;
365 | case 'watch':
366 | $name = WP_CLI::colorize( "%y$name%n" );
367 | break;
368 | default:
369 | break;
370 | }
371 | }
372 |
373 | // these keys must match the column headings in the formatter (extras ok).
374 | $data[] = array(
375 | 'name' => $name,
376 | 'slug' => $slug,
377 | 'installed version' => $version,
378 | 'id' => $stat['id'],
379 | 'status' => $stat['title'],
380 | 'fixed in' => $stat['fixed in'],
381 | 'severity' => $stat['severity'],
382 | 'introduced in' => $stat['introduced_in'],
383 | 'reference' => $stat['reference'],
384 | 'action' => $stat['action'],
385 | );
386 | $last_item = $slug;
387 | }
388 |
389 | return $data;
390 | }
391 |
392 | /**
393 | * Worker. Run through checking the status of a plugin/theme
394 | *
395 | * @param string|array $type The "thing" we're checking.
396 | * If string, it's pluralized with an "s"
397 | * If array, should be [ single, plural ].
398 | *
399 | * @return array Statuses for all themes
400 | */
401 | public function check_thing( $type ) {
402 | list( $singular_type ) = $this->get_slugs( $type );
403 |
404 | $list = WP_CLI::launch_self(
405 | $singular_type,
406 | array( 'list' ),
407 | array(
408 | 'format' => 'csv',
409 | 'fields' => 'name,version',
410 | ),
411 | true,
412 | true
413 | );
414 |
415 | $list = $this->parse_list( $list );
416 |
417 | // test list.
418 | if ( isset( $this->assoc_args['test'] ) && $this->assoc_args['test'] ) {
419 | $list = $this->get_test_list( $type );
420 | }
421 |
422 | $data = array();
423 |
424 | foreach ( $list as $thing ) {
425 |
426 | $status = $this->check_status( $thing['name'], $thing['version'], $singular_type );
427 |
428 | $data = array_merge( $data, $status );
429 |
430 | }
431 |
432 | return $data;
433 | }
434 |
435 | /**
436 | * Call the VulnDB API.
437 | *
438 | * @param string $endpoint The endpoint.
439 | *
440 | * @return array|mixed|WP_Error
441 | */
442 | protected function call( $endpoint ) {
443 |
444 | if ( ! defined( 'VULN_API_TOKEN' ) ) {
445 | WP_CLI::error( 'VULN_API_TOKEN is not set.' );
446 | die();
447 | }
448 |
449 | $url = $this->api_url . $endpoint;
450 |
451 | $key = 'vuln_check-' . md5( $url );
452 |
453 | $args = array(
454 | 'headers' => array(
455 | 'Authorization' => 'Token token=' . VULN_API_TOKEN,
456 | ),
457 | 'method' => 'GET',
458 | );
459 |
460 | $response = get_transient( $key );
461 | if ( ! $response ) {
462 | $response = wp_remote_request( $url, $args );
463 | set_transient( $key, $response, HOUR_IN_SECONDS );
464 | } else {
465 | WP_CLI::debug( "Use response cache for $url" );
466 | }
467 |
468 | return $response;
469 | }
470 | }
471 |
--------------------------------------------------------------------------------
/includes/class-vulnerability-cli.php:
--------------------------------------------------------------------------------
1 | ]
88 | * : Accepted values: table, csv, json, count, ids, yaml. Default: table
89 | *
90 | * ## EXAMPLES
91 | *
92 | * wp vuln status
93 | * wp vuln status --not-themes
94 | *
95 | * @subcommand status
96 | */
97 | public function status( $args, $assoc_args ) {
98 |
99 | $this->init( $assoc_args );
100 | $format = isset( $assoc_args['format'] ) ? $assoc_args['format'] : 'table';
101 | $this->porcelain = false;
102 | $this->test = isset( $assoc_args['test'] );
103 |
104 | $this->nagios_op = isset( $assoc_args['nagios'] );
105 | $this->reference = isset( $assoc_args['reference'] );
106 | $this->mail = isset( $assoc_args['mail'] ) ? $assoc_args['mail'] : '';
107 |
108 | $this->update_list = array();
109 |
110 | // need a copy because it's passed by ref and destroyed in the formatter.
111 | $this->assoc_args_plugin = $assoc_args;
112 | $this->assoc_args_theme = $assoc_args;
113 | $this->assoc_args_wordpress = $assoc_args;
114 |
115 | global $wp_version;
116 |
117 | if ( $this->nagios_op ) {
118 | $this->do_nagios_op( array( 'wordpress', 'plugin', 'theme' ) );
119 | }
120 |
121 | if ( 'json' === $format ) {
122 | echo '{"core":';
123 | $this->do_wordpress();
124 | echo ',"plugins":';
125 | $this->do_plugins();
126 | echo ',"themes":';
127 | $this->do_themes();
128 | echo '}';
129 | exit( 0 );
130 | }
131 |
132 | if ( ! $this->porcelain && 'table' === $format ) {
133 | WP_CLI::log( $this->get_api_provider_credit() );
134 | }
135 | WP_CLI::log( WP_CLI::colorize( '%GWordPress ' . $wp_version . ' %n' ) );
136 | $this->do_wordpress();
137 | WP_CLI::log( WP_CLI::colorize( '%GPlugins%n' ) );
138 | $this->do_plugins();
139 | WP_CLI::log( WP_CLI::colorize( '%GThemes%n' ) );
140 | $this->do_themes();
141 | if ( ! $this->porcelain && 'table' === $format && ! empty( $this->get_copyright_notice() ) ) {
142 | WP_CLI::log( $this->get_copyright_notice() );
143 | }
144 | }
145 |
146 | /**
147 | * Check WordPress core for reported vulnerabilities.
148 | *
149 | * ## OPTIONS
150 | *
151 | * [--nagios]
152 | * : Output for nagios
153 | *
154 | * [--mail]
155 | * : Mail nagios output if any vulnerability found
156 | *
157 | * [--reference]
158 | * : Add vulnerability reference link to the output
159 | *
160 | * [--format=]
161 | * : Accepted values: table, csv, json, count, ids, yaml. Default: table
162 | *
163 | * ## EXAMPLES
164 | *
165 | * wp vuln core-status
166 | *
167 | * @subcommand core-status
168 | */
169 | public function core_status( $args, $assoc_args ) {
170 |
171 | $this->init( $assoc_args );
172 | $format = isset( $assoc_args['format'] ) ? $assoc_args['format'] : 'table';
173 | $this->porcelain = isset( $assoc_args['porcelain'] );
174 | $this->test = isset( $assoc_args['test'] );
175 |
176 | $this->nagios_op = isset( $assoc_args['nagios'] );
177 | $this->reference = isset( $assoc_args['reference'] );
178 | $this->mail = isset( $assoc_args['mail'] ) ? $assoc_args['mail'] : '';
179 |
180 | $this->update_list = array();
181 |
182 | // need a copy because it's passed by ref and destroyed in the formatter.
183 | $this->assoc_args_wordpress = $assoc_args;
184 |
185 | if ( $this->nagios_op ) {
186 | $this->do_nagios_op( array( 'wordpress' ) );
187 | }
188 |
189 | if ( ! $this->porcelain && 'table' === $format ) {
190 | WP_CLI::log( $this->get_api_provider_credit() );
191 | }
192 | $this->do_wordpress();
193 |
194 | // Display the copyright notice.
195 | if ( ! $this->porcelain && 'table' === $format && ! empty( $this->get_copyright_notice() ) ) {
196 | WP_CLI::log( $this->get_copyright_notice() );
197 | }
198 | }
199 |
200 | /**
201 | * Check plugins for reported vulnerabilities.
202 | *
203 | * ## OPTIONS
204 | *
205 | * [--porcelain]
206 | * : Print only slugs of plugins with updates
207 | *
208 | * [--test]
209 | * : Load test plugin/theme data
210 | *
211 | * [--nagios]
212 | * : Output for nagios
213 | *
214 | * [--mail]
215 | * : Mail nagios output if any vulnerability found
216 | *
217 | * [--reference]
218 | * : Add vulnerability reference link to the output
219 | *
220 | * [--format=]
221 | * : Accepted values: table, csv, json, count, ids, yaml. Default: table
222 | *
223 | * ## EXAMPLES
224 | *
225 | * wp vuln plugin-status
226 | *
227 | * @subcommand plugin-status
228 | */
229 | public function plugin_status( $args, $assoc_args ) {
230 |
231 | $this->init( $assoc_args );
232 | $format = isset( $assoc_args['format'] ) ? $assoc_args['format'] : 'table';
233 | $this->porcelain = isset( $assoc_args['porcelain'] );
234 | $this->test = isset( $assoc_args['test'] );
235 |
236 | $this->nagios_op = isset( $assoc_args['nagios'] );
237 | $this->reference = isset( $assoc_args['reference'] );
238 | $this->mail = isset( $assoc_args['mail'] ) ? $assoc_args['mail'] : '';
239 |
240 | $this->update_list = array();
241 | // need a copy because it's passed by ref and destroyed in the formatter.
242 | $this->assoc_args_plugin = $assoc_args;
243 |
244 | if ( $this->nagios_op ) {
245 | $this->do_nagios_op( array( 'plugin' ) );
246 | }
247 |
248 | if ( ! $this->porcelain && 'table' === $format ) {
249 | WP_CLI::log( $this->get_api_provider_credit() );
250 | }
251 | $this->do_plugins();
252 |
253 | // Display the copyright notice.
254 | if ( ! $this->porcelain && 'table' === $format && ! empty( $this->get_copyright_notice() ) ) {
255 | WP_CLI::log( $this->get_copyright_notice() );
256 | }
257 | }
258 |
259 | /**
260 | * Check themes for reported vulnerabilities.
261 | *
262 | * ## OPTIONS
263 | *
264 | * [--porcelain]
265 | * : Print only slugs of themes with updates
266 | *
267 | * [--test]
268 | * : Load test theme/theme data
269 | *
270 | * [--nagios]
271 | * : Output for nagios
272 | *
273 | * [--reference]
274 | * : Add vulnerability reference link to the output
275 | *
276 | * [--format=]
277 | * : Accepted values: table, csv, json, count, ids, yaml. Default: table
278 | *
279 | * ## EXAMPLES
280 | *
281 | * wp vuln theme-status
282 | *
283 | * @subcommand theme-status
284 | */
285 | public function theme_status( $args, $assoc_args ) {
286 |
287 | $this->init( $assoc_args );
288 | $format = isset( $assoc_args['format'] ) ? $assoc_args['format'] : 'table';
289 | $this->porcelain = isset( $assoc_args['porcelain'] );
290 | $this->test = isset( $assoc_args['test'] );
291 | $this->reference = isset( $assoc_args['reference'] );
292 |
293 | $this->nagios_op = isset( $assoc_args['nagios'] );
294 |
295 | $this->update_list = array();
296 |
297 | // need a copy because it's passed by ref and destroyed in the formatter.
298 | $this->assoc_args_theme = $assoc_args;
299 |
300 | if ( $this->nagios_op ) {
301 | $this->do_nagios_op( array( 'theme' ) );
302 | }
303 |
304 | if ( ! $this->porcelain && 'table' === $format ) {
305 | WP_CLI::log( $this->get_api_provider_credit() );
306 | }
307 | $this->do_themes();
308 |
309 | // Display the copyright notice.
310 | if ( ! $this->porcelain && 'table' === $format && ! empty( $this->get_copyright_notice() ) ) {
311 | WP_CLI::log( $this->get_copyright_notice() );
312 | }
313 | }
314 |
315 | /**
316 | * Check any given theme.
317 | *
318 | * ## OPTIONS
319 | *
320 | * [...]
321 | * : theme slug to check
322 | *
323 | * [--version]
324 | * : Version if other than latest. Only applies if one slug provided
325 | *
326 | * [--reference]
327 | * : Add vulnerability reference link to the output
328 | *
329 | * [--format=]
330 | * : Accepted values: table, csv, json, count, ids, yaml. Default: table
331 | *
332 | * ## EXAMPLES
333 | *
334 | * wp vuln theme-check
335 | *
336 | * @subcommand theme-check
337 | */
338 | public function theme_check( $args, $assoc_args ) {
339 | $this->reference = isset( $assoc_args['reference'] );
340 | $this->init( $assoc_args );
341 | if ( count( $args ) > 1 ) {
342 | $version = 0;
343 | } else {
344 | $version = isset( $assoc_args['version'] ) ? $assoc_args['version'] : 0;
345 |
346 | }
347 |
348 | $display = array();
349 | $format = isset( $assoc_args['format'] ) ? $assoc_args['format'] : 'table';
350 | foreach ( $args as $slug ) {
351 | $status = $this->service->check_status( $slug, $version, 'theme' );
352 | $display = array_merge( $display, $status );
353 | }
354 |
355 | // Collect copyrights data.
356 | $this->collect_copyrights_data( $display );
357 |
358 | $fields = array(
359 | 'name' => true,
360 | 'installed version' => false,
361 | 'status' => false,
362 | 'fixed in' => false,
363 | 'severity' => false,
364 | );
365 | if ( $this->reference ) {
366 | $fields['reference'] = false;
367 | }
368 |
369 | $formatter = new \WP_CLI\Formatter(
370 | $assoc_args,
371 | array_keys( $fields ),
372 | 'themes'
373 | );
374 |
375 | if ( 'table' === $format ) {
376 | WP_CLI::log( $this->get_api_provider_credit() );
377 | }
378 |
379 | // Add second array parameter to indicate the position of the column having a maybe colorized item.
380 | $formatter->display_items( $display, array_values( $fields ) );
381 |
382 | // Display the copyright notice.
383 | if ( ! $this->porcelain && 'table' === $format && ! empty( $this->get_copyright_notice() ) ) {
384 | WP_CLI::log( $this->get_copyright_notice() );
385 | }
386 | }
387 |
388 | /**
389 | * Check any given plugin.
390 | *
391 | * ## OPTIONS
392 | *
393 | * [...]
394 | * : plugin slug to check
395 | *
396 | * [--version]
397 | * : Version if other than latest. Only applies if one slug provided
398 | *
399 | * [--reference]
400 | * : Add vulnerability reference link to the output
401 | *
402 | * [--format=]
403 | * : Accepted values: table, csv, json, count, ids, yaml. Default: table
404 | *
405 | * ## EXAMPLES
406 | *
407 | * wp vuln plugin-check
408 | *
409 | * @subcommand plugin-check
410 | */
411 | public function plugin_check( $args, $assoc_args ) {
412 | $this->reference = isset( $assoc_args['reference'] );
413 | $this->init( $assoc_args );
414 | if ( count( $args ) > 1 ) {
415 | $version = 0;
416 | } else {
417 | $version = isset( $assoc_args['version'] ) ? $assoc_args['version'] : 0;
418 |
419 | }
420 |
421 | $display = array();
422 | $format = isset( $assoc_args['format'] ) ? $assoc_args['format'] : 'table';
423 | foreach ( $args as $slug ) {
424 | $status = $this->service->check_status( $slug, $version, 'plugin' );
425 | $display = array_merge( $display, $status );
426 | }
427 |
428 | // Collect copyrights data.
429 | $this->collect_copyrights_data( $display );
430 |
431 | $fields = array(
432 | 'name' => true,
433 | 'installed version' => false,
434 | 'status' => false,
435 | 'fixed in' => false,
436 | 'severity' => false,
437 | );
438 | if ( $this->reference ) {
439 | $fields['reference'] = false;
440 | }
441 |
442 | $formatter = new \WP_CLI\Formatter(
443 | $assoc_args,
444 | array_keys( $fields ),
445 | 'plugins'
446 | );
447 |
448 | if ( 'table' === $format ) {
449 | WP_CLI::log( $this->get_api_provider_credit() );
450 | }
451 |
452 | // Add second array parameter to indicate the position of the column having a maybe colorized item.
453 | $formatter->display_items( $display, array_values( $fields ) );
454 |
455 | // Display the copyright notice.
456 | if ( ! $this->porcelain && 'table' === $format && ! empty( $this->get_copyright_notice() ) ) {
457 | WP_CLI::log( $this->get_copyright_notice() );
458 | }
459 | }
460 |
461 | /**
462 | * Init Scanner API service
463 | *
464 | * @param array $assoc_args Array of command arguments.
465 | *
466 | * @since 2.0.0
467 | */
468 | private function init( $assoc_args ) {
469 | if ( defined( 'VULN_API_PROVIDER' ) && 'patchstack' === VULN_API_PROVIDER ) {
470 | $this->service = new Vuln_Patchstack_Service( $assoc_args );
471 | } elseif ( defined( 'VULN_API_PROVIDER' ) && 'wordfence' === VULN_API_PROVIDER ) {
472 | $this->service = new Vuln_Wordfence_Service( $assoc_args );
473 | } else {
474 | $this->service = new Vuln_WPScan_Service( $assoc_args );
475 | }
476 | }
477 |
478 | /**
479 | * Do WordPress core check
480 | *
481 | * @return void
482 | */
483 | private function do_wordpress() {
484 |
485 | $singular_type = 'wordpress'; // phpcs:ignore WordPress.WP.CapitalPDangit.Misspelled
486 | $plural_type = 'wordpresses';
487 |
488 | $assoc_args = "assoc_args_{$singular_type}";
489 |
490 | $display = $this->service->check_wordpress();
491 | $update_list = $this->extract_updatable_items( $display );
492 |
493 | $display_format = isset( $this->{$assoc_args}['format'] ) && ! empty( $this->{$assoc_args}['format'] ) ? $this->{$assoc_args}['format'] : 'table';
494 | $display = $this->format_data_for_return( $display_format, $display );
495 |
496 | // Collect copyrights data.
497 | $this->collect_copyrights_data( $display );
498 |
499 | // Pretty print.
500 | if ( ! $this->porcelain ) {
501 | $fields = array(
502 | 'name' => true,
503 | 'installed version' => false,
504 | 'status' => false,
505 | 'introduced in' => false,
506 | 'fixed in' => false,
507 | 'severity' => false,
508 | );
509 | if ( $this->reference ) {
510 | $fields['reference'] = false;
511 | }
512 |
513 | $formatter = new \WP_CLI\Formatter(
514 | $this->$assoc_args,
515 | array_keys( $fields ),
516 | $plural_type
517 | );
518 |
519 | // Add second array parameter to indicate the position of the column having a maybe colorized item.
520 | $formatter->display_items(
521 | $display,
522 | array_values( $fields )
523 | );
524 | // Improve readeability: force new line.
525 | if ( 'ids' === $display_format ) {
526 | WP_CLI::log( '' );
527 | }
528 | } elseif ( $update_list ) {
529 | WP_CLI::log( implode( ' ', $update_list ) );
530 | die;
531 | }
532 | }
533 |
534 | /**
535 | * Helper. Call worker for plugin tasks
536 | *
537 | * @return void
538 | */
539 | private function do_plugins() {
540 | $this->do_thing( 'plugin' );
541 | }
542 |
543 | /**
544 | * Helper. Call worker for theme tasks
545 | *
546 | * @return void
547 | */
548 | private function do_themes() {
549 | $this->do_thing( 'theme' );
550 | }
551 |
552 | /**
553 | * Worker. Do what's needed for plugins/themes
554 | * Display table, or if --porcelain, display only updatable slugs
555 | *
556 | * @param string|array $type The "thing" we're checking.
557 | * If string, it's pluralized with an "s"
558 | * If array, should be [ single, plural ].
559 | *
560 | * @return void
561 | */
562 | private function do_thing( $type ) {
563 | list( $singular_type, $plural_type ) = $this->get_slugs( $type );
564 |
565 | $assoc_args = "assoc_args_{$singular_type}";
566 |
567 | $display = $this->service->check_thing( $singular_type );
568 | $update_list = $this->extract_updatable_items( $display );
569 |
570 | $display_format = isset( $this->{$assoc_args}['format'] ) && ! empty( $this->{$assoc_args}['format'] ) ? $this->{$assoc_args}['format'] : 'table';
571 | $display = $this->format_data_for_return( $display_format, $display );
572 |
573 | // Collect copyrights data.
574 | $this->collect_copyrights_data( $display );
575 |
576 | // Pretty print.
577 | if ( ! $this->porcelain ) {
578 |
579 | $fields = array(
580 | 'name' => true,
581 | 'installed version' => false,
582 | 'status' => false,
583 | 'introduced in' => false,
584 | 'fixed in' => false,
585 | 'severity' => false,
586 | );
587 | if ( $this->reference ) {
588 | $fields['reference'] = false;
589 | }
590 |
591 | $formatter = new \WP_CLI\Formatter(
592 | $this->$assoc_args,
593 | array_keys( $fields ),
594 | $plural_type
595 | );
596 |
597 | // Add second array parameter to indicate the position of the column having a maybe colorized item.
598 | $formatter->display_items(
599 | $display,
600 | array_values( $fields )
601 | );
602 |
603 | // if plugins need updating, do or tell the user.
604 | if ( $update_list ) { // phpcs:ignore Generic.CodeAnalysis.EmptyStatement.DetectedIf
605 | // it would be nice to show this, but we'd need to rewrite the unit test
606 | // $update_list = implode( ' ', $update_list );
607 | // WP_CLI::log( "Run `wp $singular_type update $update_list`" );
608 | } elseif ( ( isset( $this->{$assoc_args}['format'] ) && 'json' !== $this->{$assoc_args}['format'] ) ) {
609 | WP_CLI::log( 'Nothing to update' );
610 | }
611 |
612 | // Improve readeability: force new line.
613 | if ( 'ids' === $display_format ) {
614 | WP_CLI::log( '' );
615 | }
616 | } elseif ( $update_list ) {
617 | WP_CLI::log( implode( ' ', $update_list ) );
618 | die;
619 | }
620 | }
621 |
622 | /**
623 | * Pull updatable items from Formatter-ready array
624 | *
625 | * @param array $data Formatter-ready data.
626 | *
627 | * @return array Simple array of plugin/theme slugs needing to be updated
628 | */
629 | private function extract_updatable_items( $data ) {
630 | $update_list = wp_list_pluck( wp_list_filter( $data, array( 'action' => 'update' ) ), 'action', 'slug' );
631 | $update_list = array_keys( $update_list );
632 |
633 | return $update_list;
634 | }
635 |
636 | /**
637 | * Get singular and plural slugs for given string or array
638 | *
639 | * @param string|array $singular_or_array If string, it's pluralized with an "s"
640 | * If array, should be [ single, plural ].
641 | *
642 | * @return array
643 | */
644 | private function get_slugs( $singular_or_array ) {
645 | if ( is_array( $singular_or_array ) ) {
646 | $singular_type = $singular_or_array[0];
647 | $plural_type = $singular_or_array[1];
648 | } else {
649 | $singular_type = $singular_or_array;
650 | $plural_type = "{$singular_or_array}s";
651 | }
652 |
653 | return array( $singular_type, $plural_type );
654 | }
655 |
656 | /**
657 | * Do nagios output.
658 | *
659 | * @param array $things type (WordPress,plugin,theme).
660 | */
661 | private function do_nagios_op( $things ) {
662 | if ( ! empty( $things ) && is_array( $things ) ) {
663 |
664 | $wp_list = 0;
665 | $pl_list = 0;
666 | $th_list = 0;
667 |
668 | // phpcs:ignore WordPress.WP.CapitalPDangit.Misspelled
669 | if ( in_array( 'wordpress', $things, true ) ) {
670 | $wp_vulns = $this->service->check_wordpress();
671 | if ( ! empty( $wp_vulns ) && is_array( $wp_vulns ) ) {
672 | foreach ( $wp_vulns as $wp_vuln ) {
673 | if ( isset( $wp_vuln['fixed in'] ) && 'n/a' !== $wp_vuln['fixed in'] ) {
674 | $wp_list ++;
675 | }
676 | }
677 | }
678 | }
679 | if ( in_array( 'plugin', $things, true ) ) {
680 | $plugins = $this->service->check_thing( 'plugin' );
681 | if ( ! empty( $plugins ) && is_array( $plugins ) ) {
682 | foreach ( $plugins as $plugin ) {
683 | if ( isset( $plugin['fixed in'] ) && 'n/a' !== $plugin['fixed in'] ) {
684 | $pl_list ++;
685 | }
686 | }
687 | }
688 | }
689 | if ( in_array( 'theme', $things, true ) ) {
690 | $themes = $this->service->check_thing( 'theme' );
691 | if ( ! empty( $themes ) && is_array( $themes ) ) {
692 | foreach ( $themes as $theme ) {
693 | if ( isset( $theme['fixed in'] ) && 'n/a' !== $theme['fixed in'] ) {
694 | $th_list ++;
695 | }
696 | }
697 | }
698 | }
699 |
700 | if ( empty( $wp_list ) && empty( $pl_list ) && empty( $th_list ) ) {
701 | WP_CLI::line( 'OK - no vulnerabilities found' );
702 | exit( 0 );
703 | } else {
704 | $message = sprintf(
705 | 'CRITICAL - %d core, %d plugin and %d theme vulnerabilities found',
706 | $wp_list,
707 | $pl_list,
708 | $th_list
709 | );
710 | WP_CLI::line( $message );
711 |
712 | // Notify via mail.
713 | if ( ! empty( $this->mail ) ) {
714 |
715 | $site_url = site_url();
716 | $headers = array();
717 |
718 | /*
719 | * In CLI if $_SERVER['SERVER_NAME'] isn't set than it fails to send mail.
720 | * Reason being, this property is used to create "From" mail header.
721 | * If it's empty, we need to pass that in header.
722 | */
723 | if ( empty( $_SERVER['SERVER_NAME'] ) ) {
724 | $sitename = $site_url;
725 | if ( substr( $sitename, 0, 8 ) === 'https://' ) {
726 | $sitename = substr( $sitename, 8 );
727 | }
728 | if ( substr( $sitename, 0, 7 ) === 'http://' ) {
729 | $sitename = substr( $sitename, 7 );
730 | }
731 | if ( substr( $sitename, 0, 4 ) === 'www.' ) {
732 | $sitename = substr( $sitename, 4 );
733 | }
734 |
735 | $from_email = 'wordpress@' . $sitename;
736 |
737 | $headers[] = "From:{$from_email}";
738 | }
739 |
740 | $subject = 'Vulnerabilities found in ' . $site_url;
741 | if ( ! wp_mail( $this->mail, $subject, $message, $headers ) ) {
742 | WP_CLI::line( 'Not able to send mail.' );
743 | }
744 | }
745 |
746 | exit( 2 );
747 | }
748 | }
749 | }
750 |
751 | /**
752 | * Format data before output based on --format parameter passed to command.
753 | *
754 | * @param string $output_format One of table, csv, json, count, ids, yaml.
755 | * @param array $data Array having report details.
756 | * @return array $applied_format Array for correct output.
757 | */
758 | protected function format_data_for_return( $output_format, $data ) {
759 |
760 | switch ( $output_format ) {
761 | case 'ids':
762 | $applied_format = array_filter( wp_list_pluck( $data, 'id' ) );
763 | break;
764 | default:
765 | $applied_format = $data;
766 | }
767 |
768 | return $applied_format;
769 | }
770 |
771 | /**
772 | * Get the credit wordings for the API provider.
773 | *
774 | * @return string
775 | */
776 | private function get_api_provider_credit() {
777 | $api_providers = array(
778 | 'wpscan' => 'WPScan',
779 | 'patchstack' => 'Patchstack',
780 | 'wordfence' => 'Wordfence Intelligence',
781 | );
782 |
783 | $api_provider = $api_providers['wpscan'];
784 | if ( defined( 'VULN_API_PROVIDER' ) ) {
785 | $api_provider = $api_providers[ VULN_API_PROVIDER ];
786 | }
787 |
788 | /* translators: %s is API provider service. */
789 | return sprintf( __( 'Vulnerability API Provider: %s', 'wpcli-vulnerability-scanner' ), $api_provider );
790 | }
791 |
792 | /**
793 | * Collect copyright notices from the API response.
794 | *
795 | * @param array $data
796 | * @return void
797 | */
798 | private function collect_copyrights_data( $data ) {
799 | if ( empty( $data ) || ! is_array( $data ) ) {
800 | return;
801 | }
802 | foreach ( $data as $value ) {
803 | if ( isset( $value['copyrights'] ) && ! empty( $value['copyrights'] ) ) {
804 | foreach ( $value['copyrights'] as $key => $copyright ) {
805 | if ( 'message' === $key || isset( $this->copyrights[ $key ] ) ) {
806 | continue;
807 | }
808 | if ( isset( $copyright->notice ) && ! empty( $copyright->notice ) ) {
809 | $this->copyrights[ $key ] = $copyright->notice;
810 | }
811 | }
812 | }
813 | }
814 | }
815 |
816 | /**
817 | * Get the copyright notice to show in the footer.
818 | *
819 | * @return string
820 | */
821 | private function get_copyright_notice() {
822 | $notice = '';
823 | if ( ! empty( $this->copyrights ) ) {
824 | /* translators: %s is Copyright notice */
825 | $notice = sprintf( __( 'Copyrights: %s', 'wpcli-vulnerability-scanner' ), implode( ', ', $this->copyrights ) );
826 | }
827 |
828 | return $notice;
829 | }
830 | }
831 |
--------------------------------------------------------------------------------
/includes/vuln.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | WPCLIPATH='/path/to/wp'
4 | if [ ! -f $WPCLIPATH ]; then
5 | WPCLIPATH=`which wp`
6 | if [ -z $WPCLIPATH ]; then exit 1; fi;
7 | fi;
8 |
9 | RECIPIENT="user@example.com"
10 | SUBJECT="Vulnerabilities detected"
11 | SUBJECT2="No vulnerabilities detected"
12 |
13 | core=$($WPCLIPATH vuln core-status --nagios --allow-root)
14 | novuln="OK - no vulnerabilities found"
15 | if echo "$core" | grep -q "$novuln"; then
16 | echo "No core vulnerabilities detected at '$path'" | mail -s "$SUBJECT2" $RECIPIENT
17 | else
18 | echo "Core vulnerability found at '$path'" | mail -s "$SUBJECT" $RECIPIENT
19 | fi
20 |
21 | plugins=$($WPCLIPATH vuln plugin-status --porcelain)
22 | if [ ! -z "$plugins" ]; then
23 | echo "Vuln plugins: $plugins" | mail -s $SUBJECT $RECIPIENT
24 | echo "$WPCLIPATH plugin update $plugins"
25 | else
26 | echo "No plugin vulnerabilities detected at '$path'" | mail -s "$SUBJECT2" $RECIPIENT
27 | fi
28 |
29 | themes=$($WPCLIPATH vuln theme-status --porcelain)
30 | if [ ! -z "$themes" ]; then
31 | echo "Vuln themes: $themes" | mail -s $SUBJECT $RECIPIENT
32 | echo "$WPCLIPATH theme update $themes"
33 | else
34 | echo "No theme vulnerabilities detected at '$path'" | mail -s "$SUBJECT2" $RECIPIENT
35 | fi
36 |
--------------------------------------------------------------------------------
/wpcli-vulnerability-scanner.php:
--------------------------------------------------------------------------------
1 |