├── .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 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 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 |