PluginProbe
WPVulnerability / 5.1.6
WPVulnerability v5.1.6
5.1.6 5.1.2 5.1.1 5.0.1 5.0.0 trunk 0.1 0.2 1.0 1.0.1 1.1 1.2.0 1.2.1 1.2.2 1.2.3 1.2.4 1.3.0 1.3.1 1.3.2 1.3.3 2.0.0 2.0.1 2.0.2 2.0.3 2.0.4 All 57 releases
wpvulnerability / wpvulnerability-process.php

wpvulnerability-process.php in WPVulnerability 5.1.6, at wpvulnerability-process.php

833 lines 31.5 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Process functions
4 *
5 * @package WPVulnerability
6 *
7 * @since 2.0.0
8 */
9
10 defined( 'ABSPATH' ) || die( 'No script kiddies please!' );
11
12 /**
13 * Map a source hostname (or provider name) to a CSS class slug for pill colouring.
14 *
15 * Accepts either a hostname extracted from a source URL (e.g. "wordfence.com",
16 * "nvd.nist.gov") or a raw provider name as a fallback.
17 *
18 * @since 5.0.0
19 *
20 * @param string $host Hostname from the source URL, or raw provider name as fallback.
21 * @return string CSS slug: wordfence|patchstack|cve|euvd|jvn|wpscan|default.
22 */
23 function wpvulnerability_source_css_slug( $host ) {
24 $lower = strtolower( (string) $host );
25 if ( false !== strpos( $lower, 'wordfence' ) ) {
26 return 'wordfence'; }
27 if ( false !== strpos( $lower, 'patchstack' ) ) {
28 return 'patchstack'; }
29 if ( false !== strpos( $lower, 'euvd' ) ) {
30 return 'euvd'; }
31 if ( false !== strpos( $lower, 'jvn' ) ) {
32 return 'jvn'; }
33 if ( false !== strpos( $lower, 'wpscan' ) ) {
34 return 'wpscan'; }
35 if ( false !== strpos( $lower, 'cve' ) || false !== strpos( $lower, 'nvd' ) ) {
36 return 'cve';
37 }
38 return 'default';
39 }
40
41 /**
42 * Build HTML for source attribution pills.
43 *
44 * Each source becomes a linked pill whose label is the hostname extracted from
45 * the source URL (e.g. "wordfence.com", "nvd.nist.gov"). Falls back to the
46 * `name` / `id` keys when no URL is present.
47 *
48 * @since 5.0.0
49 *
50 * @param array<mixed> $sources Array of source objects from the vulnerability API.
51 * @return string HTML div.wpvuln-source-pills, or empty string if no sources.
52 */
53 function wpvulnerability_render_source_pills( $sources ) {
54 if ( empty( $sources ) ) {
55 return '';
56 }
57 $pills = array();
58 foreach ( $sources as $src ) {
59 if ( ! is_array( $src ) ) {
60 continue;
61 }
62 $link = is_scalar( $src['link'] ?? '' ) ? (string) ( $src['link'] ?? '' ) : '';
63
64 // Derive label and CSS slug from the URL hostname; fall back to name/id.
65 $label = '';
66 $slug_input = '';
67 if ( '' !== $link ) {
68 $parsed = wp_parse_url( $link );
69 $host = ( is_array( $parsed ) && isset( $parsed['host'] ) ) ? (string) $parsed['host'] : '';
70 if ( 0 === strpos( $host, 'www.' ) ) {
71 $host = substr( $host, 4 );
72 }
73 if ( '' !== $host ) {
74 $label = $host;
75 $slug_input = $host;
76 }
77 }
78 if ( '' === $label ) {
79 $name = is_scalar( $src['name'] ?? '' ) ? (string) ( $src['name'] ?? '' ) : '';
80 if ( '' === $name ) {
81 $name = is_scalar( $src['id'] ?? '' ) ? (string) ( $src['id'] ?? '' ) : '';
82 }
83 $label = $name;
84 $slug_input = $name;
85 }
86 if ( '' === $label ) {
87 continue;
88 }
89
90 $slug = wpvulnerability_source_css_slug( $slug_input );
91 $cls = esc_attr( 'wpvuln-source-pill wpvuln-source-' . $slug );
92 $inner = esc_html( $label );
93 if ( '' !== $link ) {
94 $pills[] = '<a href="' . esc_url( $link ) . '" class="' . $cls . '" target="_blank" rel="external nofollow noopener noreferrer">' . $inner . '</a>';
95 } else {
96 $pills[] = '<span class="' . $cls . '">' . $inner . '</span>';
97 }
98 }
99 if ( empty( $pills ) ) {
100 return '';
101 }
102 return '<div class="wpvuln-source-pills">' . implode( '', $pills ) . '</div>';
103 }
104
105 /**
106 * Build a colour-coded CVSS score + severity badge, optionally followed by an EPSS badge.
107 *
108 * @since 5.0.0
109 *
110 * @param string|null $score Formatted CVSS score (e.g. "7.5") or null.
111 * @param string|null $sev_raw Raw severity string (single-char or full word) or null.
112 * @param float|null $epss EPSS exploitation probability 0–1, or null if not available.
113 * @return string HTML span(s) for score and/or EPSS, or empty string if no data.
114 */
115 function wpvulnerability_render_score_badge( $score, $sev_raw, $epss = null ) {
116 if ( is_null( $score ) && ( is_null( $sev_raw ) || '' === $sev_raw ) && is_null( $epss ) ) {
117 return '';
118 }
119 $sev_lower = is_string( $sev_raw ) ? strtolower( trim( $sev_raw ) ) : '';
120 $css_map = array(
121 'c' => 'critical',
122 'critical' => 'critical',
123 'h' => 'high',
124 'high' => 'high',
125 'm' => 'medium',
126 'medium' => 'medium',
127 'l' => 'low',
128 'low' => 'low',
129 'n' => 'none',
130 'none' => 'none',
131 );
132 $css_key = isset( $css_map[ $sev_lower ] ) ? $css_map[ $sev_lower ] : 'none';
133 $sev_label = ( is_string( $sev_raw ) && '' !== $sev_raw ) ? wpvulnerability_severity( $sev_raw ) : null;
134 $parts = array();
135 if ( ! is_null( $score ) ) {
136 $parts[] = esc_html( $score );
137 }
138 if ( ! is_null( $sev_label ) ) {
139 $parts[] = esc_html( $sev_label );
140 }
141 $badge = '';
142 if ( ! empty( $parts ) ) {
143 $badge = '<span class="wpvuln-score-badge wpvuln-score-' . esc_attr( $css_key ) . '">' . implode( ' &middot; ', $parts ) . '</span>';
144 }
145 if ( null !== $epss ) {
146 $badge .= '<span class="wpvuln-epss-badge">EPSS&thinsp;' . esc_html( number_format( $epss * 100, 1 ) ) . '%</span>';
147 }
148 return $badge;
149 }
150
151 /**
152 * Extract the best available description from a vulnerability's source array.
153 *
154 * Iterates source objects and returns the first non-empty description string,
155 * stripping any leading language tag (e.g. "[en-US] ") added by the CVE API.
156 *
157 * @since 5.0.0
158 *
159 * @param array<mixed> $sources Source objects from the vulnerability API.
160 * @return string|null First non-empty description found, or null if none.
161 */
162 function wpvulnerability_get_source_description( $sources ) {
163 if ( empty( $sources ) ) {
164 return null;
165 }
166 foreach ( $sources as $src ) {
167 if ( ! is_array( $src ) ) {
168 continue;
169 }
170 $raw = isset( $src['description'] ) && is_string( $src['description'] ) ? trim( $src['description'] ) : '';
171 if ( '' === $raw ) {
172 continue;
173 }
174 // Strip leading language tag like "[en-US] " or "[ja] ".
175 if ( '[' === $raw[0] ) {
176 $close = strpos( $raw, '] ' );
177 if ( false !== $close ) {
178 $raw = substr( $raw, $close + 2 );
179 }
180 }
181 if ( '' !== $raw ) {
182 return $raw;
183 }
184 }
185 return null;
186 }
187
188 /**
189 * Clean a raw API version range string for display.
190 *
191 * Examples:
192 * "* - < 1.0.0" → "< 1.0.0"
193 * "- - < 1.0.0" → "< 1.0.0"
194 * "- < 1.0.0" → "< 1.0.0" (no space-dash-space separator)
195 * "1.0.0 - < 2.0" → "≥ 1.0.0 – < 2.0"
196 *
197 * @since 5.0.0
198 *
199 * @param string $versions Raw versions string from the API.
200 * @return string Cleaned version range for display (HTML-safe).
201 */
202 function wpvulnerability_clean_version_range( $versions ) {
203 $v = trim( (string) $versions );
204 if ( '' === $v ) {
205 return '';
206 }
207 // Handle "- < 1.0" / "* < 1.0": leading wildcard without a space-dash-space separator.
208 if ( 0 === strpos( $v, '- ' ) || 0 === strpos( $v, '* ' ) ) {
209 return esc_html( ltrim( substr( $v, 2 ) ) );
210 }
211 $parts = explode( ' - ', $v, 2 );
212 if ( 2 === count( $parts ) ) {
213 $from = trim( $parts[0] );
214 $to = trim( $parts[1] );
215 if ( '' === $from || '*' === $from || '-' === $from ) {
216 return esc_html( $to );
217 }
218 return '&ge;&thinsp;' . esc_html( $from ) . ' &ndash; ' . esc_html( $to );
219 }
220 return esc_html( $v );
221 }
222
223 /**
224 * Return an img tag for a component-type icon.
225 *
226 * @since 5.0.0
227 *
228 * @param string $type Component type: plugin, theme, core, php, apache, nginx, mariadb, mysql, imagemagick, curl, memcached, redis, sqlite.
229 * @return string HTML img tag with class wpvuln-component-icon, or empty string.
230 */
231 function wpvulnerability_component_icon_html( $type ) {
232 $icon_map = array(
233 'plugin' => 'icon-plugin.svg',
234 'theme' => 'icon-theme.svg',
235 'core' => 'icon-wordpress.svg',
236 'php' => 'icon-php.svg',
237 'apache' => 'icon-apache.svg',
238 'nginx' => 'icon-nginx.svg',
239 'mariadb' => 'icon-mariadb.svg',
240 'mysql' => 'icon-mysql.svg',
241 'imagemagick' => 'icon-imagemagick.svg',
242 'curl' => 'icon-curl.svg',
243 'memcached' => 'icon-memcached.svg',
244 'redis' => 'icon-redis.svg',
245 'sqlite' => 'icon-sqlite.svg',
246 );
247 if ( ! isset( $icon_map[ $type ] ) ) {
248 return '';
249 }
250
251 // Explicit dimensions: this HTML is also embedded where the plugin CSS is
252 // not loaded (notification emails, Site Health), and without width/height
253 // the SVG renders at its intrinsic 800x800 size.
254 $alt = sprintf(
255 /* translators: %s: component type (plugin, theme, core, php, ...) */
256 __( '%s icon', 'wpvulnerability' ),
257 (string) $type
258 );
259
260 return '<img src="' . esc_url( WPVULNERABILITY_PLUGIN_URL . 'assets/' . $icon_map[ $type ] ) . '" class="wpvuln-component-icon" width="16" height="16" alt="' . esc_attr( $alt ) . '" />';
261 }
262
263 /**
264 * Convert vulnerabilities into pretty HTML
265 *
266 * @since 2.0.0
267 *
268 * @param string $type Type: core, plugin, theme, php, apache, nginx, mariadb, mysql, imagemagick, curl.
269 * @param array<mixed> $vulnerabilities Vulnerability data.
270 *
271 * @return string The HTML representation of vulnerabilities.
272 */
273 function wpvulnerability_html( $type, $vulnerabilities ) {
274 $html = '';
275
276 if ( in_array( $type, array( 'plugin', 'theme' ), true ) ) {
277 foreach ( $vulnerabilities as $vulnerability ) {
278 if ( ! is_array( $vulnerability ) ) {
279 continue; }
280 $vuln_impact_raw = $vulnerability['impact'] ?? null;
281 $vuln_impact = is_array( $vuln_impact_raw ) ? $vuln_impact_raw : array();
282 $vuln_cvss_raw = $vuln_impact['cvss'] ?? null;
283 $vuln_cvss = is_array( $vuln_cvss_raw ) ? $vuln_cvss_raw : array();
284 $vuln_cvss2_raw = $vuln_impact['cvss2'] ?? null;
285 $vuln_cvss2 = is_array( $vuln_cvss2_raw ) ? $vuln_cvss2_raw : array();
286 $vuln_cvss3_raw = $vuln_impact['cvss3'] ?? null;
287 $vuln_cvss3 = is_array( $vuln_cvss3_raw ) ? $vuln_cvss3_raw : array();
288 $vuln_cvss4_raw = $vuln_impact['cvss4'] ?? null;
289 $vuln_cvss4 = is_array( $vuln_cvss4_raw ) ? $vuln_cvss4_raw : array();
290 $vuln_ssvc_raw = $vuln_impact['ssvc'] ?? null;
291 $vuln_ssvc = is_array( $vuln_ssvc_raw ) ? $vuln_ssvc_raw : array();
292 $vuln_cwe_raw = $vuln_impact['cwe'] ?? null;
293 $vuln_cwe = is_array( $vuln_cwe_raw ) ? $vuln_cwe_raw : array();
294 $vuln_src_raw = $vulnerability['source'] ?? null;
295 $vuln_sources = is_array( $vuln_src_raw ) ? $vuln_src_raw : array();
296
297 $kev = ( isset( $vuln_ssvc['kev'] ) && true === $vuln_ssvc['kev'] );
298 $exploitation = isset( $vuln_ssvc['exploitation'] ) && is_string( $vuln_ssvc['exploitation'] ) ? $vuln_ssvc['exploitation'] : '';
299 $automatable = isset( $vuln_ssvc['automatable'] ) && is_string( $vuln_ssvc['automatable'] ) ? $vuln_ssvc['automatable'] : '';
300 $kev_date_raw = $vuln_ssvc['kev_date'] ?? null;
301 $kev_date = is_string( $kev_date_raw ) && '' !== $kev_date_raw ? $kev_date_raw : null;
302 $epss_raw = $vuln_impact['epss'] ?? null;
303 $epss = is_numeric( $epss_raw ) ? (float) $epss_raw : null;
304 $description = wpvulnerability_get_source_description( $vuln_sources );
305
306 $what = array();
307 foreach ( $vuln_cwe as $vulnerability_cwe ) {
308 if ( ! is_array( $vulnerability_cwe ) ) {
309 continue; }
310 $cwe_name = is_scalar( $vulnerability_cwe['name'] ?? '' ) ? (string) ( $vulnerability_cwe['name'] ?? '' ) : '';
311 $cwe_desc = is_scalar( $vulnerability_cwe['description'] ?? '' ) ? (string) ( $vulnerability_cwe['description'] ?? '' ) : '';
312 $what[] = '<div><b>' . wp_kses( $cwe_name, 'strip' ) . '</b></div><div><i>' . esc_html( $cwe_desc ) . '</i></div>';
313 }
314
315 $source = wpvulnerability_render_source_pills( $vuln_sources );
316
317 // Best available CVSS score and severity: cvss4 > cvss3 > cvss2 > legacy cvss.
318 $score = null;
319 $sev_raw = null;
320 foreach ( array( $vuln_cvss4, $vuln_cvss3, $vuln_cvss2, $vuln_cvss ) as $cvss_c ) {
321 if ( empty( $cvss_c ) ) {
322 continue; }
323 $s_raw = $cvss_c['score'] ?? null;
324 $v_raw = $cvss_c['severity'] ?? null;
325 $s = is_numeric( $s_raw ) ? number_format( (float) $s_raw, 1, '.', '' ) : null;
326 $v = is_string( $v_raw ) && '' !== $v_raw ? $v_raw : null;
327 if ( null !== $s || null !== $v ) {
328 $score = $s;
329 $sev_raw = $v;
330 break;
331 }
332 }
333
334 $vuln_name = is_scalar( $vulnerability['name'] ?? '' ) ? (string) ( $vulnerability['name'] ?? '' ) : '';
335 $html .= '<h4>' . wp_kses( $vuln_name, 'strip' ) . '</h4>';
336 $vuln_closed = is_numeric( $vulnerability['closed'] ?? 0 ) ? (int) ( $vulnerability['closed'] ?? 0 ) : 0;
337 $vuln_unfixed = is_numeric( $vulnerability['unfixed'] ?? 0 ) ? (int) ( $vulnerability['unfixed'] ?? 0 ) : 0;
338 $score_badge = wpvulnerability_render_score_badge( $score, $sev_raw, $epss );
339 $show_active = $kev || 'active' === $exploitation;
340 $show_poc = 'poc' === $exploitation;
341 $show_auto = 'yes' === $automatable;
342 if ( $show_active || $show_poc || $show_auto || '' !== $score_badge ) {
343 $html .= '<div style="display:flex; align-items:center; gap:6px; flex-wrap:wrap; margin-bottom:5px;">';
344 if ( $show_active ) {
345 $html .= '<span class="wpvuln-kev-label">&#9888; ' . esc_html__( 'Actively exploited', 'wpvulnerability' );
346 if ( $kev && null !== $kev_date ) {
347 $html .= ' &middot; ' . esc_html( $kev_date );
348 }
349 $html .= '</span>';
350 }
351 if ( $show_poc ) {
352 $html .= '<span class="wpvuln-poc-label">&#9889; ' . esc_html__( 'Public exploit', 'wpvulnerability' ) . '</span>';
353 }
354 if ( $show_auto ) {
355 $html .= '<span class="wpvuln-auto-label">&#9881; ' . esc_html__( 'Automatable', 'wpvulnerability' ) . '</span>';
356 }
357 if ( '' !== $score_badge ) {
358 $html .= $score_badge;
359 }
360 $html .= '</div>';
361 }
362 if ( null !== $description ) {
363 $html .= '<div style="padding-bottom: 5px;">' . esc_html( $description ) . '</div>';
364 }
365 if ( $vuln_closed || $vuln_unfixed ) {
366 $html .= '<div style="padding-bottom: 5px;">';
367 if ( $vuln_closed ) {
368 $html .= '<div class="text-red">' . esc_html__( 'This plugin is closed. Please replace it with another.', 'wpvulnerability' ) . '</div>';
369 }
370 if ( $vuln_unfixed ) {
371 $html .= '<div class="text-red">' . esc_html__( 'This vulnerability appears to be unpatched. Stay tuned for upcoming plugin updates.', 'wpvulnerability' ) . '</div>';
372 }
373 $html .= '</div>';
374 }
375
376 if ( count( $what ) ) {
377 $html .= '<div style="padding-bottom: 5px;">' . implode( '', $what ) . '</div>';
378 }
379
380 if ( '' !== $source ) {
381 $html .= '<div class="wpvuln-refs-row"><span class="wpvuln-refs-label">' . esc_html__( 'References:', 'wpvulnerability' ) . '</span>';
382 $html .= $source;
383 $html .= '</div>';
384 }
385 }
386 } elseif ( 'core' === $type ) {
387 foreach ( $vulnerabilities as $vulnerability ) {
388 if ( ! is_array( $vulnerability ) ) {
389 continue; }
390 $vuln_impact_raw = $vulnerability['impact'] ?? null;
391 $vuln_impact = is_array( $vuln_impact_raw ) ? $vuln_impact_raw : array();
392 $vuln_cvss_raw = $vuln_impact['cvss'] ?? null;
393 $vuln_cvss = is_array( $vuln_cvss_raw ) ? $vuln_cvss_raw : array();
394 $vuln_cvss2_raw = $vuln_impact['cvss2'] ?? null;
395 $vuln_cvss2 = is_array( $vuln_cvss2_raw ) ? $vuln_cvss2_raw : array();
396 $vuln_cvss3_raw = $vuln_impact['cvss3'] ?? null;
397 $vuln_cvss3 = is_array( $vuln_cvss3_raw ) ? $vuln_cvss3_raw : array();
398 $vuln_cvss4_raw = $vuln_impact['cvss4'] ?? null;
399 $vuln_cvss4 = is_array( $vuln_cvss4_raw ) ? $vuln_cvss4_raw : array();
400 $vuln_ssvc_raw = $vuln_impact['ssvc'] ?? null;
401 $vuln_ssvc = is_array( $vuln_ssvc_raw ) ? $vuln_ssvc_raw : array();
402 $vuln_cwe_raw = $vuln_impact['cwe'] ?? null;
403 $vuln_cwe = is_array( $vuln_cwe_raw ) ? $vuln_cwe_raw : array();
404 $vuln_src_raw = $vulnerability['source'] ?? null;
405 $vuln_sources = is_array( $vuln_src_raw ) ? $vuln_src_raw : array();
406
407 $kev = ( isset( $vuln_ssvc['kev'] ) && true === $vuln_ssvc['kev'] );
408 $exploitation = isset( $vuln_ssvc['exploitation'] ) && is_string( $vuln_ssvc['exploitation'] ) ? $vuln_ssvc['exploitation'] : '';
409 $automatable = isset( $vuln_ssvc['automatable'] ) && is_string( $vuln_ssvc['automatable'] ) ? $vuln_ssvc['automatable'] : '';
410 $kev_date_raw = $vuln_ssvc['kev_date'] ?? null;
411 $kev_date = is_string( $kev_date_raw ) && '' !== $kev_date_raw ? $kev_date_raw : null;
412 $epss_raw = $vuln_impact['epss'] ?? null;
413 $epss = is_numeric( $epss_raw ) ? (float) $epss_raw : null;
414 $description = wpvulnerability_get_source_description( $vuln_sources );
415
416 $what = array();
417 foreach ( $vuln_cwe as $vulnerability_cwe ) {
418 if ( ! is_array( $vulnerability_cwe ) ) {
419 continue; }
420 $cwe_name = is_scalar( $vulnerability_cwe['name'] ?? '' ) ? (string) ( $vulnerability_cwe['name'] ?? '' ) : '';
421 $cwe_desc = is_scalar( $vulnerability_cwe['description'] ?? '' ) ? (string) ( $vulnerability_cwe['description'] ?? '' ) : '';
422 $what[] = '<div><b>' . wp_kses( $cwe_name, 'strip' ) . '</b></div><div><i>' . esc_html( $cwe_desc ) . '</i></div>';
423 }
424
425 $source = wpvulnerability_render_source_pills( $vuln_sources );
426
427 // Best available CVSS score and severity: cvss4 > cvss3 > cvss2 > legacy cvss.
428 $score = null;
429 $sev_raw = null;
430 foreach ( array( $vuln_cvss4, $vuln_cvss3, $vuln_cvss2, $vuln_cvss ) as $cvss_c ) {
431 if ( empty( $cvss_c ) ) {
432 continue; }
433 $s_raw = $cvss_c['score'] ?? null;
434 $v_raw = $cvss_c['severity'] ?? null;
435 $s = is_numeric( $s_raw ) ? number_format( (float) $s_raw, 1, '.', '' ) : null;
436 $v = is_string( $v_raw ) && '' !== $v_raw ? $v_raw : null;
437 if ( null !== $s || null !== $v ) {
438 $score = $s;
439 $sev_raw = $v;
440 break;
441 }
442 }
443
444 $vuln_name = is_scalar( $vulnerability['name'] ?? '' ) ? (string) ( $vulnerability['name'] ?? '' ) : '';
445 $html .= '<h3>' . wpvulnerability_component_icon_html( 'core' ) . ' WordPress ' . wp_kses( $vuln_name, 'strip' ) . '</h3>';
446 $score_badge = wpvulnerability_render_score_badge( $score, $sev_raw, $epss );
447 $show_active = $kev || 'active' === $exploitation;
448 $show_poc = 'poc' === $exploitation;
449 $show_auto = 'yes' === $automatable;
450 if ( $show_active || $show_poc || $show_auto || '' !== $score_badge ) {
451 $html .= '<div style="display:flex; align-items:center; gap:6px; flex-wrap:wrap; margin-bottom:5px;">';
452 if ( $show_active ) {
453 $html .= '<span class="wpvuln-kev-label">&#9888; ' . esc_html__( 'Actively exploited', 'wpvulnerability' );
454 if ( $kev && null !== $kev_date ) {
455 $html .= ' &middot; ' . esc_html( $kev_date );
456 }
457 $html .= '</span>';
458 }
459 if ( $show_poc ) {
460 $html .= '<span class="wpvuln-poc-label">&#9889; ' . esc_html__( 'Public exploit', 'wpvulnerability' ) . '</span>';
461 }
462 if ( $show_auto ) {
463 $html .= '<span class="wpvuln-auto-label">&#9881; ' . esc_html__( 'Automatable', 'wpvulnerability' ) . '</span>';
464 }
465 if ( '' !== $score_badge ) {
466 $html .= $score_badge;
467 }
468 $html .= '</div>';
469 }
470 if ( null !== $description ) {
471 $html .= '<div style="padding-bottom: 5px;">' . esc_html( $description ) . '</div>';
472 }
473
474 if ( count( $what ) ) {
475 $html .= '<div style="padding-bottom: 5px;">' . implode( '', $what ) . '</div>';
476 }
477
478 if ( '' !== $source ) {
479 $html .= '<div class="wpvuln-refs-row"><span class="wpvuln-refs-label">' . esc_html__( 'References:', 'wpvulnerability' ) . '</span>';
480 $html .= $source;
481 $html .= '</div>';
482 }
483 }
484 } elseif ( in_array( $type, array( 'php', 'apache', 'nginx', 'mariadb', 'mysql', 'imagemagick', 'curl', 'memcached', 'redis', 'sqlite' ), true ) ) {
485 foreach ( $vulnerabilities as $vulnerability ) {
486 if ( ! is_array( $vulnerability ) ) {
487 continue; }
488 $vuln_impact_raw = $vulnerability['impact'] ?? null;
489 $vuln_impact = is_array( $vuln_impact_raw ) ? $vuln_impact_raw : array();
490 $vuln_cvss2_raw = $vuln_impact['cvss2'] ?? null;
491 $vuln_cvss2 = is_array( $vuln_cvss2_raw ) ? $vuln_cvss2_raw : array();
492 $vuln_cvss3_raw = $vuln_impact['cvss3'] ?? null;
493 $vuln_cvss3 = is_array( $vuln_cvss3_raw ) ? $vuln_cvss3_raw : array();
494 $vuln_cvss4_raw = $vuln_impact['cvss4'] ?? null;
495 $vuln_cvss4 = is_array( $vuln_cvss4_raw ) ? $vuln_cvss4_raw : array();
496 $vuln_cwe_raw = $vuln_impact['cwe'] ?? null;
497 $vuln_cwe = is_array( $vuln_cwe_raw ) ? $vuln_cwe_raw : array();
498 $vuln_src_raw = $vulnerability['source'] ?? null;
499 $vuln_sources = is_array( $vuln_src_raw ) ? $vuln_src_raw : array();
500
501 // For software endpoints, kev is at impact.kev (not inside ssvc).
502 $kev = ( isset( $vuln_impact['kev'] ) && true === $vuln_impact['kev'] );
503 $description = wpvulnerability_get_source_description( $vuln_sources );
504
505 $what = array();
506 foreach ( $vuln_cwe as $vulnerability_cwe ) {
507 if ( ! is_array( $vulnerability_cwe ) ) {
508 continue; }
509 $cwe_name = is_scalar( $vulnerability_cwe['name'] ?? '' ) ? (string) ( $vulnerability_cwe['name'] ?? '' ) : '';
510 $cwe_desc = is_scalar( $vulnerability_cwe['description'] ?? '' ) ? (string) ( $vulnerability_cwe['description'] ?? '' ) : '';
511 $what[] = '<div><b>' . wp_kses( $cwe_name, 'strip' ) . '</b></div><div><i>' . esc_html( $cwe_desc ) . '</i></div>';
512 }
513
514 $source = wpvulnerability_render_source_pills( $vuln_sources );
515
516 // Best available CVSS score and severity: cvss4 > cvss3 > cvss2.
517 $score = null;
518 $sev_raw = null;
519 foreach ( array( $vuln_cvss4, $vuln_cvss3, $vuln_cvss2 ) as $cvss_c ) {
520 if ( empty( $cvss_c ) ) {
521 continue; }
522 $s_raw = $cvss_c['score'] ?? null;
523 $v_raw = $cvss_c['severity'] ?? null;
524 $s = is_numeric( $s_raw ) ? number_format( (float) $s_raw, 1, '.', '' ) : null;
525 $v = is_string( $v_raw ) && '' !== $v_raw ? $v_raw : null;
526 if ( null !== $s || null !== $v ) {
527 $score = $s;
528 $sev_raw = $v;
529 break;
530 }
531 }
532
533 $vuln_name = is_scalar( $vulnerability['name'] ?? '' ) ? (string) ( $vulnerability['name'] ?? '' ) : '';
534 $html .= '<h4>' . wp_kses( $vuln_name, 'strip' ) . '</h4>';
535 $score_badge = wpvulnerability_render_score_badge( $score, $sev_raw );
536 if ( $kev || '' !== $score_badge ) {
537 $html .= '<div style="display:flex; align-items:center; gap:6px; flex-wrap:wrap; margin-bottom:5px;">';
538 if ( $kev ) {
539 $html .= '<span class="wpvuln-kev-label">&#9888; ' . esc_html__( 'Actively exploited', 'wpvulnerability' ) . '</span>';
540 }
541 if ( '' !== $score_badge ) {
542 $html .= $score_badge;
543 }
544 $html .= '</div>';
545 }
546 if ( null !== $description ) {
547 $html .= '<div style="padding-bottom: 5px;">' . esc_html( $description ) . '</div>';
548 }
549 if ( count( $what ) ) {
550 $html .= '<div style="padding-bottom: 5px;">' . implode( '', $what ) . '</div>';
551 }
552
553 if ( '' !== $source ) {
554 $html .= '<div class="wpvuln-refs-row"><span class="wpvuln-refs-label">' . esc_html__( 'References:', 'wpvulnerability' ) . '</span>';
555 $html .= $source;
556 $html .= '</div>';
557 }
558 }
559 }
560
561 return $html;
562 }
563
564 /**
565 * Convert vulnerabilities into HTML format.
566 *
567 * @since 3.5.0
568 *
569 * @param string $type Type of software (php, apache, nginx, mariadb, mysql, imagemagick, curl).
570 * @return string|false The HTML output if vulnerabilities were found, false otherwise.
571 */
572 function wpvulnerability_html_software( $type ) {
573 $html = '';
574 $found = false;
575 $software_name = null;
576
577 // Map software types to their names.
578 $software_names = array(
579 'php' => 'PHP',
580 'apache' => 'Apache HTTP',
581 'nginx' => 'Nginx',
582 'mariadb' => 'MariaDB',
583 'mysql' => 'MySQL',
584 'imagemagick' => 'ImageMagick',
585 'curl' => 'curl',
586 'memcached' => 'memcached',
587 'redis' => 'redis',
588 'sqlite' => 'sqlite',
589 );
590
591 // Check if the type is valid and get the software name.
592 if ( isset( $software_names[ $type ] ) ) {
593 $software_name = $software_names[ $type ];
594 } else {
595 return false; // Invalid type.
596 }
597
598 $version = wpvulnerability_sanitize_and_validate_version( wpvulnerability_get_software_version( $type ) );
599 $software_data = wpvulnerability_software_get_vulnerabilities( $type );
600 $vulnerabilities = array();
601
602 if ( is_array( $software_data ) && isset( $software_data['vulnerabilities'] ) && is_array( $software_data['vulnerabilities'] ) ) {
603 $vulnerabilities = $software_data['vulnerabilities'];
604 }
605
606 // Check if vulnerabilities were found.
607 if ( 0 < count( $vulnerabilities ) ) {
608 $found = true;
609
610 // translators: %s: software name.
611 $html .= '<h3>' . wpvulnerability_component_icon_html( $type ) . sprintf( esc_html__( '%s running', 'wpvulnerability' ), esc_html( $software_name ) ) . ': ' . wp_kses( (string) $version, 'strip' ) . '</h3>';
612
613 // Show lifecycle status if available.
614 $lifecycle = isset( $software_data['lifecycle'] ) && is_array( $software_data['lifecycle'] ) ? $software_data['lifecycle'] : array();
615 $lc_status = is_scalar( $lifecycle['status'] ?? '' ) ? (string) ( $lifecycle['status'] ?? '' ) : '';
616 $lc_date_end = is_scalar( $lifecycle['date_end'] ?? '' ) ? (string) ( $lifecycle['date_end'] ?? '' ) : '';
617
618 if ( 'e' === $lc_status || 's' === $lc_status ) {
619 $html .= '<div style="padding: 4px 0 8px;">';
620 if ( 'e' === $lc_status ) {
621 $html .= '<span class="text-red">&#9679; ' . esc_html__( 'End of Life', 'wpvulnerability' ) . '</span>';
622 if ( '' !== $lc_date_end ) {
623 $html .= ' &mdash; ' . esc_html__( 'End of life:', 'wpvulnerability' ) . ' <strong>' . esc_html( $lc_date_end ) . '</strong>';
624 }
625 } else {
626 $html .= '<span class="text-green">&#9679; ' . esc_html__( 'Supported', 'wpvulnerability' ) . '</span>';
627 if ( '' !== $lc_date_end ) {
628 $html .= ' &mdash; ' . esc_html__( 'End of life:', 'wpvulnerability' ) . ' ' . esc_html( $lc_date_end );
629 }
630 }
631 $html .= '</div>';
632 }
633
634 $html .= wpvulnerability_html( $type, $vulnerabilities );
635 }
636
637 return $found ? $html : false;
638 }
639
640 /**
641 * Convert plugin vulnerabilities into HTML format.
642 *
643 * @since 2.0.0
644 *
645 * @return string|false The HTML output if plugin vulnerabilities were found, false otherwise.
646 */
647 function wpvulnerability_html_plugins() {
648 $html = '';
649 $found = false;
650
651 $plugins = wpvulnerability_plugin_get_vulnerabilities();
652
653 foreach ( $plugins as $file_path => $plugin_data ) {
654 if ( ! is_array( $plugin_data ) ) {
655 continue; }
656 // Check if the plugin is marked as vulnerable.
657 if ( isset( $plugin_data['vulnerable'] ) && 1 === $plugin_data['vulnerable'] ) {
658 $found = true;
659
660 // Generate HTML markup for the plugin vulnerability.
661 $plugin_name = is_scalar( $plugin_data['Name'] ?? '' ) ? (string) ( $plugin_data['Name'] ?? '' ) : '';
662 $html .= '<h3>' . wpvulnerability_component_icon_html( 'plugin' ) . esc_html__( 'Plugin', 'wpvulnerability' ) . ': ' . wp_kses( $plugin_name, 'strip' ) . '</h3>';
663 $plugin_vulns = isset( $plugin_data['vulnerabilities'] ) && is_array( $plugin_data['vulnerabilities'] ) ? $plugin_data['vulnerabilities'] : array();
664 $html .= wpvulnerability_html( 'plugin', $plugin_vulns );
665 }
666 }
667
668 // Return the HTML if vulnerabilities were found.
669 return $found ? $html : false;
670 }
671
672 /**
673 * Convert plugin vulnerabilities into list format.
674 *
675 * @since 2.2.0
676 *
677 * @return string|false The HTML output if plugin vulnerabilities were found, false otherwise.
678 */
679 function wpvulnerability_list_plugins() {
680 $html = '<ul class="inside">';
681 $found = false;
682
683 // Get vulnerabilities data for plugins.
684 $plugins = wpvulnerability_plugin_get_vulnerabilities();
685
686 // Iterate through each plugin's data.
687 foreach ( $plugins as $file_path => $plugin_data ) {
688 if ( ! is_array( $plugin_data ) ) {
689 continue; }
690 // Check if the plugin is marked as vulnerable.
691 if ( isset( $plugin_data['vulnerable'] ) && 1 === $plugin_data['vulnerable'] ) {
692 $found = true;
693
694 // Generate HTML markup for the plugin vulnerability.
695 $plugin_name = is_scalar( $plugin_data['Name'] ?? '' ) ? (string) ( $plugin_data['Name'] ?? '' ) : '';
696 $html .= '<li>' . wp_kses( $plugin_name, 'strip' ) . '</li>';
697 }
698 }
699
700 $html .= '</ul>';
701
702 // Return the HTML if vulnerabilities were found.
703 return $found ? $html : false;
704 }
705
706 /**
707 * Convert theme vulnerabilities into HTML format.
708 *
709 * @since 2.0.0
710 *
711 * @return string|false The HTML output if theme vulnerabilities were found, false otherwise.
712 */
713 function wpvulnerability_html_themes() {
714 $html = '';
715 $found = false;
716
717 // Get vulnerabilities data for themes.
718 $themes = wpvulnerability_theme_get_vulnerabilities();
719
720 // Iterate through each theme's data.
721 foreach ( $themes as $theme_data ) {
722 if ( ! is_array( $theme_data ) ) {
723 continue; }
724 $td_wpv = isset( $theme_data['wpvulnerability'] ) && is_array( $theme_data['wpvulnerability'] ) ? $theme_data['wpvulnerability'] : array();
725 // Check if the theme is marked as vulnerable.
726 if ( isset( $td_wpv['vulnerable'] ) && 1 === ( is_numeric( $td_wpv['vulnerable'] ) ? (int) $td_wpv['vulnerable'] : 0 ) ) {
727 $found = true;
728
729 // Generate HTML markup for the theme vulnerability.
730 $theme_name = is_scalar( $td_wpv['name'] ?? '' ) ? (string) ( $td_wpv['name'] ?? '' ) : '';
731 $html .= '<h3>' . wpvulnerability_component_icon_html( 'theme' ) . esc_html__( 'Theme', 'wpvulnerability' ) . ': ' . wp_kses( $theme_name, 'strip' ) . '</h3>';
732 $vuln_list = isset( $td_wpv['vulnerabilities'] ) && is_array( $td_wpv['vulnerabilities'] ) ? $td_wpv['vulnerabilities'] : array();
733 $html .= wpvulnerability_html( 'theme', $vuln_list );
734 }
735 }
736
737 // Return the HTML if vulnerabilities were found.
738 return $found ? $html : false;
739 }
740
741 /**
742 * Convert theme vulnerabilities into list format.
743 *
744 * @since 2.2.0
745 *
746 * @return string|false The HTML output if theme vulnerabilities were found, false otherwise.
747 */
748 function wpvulnerability_list_themes() {
749 $html = '<ul class="inside">';
750 $found = false;
751
752 // Get vulnerabilities data for themes.
753 $themes = wpvulnerability_theme_get_vulnerabilities();
754
755 // Iterate through each theme's data.
756 foreach ( $themes as $theme_data ) {
757 if ( ! is_array( $theme_data ) ) {
758 continue; }
759 $td_wpv = isset( $theme_data['wpvulnerability'] ) && is_array( $theme_data['wpvulnerability'] ) ? $theme_data['wpvulnerability'] : array();
760 // Check if the theme is marked as vulnerable.
761 if ( isset( $td_wpv['vulnerable'] ) && 1 === ( is_numeric( $td_wpv['vulnerable'] ) ? (int) $td_wpv['vulnerable'] : 0 ) ) {
762 $found = true;
763
764 // Generate HTML markup for the theme vulnerability.
765 $theme_name = is_scalar( $td_wpv['name'] ?? '' ) ? (string) ( $td_wpv['name'] ?? '' ) : '';
766 $html .= '<li>' . wp_kses( $theme_name, 'strip' ) . '</li>';
767 }
768 }
769
770 $html .= '</ul>';
771
772 // Return the HTML if vulnerabilities were found.
773 return $found ? $html : false;
774 }
775
776 /**
777 * Returns an EOL badge HTML span for a software component.
778 *
779 * Reads the cached lifecycle data for the given software type and returns
780 * a styled badge when the component has reached end-of-life status.
781 *
782 * @since 5.0.0
783 *
784 * @param string $type The software type (e.g., 'php', 'apache', 'mariadb').
785 *
786 * @return string HTML badge string, or empty string if not EOL or no data.
787 */
788 function wpvulnerability_eol_badge_html( $type ) {
789 $sw_data = wpvulnerability_software_get_vulnerabilities( $type );
790 $lc = isset( $sw_data['lifecycle'] ) && is_array( $sw_data['lifecycle'] ) ? $sw_data['lifecycle'] : array();
791 $status = is_scalar( $lc['status'] ?? '' ) ? (string) ( $lc['status'] ?? '' ) : '';
792 $date_end = is_scalar( $lc['date_end'] ?? '' ) ? (string) ( $lc['date_end'] ?? '' ) : '';
793
794 if ( 'e' !== $status ) {
795 return '';
796 }
797
798 if ( '' !== $date_end ) {
799 /* translators: %s: end-of-life date */
800 $title = sprintf( __( 'End of life: %s', 'wpvulnerability' ), $date_end );
801 } else {
802 $title = __( 'End of Life', 'wpvulnerability' );
803 }
804
805 return '<span class="wpvuln-badge wpvuln-badge-eol" title="' . esc_attr( $title ) . '">' . esc_html__( 'EOL', 'wpvulnerability' ) . '</span>';
806 }
807
808 /**
809 * Convert core vulnerabilities into HTML format.
810 *
811 * @since 2.0.0
812 *
813 * @return string|false The HTML output if core vulnerabilities were found, false otherwise.
814 */
815 function wpvulnerability_html_core() {
816 $html = '';
817 $found = false;
818
819 // Get vulnerabilities data for WordPress core.
820 $core = wpvulnerability_core_get_vulnerabilities();
821
822 // Check if there are any vulnerabilities.
823 if ( count( $core ) ) {
824 $found = true;
825
826 // Generate HTML markup for the core vulnerabilities.
827 $html .= wpvulnerability_html( 'core', $core );
828 }
829
830 // Return the HTML if vulnerabilities were found.
831 return $found ? $html : false;
832 }
833