Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Protect Status: Combine multiple vulnerabilities for the same extension into single threat objects #40863

Open
wants to merge 1 commit into
base: trunk
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
Significance: minor
Type: changed

Combine vulnerabilities for the same extension into single vulnerable extension threats.
3 changes: 2 additions & 1 deletion projects/packages/protect-models/composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@
"type": "jetpack-library",
"license": "GPL-2.0-or-later",
"require": {
"php": ">=7.2"
"php": ">=7.2",
"automattic/jetpack-redirect": "@dev"
},
"require-dev": {
"yoast/phpunit-polyfills": "^1.1.1",
Expand Down
123 changes: 123 additions & 0 deletions projects/packages/protect-models/src/class-threat-model.php
Original file line number Diff line number Diff line change
Expand Up @@ -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
*
Expand All @@ -139,4 +148,118 @@ public function __construct( $threat ) {
}
}
}

/**
* Get the ID value of the threat based on its related extension and vulnerabilities.
*
* @since $$next-version$$
*
* @param Extension_Model $extension The extension to get the ID from.
*
* @return string
*/
private static function get_id_from_vulnerable_extension( Extension_Model $extension ) {
return "$extension->type-$extension->slug-$extension->version";
}

/**
* Get the title from a vulnerable extension.
*
* @since $$next-version$$
*
* @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.
*
* @since $$next-version$$
*
* @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
);
}

/**
* Get the latest fixed_in version from a list of vulnerabilities.
*
* @since $$next-version$$
*
* @param array $vulnerabilities The vulnerabilities to get the fixed_in version from.
*
* @return string|bool|null The latest fixed_in version, or false if any of the vulnerabilities are not fixed.
*/
private static function get_fixed_in_from_vulnerabilities( array $vulnerabilities ) {
$fixed_in = null;

foreach ( $vulnerabilities as $vulnerability ) {
// If any of the vulnerabilities are not fixed, the threat is not fixed.
if ( ! $vulnerability->fixed_in ) {
break;
}

// Use the latest available fixed_in version.
if ( ! $fixed_in || ( $fixed_in && version_compare( $vulnerability->fixed_in, $fixed_in, '>' ) ) ) {
$fixed_in = $vulnerability->fixed_in;
}
}

return $fixed_in;
}

/**
* Generate a threat from extension vulnerabilities.
*
* @since $$next-version$$
*
* @param Extension_Model $extension The extension to generate the threat for.
* @param Vulnerability_Model[] $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 ),
'title' => self::get_title_from_vulnerable_extension( $extension ),
'description' => self::get_description_from_vulnerable_extension( $extension, $vulnerabilities ),
'fixed_in' => self::get_fixed_in_from_vulnerabilities( $vulnerabilities ),
'vulnerabilities' => $vulnerabilities,
)
);
}
}
94 changes: 94 additions & 0 deletions projects/packages/protect-models/src/class-vulnerability-model.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
<?php
/**
* Model class for vulnerability data.
*
* @package automattic/jetpack-protect-models
*/

namespace Automattic\Jetpack\Protect_Models;

use Automattic\Jetpack\Redirect;

/**
* Model class for vulnerability data.
*/
class Vulnerability_Model {
/**
* Threat ID.
*
* @var null|string
*/
public $id;

/**
* Threat Title.
*
* @var null|string
*/
public $title;

/**
* Threat Description.
*
* @var null|string
*/
public $description;

/**
* The version the threat is fixed in.
*
* @var null|string
*/
public $fixed_in;

/**
* The version the threat was introduced.
*
* @var null|string
*/
public $introduced_in;

/**
* The type of threat.
*
* @var null|string
*/
public $type;

/**
* The source URL for the threat.
*
* @var null|string
*/
public $source;

/**
* Threat Constructor
*
* @param array|object $threat Threat data to load into the class instance.
*/
public function __construct( $threat ) {
// Initialize the threat data.
foreach ( $threat as $property => $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;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
Significance: minor
Type: changed

Combine multiple vulnerability results for the same extension into a single vulnerable extension threat result.
61 changes: 34 additions & 27 deletions projects/packages/protect-status/src/class-protect-status.php
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
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\Protect_Models\Vulnerability_Model;
use Automattic\Jetpack\Sync\Functions as Sync_Functions;
use Jetpack_Options;
use WP_Error;
Expand Down Expand Up @@ -223,21 +223,29 @@ 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 ) ) {
// normalize the vulnerabilities data
$vulnerabilities = array_map(
function ( $vulnerability ) {
return new Vulnerability_Model( $vulnerability );
},
$checked_extension->vulnerabilities
);

// convert the detected vulnerabilities into a vulnerable extension threat
$threat = Threat_Model::generate_from_extension_vulnerabilities( $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;

}
}

Expand Down Expand Up @@ -282,27 +290,26 @@ 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,
)
);
// Generate a threat from core vulnerabilities.
if ( ! empty( $report_data->core->vulnerabilities ) ) {
// normalize the vulnerabilities data
$vulnerabilities = array_map(
function ( $vulnerability ) {
return new Vulnerability_Model( $vulnerability );
},
$report_data->core->vulnerabilities
);

$threat_extension = clone $core;
$extension_threat = clone $threat;
// convert the detected vulnerabilities into a vulnerable extension threat
$threat = Threat_Model::generate_from_extension_vulnerabilities( $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;
Expand Down
Loading
Loading