diff --git a/projects/packages/threat-fixers/.gitattributes b/projects/packages/threat-fixers/.gitattributes
new file mode 100644
index 0000000000000..b0b228d4ad6ad
--- /dev/null
+++ b/projects/packages/threat-fixers/.gitattributes
@@ -0,0 +1,17 @@
+# Files not needed to be distributed in the package.
+.gitattributes export-ignore
+.github/ export-ignore
+package.json export-ignore
+
+# Files to include in the mirror repo, but excluded via gitignore
+# Remember to end all directories with `/**` to properly tag every file.
+# /src/js/example.min.js production-include
+
+# Files to exclude from the mirror repo, but included in the monorepo.
+# Remember to end all directories with `/**` to properly tag every file.
+.gitignore production-exclude
+changelog/** production-exclude
+phpunit.xml.dist production-exclude
+.phpcs.dir.xml production-exclude
+tests/** production-exclude
+.phpcsignore production-exclude
diff --git a/projects/packages/threat-fixers/.gitignore b/projects/packages/threat-fixers/.gitignore
new file mode 100644
index 0000000000000..688e2469bdf27
--- /dev/null
+++ b/projects/packages/threat-fixers/.gitignore
@@ -0,0 +1,3 @@
+vendor/
+node_modules/
+wordpress/
diff --git a/projects/packages/threat-fixers/.phan/baseline.php b/projects/packages/threat-fixers/.phan/baseline.php
new file mode 100644
index 0000000000000..c7aa9a4dd4f30
--- /dev/null
+++ b/projects/packages/threat-fixers/.phan/baseline.php
@@ -0,0 +1,12 @@
+ [],
+ // 'directory_suppressions' => ['src/directory_name' => ['PhanIssueName1', 'PhanIssueName2']] can be manually added if needed.
+ // (directory_suppressions will currently be ignored by subsequent calls to --save-baseline, but may be preserved in future Phan releases)
+];
diff --git a/projects/packages/threat-fixers/.phan/config.php b/projects/packages/threat-fixers/.phan/config.php
new file mode 100644
index 0000000000000..ed111b28290c0
--- /dev/null
+++ b/projects/packages/threat-fixers/.phan/config.php
@@ -0,0 +1,13 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/projects/packages/threat-fixers/CHANGELOG.md b/projects/packages/threat-fixers/CHANGELOG.md
new file mode 100644
index 0000000000000..721294abd00ad
--- /dev/null
+++ b/projects/packages/threat-fixers/CHANGELOG.md
@@ -0,0 +1,7 @@
+# Changelog
+
+All notable changes to this project will be documented in this file.
+
+The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/)
+and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
+
diff --git a/projects/packages/threat-fixers/README.md b/projects/packages/threat-fixers/README.md
new file mode 100644
index 0000000000000..ab6379375dc95
--- /dev/null
+++ b/projects/packages/threat-fixers/README.md
@@ -0,0 +1,24 @@
+# threat-fixers
+
+Library of auto-fixers for security threats detected by Jetpack.
+
+## How to install threat-fixers
+
+### Installation From Git Repo
+
+## Contribute
+
+## Get Help
+
+## Using this package in your WordPress plugin
+
+If you plan on using this package in your WordPress plugin, we would recommend that you use [Jetpack Autoloader](https://packagist.org/packages/automattic/jetpack-autoloader) as your autoloader. This will allow for maximum interoperability with other plugins that use this package as well.
+
+## Security
+
+Need to report a security vulnerability? Go to [https://automattic.com/security/](https://automattic.com/security/) or directly to our security bug bounty site [https://hackerone.com/automattic](https://hackerone.com/automattic).
+
+## License
+
+threat-fixers is licensed under [GNU General Public License v2 (or later)](./LICENSE.txt)
+
diff --git a/projects/packages/threat-fixers/changelog/.gitkeep b/projects/packages/threat-fixers/changelog/.gitkeep
new file mode 100644
index 0000000000000..e69de29bb2d1d
diff --git a/projects/packages/threat-fixers/changelog/initial-version b/projects/packages/threat-fixers/changelog/initial-version
new file mode 100644
index 0000000000000..fb1837c901e51
--- /dev/null
+++ b/projects/packages/threat-fixers/changelog/initial-version
@@ -0,0 +1,4 @@
+Significance: patch
+Type: added
+
+Initial version.
diff --git a/projects/packages/threat-fixers/composer.json b/projects/packages/threat-fixers/composer.json
new file mode 100644
index 0000000000000..fcc0cd1eb56bb
--- /dev/null
+++ b/projects/packages/threat-fixers/composer.json
@@ -0,0 +1,62 @@
+{
+ "name": "automattic/jetpack-threat-fixers",
+ "description": "Library of auto-fixers for security threats detected by Jetpack.",
+ "type": "jetpack-library",
+ "license": "GPL-2.0-or-later",
+ "require": {
+ "php": ">=7.2"
+ },
+ "require-dev": {
+ "yoast/phpunit-polyfills": "^1.1.1",
+ "automattic/jetpack-changelogger": "@dev",
+ "automattic/wordbless": "dev-master"
+ },
+ "autoload": {
+ "classmap": [
+ "src/"
+ ]
+ },
+ "scripts": {
+ "build-development": "echo 'Add your build step to composer.json, please!'",
+ "build-production": "echo 'Add your build step to composer.json, please!'",
+ "phpunit": [
+ "./vendor/phpunit/phpunit/phpunit --colors=always"
+ ],
+ "post-install-cmd": "WorDBless\\Composer\\InstallDropin::copy",
+ "post-update-cmd": "WorDBless\\Composer\\InstallDropin::copy",
+ "test-coverage": [
+ "php -dpcov.directory=. ./vendor/bin/phpunit --coverage-php \"$COVERAGE_DIR/php.cov\""
+ ],
+ "test-php": [
+ "@composer phpunit"
+ ]
+ },
+ "repositories": [
+ {
+ "type": "path",
+ "url": "../../packages/*",
+ "options": {
+ "monorepo": true
+ }
+ }
+ ],
+ "minimum-stability": "dev",
+ "prefer-stable": true,
+ "config": {
+ "allow-plugins": {
+ "roots/wordpress-core-installer": true
+ }
+ },
+ "extra": {
+ "branch-alias": {
+ "dev-trunk": "0.1.x-dev"
+ },
+ "textdomain": "jetpack-threat-fixers",
+ "version-constants": {
+ "::PACKAGE_VERSION": "src/class-threat-fixers.php"
+ }
+ },
+ "suggest": {
+ "automattic/jetpack-autoloader": "Allow for better interoperability with other plugins that use this package."
+ }
+}
diff --git a/projects/packages/threat-fixers/package.json b/projects/packages/threat-fixers/package.json
new file mode 100644
index 0000000000000..a2c5581caef41
--- /dev/null
+++ b/projects/packages/threat-fixers/package.json
@@ -0,0 +1,25 @@
+{
+ "private": true,
+ "name": "@automattic/jetpack-threat-fixers",
+ "version": "0.1.0-alpha",
+ "description": "Library of auto-fixers for security threats detected by Jetpack.",
+ "homepage": "https://github.com/Automattic/jetpack/tree/HEAD/projects/packages/threat-fixers/#readme",
+ "bugs": {
+ "url": "https://github.com/Automattic/jetpack/labels/[Package] Threat Fixers"
+ },
+ "repository": {
+ "type": "git",
+ "url": "https://github.com/Automattic/jetpack.git",
+ "directory": "projects/packages/threat-fixers"
+ },
+ "license": "GPL-2.0-or-later",
+ "author": "Automattic",
+ "scripts": {
+ "build": "echo 'Not implemented.'",
+ "build-js": "echo 'Not implemented.'",
+ "build-production": "echo 'Not implemented.'",
+ "build-production-js": "echo 'Not implemented.'",
+ "clean": "true"
+ },
+ "devDependencies": {}
+}
diff --git a/projects/packages/threat-fixers/phpunit.xml.dist b/projects/packages/threat-fixers/phpunit.xml.dist
new file mode 100644
index 0000000000000..3223c32458db2
--- /dev/null
+++ b/projects/packages/threat-fixers/phpunit.xml.dist
@@ -0,0 +1,14 @@
+
+
+
+ tests/php
+
+
+
+
+
+
+ src
+
+
+
diff --git a/projects/packages/threat-fixers/src/class-extension-update-fixer.php b/projects/packages/threat-fixers/src/class-extension-update-fixer.php
new file mode 100644
index 0000000000000..49d7872c41221
--- /dev/null
+++ b/projects/packages/threat-fixers/src/class-extension-update-fixer.php
@@ -0,0 +1,194 @@
+extension_type = $extension_type;
+ } else {
+ throw new \InvalidArgumentException( 'Invalid extension type.' );
+ }
+
+ if ( ! empty( $extension_slug ) && is_string( $extension_slug ) ) {
+ $this->extension_slug = $extension_slug;
+ } else {
+ throw new \InvalidArgumentException( 'Invalid extension slug.' );
+ }
+
+ if ( empty( $target_version ) || is_string( $target_version ) ) {
+ $this->target_version = $target_version;
+ } else {
+ throw new \InvalidArgumentException( 'Invalid target version.' );
+ }
+ }
+
+ /**
+ * Initialize the fixer's WP_Upgrader instance.
+ *
+ * @throws \InvalidArgumentException If the extension type is invalid.
+ *
+ * @return Theme_Upgrader|Plugin_Upgrader Upgrader instance.
+ */
+ protected function get_upgrader() {
+ if ( $this->upgrader ) {
+ return $this->upgrader;
+ }
+
+ if ( ! class_exists( '\WP_Upgrader' ) ) {
+ if ( file_exists( ABSPATH . 'wp-admin/includes/class-wp-upgrader.php' ) ) {
+ include ABSPATH . 'wp-admin/includes/class-wp-upgrader.php';
+ }
+ }
+
+ if ( ! class_exists( '\WP_Upgrader_Skin' ) ) {
+ if ( file_exists( ABSPATH . 'wp-admin/includes/class-wp-upgrader-skin.php' ) ) {
+ include ABSPATH . 'wp-admin/includes/class-wp-upgrader-skin.php';
+ }
+ }
+
+ switch ( $this->extension_type ) {
+ case 'themes':
+ $this->upgrader = new Theme_Upgrader( new Theme_Upgrader_Skin() );
+ return $this->upgrader;
+ case 'plugins':
+ $this->upgrader = new Plugin_Upgrader( new Plugin_Upgrader_Skin() );
+ return $this->upgrader;
+ default:
+ throw new \InvalidArgumentException( 'Invalid extension type.' );
+ }
+ }
+
+ /**
+ * Get extension data from the WordPress.org API.
+ *
+ * @return object|WP_Error The extension data on success, WP_Error on failure.
+ */
+ public function get_extension_api_data() {
+ switch ( $this->extension_type ) {
+ case 'themes':
+ return themes_api( 'theme_information', array( 'slug' => $this->extension_slug ) );
+ case 'plugins':
+ return plugins_api( 'plugin_information', array( 'slug' => $this->extension_slug ) );
+ default:
+ return new WP_Error( 'invalid_extension_type', 'Invalid extension type.' );
+ }
+ }
+
+ /**
+ * Get extension download link.
+ *
+ * @return string|WP_Error The download link on success, WP_Error on failure.
+ */
+ public function get_download_link_from_wporg() {
+ // Fetch theme installation information from the WordPress.org API.
+ $api_data = $this->get_extension_api_data();
+ if ( is_wp_error( $api_data ) ) {
+ return $api_data;
+ }
+
+ // If no version is specified, use the latest version.
+ $version = $this->target_version ?? $api_data->version;
+
+ // Get the base download link.
+ $download_link = strstr( $api_data->download_link, $api_data->slug, true );
+ // Append the specific version.
+ $download_link = $download_link . $api_data->slug . '.' . $version . '.zip';
+ // WordPress.org requires HTTPS.
+ $download_link = str_replace( 'http://', 'https://', $download_link );
+
+ // Ensure the requested version exists.
+ $response = wp_remote_head( $download_link );
+ $response_code = wp_remote_retrieve_response_code( $response );
+
+ if ( 200 !== (int) $response_code ) {
+ return is_wp_error( $response )
+ ? $response
+ : new WP_Error( $response_code, sprintf( 'HTTP code %d', $response_code ) );
+ }
+
+ return $download_link;
+ }
+
+ /**
+ * Run the fixer by installing the fixed version of the extension.
+ *
+ * @return bool|WP_Error True on success, WP_Error on failure.
+ */
+ public function run() {
+ // Get the link to download the extension zip from WordPress.org.
+ $download_link = $this->get_download_link_from_wporg();
+
+ if ( is_wp_error( $download_link ) ) {
+ return $download_link;
+ }
+
+ // Install the updated extension.
+ $result = $this->get_upgrader()->install( $download_link );
+
+ // Handle the result.
+ if ( is_wp_error( $result ) ) {
+ $key = $result->get_error_code();
+ if ( in_array( $key, array( 'plugins_api_failed', 'themes_api_failed' ), true ) && ! empty( $result->error_data[ $key ] ) && in_array( $result->error_data[ $key ], array( 'N;', 'b:0;' ), true ) ) {
+ return new WP_Error( 'plugin_not_found', "Couldn't find '{$this->extension_slug}' in the WordPress.org {$this->extension_type} directory." );
+ }
+ return new WP_Error( 'error', "{$this->extension_slug}: " . $result->get_error_message() );
+ }
+
+ return true;
+ }
+}
diff --git a/projects/packages/threat-fixers/src/class-threat-fixers.php b/projects/packages/threat-fixers/src/class-threat-fixers.php
new file mode 100644
index 0000000000000..f1364ac8a1982
--- /dev/null
+++ b/projects/packages/threat-fixers/src/class-threat-fixers.php
@@ -0,0 +1,16 @@
+
+
+
+
diff --git a/projects/packages/threat-fixers/tests/php/bootstrap.php b/projects/packages/threat-fixers/tests/php/bootstrap.php
new file mode 100644
index 0000000000000..568758615a4c2
--- /dev/null
+++ b/projects/packages/threat-fixers/tests/php/bootstrap.php
@@ -0,0 +1,16 @@
+getMockForAbstractClass(
+ Extension_Update_Fixer::class,
+ array( 'themes', 'twentytwentyfive' )
+ );
+
+ $mock_response = (object) array(
+ 'slug' => 'twentytwentyfive',
+ 'version' => '1.0',
+ );
+
+ $fixer = $this->getMockBuilder( get_class( $fixer ) )
+ ->onlyMethods( array( 'get_extension_api_data' ) )
+ ->setConstructorArgs( array( 'themes', 'twentytwentyfive' ) )
+ ->getMock();
+
+ $fixer->expects( $this->once() )
+ ->method( 'get_extension_api_data' )
+ ->willReturn( $mock_response );
+
+ $api_data = $fixer->get_extension_api_data();
+
+ $this->assertIsObject( $api_data );
+ $this->assertSame( 'twentytwentyfive', $api_data->slug );
+ }
+
+ public function test_get_download_link_from_wporg() {
+ $fixer = $this->getMockForAbstractClass(
+ Extension_Update_Fixer::class,
+ array( 'themes', 'twentytwentyfive' )
+ );
+
+ $mock_api_data = (object) array(
+ 'slug' => 'twentytwentyfive',
+ 'version' => '1.0',
+ 'download_link' => 'http://example.com/twentytwentyfive.zip',
+ );
+
+ // Mock the method to return API data.
+ $fixer = $this->getMockBuilder( get_class( $fixer ) )
+ ->onlyMethods( array( 'get_extension_api_data' ) )
+ ->setConstructorArgs( array( 'themes', 'twentytwentyfive' ) )
+ ->getMock();
+
+ $fixer->expects( $this->once() )
+ ->method( 'get_extension_api_data' )
+ ->willReturn( $mock_api_data );
+
+ // Use the pre_http_request hook to intercept HTTP requests.
+ add_filter(
+ 'pre_http_request',
+ function () {
+ return array(
+ 'response' => array( 'code' => 200 ),
+ 'body' => array(
+ 'download_link' => 'https://example.com/twentytwentyfive.1.0.zip',
+ 'slug' => 'twentytwentyfive',
+ 'version' => '1.0',
+ ),
+ );
+ },
+ 10,
+ 3
+ );
+
+ // Run the method and assert the result.
+ $download_link = $fixer->get_download_link_from_wporg();
+ $this->assertSame( 'https://example.com/twentytwentyfive.1.0.zip', $download_link );
+
+ // Remove the filter after the test.
+ remove_filter( 'pre_http_request', '__return_null', 10 );
+ }
+
+ /**
+ * Test run with successful installation.
+ */
+ public function test_run_successful_installation() {
+ // phpcs:disble Squiz.PHP.CommentedOutCode.Found
+ // $fixer = $this->getMockForAbstractClass(
+ // Extension_Update_Fixer::class,
+ // ['plugins', 'jetpack']
+ // );
+ // $mock_upgrader = $this->createMock(Plugin_Upgrader::class);
+ // $mock_upgrader->expects($this->once())
+ // ->method('install')
+ // ->willReturn(true);
+ // // Mock the methods used in the process.
+ // $fixer = $this->getMockBuilder(get_class($fixer))
+ // ->onlyMethods(['get_upgrader', 'get_download_link_from_wporg'])
+ // ->setConstructorArgs(['plugins', 'jetpack'])
+ // ->getMock();
+ // $fixer->expects($this->once())
+ // ->method('get_upgrader')
+ // ->willReturn($mock_upgrader);
+ // $fixer->expects($this->once())
+ // ->method('get_download_link_from_wporg')
+ // ->willReturn('https://example.com/plugin-slug.zip');
+ // $result = $fixer->run();
+ // $this->assertTrue($result);
+ }
+
+ /**
+ * Test run with installation failure.
+ */
+ public function test_run_installation_failure() {
+ // phpcs:disable Squiz.PHP.CommentedOutCode.Found
+ // $fixer = $this->getMockForAbstractClass(
+ // Extension_Update_Fixer::class,
+ // ['plugins', 'jetpack']
+ // );
+ // $mock_upgrader = $this->createMock(\Plugin_Upgrader::class);
+ // $mock_upgrader->expects($this->once())
+ // ->method('install')
+ // ->willReturn(new WP_Error('install_failed', 'Installation failed.'));
+ // // Mock the methods used in the process.
+ // $fixer = $this->getMockBuilder(get_class($fixer))
+ // ->onlyMethods(['get_upgrader', 'get_download_link_from_wporg'])
+ // ->setConstructorArgs(['plugins', 'jetpack'])
+ // ->getMock();
+ // $fixer->expects($this->once())
+ // ->method('get_upgrader')
+ // ->willReturn($mock_upgrader);
+ // $fixer->expects($this->once())
+ // ->method('get_download_link_from_wporg')
+ // ->willReturn('https://example.com/plugin-slug.zip');
+ // $result = $fixer->run();
+ // $this->assertInstanceOf(WP_Error::class, $result);
+ // $this->assertSame('install_failed', $result->get_error_code());
+ }
+}