wpvulnerability/wpvulnerability-process.php
2026-06-02 13:41:43 +00:00

822 lines
31 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<?php
/**
* Process functions
*
* @package WPVulnerability
*
* @since 2.0.0
*/
defined( 'ABSPATH' ) || die( 'No script kiddies please!' );
/**
* Map a source hostname (or provider name) to a CSS class slug for pill colouring.
*
* Accepts either a hostname extracted from a source URL (e.g. "wordfence.com",
* "nvd.nist.gov") or a raw provider name as a fallback.
*
* @since 5.0.0
*
* @param string $host Hostname from the source URL, or raw provider name as fallback.
* @return string CSS slug: wordfence|patchstack|cve|euvd|jvn|wpscan|default.
*/
function wpvulnerability_source_css_slug( $host ) {
$lower = strtolower( (string) $host );
if ( false !== strpos( $lower, 'wordfence' ) ) {
return 'wordfence'; }
if ( false !== strpos( $lower, 'patchstack' ) ) {
return 'patchstack'; }
if ( false !== strpos( $lower, 'euvd' ) ) {
return 'euvd'; }
if ( false !== strpos( $lower, 'jvn' ) ) {
return 'jvn'; }
if ( false !== strpos( $lower, 'wpscan' ) ) {
return 'wpscan'; }
if ( false !== strpos( $lower, 'cve' ) || false !== strpos( $lower, 'nvd' ) ) {
return 'cve';
}
return 'default';
}
/**
* Build HTML for source attribution pills.
*
* Each source becomes a linked pill whose label is the hostname extracted from
* the source URL (e.g. "wordfence.com", "nvd.nist.gov"). Falls back to the
* `name` / `id` keys when no URL is present.
*
* @since 5.0.0
*
* @param array<mixed> $sources Array of source objects from the vulnerability API.
* @return string HTML div.wpvuln-source-pills, or empty string if no sources.
*/
function wpvulnerability_render_source_pills( $sources ) {
if ( empty( $sources ) ) {
return '';
}
$pills = array();
foreach ( $sources as $src ) {
if ( ! is_array( $src ) ) {
continue;
}
$link = is_scalar( $src['link'] ?? '' ) ? (string) ( $src['link'] ?? '' ) : '';
// Derive label and CSS slug from the URL hostname; fall back to name/id.
$label = '';
$slug_input = '';
if ( '' !== $link ) {
$parsed = wp_parse_url( $link );
$host = ( is_array( $parsed ) && isset( $parsed['host'] ) ) ? (string) $parsed['host'] : '';
if ( 0 === strpos( $host, 'www.' ) ) {
$host = substr( $host, 4 );
}
if ( '' !== $host ) {
$label = $host;
$slug_input = $host;
}
}
if ( '' === $label ) {
$name = is_scalar( $src['name'] ?? '' ) ? (string) ( $src['name'] ?? '' ) : '';
if ( '' === $name ) {
$name = is_scalar( $src['id'] ?? '' ) ? (string) ( $src['id'] ?? '' ) : '';
}
$label = $name;
$slug_input = $name;
}
if ( '' === $label ) {
continue;
}
$slug = wpvulnerability_source_css_slug( $slug_input );
$cls = esc_attr( 'wpvuln-source-pill wpvuln-source-' . $slug );
$inner = esc_html( $label );
if ( '' !== $link ) {
$pills[] = '<a href="' . esc_url( $link ) . '" class="' . $cls . '" target="_blank" rel="external nofollow noopener noreferrer">' . $inner . '</a>';
} else {
$pills[] = '<span class="' . $cls . '">' . $inner . '</span>';
}
}
if ( empty( $pills ) ) {
return '';
}
return '<div class="wpvuln-source-pills">' . implode( '', $pills ) . '</div>';
}
/**
* Build a colour-coded CVSS score + severity badge, optionally followed by an EPSS badge.
*
* @since 5.0.0
*
* @param string|null $score Formatted CVSS score (e.g. "7.5") or null.
* @param string|null $sev_raw Raw severity string (single-char or full word) or null.
* @param float|null $epss EPSS exploitation probability 01, or null if not available.
* @return string HTML span(s) for score and/or EPSS, or empty string if no data.
*/
function wpvulnerability_render_score_badge( $score, $sev_raw, $epss = null ) {
if ( is_null( $score ) && ( is_null( $sev_raw ) || '' === $sev_raw ) && is_null( $epss ) ) {
return '';
}
$sev_lower = is_string( $sev_raw ) ? strtolower( trim( $sev_raw ) ) : '';
$css_map = array(
'c' => 'critical',
'critical' => 'critical',
'h' => 'high',
'high' => 'high',
'm' => 'medium',
'medium' => 'medium',
'l' => 'low',
'low' => 'low',
'n' => 'none',
'none' => 'none',
);
$css_key = isset( $css_map[ $sev_lower ] ) ? $css_map[ $sev_lower ] : 'none';
$sev_label = ( is_string( $sev_raw ) && '' !== $sev_raw ) ? wpvulnerability_severity( $sev_raw ) : null;
$parts = array();
if ( ! is_null( $score ) ) {
$parts[] = esc_html( $score );
}
if ( ! is_null( $sev_label ) ) {
$parts[] = esc_html( $sev_label );
}
$badge = '';
if ( ! empty( $parts ) ) {
$badge = '<span class="wpvuln-score-badge wpvuln-score-' . esc_attr( $css_key ) . '">' . implode( ' &middot; ', $parts ) . '</span>';
}
if ( null !== $epss ) {
$badge .= '<span class="wpvuln-epss-badge">EPSS&thinsp;' . esc_html( number_format( $epss * 100, 1 ) ) . '%</span>';
}
return $badge;
}
/**
* Extract the best available description from a vulnerability's source array.
*
* Iterates source objects and returns the first non-empty description string,
* stripping any leading language tag (e.g. "[en-US] ") added by the CVE API.
*
* @since 5.0.0
*
* @param array<mixed> $sources Source objects from the vulnerability API.
* @return string|null First non-empty description found, or null if none.
*/
function wpvulnerability_get_source_description( $sources ) {
if ( empty( $sources ) ) {
return null;
}
foreach ( $sources as $src ) {
if ( ! is_array( $src ) ) {
continue;
}
$raw = isset( $src['description'] ) && is_string( $src['description'] ) ? trim( $src['description'] ) : '';
if ( '' === $raw ) {
continue;
}
// Strip leading language tag like "[en-US] " or "[ja] ".
if ( '[' === $raw[0] ) {
$close = strpos( $raw, '] ' );
if ( false !== $close ) {
$raw = substr( $raw, $close + 2 );
}
}
if ( '' !== $raw ) {
return $raw;
}
}
return null;
}
/**
* Clean a raw API version range string for display.
*
* Examples:
* "* - < 1.0.0" → "< 1.0.0"
* "- - < 1.0.0" → "< 1.0.0"
* "- < 1.0.0" → "< 1.0.0" (no space-dash-space separator)
* "1.0.0 - < 2.0" → "≥ 1.0.0 < 2.0"
*
* @since 5.0.0
*
* @param string $versions Raw versions string from the API.
* @return string Cleaned version range for display (HTML-safe).
*/
function wpvulnerability_clean_version_range( $versions ) {
$v = trim( (string) $versions );
if ( '' === $v ) {
return '';
}
// Handle "- < 1.0" / "* < 1.0": leading wildcard without a space-dash-space separator.
if ( 0 === strpos( $v, '- ' ) || 0 === strpos( $v, '* ' ) ) {
return esc_html( ltrim( substr( $v, 2 ) ) );
}
$parts = explode( ' - ', $v, 2 );
if ( 2 === count( $parts ) ) {
$from = trim( $parts[0] );
$to = trim( $parts[1] );
if ( '' === $from || '*' === $from || '-' === $from ) {
return esc_html( $to );
}
return '&ge;&thinsp;' . esc_html( $from ) . ' &ndash; ' . esc_html( $to );
}
return esc_html( $v );
}
/**
* Return an img tag for a component-type icon.
*
* @since 5.0.0
*
* @param string $type Component type: plugin, theme, core, php, apache, nginx, mariadb, mysql, imagemagick, curl, memcached, redis, sqlite.
* @return string HTML img tag with class wpvuln-component-icon, or empty string.
*/
function wpvulnerability_component_icon_html( $type ) {
$icon_map = array(
'plugin' => 'icon-plugin.svg',
'theme' => 'icon-theme.svg',
'core' => 'icon-wordpress.svg',
'php' => 'icon-php.svg',
'apache' => 'icon-apache.svg',
'nginx' => 'icon-nginx.svg',
'mariadb' => 'icon-mariadb.svg',
'mysql' => 'icon-mysql.svg',
'imagemagick' => 'icon-imagemagick.svg',
'curl' => 'icon-curl.svg',
'memcached' => 'icon-memcached.svg',
'redis' => 'icon-redis.svg',
'sqlite' => 'icon-sqlite.svg',
);
if ( ! isset( $icon_map[ $type ] ) ) {
return '';
}
return '<img src="' . esc_url( WPVULNERABILITY_PLUGIN_URL . 'assets/' . $icon_map[ $type ] ) . '" class="wpvuln-component-icon" alt="" />';
}
/**
* Convert vulnerabilities into pretty HTML
*
* @since 2.0.0
*
* @param string $type Type: core, plugin, theme, php, apache, nginx, mariadb, mysql, imagemagick, curl.
* @param array<mixed> $vulnerabilities Vulnerability data.
*
* @return string The HTML representation of vulnerabilities.
*/
function wpvulnerability_html( $type, $vulnerabilities ) {
$html = '';
if ( in_array( $type, array( 'plugin', 'theme' ), true ) ) {
foreach ( $vulnerabilities as $vulnerability ) {
if ( ! is_array( $vulnerability ) ) {
continue; }
$vuln_impact_raw = $vulnerability['impact'] ?? null;
$vuln_impact = is_array( $vuln_impact_raw ) ? $vuln_impact_raw : array();
$vuln_cvss_raw = $vuln_impact['cvss'] ?? null;
$vuln_cvss = is_array( $vuln_cvss_raw ) ? $vuln_cvss_raw : array();
$vuln_cvss2_raw = $vuln_impact['cvss2'] ?? null;
$vuln_cvss2 = is_array( $vuln_cvss2_raw ) ? $vuln_cvss2_raw : array();
$vuln_cvss3_raw = $vuln_impact['cvss3'] ?? null;
$vuln_cvss3 = is_array( $vuln_cvss3_raw ) ? $vuln_cvss3_raw : array();
$vuln_cvss4_raw = $vuln_impact['cvss4'] ?? null;
$vuln_cvss4 = is_array( $vuln_cvss4_raw ) ? $vuln_cvss4_raw : array();
$vuln_ssvc_raw = $vuln_impact['ssvc'] ?? null;
$vuln_ssvc = is_array( $vuln_ssvc_raw ) ? $vuln_ssvc_raw : array();
$vuln_cwe_raw = $vuln_impact['cwe'] ?? null;
$vuln_cwe = is_array( $vuln_cwe_raw ) ? $vuln_cwe_raw : array();
$vuln_src_raw = $vulnerability['source'] ?? null;
$vuln_sources = is_array( $vuln_src_raw ) ? $vuln_src_raw : array();
$kev = ( isset( $vuln_ssvc['kev'] ) && true === $vuln_ssvc['kev'] );
$exploitation = isset( $vuln_ssvc['exploitation'] ) && is_string( $vuln_ssvc['exploitation'] ) ? $vuln_ssvc['exploitation'] : '';
$automatable = isset( $vuln_ssvc['automatable'] ) && is_string( $vuln_ssvc['automatable'] ) ? $vuln_ssvc['automatable'] : '';
$kev_date_raw = $vuln_ssvc['kev_date'] ?? null;
$kev_date = is_string( $kev_date_raw ) && '' !== $kev_date_raw ? $kev_date_raw : null;
$epss_raw = $vuln_impact['epss'] ?? null;
$epss = is_numeric( $epss_raw ) ? (float) $epss_raw : null;
$description = wpvulnerability_get_source_description( $vuln_sources );
$what = array();
foreach ( $vuln_cwe as $vulnerability_cwe ) {
if ( ! is_array( $vulnerability_cwe ) ) {
continue; }
$cwe_name = is_scalar( $vulnerability_cwe['name'] ?? '' ) ? (string) ( $vulnerability_cwe['name'] ?? '' ) : '';
$cwe_desc = is_scalar( $vulnerability_cwe['description'] ?? '' ) ? (string) ( $vulnerability_cwe['description'] ?? '' ) : '';
$what[] = '<div><b>' . wp_kses( $cwe_name, 'strip' ) . '</b></div><div><i>' . esc_html( $cwe_desc ) . '</i></div>';
}
$source = wpvulnerability_render_source_pills( $vuln_sources );
// Best available CVSS score and severity: cvss4 > cvss3 > cvss2 > legacy cvss.
$score = null;
$sev_raw = null;
foreach ( array( $vuln_cvss4, $vuln_cvss3, $vuln_cvss2, $vuln_cvss ) as $cvss_c ) {
if ( empty( $cvss_c ) ) {
continue; }
$s_raw = $cvss_c['score'] ?? null;
$v_raw = $cvss_c['severity'] ?? null;
$s = is_numeric( $s_raw ) ? number_format( (float) $s_raw, 1, '.', '' ) : null;
$v = is_string( $v_raw ) && '' !== $v_raw ? $v_raw : null;
if ( null !== $s || null !== $v ) {
$score = $s;
$sev_raw = $v;
break;
}
}
$vuln_name = is_scalar( $vulnerability['name'] ?? '' ) ? (string) ( $vulnerability['name'] ?? '' ) : '';
$html .= '<h4>' . wp_kses( $vuln_name, 'strip' ) . '</h4>';
$vuln_closed = is_numeric( $vulnerability['closed'] ?? 0 ) ? (int) ( $vulnerability['closed'] ?? 0 ) : 0;
$vuln_unfixed = is_numeric( $vulnerability['unfixed'] ?? 0 ) ? (int) ( $vulnerability['unfixed'] ?? 0 ) : 0;
$score_badge = wpvulnerability_render_score_badge( $score, $sev_raw, $epss );
$show_active = $kev || 'active' === $exploitation;
$show_poc = 'poc' === $exploitation;
$show_auto = 'yes' === $automatable;
if ( $show_active || $show_poc || $show_auto || '' !== $score_badge ) {
$html .= '<div style="display:flex; align-items:center; gap:6px; flex-wrap:wrap; margin-bottom:5px;">';
if ( $show_active ) {
$html .= '<span class="wpvuln-kev-label">&#9888; ' . esc_html__( 'Actively exploited', 'wpvulnerability' );
if ( $kev && null !== $kev_date ) {
$html .= ' &middot; ' . esc_html( $kev_date );
}
$html .= '</span>';
}
if ( $show_poc ) {
$html .= '<span class="wpvuln-poc-label">&#9889; ' . esc_html__( 'Public exploit', 'wpvulnerability' ) . '</span>';
}
if ( $show_auto ) {
$html .= '<span class="wpvuln-auto-label">&#9881; ' . esc_html__( 'Automatable', 'wpvulnerability' ) . '</span>';
}
if ( '' !== $score_badge ) {
$html .= $score_badge;
}
$html .= '</div>';
}
if ( null !== $description ) {
$html .= '<div style="padding-bottom: 5px;">' . esc_html( $description ) . '</div>';
}
if ( $vuln_closed || $vuln_unfixed ) {
$html .= '<div style="padding-bottom: 5px;">';
if ( $vuln_closed ) {
$html .= '<div class="text-red">' . esc_html__( 'This plugin is closed. Please replace it with another.', 'wpvulnerability' ) . '</div>';
}
if ( $vuln_unfixed ) {
$html .= '<div class="text-red">' . esc_html__( 'This vulnerability appears to be unpatched. Stay tuned for upcoming plugin updates.', 'wpvulnerability' ) . '</div>';
}
$html .= '</div>';
}
if ( count( $what ) ) {
$html .= '<div style="padding-bottom: 5px;">' . implode( '', $what ) . '</div>';
}
if ( '' !== $source ) {
$html .= '<div class="wpvuln-refs-row"><span class="wpvuln-refs-label">' . esc_html__( 'References:', 'wpvulnerability' ) . '</span>';
$html .= $source;
$html .= '</div>';
}
}
} elseif ( 'core' === $type ) {
foreach ( $vulnerabilities as $vulnerability ) {
if ( ! is_array( $vulnerability ) ) {
continue; }
$vuln_impact_raw = $vulnerability['impact'] ?? null;
$vuln_impact = is_array( $vuln_impact_raw ) ? $vuln_impact_raw : array();
$vuln_cvss_raw = $vuln_impact['cvss'] ?? null;
$vuln_cvss = is_array( $vuln_cvss_raw ) ? $vuln_cvss_raw : array();
$vuln_cvss2_raw = $vuln_impact['cvss2'] ?? null;
$vuln_cvss2 = is_array( $vuln_cvss2_raw ) ? $vuln_cvss2_raw : array();
$vuln_cvss3_raw = $vuln_impact['cvss3'] ?? null;
$vuln_cvss3 = is_array( $vuln_cvss3_raw ) ? $vuln_cvss3_raw : array();
$vuln_cvss4_raw = $vuln_impact['cvss4'] ?? null;
$vuln_cvss4 = is_array( $vuln_cvss4_raw ) ? $vuln_cvss4_raw : array();
$vuln_ssvc_raw = $vuln_impact['ssvc'] ?? null;
$vuln_ssvc = is_array( $vuln_ssvc_raw ) ? $vuln_ssvc_raw : array();
$vuln_cwe_raw = $vuln_impact['cwe'] ?? null;
$vuln_cwe = is_array( $vuln_cwe_raw ) ? $vuln_cwe_raw : array();
$vuln_src_raw = $vulnerability['source'] ?? null;
$vuln_sources = is_array( $vuln_src_raw ) ? $vuln_src_raw : array();
$kev = ( isset( $vuln_ssvc['kev'] ) && true === $vuln_ssvc['kev'] );
$exploitation = isset( $vuln_ssvc['exploitation'] ) && is_string( $vuln_ssvc['exploitation'] ) ? $vuln_ssvc['exploitation'] : '';
$automatable = isset( $vuln_ssvc['automatable'] ) && is_string( $vuln_ssvc['automatable'] ) ? $vuln_ssvc['automatable'] : '';
$kev_date_raw = $vuln_ssvc['kev_date'] ?? null;
$kev_date = is_string( $kev_date_raw ) && '' !== $kev_date_raw ? $kev_date_raw : null;
$epss_raw = $vuln_impact['epss'] ?? null;
$epss = is_numeric( $epss_raw ) ? (float) $epss_raw : null;
$description = wpvulnerability_get_source_description( $vuln_sources );
$what = array();
foreach ( $vuln_cwe as $vulnerability_cwe ) {
if ( ! is_array( $vulnerability_cwe ) ) {
continue; }
$cwe_name = is_scalar( $vulnerability_cwe['name'] ?? '' ) ? (string) ( $vulnerability_cwe['name'] ?? '' ) : '';
$cwe_desc = is_scalar( $vulnerability_cwe['description'] ?? '' ) ? (string) ( $vulnerability_cwe['description'] ?? '' ) : '';
$what[] = '<div><b>' . wp_kses( $cwe_name, 'strip' ) . '</b></div><div><i>' . esc_html( $cwe_desc ) . '</i></div>';
}
$source = wpvulnerability_render_source_pills( $vuln_sources );
// Best available CVSS score and severity: cvss4 > cvss3 > cvss2 > legacy cvss.
$score = null;
$sev_raw = null;
foreach ( array( $vuln_cvss4, $vuln_cvss3, $vuln_cvss2, $vuln_cvss ) as $cvss_c ) {
if ( empty( $cvss_c ) ) {
continue; }
$s_raw = $cvss_c['score'] ?? null;
$v_raw = $cvss_c['severity'] ?? null;
$s = is_numeric( $s_raw ) ? number_format( (float) $s_raw, 1, '.', '' ) : null;
$v = is_string( $v_raw ) && '' !== $v_raw ? $v_raw : null;
if ( null !== $s || null !== $v ) {
$score = $s;
$sev_raw = $v;
break;
}
}
$vuln_name = is_scalar( $vulnerability['name'] ?? '' ) ? (string) ( $vulnerability['name'] ?? '' ) : '';
$html .= '<h3>' . wpvulnerability_component_icon_html( 'core' ) . ' WordPress ' . wp_kses( $vuln_name, 'strip' ) . '</h3>';
$score_badge = wpvulnerability_render_score_badge( $score, $sev_raw, $epss );
$show_active = $kev || 'active' === $exploitation;
$show_poc = 'poc' === $exploitation;
$show_auto = 'yes' === $automatable;
if ( $show_active || $show_poc || $show_auto || '' !== $score_badge ) {
$html .= '<div style="display:flex; align-items:center; gap:6px; flex-wrap:wrap; margin-bottom:5px;">';
if ( $show_active ) {
$html .= '<span class="wpvuln-kev-label">&#9888; ' . esc_html__( 'Actively exploited', 'wpvulnerability' );
if ( $kev && null !== $kev_date ) {
$html .= ' &middot; ' . esc_html( $kev_date );
}
$html .= '</span>';
}
if ( $show_poc ) {
$html .= '<span class="wpvuln-poc-label">&#9889; ' . esc_html__( 'Public exploit', 'wpvulnerability' ) . '</span>';
}
if ( $show_auto ) {
$html .= '<span class="wpvuln-auto-label">&#9881; ' . esc_html__( 'Automatable', 'wpvulnerability' ) . '</span>';
}
if ( '' !== $score_badge ) {
$html .= $score_badge;
}
$html .= '</div>';
}
if ( null !== $description ) {
$html .= '<div style="padding-bottom: 5px;">' . esc_html( $description ) . '</div>';
}
if ( count( $what ) ) {
$html .= '<div style="padding-bottom: 5px;">' . implode( '', $what ) . '</div>';
}
if ( '' !== $source ) {
$html .= '<div class="wpvuln-refs-row"><span class="wpvuln-refs-label">' . esc_html__( 'References:', 'wpvulnerability' ) . '</span>';
$html .= $source;
$html .= '</div>';
}
}
} elseif ( in_array( $type, array( 'php', 'apache', 'nginx', 'mariadb', 'mysql', 'imagemagick', 'curl', 'memcached', 'redis', 'sqlite' ), true ) ) {
foreach ( $vulnerabilities as $vulnerability ) {
if ( ! is_array( $vulnerability ) ) {
continue; }
$vuln_impact_raw = $vulnerability['impact'] ?? null;
$vuln_impact = is_array( $vuln_impact_raw ) ? $vuln_impact_raw : array();
$vuln_cvss2_raw = $vuln_impact['cvss2'] ?? null;
$vuln_cvss2 = is_array( $vuln_cvss2_raw ) ? $vuln_cvss2_raw : array();
$vuln_cvss3_raw = $vuln_impact['cvss3'] ?? null;
$vuln_cvss3 = is_array( $vuln_cvss3_raw ) ? $vuln_cvss3_raw : array();
$vuln_cvss4_raw = $vuln_impact['cvss4'] ?? null;
$vuln_cvss4 = is_array( $vuln_cvss4_raw ) ? $vuln_cvss4_raw : array();
$vuln_cwe_raw = $vuln_impact['cwe'] ?? null;
$vuln_cwe = is_array( $vuln_cwe_raw ) ? $vuln_cwe_raw : array();
$vuln_src_raw = $vulnerability['source'] ?? null;
$vuln_sources = is_array( $vuln_src_raw ) ? $vuln_src_raw : array();
// For software endpoints, kev is at impact.kev (not inside ssvc).
$kev = ( isset( $vuln_impact['kev'] ) && true === $vuln_impact['kev'] );
$description = wpvulnerability_get_source_description( $vuln_sources );
$what = array();
foreach ( $vuln_cwe as $vulnerability_cwe ) {
if ( ! is_array( $vulnerability_cwe ) ) {
continue; }
$cwe_name = is_scalar( $vulnerability_cwe['name'] ?? '' ) ? (string) ( $vulnerability_cwe['name'] ?? '' ) : '';
$cwe_desc = is_scalar( $vulnerability_cwe['description'] ?? '' ) ? (string) ( $vulnerability_cwe['description'] ?? '' ) : '';
$what[] = '<div><b>' . wp_kses( $cwe_name, 'strip' ) . '</b></div><div><i>' . esc_html( $cwe_desc ) . '</i></div>';
}
$source = wpvulnerability_render_source_pills( $vuln_sources );
// Best available CVSS score and severity: cvss4 > cvss3 > cvss2.
$score = null;
$sev_raw = null;
foreach ( array( $vuln_cvss4, $vuln_cvss3, $vuln_cvss2 ) as $cvss_c ) {
if ( empty( $cvss_c ) ) {
continue; }
$s_raw = $cvss_c['score'] ?? null;
$v_raw = $cvss_c['severity'] ?? null;
$s = is_numeric( $s_raw ) ? number_format( (float) $s_raw, 1, '.', '' ) : null;
$v = is_string( $v_raw ) && '' !== $v_raw ? $v_raw : null;
if ( null !== $s || null !== $v ) {
$score = $s;
$sev_raw = $v;
break;
}
}
$vuln_name = is_scalar( $vulnerability['name'] ?? '' ) ? (string) ( $vulnerability['name'] ?? '' ) : '';
$html .= '<h4>' . wp_kses( $vuln_name, 'strip' ) . '</h4>';
$score_badge = wpvulnerability_render_score_badge( $score, $sev_raw );
if ( $kev || '' !== $score_badge ) {
$html .= '<div style="display:flex; align-items:center; gap:6px; flex-wrap:wrap; margin-bottom:5px;">';
if ( $kev ) {
$html .= '<span class="wpvuln-kev-label">&#9888; ' . esc_html__( 'Actively exploited', 'wpvulnerability' ) . '</span>';
}
if ( '' !== $score_badge ) {
$html .= $score_badge;
}
$html .= '</div>';
}
if ( null !== $description ) {
$html .= '<div style="padding-bottom: 5px;">' . esc_html( $description ) . '</div>';
}
if ( count( $what ) ) {
$html .= '<div style="padding-bottom: 5px;">' . implode( '', $what ) . '</div>';
}
if ( '' !== $source ) {
$html .= '<div class="wpvuln-refs-row"><span class="wpvuln-refs-label">' . esc_html__( 'References:', 'wpvulnerability' ) . '</span>';
$html .= $source;
$html .= '</div>';
}
}
}
return $html;
}
/**
* Convert vulnerabilities into HTML format.
*
* @since 3.5.0
*
* @param string $type Type of software (php, apache, nginx, mariadb, mysql, imagemagick, curl).
* @return string|false The HTML output if vulnerabilities were found, false otherwise.
*/
function wpvulnerability_html_software( $type ) {
$html = '';
$found = false;
$software_name = null;
// Map software types to their names.
$software_names = array(
'php' => 'PHP',
'apache' => 'Apache HTTP',
'nginx' => 'Nginx',
'mariadb' => 'MariaDB',
'mysql' => 'MySQL',
'imagemagick' => 'ImageMagick',
'curl' => 'curl',
'memcached' => 'memcached',
'redis' => 'redis',
'sqlite' => 'sqlite',
);
// Check if the type is valid and get the software name.
if ( isset( $software_names[ $type ] ) ) {
$software_name = $software_names[ $type ];
} else {
return false; // Invalid type.
}
$version = wpvulnerability_sanitize_and_validate_version( wpvulnerability_get_software_version( $type ) );
$software_data = wpvulnerability_software_get_vulnerabilities( $type );
$vulnerabilities = array();
if ( is_array( $software_data ) && isset( $software_data['vulnerabilities'] ) && is_array( $software_data['vulnerabilities'] ) ) {
$vulnerabilities = $software_data['vulnerabilities'];
}
// Check if vulnerabilities were found.
if ( 0 < count( $vulnerabilities ) ) {
$found = true;
// translators: %s: software name.
$html .= '<h3>' . wpvulnerability_component_icon_html( $type ) . sprintf( esc_html__( '%s running', 'wpvulnerability' ), esc_html( $software_name ) ) . ': ' . wp_kses( (string) $version, 'strip' ) . '</h3>';
// Show lifecycle status if available.
$lifecycle = isset( $software_data['lifecycle'] ) && is_array( $software_data['lifecycle'] ) ? $software_data['lifecycle'] : array();
$lc_status = is_scalar( $lifecycle['status'] ?? '' ) ? (string) ( $lifecycle['status'] ?? '' ) : '';
$lc_date_end = is_scalar( $lifecycle['date_end'] ?? '' ) ? (string) ( $lifecycle['date_end'] ?? '' ) : '';
if ( 'e' === $lc_status || 's' === $lc_status ) {
$html .= '<div style="padding: 4px 0 8px;">';
if ( 'e' === $lc_status ) {
$html .= '<span class="text-red">&#9679; ' . esc_html__( 'End of Life', 'wpvulnerability' ) . '</span>';
if ( '' !== $lc_date_end ) {
$html .= ' &mdash; ' . esc_html__( 'End of life:', 'wpvulnerability' ) . ' <strong>' . esc_html( $lc_date_end ) . '</strong>';
}
} else {
$html .= '<span class="text-green">&#9679; ' . esc_html__( 'Supported', 'wpvulnerability' ) . '</span>';
if ( '' !== $lc_date_end ) {
$html .= ' &mdash; ' . esc_html__( 'End of life:', 'wpvulnerability' ) . ' ' . esc_html( $lc_date_end );
}
}
$html .= '</div>';
}
$html .= wpvulnerability_html( $type, $vulnerabilities );
}
return $found ? $html : false;
}
/**
* Convert plugin vulnerabilities into HTML format.
*
* @since 2.0.0
*
* @return string|false The HTML output if plugin vulnerabilities were found, false otherwise.
*/
function wpvulnerability_html_plugins() {
$html = '';
$found = false;
$plugins = wpvulnerability_plugin_get_vulnerabilities();
foreach ( $plugins as $file_path => $plugin_data ) {
if ( ! is_array( $plugin_data ) ) {
continue; }
// Check if the plugin is marked as vulnerable.
if ( isset( $plugin_data['vulnerable'] ) && 1 === $plugin_data['vulnerable'] ) {
$found = true;
// Generate HTML markup for the plugin vulnerability.
$plugin_name = is_scalar( $plugin_data['Name'] ?? '' ) ? (string) ( $plugin_data['Name'] ?? '' ) : '';
$html .= '<h3>' . wpvulnerability_component_icon_html( 'plugin' ) . esc_html__( 'Plugin', 'wpvulnerability' ) . ': ' . wp_kses( $plugin_name, 'strip' ) . '</h3>';
$plugin_vulns = isset( $plugin_data['vulnerabilities'] ) && is_array( $plugin_data['vulnerabilities'] ) ? $plugin_data['vulnerabilities'] : array();
$html .= wpvulnerability_html( 'plugin', $plugin_vulns );
}
}
// Return the HTML if vulnerabilities were found.
return $found ? $html : false;
}
/**
* Convert plugin vulnerabilities into list format.
*
* @since 2.2.0
*
* @return string|false The HTML output if plugin vulnerabilities were found, false otherwise.
*/
function wpvulnerability_list_plugins() {
$html = '<ul class="inside">';
$found = false;
// Get vulnerabilities data for plugins.
$plugins = wpvulnerability_plugin_get_vulnerabilities();
// Iterate through each plugin's data.
foreach ( $plugins as $file_path => $plugin_data ) {
if ( ! is_array( $plugin_data ) ) {
continue; }
// Check if the plugin is marked as vulnerable.
if ( isset( $plugin_data['vulnerable'] ) && 1 === $plugin_data['vulnerable'] ) {
$found = true;
// Generate HTML markup for the plugin vulnerability.
$plugin_name = is_scalar( $plugin_data['Name'] ?? '' ) ? (string) ( $plugin_data['Name'] ?? '' ) : '';
$html .= '<li>' . wp_kses( $plugin_name, 'strip' ) . '</li>';
}
}
$html .= '</ul>';
// Return the HTML if vulnerabilities were found.
return $found ? $html : false;
}
/**
* Convert theme vulnerabilities into HTML format.
*
* @since 2.0.0
*
* @return string|false The HTML output if theme vulnerabilities were found, false otherwise.
*/
function wpvulnerability_html_themes() {
$html = '';
$found = false;
// Get vulnerabilities data for themes.
$themes = wpvulnerability_theme_get_vulnerabilities();
// Iterate through each theme's data.
foreach ( $themes as $theme_data ) {
if ( ! is_array( $theme_data ) ) {
continue; }
$td_wpv = isset( $theme_data['wpvulnerability'] ) && is_array( $theme_data['wpvulnerability'] ) ? $theme_data['wpvulnerability'] : array();
// Check if the theme is marked as vulnerable.
if ( isset( $td_wpv['vulnerable'] ) && 1 === ( is_numeric( $td_wpv['vulnerable'] ) ? (int) $td_wpv['vulnerable'] : 0 ) ) {
$found = true;
// Generate HTML markup for the theme vulnerability.
$theme_name = is_scalar( $td_wpv['name'] ?? '' ) ? (string) ( $td_wpv['name'] ?? '' ) : '';
$html .= '<h3>' . wpvulnerability_component_icon_html( 'theme' ) . esc_html__( 'Theme', 'wpvulnerability' ) . ': ' . wp_kses( $theme_name, 'strip' ) . '</h3>';
$vuln_list = isset( $td_wpv['vulnerabilities'] ) && is_array( $td_wpv['vulnerabilities'] ) ? $td_wpv['vulnerabilities'] : array();
$html .= wpvulnerability_html( 'theme', $vuln_list );
}
}
// Return the HTML if vulnerabilities were found.
return $found ? $html : false;
}
/**
* Convert theme vulnerabilities into list format.
*
* @since 2.2.0
*
* @return string|false The HTML output if theme vulnerabilities were found, false otherwise.
*/
function wpvulnerability_list_themes() {
$html = '<ul class="inside">';
$found = false;
// Get vulnerabilities data for themes.
$themes = wpvulnerability_theme_get_vulnerabilities();
// Iterate through each theme's data.
foreach ( $themes as $theme_data ) {
if ( ! is_array( $theme_data ) ) {
continue; }
$td_wpv = isset( $theme_data['wpvulnerability'] ) && is_array( $theme_data['wpvulnerability'] ) ? $theme_data['wpvulnerability'] : array();
// Check if the theme is marked as vulnerable.
if ( isset( $td_wpv['vulnerable'] ) && 1 === ( is_numeric( $td_wpv['vulnerable'] ) ? (int) $td_wpv['vulnerable'] : 0 ) ) {
$found = true;
// Generate HTML markup for the theme vulnerability.
$theme_name = is_scalar( $td_wpv['name'] ?? '' ) ? (string) ( $td_wpv['name'] ?? '' ) : '';
$html .= '<li>' . wp_kses( $theme_name, 'strip' ) . '</li>';
}
}
$html .= '</ul>';
// Return the HTML if vulnerabilities were found.
return $found ? $html : false;
}
/**
* Returns an EOL badge HTML span for a software component.
*
* Reads the cached lifecycle data for the given software type and returns
* a styled badge when the component has reached end-of-life status.
*
* @since 5.0.0
*
* @param string $type The software type (e.g., 'php', 'apache', 'mariadb').
*
* @return string HTML badge string, or empty string if not EOL or no data.
*/
function wpvulnerability_eol_badge_html( $type ) {
$sw_data = wpvulnerability_software_get_vulnerabilities( $type );
$lc = isset( $sw_data['lifecycle'] ) && is_array( $sw_data['lifecycle'] ) ? $sw_data['lifecycle'] : array();
$status = is_scalar( $lc['status'] ?? '' ) ? (string) ( $lc['status'] ?? '' ) : '';
$date_end = is_scalar( $lc['date_end'] ?? '' ) ? (string) ( $lc['date_end'] ?? '' ) : '';
if ( 'e' !== $status ) {
return '';
}
if ( '' !== $date_end ) {
/* translators: %s: end-of-life date */
$title = sprintf( __( 'End of life: %s', 'wpvulnerability' ), $date_end );
} else {
$title = __( 'End of Life', 'wpvulnerability' );
}
return '<span class="wpvuln-badge wpvuln-badge-eol" title="' . esc_attr( $title ) . '">' . esc_html__( 'EOL', 'wpvulnerability' ) . '</span>';
}
/**
* Convert core vulnerabilities into HTML format.
*
* @since 2.0.0
*
* @return string|false The HTML output if core vulnerabilities were found, false otherwise.
*/
function wpvulnerability_html_core() {
$html = '';
$found = false;
// Get vulnerabilities data for WordPress core.
$core = wpvulnerability_core_get_vulnerabilities();
// Check if there are any vulnerabilities.
if ( count( $core ) ) {
$found = true;
// Generate HTML markup for the core vulnerabilities.
$html .= wpvulnerability_html( 'core', $core );
}
// Return the HTML if vulnerabilities were found.
return $found ? $html : false;
}