PluginProbe
WPVulnerability / 5.1.2
WPVulnerability v5.1.2
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.2, at wpvulnerability-process.php

823 lines 31.1 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 return '<img src="' . esc_url( WPVULNERABILITY_PLUGIN_URL . 'assets/' . $icon_map[ $type ] ) . '" class="wpvuln-component-icon" alt="" />';
251 }
252
253 /**
254 * Convert vulnerabilities into pretty HTML
255 *
256 * @since 2.0.0
257 *
258 * @param string $type Type: core, plugin, theme, php, apache, nginx, mariadb, mysql, imagemagick, curl.
259 * @param array<mixed> $vulnerabilities Vulnerability data.
260 *
261 * @return string The HTML representation of vulnerabilities.
262 */
263 function wpvulnerability_html( $type, $vulnerabilities ) {
264 $html = '';
265
266 if ( in_array( $type, array( 'plugin', 'theme' ), true ) ) {
267 foreach ( $vulnerabilities as $vulnerability ) {
268 if ( ! is_array( $vulnerability ) ) {
269 continue; }
270 $vuln_impact_raw = $vulnerability['impact'] ?? null;
271 $vuln_impact = is_array( $vuln_impact_raw ) ? $vuln_impact_raw : array();
272 $vuln_cvss_raw = $vuln_impact['cvss'] ?? null;
273 $vuln_cvss = is_array( $vuln_cvss_raw ) ? $vuln_cvss_raw : array();
274 $vuln_cvss2_raw = $vuln_impact['cvss2'] ?? null;
275 $vuln_cvss2 = is_array( $vuln_cvss2_raw ) ? $vuln_cvss2_raw : array();
276 $vuln_cvss3_raw = $vuln_impact['cvss3'] ?? null;
277 $vuln_cvss3 = is_array( $vuln_cvss3_raw ) ? $vuln_cvss3_raw : array();
278 $vuln_cvss4_raw = $vuln_impact['cvss4'] ?? null;
279 $vuln_cvss4 = is_array( $vuln_cvss4_raw ) ? $vuln_cvss4_raw : array();
280 $vuln_ssvc_raw = $vuln_impact['ssvc'] ?? null;
281 $vuln_ssvc = is_array( $vuln_ssvc_raw ) ? $vuln_ssvc_raw : array();
282 $vuln_cwe_raw = $vuln_impact['cwe'] ?? null;
283 $vuln_cwe = is_array( $vuln_cwe_raw ) ? $vuln_cwe_raw : array();
284 $vuln_src_raw = $vulnerability['source'] ?? null;
285 $vuln_sources = is_array( $vuln_src_raw ) ? $vuln_src_raw : array();
286
287 $kev = ( isset( $vuln_ssvc['kev'] ) && true === $vuln_ssvc['kev'] );
288 $exploitation = isset( $vuln_ssvc['exploitation'] ) && is_string( $vuln_ssvc['exploitation'] ) ? $vuln_ssvc['exploitation'] : '';
289 $automatable = isset( $vuln_ssvc['automatable'] ) && is_string( $vuln_ssvc['automatable'] ) ? $vuln_ssvc['automatable'] : '';
290 $kev_date_raw = $vuln_ssvc['kev_date'] ?? null;
291 $kev_date = is_string( $kev_date_raw ) && '' !== $kev_date_raw ? $kev_date_raw : null;
292 $epss_raw = $vuln_impact['epss'] ?? null;
293 $epss = is_numeric( $epss_raw ) ? (float) $epss_raw : null;
294 $description = wpvulnerability_get_source_description( $vuln_sources );
295
296 $what = array();
297 foreach ( $vuln_cwe as $vulnerability_cwe ) {
298 if ( ! is_array( $vulnerability_cwe ) ) {
299 continue; }
300 $cwe_name = is_scalar( $vulnerability_cwe['name'] ?? '' ) ? (string) ( $vulnerability_cwe['name'] ?? '' ) : '';
301 $cwe_desc = is_scalar( $vulnerability_cwe['description'] ?? '' ) ? (string) ( $vulnerability_cwe['description'] ?? '' ) : '';
302 $what[] = '<div><b>' . wp_kses( $cwe_name, 'strip' ) . '</b></div><div><i>' . esc_html( $cwe_desc ) . '</i></div>';
303 }
304
305 $source = wpvulnerability_render_source_pills( $vuln_sources );
306
307 // Best available CVSS score and severity: cvss4 > cvss3 > cvss2 > legacy cvss.
308 $score = null;
309 $sev_raw = null;
310 foreach ( array( $vuln_cvss4, $vuln_cvss3, $vuln_cvss2, $vuln_cvss ) as $cvss_c ) {
311 if ( empty( $cvss_c ) ) {
312 continue; }
313 $s_raw = $cvss_c['score'] ?? null;
314 $v_raw = $cvss_c['severity'] ?? null;
315 $s = is_numeric( $s_raw ) ? number_format( (float) $s_raw, 1, '.', '' ) : null;
316 $v = is_string( $v_raw ) && '' !== $v_raw ? $v_raw : null;
317 if ( null !== $s || null !== $v ) {
318 $score = $s;
319 $sev_raw = $v;
320 break;
321 }
322 }
323
324 $vuln_name = is_scalar( $vulnerability['name'] ?? '' ) ? (string) ( $vulnerability['name'] ?? '' ) : '';
325 $html .= '<h4>' . wp_kses( $vuln_name, 'strip' ) . '</h4>';
326 $vuln_closed = is_numeric( $vulnerability['closed'] ?? 0 ) ? (int) ( $vulnerability['closed'] ?? 0 ) : 0;
327 $vuln_unfixed = is_numeric( $vulnerability['unfixed'] ?? 0 ) ? (int) ( $vulnerability['unfixed'] ?? 0 ) : 0;
328 $score_badge = wpvulnerability_render_score_badge( $score, $sev_raw, $epss );
329 $show_active = $kev || 'active' === $exploitation;
330 $show_poc = 'poc' === $exploitation;
331 $show_auto = 'yes' === $automatable;
332 if ( $show_active || $show_poc || $show_auto || '' !== $score_badge ) {
333 $html .= '<div style="display:flex; align-items:center; gap:6px; flex-wrap:wrap; margin-bottom:5px;">';
334 if ( $show_active ) {
335 $html .= '<span class="wpvuln-kev-label">&#9888; ' . esc_html__( 'Actively exploited', 'wpvulnerability' );
336 if ( $kev && null !== $kev_date ) {
337 $html .= ' &middot; ' . esc_html( $kev_date );
338 }
339 $html .= '</span>';
340 }
341 if ( $show_poc ) {
342 $html .= '<span class="wpvuln-poc-label">&#9889; ' . esc_html__( 'Public exploit', 'wpvulnerability' ) . '</span>';
343 }
344 if ( $show_auto ) {
345 $html .= '<span class="wpvuln-auto-label">&#9881; ' . esc_html__( 'Automatable', 'wpvulnerability' ) . '</span>';
346 }
347 if ( '' !== $score_badge ) {
348 $html .= $score_badge;
349 }
350 $html .= '</div>';
351 }
352 if ( null !== $description ) {
353 $html .= '<div style="padding-bottom: 5px;">' . esc_html( $description ) . '</div>';
354 }
355 if ( $vuln_closed || $vuln_unfixed ) {
356 $html .= '<div style="padding-bottom: 5px;">';
357 if ( $vuln_closed ) {
358 $html .= '<div class="text-red">' . esc_html__( 'This plugin is closed. Please replace it with another.', 'wpvulnerability' ) . '</div>';
359 }
360 if ( $vuln_unfixed ) {
361 $html .= '<div class="text-red">' . esc_html__( 'This vulnerability appears to be unpatched. Stay tuned for upcoming plugin updates.', 'wpvulnerability' ) . '</div>';
362 }
363 $html .= '</div>';
364 }
365
366 if ( count( $what ) ) {
367 $html .= '<div style="padding-bottom: 5px;">' . implode( '', $what ) . '</div>';
368 }
369
370 if ( '' !== $source ) {
371 $html .= '<div class="wpvuln-refs-row"><span class="wpvuln-refs-label">' . esc_html__( 'References:', 'wpvulnerability' ) . '</span>';
372 $html .= $source;
373 $html .= '</div>';
374 }
375 }
376 } elseif ( 'core' === $type ) {
377 foreach ( $vulnerabilities as $vulnerability ) {
378 if ( ! is_array( $vulnerability ) ) {
379 continue; }
380 $vuln_impact_raw = $vulnerability['impact'] ?? null;
381 $vuln_impact = is_array( $vuln_impact_raw ) ? $vuln_impact_raw : array();
382 $vuln_cvss_raw = $vuln_impact['cvss'] ?? null;
383 $vuln_cvss = is_array( $vuln_cvss_raw ) ? $vuln_cvss_raw : array();
384 $vuln_cvss2_raw = $vuln_impact['cvss2'] ?? null;
385 $vuln_cvss2 = is_array( $vuln_cvss2_raw ) ? $vuln_cvss2_raw : array();
386 $vuln_cvss3_raw = $vuln_impact['cvss3'] ?? null;
387 $vuln_cvss3 = is_array( $vuln_cvss3_raw ) ? $vuln_cvss3_raw : array();
388 $vuln_cvss4_raw = $vuln_impact['cvss4'] ?? null;
389 $vuln_cvss4 = is_array( $vuln_cvss4_raw ) ? $vuln_cvss4_raw : array();
390 $vuln_ssvc_raw = $vuln_impact['ssvc'] ?? null;
391 $vuln_ssvc = is_array( $vuln_ssvc_raw ) ? $vuln_ssvc_raw : array();
392 $vuln_cwe_raw = $vuln_impact['cwe'] ?? null;
393 $vuln_cwe = is_array( $vuln_cwe_raw ) ? $vuln_cwe_raw : array();
394 $vuln_src_raw = $vulnerability['source'] ?? null;
395 $vuln_sources = is_array( $vuln_src_raw ) ? $vuln_src_raw : array();
396
397 $kev = ( isset( $vuln_ssvc['kev'] ) && true === $vuln_ssvc['kev'] );
398 $exploitation = isset( $vuln_ssvc['exploitation'] ) && is_string( $vuln_ssvc['exploitation'] ) ? $vuln_ssvc['exploitation'] : '';
399 $automatable = isset( $vuln_ssvc['automatable'] ) && is_string( $vuln_ssvc['automatable'] ) ? $vuln_ssvc['automatable'] : '';
400 $kev_date_raw = $vuln_ssvc['kev_date'] ?? null;
401 $kev_date = is_string( $kev_date_raw ) && '' !== $kev_date_raw ? $kev_date_raw : null;
402 $epss_raw = $vuln_impact['epss'] ?? null;
403 $epss = is_numeric( $epss_raw ) ? (float) $epss_raw : null;
404 $description = wpvulnerability_get_source_description( $vuln_sources );
405
406 $what = array();
407 foreach ( $vuln_cwe as $vulnerability_cwe ) {
408 if ( ! is_array( $vulnerability_cwe ) ) {
409 continue; }
410 $cwe_name = is_scalar( $vulnerability_cwe['name'] ?? '' ) ? (string) ( $vulnerability_cwe['name'] ?? '' ) : '';
411 $cwe_desc = is_scalar( $vulnerability_cwe['description'] ?? '' ) ? (string) ( $vulnerability_cwe['description'] ?? '' ) : '';
412 $what[] = '<div><b>' . wp_kses( $cwe_name, 'strip' ) . '</b></div><div><i>' . esc_html( $cwe_desc ) . '</i></div>';
413 }
414
415 $source = wpvulnerability_render_source_pills( $vuln_sources );
416
417 // Best available CVSS score and severity: cvss4 > cvss3 > cvss2 > legacy cvss.
418 $score = null;
419 $sev_raw = null;
420 foreach ( array( $vuln_cvss4, $vuln_cvss3, $vuln_cvss2, $vuln_cvss ) as $cvss_c ) {
421 if ( empty( $cvss_c ) ) {
422 continue; }
423 $s_raw = $cvss_c['score'] ?? null;
424 $v_raw = $cvss_c['severity'] ?? null;
425 $s = is_numeric( $s_raw ) ? number_format( (float) $s_raw, 1, '.', '' ) : null;
426 $v = is_string( $v_raw ) && '' !== $v_raw ? $v_raw : null;
427 if ( null !== $s || null !== $v ) {
428 $score = $s;
429 $sev_raw = $v;
430 break;
431 }
432 }
433
434 $vuln_name = is_scalar( $vulnerability['name'] ?? '' ) ? (string) ( $vulnerability['name'] ?? '' ) : '';
435 $html .= '<h3>' . wpvulnerability_component_icon_html( 'core' ) . ' WordPress ' . wp_kses( $vuln_name, 'strip' ) . '</h3>';
436 $score_badge = wpvulnerability_render_score_badge( $score, $sev_raw, $epss );
437 $show_active = $kev || 'active' === $exploitation;
438 $show_poc = 'poc' === $exploitation;
439 $show_auto = 'yes' === $automatable;
440 if ( $show_active || $show_poc || $show_auto || '' !== $score_badge ) {
441 $html .= '<div style="display:flex; align-items:center; gap:6px; flex-wrap:wrap; margin-bottom:5px;">';
442 if ( $show_active ) {
443 $html .= '<span class="wpvuln-kev-label">&#9888; ' . esc_html__( 'Actively exploited', 'wpvulnerability' );
444 if ( $kev && null !== $kev_date ) {
445 $html .= ' &middot; ' . esc_html( $kev_date );
446 }
447 $html .= '</span>';
448 }
449 if ( $show_poc ) {
450 $html .= '<span class="wpvuln-poc-label">&#9889; ' . esc_html__( 'Public exploit', 'wpvulnerability' ) . '</span>';
451 }
452 if ( $show_auto ) {
453 $html .= '<span class="wpvuln-auto-label">&#9881; ' . esc_html__( 'Automatable', 'wpvulnerability' ) . '</span>';
454 }
455 if ( '' !== $score_badge ) {
456 $html .= $score_badge;
457 }
458 $html .= '</div>';
459 }
460 if ( null !== $description ) {
461 $html .= '<div style="padding-bottom: 5px;">' . esc_html( $description ) . '</div>';
462 }
463
464 if ( count( $what ) ) {
465 $html .= '<div style="padding-bottom: 5px;">' . implode( '', $what ) . '</div>';
466 }
467
468 if ( '' !== $source ) {
469 $html .= '<div class="wpvuln-refs-row"><span class="wpvuln-refs-label">' . esc_html__( 'References:', 'wpvulnerability' ) . '</span>';
470 $html .= $source;
471 $html .= '</div>';
472 }
473 }
474 } elseif ( in_array( $type, array( 'php', 'apache', 'nginx', 'mariadb', 'mysql', 'imagemagick', 'curl', 'memcached', 'redis', 'sqlite' ), true ) ) {
475 foreach ( $vulnerabilities as $vulnerability ) {
476 if ( ! is_array( $vulnerability ) ) {
477 continue; }
478 $vuln_impact_raw = $vulnerability['impact'] ?? null;
479 $vuln_impact = is_array( $vuln_impact_raw ) ? $vuln_impact_raw : array();
480 $vuln_cvss2_raw = $vuln_impact['cvss2'] ?? null;
481 $vuln_cvss2 = is_array( $vuln_cvss2_raw ) ? $vuln_cvss2_raw : array();
482 $vuln_cvss3_raw = $vuln_impact['cvss3'] ?? null;
483 $vuln_cvss3 = is_array( $vuln_cvss3_raw ) ? $vuln_cvss3_raw : array();
484 $vuln_cvss4_raw = $vuln_impact['cvss4'] ?? null;
485 $vuln_cvss4 = is_array( $vuln_cvss4_raw ) ? $vuln_cvss4_raw : array();
486 $vuln_cwe_raw = $vuln_impact['cwe'] ?? null;
487 $vuln_cwe = is_array( $vuln_cwe_raw ) ? $vuln_cwe_raw : array();
488 $vuln_src_raw = $vulnerability['source'] ?? null;
489 $vuln_sources = is_array( $vuln_src_raw ) ? $vuln_src_raw : array();
490
491 // For software endpoints, kev is at impact.kev (not inside ssvc).
492 $kev = ( isset( $vuln_impact['kev'] ) && true === $vuln_impact['kev'] );
493 $description = wpvulnerability_get_source_description( $vuln_sources );
494
495 $what = array();
496 foreach ( $vuln_cwe as $vulnerability_cwe ) {
497 if ( ! is_array( $vulnerability_cwe ) ) {
498 continue; }
499 $cwe_name = is_scalar( $vulnerability_cwe['name'] ?? '' ) ? (string) ( $vulnerability_cwe['name'] ?? '' ) : '';
500 $cwe_desc = is_scalar( $vulnerability_cwe['description'] ?? '' ) ? (string) ( $vulnerability_cwe['description'] ?? '' ) : '';
501 $what[] = '<div><b>' . wp_kses( $cwe_name, 'strip' ) . '</b></div><div><i>' . esc_html( $cwe_desc ) . '</i></div>';
502 }
503
504 $source = wpvulnerability_render_source_pills( $vuln_sources );
505
506 // Best available CVSS score and severity: cvss4 > cvss3 > cvss2.
507 $score = null;
508 $sev_raw = null;
509 foreach ( array( $vuln_cvss4, $vuln_cvss3, $vuln_cvss2 ) as $cvss_c ) {
510 if ( empty( $cvss_c ) ) {
511 continue; }
512 $s_raw = $cvss_c['score'] ?? null;
513 $v_raw = $cvss_c['severity'] ?? null;
514 $s = is_numeric( $s_raw ) ? number_format( (float) $s_raw, 1, '.', '' ) : null;
515 $v = is_string( $v_raw ) && '' !== $v_raw ? $v_raw : null;
516 if ( null !== $s || null !== $v ) {
517 $score = $s;
518 $sev_raw = $v;
519 break;
520 }
521 }
522
523 $vuln_name = is_scalar( $vulnerability['name'] ?? '' ) ? (string) ( $vulnerability['name'] ?? '' ) : '';
524 $html .= '<h4>' . wp_kses( $vuln_name, 'strip' ) . '</h4>';
525 $score_badge = wpvulnerability_render_score_badge( $score, $sev_raw );
526 if ( $kev || '' !== $score_badge ) {
527 $html .= '<div style="display:flex; align-items:center; gap:6px; flex-wrap:wrap; margin-bottom:5px;">';
528 if ( $kev ) {
529 $html .= '<span class="wpvuln-kev-label">&#9888; ' . esc_html__( 'Actively exploited', 'wpvulnerability' ) . '</span>';
530 }
531 if ( '' !== $score_badge ) {
532 $html .= $score_badge;
533 }
534 $html .= '</div>';
535 }
536 if ( null !== $description ) {
537 $html .= '<div style="padding-bottom: 5px;">' . esc_html( $description ) . '</div>';
538 }
539 if ( count( $what ) ) {
540 $html .= '<div style="padding-bottom: 5px;">' . implode( '', $what ) . '</div>';
541 }
542
543 if ( '' !== $source ) {
544 $html .= '<div class="wpvuln-refs-row"><span class="wpvuln-refs-label">' . esc_html__( 'References:', 'wpvulnerability' ) . '</span>';
545 $html .= $source;
546 $html .= '</div>';
547 }
548 }
549 }
550
551 return $html;
552 }
553
554 /**
555 * Convert vulnerabilities into HTML format.
556 *
557 * @since 3.5.0
558 *
559 * @param string $type Type of software (php, apache, nginx, mariadb, mysql, imagemagick, curl).
560 * @return string|false The HTML output if vulnerabilities were found, false otherwise.
561 */
562 function wpvulnerability_html_software( $type ) {
563 $html = '';
564 $found = false;
565 $software_name = null;
566
567 // Map software types to their names.
568 $software_names = array(
569 'php' => 'PHP',
570 'apache' => 'Apache HTTP',
571 'nginx' => 'Nginx',
572 'mariadb' => 'MariaDB',
573 'mysql' => 'MySQL',
574 'imagemagick' => 'ImageMagick',
575 'curl' => 'curl',
576 'memcached' => 'memcached',
577 'redis' => 'redis',
578 'sqlite' => 'sqlite',
579 );
580
581 // Check if the type is valid and get the software name.
582 if ( isset( $software_names[ $type ] ) ) {
583 $software_name = $software_names[ $type ];
584 } else {
585 return false; // Invalid type.
586 }
587
588 $version = wpvulnerability_sanitize_and_validate_version( wpvulnerability_get_software_version( $type ) );
589 $software_data = wpvulnerability_software_get_vulnerabilities( $type );
590 $vulnerabilities = array();
591
592 if ( is_array( $software_data ) && isset( $software_data['vulnerabilities'] ) && is_array( $software_data['vulnerabilities'] ) ) {
593 $vulnerabilities = $software_data['vulnerabilities'];
594 }
595
596 // Check if vulnerabilities were found.
597 if ( 0 < count( $vulnerabilities ) ) {
598 $found = true;
599
600 // translators: %s: software name.
601 $html .= '<h3>' . wpvulnerability_component_icon_html( $type ) . sprintf( esc_html__( '%s running', 'wpvulnerability' ), esc_html( $software_name ) ) . ': ' . wp_kses( (string) $version, 'strip' ) . '</h3>';
602
603 // Show lifecycle status if available.
604 $lifecycle = isset( $software_data['lifecycle'] ) && is_array( $software_data['lifecycle'] ) ? $software_data['lifecycle'] : array();
605 $lc_status = is_scalar( $lifecycle['status'] ?? '' ) ? (string) ( $lifecycle['status'] ?? '' ) : '';
606 $lc_date_end = is_scalar( $lifecycle['date_end'] ?? '' ) ? (string) ( $lifecycle['date_end'] ?? '' ) : '';
607
608 if ( 'e' === $lc_status || 's' === $lc_status ) {
609 $html .= '<div style="padding: 4px 0 8px;">';
610 if ( 'e' === $lc_status ) {
611 $html .= '<span class="text-red">&#9679; ' . esc_html__( 'End of Life', 'wpvulnerability' ) . '</span>';
612 if ( '' !== $lc_date_end ) {
613 $html .= ' &mdash; ' . esc_html__( 'End of life:', 'wpvulnerability' ) . ' <strong>' . esc_html( $lc_date_end ) . '</strong>';
614 }
615 } else {
616 $html .= '<span class="text-green">&#9679; ' . esc_html__( 'Supported', 'wpvulnerability' ) . '</span>';
617 if ( '' !== $lc_date_end ) {
618 $html .= ' &mdash; ' . esc_html__( 'End of life:', 'wpvulnerability' ) . ' ' . esc_html( $lc_date_end );
619 }
620 }
621 $html .= '</div>';
622 }
623
624 $html .= wpvulnerability_html( $type, $vulnerabilities );
625 }
626
627 return $found ? $html : false;
628 }
629
630 /**
631 * Convert plugin vulnerabilities into HTML format.
632 *
633 * @since 2.0.0
634 *
635 * @return string|false The HTML output if plugin vulnerabilities were found, false otherwise.
636 */
637 function wpvulnerability_html_plugins() {
638 $html = '';
639 $found = false;
640
641 $plugins = wpvulnerability_plugin_get_vulnerabilities();
642
643 foreach ( $plugins as $file_path => $plugin_data ) {
644 if ( ! is_array( $plugin_data ) ) {
645 continue; }
646 // Check if the plugin is marked as vulnerable.
647 if ( isset( $plugin_data['vulnerable'] ) && 1 === $plugin_data['vulnerable'] ) {
648 $found = true;
649
650 // Generate HTML markup for the plugin vulnerability.
651 $plugin_name = is_scalar( $plugin_data['Name'] ?? '' ) ? (string) ( $plugin_data['Name'] ?? '' ) : '';
652 $html .= '<h3>' . wpvulnerability_component_icon_html( 'plugin' ) . esc_html__( 'Plugin', 'wpvulnerability' ) . ': ' . wp_kses( $plugin_name, 'strip' ) . '</h3>';
653 $plugin_vulns = isset( $plugin_data['vulnerabilities'] ) && is_array( $plugin_data['vulnerabilities'] ) ? $plugin_data['vulnerabilities'] : array();
654 $html .= wpvulnerability_html( 'plugin', $plugin_vulns );
655 }
656 }
657
658 // Return the HTML if vulnerabilities were found.
659 return $found ? $html : false;
660 }
661
662 /**
663 * Convert plugin vulnerabilities into list format.
664 *
665 * @since 2.2.0
666 *
667 * @return string|false The HTML output if plugin vulnerabilities were found, false otherwise.
668 */
669 function wpvulnerability_list_plugins() {
670 $html = '<ul class="inside">';
671 $found = false;
672
673 // Get vulnerabilities data for plugins.
674 $plugins = wpvulnerability_plugin_get_vulnerabilities();
675
676 // Iterate through each plugin's data.
677 foreach ( $plugins as $file_path => $plugin_data ) {
678 if ( ! is_array( $plugin_data ) ) {
679 continue; }
680 // Check if the plugin is marked as vulnerable.
681 if ( isset( $plugin_data['vulnerable'] ) && 1 === $plugin_data['vulnerable'] ) {
682 $found = true;
683
684 // Generate HTML markup for the plugin vulnerability.
685 $plugin_name = is_scalar( $plugin_data['Name'] ?? '' ) ? (string) ( $plugin_data['Name'] ?? '' ) : '';
686 $html .= '<li>' . wp_kses( $plugin_name, 'strip' ) . '</li>';
687 }
688 }
689
690 $html .= '</ul>';
691
692 // Return the HTML if vulnerabilities were found.
693 return $found ? $html : false;
694 }
695
696 /**
697 * Convert theme vulnerabilities into HTML format.
698 *
699 * @since 2.0.0
700 *
701 * @return string|false The HTML output if theme vulnerabilities were found, false otherwise.
702 */
703 function wpvulnerability_html_themes() {
704 $html = '';
705 $found = false;
706
707 // Get vulnerabilities data for themes.
708 $themes = wpvulnerability_theme_get_vulnerabilities();
709
710 // Iterate through each theme's data.
711 foreach ( $themes as $theme_data ) {
712 if ( ! is_array( $theme_data ) ) {
713 continue; }
714 $td_wpv = isset( $theme_data['wpvulnerability'] ) && is_array( $theme_data['wpvulnerability'] ) ? $theme_data['wpvulnerability'] : array();
715 // Check if the theme is marked as vulnerable.
716 if ( isset( $td_wpv['vulnerable'] ) && 1 === ( is_numeric( $td_wpv['vulnerable'] ) ? (int) $td_wpv['vulnerable'] : 0 ) ) {
717 $found = true;
718
719 // Generate HTML markup for the theme vulnerability.
720 $theme_name = is_scalar( $td_wpv['name'] ?? '' ) ? (string) ( $td_wpv['name'] ?? '' ) : '';
721 $html .= '<h3>' . wpvulnerability_component_icon_html( 'theme' ) . esc_html__( 'Theme', 'wpvulnerability' ) . ': ' . wp_kses( $theme_name, 'strip' ) . '</h3>';
722 $vuln_list = isset( $td_wpv['vulnerabilities'] ) && is_array( $td_wpv['vulnerabilities'] ) ? $td_wpv['vulnerabilities'] : array();
723 $html .= wpvulnerability_html( 'theme', $vuln_list );
724 }
725 }
726
727 // Return the HTML if vulnerabilities were found.
728 return $found ? $html : false;
729 }
730
731 /**
732 * Convert theme vulnerabilities into list format.
733 *
734 * @since 2.2.0
735 *
736 * @return string|false The HTML output if theme vulnerabilities were found, false otherwise.
737 */
738 function wpvulnerability_list_themes() {
739 $html = '<ul class="inside">';
740 $found = false;
741
742 // Get vulnerabilities data for themes.
743 $themes = wpvulnerability_theme_get_vulnerabilities();
744
745 // Iterate through each theme's data.
746 foreach ( $themes as $theme_data ) {
747 if ( ! is_array( $theme_data ) ) {
748 continue; }
749 $td_wpv = isset( $theme_data['wpvulnerability'] ) && is_array( $theme_data['wpvulnerability'] ) ? $theme_data['wpvulnerability'] : array();
750 // Check if the theme is marked as vulnerable.
751 if ( isset( $td_wpv['vulnerable'] ) && 1 === ( is_numeric( $td_wpv['vulnerable'] ) ? (int) $td_wpv['vulnerable'] : 0 ) ) {
752 $found = true;
753
754 // Generate HTML markup for the theme vulnerability.
755 $theme_name = is_scalar( $td_wpv['name'] ?? '' ) ? (string) ( $td_wpv['name'] ?? '' ) : '';
756 $html .= '<li>' . wp_kses( $theme_name, 'strip' ) . '</li>';
757 }
758 }
759
760 $html .= '</ul>';
761
762 // Return the HTML if vulnerabilities were found.
763 return $found ? $html : false;
764 }
765
766 /**
767 * Returns an EOL badge HTML span for a software component.
768 *
769 * Reads the cached lifecycle data for the given software type and returns
770 * a styled badge when the component has reached end-of-life status.
771 *
772 * @since 5.0.0
773 *
774 * @param string $type The software type (e.g., 'php', 'apache', 'mariadb').
775 *
776 * @return string HTML badge string, or empty string if not EOL or no data.
777 */
778 function wpvulnerability_eol_badge_html( $type ) {
779 $sw_data = wpvulnerability_software_get_vulnerabilities( $type );
780 $lc = isset( $sw_data['lifecycle'] ) && is_array( $sw_data['lifecycle'] ) ? $sw_data['lifecycle'] : array();
781 $status = is_scalar( $lc['status'] ?? '' ) ? (string) ( $lc['status'] ?? '' ) : '';
782 $date_end = is_scalar( $lc['date_end'] ?? '' ) ? (string) ( $lc['date_end'] ?? '' ) : '';
783
784 if ( 'e' !== $status ) {
785 return '';
786 }
787
788 if ( '' !== $date_end ) {
789 /* translators: %s: end-of-life date */
790 $title = sprintf( __( 'End of life: %s', 'wpvulnerability' ), $date_end );
791 } else {
792 $title = __( 'End of Life', 'wpvulnerability' );
793 }
794
795 return '<span class="wpvuln-badge wpvuln-badge-eol" title="' . esc_attr( $title ) . '">' . esc_html__( 'EOL', 'wpvulnerability' ) . '</span>';
796 }
797
798 /**
799 * Convert core vulnerabilities into HTML format.
800 *
801 * @since 2.0.0
802 *
803 * @return string|false The HTML output if core vulnerabilities were found, false otherwise.
804 */
805 function wpvulnerability_html_core() {
806 $html = '';
807 $found = false;
808
809 // Get vulnerabilities data for WordPress core.
810 $core = wpvulnerability_core_get_vulnerabilities();
811
812 // Check if there are any vulnerabilities.
813 if ( count( $core ) ) {
814 $found = true;
815
816 // Generate HTML markup for the core vulnerabilities.
817 $html .= wpvulnerability_html( 'core', $core );
818 }
819
820 // Return the HTML if vulnerabilities were found.
821 return $found ? $html : false;
822 }
823