From 51ecb631cd769fc05655a2dde15078cf98eaff1f Mon Sep 17 00:00:00 2001 From: Nate Weller Date: Mon, 6 Jan 2025 13:55:16 -0700 Subject: [PATCH] Combine multiple vulnerabilities for a single extension into one vulnerable extension threat changelog minor adjustments changelog add source to generator minor adjustments Add typed params minor adjustments use generator for core vulns threat --- ...status-combine-vulns-into-extension-threat | 4 + .../protect-models/src/class-threat-model.php | 97 +++++++++++++++++++ .../src/class-vulnerability-model.php | 94 ++++++++++++++++++ .../changelog/combine-vulns-into-threat | 4 + .../src/class-protect-status.php | 45 ++++----- 5 files changed, 216 insertions(+), 28 deletions(-) create mode 100644 projects/packages/protect-models/changelog/protect-status-combine-vulns-into-extension-threat create mode 100644 projects/packages/protect-models/src/class-vulnerability-model.php create mode 100644 projects/packages/protect-status/changelog/combine-vulns-into-threat diff --git a/projects/packages/protect-models/changelog/protect-status-combine-vulns-into-extension-threat b/projects/packages/protect-models/changelog/protect-status-combine-vulns-into-extension-threat new file mode 100644 index 0000000000000..2eee7b4277d3d --- /dev/null +++ b/projects/packages/protect-models/changelog/protect-status-combine-vulns-into-extension-threat @@ -0,0 +1,4 @@ +Significance: minor +Type: changed + +TBD diff --git a/projects/packages/protect-models/src/class-threat-model.php b/projects/packages/protect-models/src/class-threat-model.php index bf8add53d530e..c34d5c1aa67b7 100644 --- a/projects/packages/protect-models/src/class-threat-model.php +++ b/projects/packages/protect-models/src/class-threat-model.php @@ -119,6 +119,15 @@ class Threat_Model { */ public $extension; + /** + * The threat's related vulnerabilities. + * + * @since $$next-version$$ + * + * @var null|Vulnerability_Model[] + */ + public $vulnerabilities; + /** * Threat Constructor * @@ -139,4 +148,92 @@ public function __construct( $threat ) { } } } + + /** + * Get the ID value of the threat based on its related extension and vulnerabilities. + * + * @param Extension_Model $extension The extension to get the ID from. + * @param array $vulnerabilities The vulnerabilities to get the ID from. + * + * @return string + */ + private static function get_id_from_vulnerable_extension( Extension_Model $extension, array $vulnerabilities ) { + // Create a single unique ID for the threat by hashing the extension and vulnerability IDs it contains. + return md5( + array_reduce( + $vulnerabilities, + function ( $carry, $vulnerability ) { + return $carry . $vulnerability->id; + }, + $extension->slug . $extension->version + ) + ); + } + + /** + * Get the title from a vulnerable extension. + * + * @param Extension_Model $extension The extension to get the title from. + * + * @return string|null + */ + private static function get_title_from_vulnerable_extension( Extension_Model $extension ) { + $titles = array( + 'plugins' => sprintf( + /* translators: placeholders are the theme name and version number. Example: "Vulnerable theme: Jetpack (version 1.2.3)" */ + __( 'Vulnerable plugin: %1$s (version %2$s)', 'jetpack-protect-models' ), + $extension->name, + $extension->version + ), + 'themes' => sprintf( + /* translators: placeholders are the theme name and version number. Example: "Vulnerable theme: Jetpack (version 1.2.3)" */ + __( 'Vulnerable theme: %1$s (version %2$s)', 'jetpack-protect-models' ), + $extension->name, + $extension->version + ), + 'core' => sprintf( + /* translators: placeholder is the version number. Example: "Vulnerable WordPress (version 1.2.3)" */ + __( 'Vulnerable WordPress (version %s)', 'jetpack-protect-models' ), + $extension->version + ), + ); + + return $titles[ $extension->type ] ?? null; + } + + /** + * Get the description from a vulnerable extension. + * + * @param Extension_Model $extension The extension to get the description from. + * @param array $vulnerabilities The vulnerabilities to get the description from. + * + * @return string + */ + private static function get_description_from_vulnerable_extension( Extension_Model $extension, array $vulnerabilities ) { + return sprintf( + /* translators: placeholders are the theme name and version number. Example: "The installed version of Jetpack (1.2.3) has a known security vulnerability." */ + _n( 'The installed version of %1$s (%2$s) has a known security vulnerability.', 'The installed version of %1$s (%2$s) has known security vulnerabilities.', count( $vulnerabilities ), 'jetpack-protect-models' ), + $extension->name, + $extension->version + ); + } + + /** + * Generate a threat from extension vulnerabilities. + * + * @param Extension_Model $extension The extension to generate the threat for. + * @param array $vulnerabilities The vulnerabilities to generate the threat from. + * + * @return Threat_Model + */ + public static function generate_from_extension_vulnerabilities( Extension_Model $extension, array $vulnerabilities ) { + return new Threat_Model( + array( + 'id' => self::get_id_from_vulnerable_extension( $extension, $vulnerabilities ), + 'title' => self::get_title_from_vulnerable_extension( $extension ), + 'description' => self::get_description_from_vulnerable_extension( $extension, $vulnerabilities ), + 'vulnerabilities' => $vulnerabilities, + ) + ); + } } diff --git a/projects/packages/protect-models/src/class-vulnerability-model.php b/projects/packages/protect-models/src/class-vulnerability-model.php new file mode 100644 index 0000000000000..210d5dc1aa74e --- /dev/null +++ b/projects/packages/protect-models/src/class-vulnerability-model.php @@ -0,0 +1,94 @@ + $value ) { + if ( property_exists( $this, $property ) ) { + $this->$property = $value; + } + } + + // Ensure the source URL is set. + $this->get_source(); + } + + /** + * Get the source URL for the threat. + * + * @return string + */ + public function get_source() { + if ( empty( $this->source ) && $this->id ) { + $this->source = Redirect::get_url( 'jetpack-protect-vul-info', array( 'path' => $this->id ) ); + } + + return $this->source; + } +} diff --git a/projects/packages/protect-status/changelog/combine-vulns-into-threat b/projects/packages/protect-status/changelog/combine-vulns-into-threat new file mode 100644 index 0000000000000..b35e964631a8c --- /dev/null +++ b/projects/packages/protect-status/changelog/combine-vulns-into-threat @@ -0,0 +1,4 @@ +Significance: minor +Type: changed + +Combine multiple vulnerability results for the same extension into a single vulnerable extension threat result. diff --git a/projects/packages/protect-status/src/class-protect-status.php b/projects/packages/protect-status/src/class-protect-status.php index 6a2aa2e0361eb..7de2db3b24d63 100644 --- a/projects/packages/protect-status/src/class-protect-status.php +++ b/projects/packages/protect-status/src/class-protect-status.php @@ -15,7 +15,6 @@ use Automattic\Jetpack\Protect_Models\Extension_Model; use Automattic\Jetpack\Protect_Models\Status_Model; use Automattic\Jetpack\Protect_Models\Threat_Model; -use Automattic\Jetpack\Redirect; use Automattic\Jetpack\Sync\Functions as Sync_Functions; use Jetpack_Options; use WP_Error; @@ -223,21 +222,21 @@ protected static function normalize_extension_data( &$status, $report_data, $ext continue; } - $extension->checked = true; + $extension->checked = true; + $extension_threats[ $slug ] = $extension; - foreach ( $checked_extension->vulnerabilities as $vulnerability ) { - $threat = new Threat_Model( $vulnerability ); - $threat->source = isset( $vulnerability->id ) ? Redirect::get_url( 'jetpack-protect-vul-info', array( 'path' => $vulnerability->id ) ) : null; + if ( ! empty( $checked_extension->vulnerabilities ) ) { + // convert the vulnerability into a threat + $threat = Threat_Model::generate_from_extension_vulnerabilities( $extension, $checked_extension->vulnerabilities ); - $threat_extension = clone $extension; - $extension_threat = clone $threat; - $extension_threat->extension = null; + $threat_extension = clone $extension; + $extension_threat = clone $threat; + $extension_threat->extension = null; $extension_threats[ $slug ]->threats[] = $extension_threat; $threat->extension = $threat_extension; $status->threats[] = $threat; - } } @@ -282,27 +281,17 @@ protected static function normalize_core_data( &$status, $report_data ) { // If we've made it this far, the core version has been checked. $core->checked = true; - // Extract threat data from the report. - if ( is_array( $report_data->core->vulnerabilities ) ) { - foreach ( $report_data->core->vulnerabilities as $vulnerability ) { - $threat = new Threat_Model( - array( - 'id' => $vulnerability->id, - 'title' => $vulnerability->title, - 'fixed_in' => $vulnerability->fixed_in, - 'description' => isset( $vulnerability->description ) ? $vulnerability->description : null, - 'source' => isset( $vulnerability->id ) ? Redirect::get_url( 'jetpack-protect-vul-info', array( 'path' => $vulnerability->id ) ) : null, - ) - ); - - $threat_extension = clone $core; - $extension_threat = clone $threat; + // Generate a threat from core vulnerabilities. + if ( ! empty( $report_data->core->vulnerabilities ) ) { + $threat = Threat_Model::generate_from_extension_vulnerabilities( $core, $report_data->core->vulnerabilities ); - $core->threats[] = $extension_threat; - $threat->extension = $threat_extension; + $threat_extension = clone $core; + $extension_threat = clone $threat; - $status->threats[] = $threat; - } + $core->threats[] = $extension_threat; + $threat->extension = $threat_extension; + + $status->threats[] = $threat; } $status->core = $core;