diff --git a/projects/js-packages/components/changelog/add-protect-firewall-stat-cards b/projects/js-packages/components/changelog/add-protect-firewall-stat-cards
new file mode 100644
index 0000000000000..510067f71ce01
--- /dev/null
+++ b/projects/js-packages/components/changelog/add-protect-firewall-stat-cards
@@ -0,0 +1,4 @@
+Significance: minor
+Type: added
+
+Added StatCard component export
diff --git a/projects/js-packages/components/index.ts b/projects/js-packages/components/index.ts
index 0b00005d00d6f..b4740fcff7404 100644
--- a/projects/js-packages/components/index.ts
+++ b/projects/js-packages/components/index.ts
@@ -77,4 +77,5 @@ export { default as ProgressBar } from './components/progress-bar';
export { default as UpsellBanner } from './components/upsell-banner';
export { getUserLocale, cleanLocale } from './lib/locale';
export { default as RadioControl } from './components/radio-control';
+export { default as StatCard } from './components/stat-card';
export * from './components/global-notices';
diff --git a/projects/packages/waf/.phan/baseline.php b/projects/packages/waf/.phan/baseline.php
index 20223292488da..f80ad2b6433a7 100644
--- a/projects/packages/waf/.phan/baseline.php
+++ b/projects/packages/waf/.phan/baseline.php
@@ -45,13 +45,12 @@
'src/class-waf-constants.php' => ['PhanCoalescingNeverNull', 'PhanUndeclaredConstant'],
'src/class-waf-operators.php' => ['PhanTypeMismatchReturn'],
'src/class-waf-rules-manager.php' => ['PhanTypeMismatchArgument'],
- 'src/class-waf-runtime.php' => ['PhanPluginDuplicateConditionalNullCoalescing', 'PhanTypeArraySuspiciousNullable', 'PhanUndeclaredConstant'],
+ 'src/class-waf-runtime.php' => ['PhanPluginDuplicateConditionalNullCoalescing', 'PhanTypeArraySuspiciousNullable'],
'src/class-waf-transforms.php' => ['PhanPluginInvalidPregRegex', 'PhanTypeInvalidDimOffset'],
'tests/php/integration/test-waf-compatibility.php' => ['PhanParamTooMany'],
'tests/php/unit/functions-wordpress.php' => ['PhanRedefineFunction'],
'tests/php/unit/test-waf-operators.php' => ['PhanTypeMismatchArgumentInternal'],
'tests/php/unit/test-waf-runtime-targets.php' => ['PhanPluginRedundantAssignment'],
- 'tests/php/unit/test-waf-runtime.php' => ['PhanImpossibleTypeComparison', 'PhanTypeMismatchArgument'],
'tests/php/unit/test-waf-standalone-bootstrap.php' => ['PhanDeprecatedFunction', 'PhanNoopNew'],
],
// 'directory_suppressions' => ['src/directory_name' => ['PhanIssueName1', 'PhanIssueName2']] can be manually added if needed.
diff --git a/projects/packages/waf/changelog/add-protect-firewall-stat-cards b/projects/packages/waf/changelog/add-protect-firewall-stat-cards
new file mode 100644
index 0000000000000..6841106ca5ac2
--- /dev/null
+++ b/projects/packages/waf/changelog/add-protect-firewall-stat-cards
@@ -0,0 +1,4 @@
+Significance: minor
+Type: added
+
+Added Waf_Blocklog_Manager class
diff --git a/projects/packages/waf/src/class-waf-blocklog-manager.php b/projects/packages/waf/src/class-waf-blocklog-manager.php
new file mode 100644
index 0000000000000..031c71ff71c3b
--- /dev/null
+++ b/projects/packages/waf/src/class-waf-blocklog-manager.php
@@ -0,0 +1,441 @@
+connect_error ) {
+ error_log( 'Could not connect to the database:' . $conn->connect_error );
+ return null;
+ }
+
+ self::$db_connection = $conn;
+ return self::$db_connection;
+ }
+
+ /**
+ * Close the database connection.
+ *
+ * @return void
+ */
+ private static function close_db_connection() {
+ if ( self::$db_connection ) {
+ self::$db_connection->close();
+ self::$db_connection = null;
+ }
+ }
+
+ /**
+ * Serialize a value for storage in a WordPress option.
+ *
+ * @param mixed $value The value to serialize.
+ * @return string The serialized value.
+ */
+ private static function serialize_option_value( $value ) {
+ return serialize( $value );
+ }
+
+ /**
+ * Unserialize a value from a WordPress option.
+ *
+ * @param string $value The serialized value.
+ * @return mixed The unserialized value.
+ */
+ private static function unserialize_option_value( string $value ) {
+ return unserialize( $value );
+ }
+
+ /**
+ * Create the log table when plugin is activated.
+ *
+ * @return void
+ */
+ public static function create_blocklog_table() {
+ global $wpdb;
+
+ require_once ABSPATH . 'wp-admin/includes/upgrade.php';
+
+ $sql = "
+ CREATE TABLE {$wpdb->prefix}jetpack_waf_blocklog (
+ log_id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
+ timestamp datetime NOT NULL,
+ rule_id BIGINT NOT NULL,
+ reason longtext NOT NULL,
+ PRIMARY KEY (log_id),
+ KEY timestamp (timestamp)
+ )
+ ";
+
+ dbDelta( $sql );
+ }
+
+ /**
+ * Write block logs to database.
+ *
+ * @param array $log_data Log data.
+ *
+ * @return void
+ */
+ private static function write_blocklog_row( $log_data ) {
+ $conn = self::connect_to_wordpress_db();
+
+ if ( ! $conn ) {
+ return;
+ }
+
+ global $table_prefix;
+
+ $statement = $conn->prepare( "INSERT INTO {$table_prefix}jetpack_waf_blocklog(reason,rule_id, timestamp) VALUES (?, ?, ?)" );
+ if ( false !== $statement ) {
+ $statement->bind_param( 'sis', $log_data['reason'], $log_data['rule_id'], $log_data['timestamp'] );
+ $statement->execute();
+
+ if ( $conn->insert_id > 100 ) {
+ $conn->query( "DELETE FROM {$table_prefix}jetpack_waf_blocklog ORDER BY log_id LIMIT 1" );
+ }
+ }
+ }
+
+ /**
+ * Get the daily summary stats from the database.
+ *
+ * @return array The daily summary stats.
+ */
+ private static function get_daily_summary() {
+ global $table_prefix;
+ $db_connection = self::connect_to_wordpress_db();
+ if ( ! $db_connection ) {
+ return array();
+ }
+
+ $result = $db_connection->query( "SELECT option_value FROM {$table_prefix}options WHERE option_name = '" . self::BLOCKLOG_OPTION_NAME_DAILY_SUMMARY . "'" );
+ if ( ! $result ) {
+ return array();
+ }
+
+ $row = $result->fetch_assoc();
+ if ( ! $row ) {
+ return array();
+ }
+
+ $daily_summary = self::unserialize_option_value( $row['option_value'] );
+ $result->free();
+
+ return is_array( $daily_summary ) ? $daily_summary : array();
+ }
+
+ /**
+ * Increments the current date's daily summary stat.
+ *
+ * @param array $current_value The current value of the daily summary.
+ *
+ * @return array The updated daily summary.
+ */
+ public static function increment_daily_summary( array $current_value ) {
+ $date = gmdate( 'Y-m-d' );
+ $value = intval( $current_value[ $date ] ?? 0 );
+ $current_value[ $date ] = $value + 1;
+
+ return $current_value;
+ }
+
+ /**
+ * Update the daily summary option in the database.
+ *
+ * @param array $value The value to update.
+ *
+ * @return void
+ */
+ private static function write_daily_summary_row( array $value ) {
+ global $table_prefix;
+ $option_name = self::BLOCKLOG_OPTION_NAME_DAILY_SUMMARY;
+
+ $db_connection = self::connect_to_wordpress_db();
+ if ( ! $db_connection ) {
+ return;
+ }
+
+ $updated_value = self::serialize_option_value( $value );
+
+ $statement = $db_connection->prepare( "INSERT INTO {$table_prefix}options (option_name, option_value) VALUES (?, ?) ON DUPLICATE KEY UPDATE option_value = ?" );
+ if ( false !== $statement ) {
+ $statement->bind_param( 'sss', $option_name, $updated_value, $updated_value );
+ $statement->execute();
+ }
+ }
+
+ /**
+ * Update the daily summary stats for the current date.
+ *
+ * @return void
+ */
+ private static function write_daily_summary() {
+ $stats = self::get_daily_summary();
+ $stats = self::increment_daily_summary( $stats );
+ $stats = self::filter_last_30_days( $stats );
+
+ self::write_daily_summary_row( $stats );
+ }
+
+ /**
+ * Get the all-time block count value from the database.
+ *
+ * @return int The all-time block count.
+ */
+ private static function get_all_time_block_count_value() {
+ global $table_prefix;
+ $db_connection = self::connect_to_wordpress_db();
+ if ( ! $db_connection ) {
+ return 0;
+ }
+
+ $result = $db_connection->query( "SELECT option_value FROM {$table_prefix}options WHERE option_name = '" . self::BLOCKLOG_OPTION_NAME_ALL_TIME_BLOCK_COUNT . "'" );
+ if ( ! $result ) {
+ return 0;
+ }
+
+ $row = $result->fetch_assoc();
+ if ( ! $row ) {
+ return 0;
+ }
+
+ $all_time_block_count = intval( $row['option_value'] );
+ $result->free();
+
+ return $all_time_block_count;
+ }
+
+ /**
+ * Update the all-time block count value in the database.
+ *
+ * @param int $value The value to update.
+ * @return void
+ */
+ private static function write_all_time_block_count_row( int $value ) {
+ global $table_prefix;
+ $option_name = self::BLOCKLOG_OPTION_NAME_ALL_TIME_BLOCK_COUNT;
+
+ $db_connection = self::connect_to_wordpress_db();
+ if ( ! $db_connection ) {
+ return;
+ }
+
+ $statement = $db_connection->prepare( "INSERT INTO {$table_prefix}options (option_name, option_value) VALUES (?, ?) ON DUPLICATE KEY UPDATE option_value = ?" );
+ if ( false !== $statement ) {
+ $statement->bind_param( 'sii', $option_name, $value, $value );
+ $statement->execute();
+ }
+ }
+
+ /**
+ * Increment the all-time stats.
+ *
+ * @return void
+ */
+ private static function write_all_time_block_count() {
+ $block_count = self::get_all_time_block_count_value();
+ if ( ! $block_count ) {
+ $block_count = self::get_default_all_time_stat_value();
+ }
+
+ self::write_all_time_block_count_row( $block_count + 1 );
+ }
+
+ /**
+ * Filters the stats to retain only data for the last 30 days.
+ *
+ * @param array $stats The array of stats to prune.
+ *
+ * @return array Pruned stats array.
+ */
+ public static function filter_last_30_days( array $stats ) {
+ $today = gmdate( 'Y-m-d' );
+ $one_month_ago = gmdate( 'Y-m-d', strtotime( '-30 days' ) );
+
+ return array_filter(
+ $stats,
+ function ( $date ) use ( $one_month_ago, $today ) {
+ return $date >= $one_month_ago && $date <= $today;
+ },
+ ARRAY_FILTER_USE_KEY
+ );
+ }
+
+ /**
+ * Get the total number of blocked requests for today.
+ *
+ * @return int
+ */
+ public static function get_current_day_block_count() {
+ $stats = get_option( self::BLOCKLOG_OPTION_NAME_DAILY_SUMMARY, array() );
+ $today = gmdate( 'Y-m-d' );
+
+ return $stats[ $today ] ?? 0;
+ }
+
+ /**
+ * Get the total number of blocked requests for last thirty days.
+ *
+ * @return int
+ */
+ public static function get_thirty_days_block_counts() {
+ $stats = get_option( self::BLOCKLOG_OPTION_NAME_DAILY_SUMMARY, array() );
+ $total_blocks = 0;
+
+ foreach ( $stats as $count ) {
+ $total_blocks += intval( $count );
+ }
+
+ return $total_blocks;
+ }
+
+ /**
+ * Get the total number of blocked requests for all time.
+ *
+ * @return int
+ */
+ public static function get_all_time_block_count() {
+ $all_time_block_count = get_option( self::BLOCKLOG_OPTION_NAME_ALL_TIME_BLOCK_COUNT, false );
+
+ if ( false !== $all_time_block_count ) {
+ return intval( $all_time_block_count );
+ }
+
+ return self::get_default_all_time_stat_value();
+ }
+
+ /**
+ * Compute the initial all-time stats value.
+ *
+ * @return int The initial all-time stats value.
+ */
+ private static function get_default_all_time_stat_value() {
+ $conn = self::connect_to_wordpress_db();
+ if ( ! $conn ) {
+ return 0;
+ }
+
+ global $table_prefix;
+
+ $last_log_id_result = $conn->query( "SELECT log_id FROM {$table_prefix}jetpack_waf_blocklog ORDER BY log_id DESC LIMIT 1" );
+
+ $all_time_block_count = 0;
+
+ if ( $last_log_id_result && $last_log_id_result->num_rows > 0 ) {
+ $row = $last_log_id_result->fetch_assoc();
+ if ( $row !== null && isset( $row['log_id'] ) ) {
+ $all_time_block_count = $row['log_id'];
+ }
+ }
+
+ return intval( $all_time_block_count );
+ }
+
+ /**
+ * Get the headers for logging purposes.
+ *
+ * @return array The headers.
+ */
+ public static function get_request_headers() {
+ $all_headers = getallheaders();
+ $exclude_headers = array( 'Authorization', 'Cookie', 'Proxy-Authorization', 'Set-Cookie' );
+
+ foreach ( $exclude_headers as $header ) {
+ unset( $all_headers[ $header ] );
+ }
+
+ return $all_headers;
+ }
+
+ /**
+ * Write block logs. We won't write to the file if it exceeds 100 mb.
+ *
+ * @param string $rule_id The rule ID that triggered the block.
+ * @param string $reason The reason for the block.
+ *
+ * @return void
+ */
+ public static function write_blocklog( $rule_id, $reason ) {
+ $log_data = array();
+ $log_data['rule_id'] = $rule_id;
+ $log_data['reason'] = $reason;
+ $log_data['timestamp'] = gmdate( 'Y-m-d H:i:s' );
+ $log_data['request_uri'] = isset( $_SERVER['REQUEST_URI'] ) ? \stripslashes( $_SERVER['REQUEST_URI'] ) : ''; // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized, WordPress.Security.ValidatedSanitizedInput.MissingUnslash
+ $log_data['user_agent'] = isset( $_SERVER['HTTP_USER_AGENT'] ) ? \stripslashes( $_SERVER['HTTP_USER_AGENT'] ) : ''; // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized, WordPress.Security.ValidatedSanitizedInput.MissingUnslash
+ $log_data['referer'] = isset( $_SERVER['HTTP_REFERER'] ) ? \stripslashes( $_SERVER['HTTP_REFERER'] ) : ''; // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized, WordPress.Security.ValidatedSanitizedInput.MissingUnslash
+ $log_data['content_type'] = isset( $_SERVER['CONTENT_TYPE'] ) ? \stripslashes( $_SERVER['CONTENT_TYPE'] ) : ''; // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized, WordPress.Security.ValidatedSanitizedInput.MissingUnslash
+ $log_data['get_params'] = json_encode( $_GET );
+
+ if ( defined( 'JETPACK_WAF_SHARE_DEBUG_DATA' ) && JETPACK_WAF_SHARE_DEBUG_DATA ) {
+ $log_data['post_params'] = json_encode( $_POST );
+ $log_data['headers'] = self::get_request_headers();
+ }
+
+ if ( defined( 'JETPACK_WAF_SHARE_DATA' ) && JETPACK_WAF_SHARE_DATA ) {
+ $file_path = JETPACK_WAF_DIR . '/waf-blocklog';
+ $file_exists = file_exists( $file_path );
+
+ if ( ! $file_exists || filesize( $file_path ) < ( 100 * 1024 * 1024 ) ) {
+ $fp = fopen( $file_path, 'a+' );
+
+ if ( $fp ) {
+ try {
+ fwrite( $fp, json_encode( $log_data ) . "\n" );
+ } finally {
+ fclose( $fp );
+ }
+ }
+ }
+ }
+
+ self::write_daily_summary();
+ self::write_all_time_block_count();
+ self::write_blocklog_row( $log_data );
+ self::close_db_connection();
+ }
+}
diff --git a/projects/packages/waf/src/class-waf-runner.php b/projects/packages/waf/src/class-waf-runner.php
index cfed64e7ec3f4..e15ce7a8290aa 100644
--- a/projects/packages/waf/src/class-waf-runner.php
+++ b/projects/packages/waf/src/class-waf-runner.php
@@ -326,7 +326,7 @@ public static function activate() {
Waf_Rules_Manager::generate_ip_rules();
Waf_Rules_Manager::generate_rules();
- self::create_blocklog_table();
+ Waf_Blocklog_Manager::create_blocklog_table();
}
/**
@@ -353,30 +353,6 @@ public static function initialize_waf_directory() {
}
}
- /**
- * Create the log table when plugin is activated.
- *
- * @return void
- */
- public static function create_blocklog_table() {
- global $wpdb;
-
- require_once ABSPATH . 'wp-admin/includes/upgrade.php';
-
- $sql = "
- CREATE TABLE {$wpdb->prefix}jetpack_waf_blocklog (
- log_id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
- timestamp datetime NOT NULL,
- rule_id BIGINT NOT NULL,
- reason longtext NOT NULL,
- PRIMARY KEY (log_id),
- KEY timestamp (timestamp)
- )
- ";
-
- dbDelta( $sql );
- }
-
/**
* Deactivates the WAF by deleting the relevant options and emptying rules file.
*
diff --git a/projects/packages/waf/src/class-waf-runtime.php b/projects/packages/waf/src/class-waf-runtime.php
index 30fd1a8d500bc..394d46aa5a6f1 100644
--- a/projects/packages/waf/src/class-waf-runtime.php
+++ b/projects/packages/waf/src/class-waf-runtime.php
@@ -271,7 +271,7 @@ public function block( $action, $rule_id, $reason, $status_code = 403 ) {
$reason = $this->sanitize_output( $reason );
}
- $this->write_blocklog( $rule_id, $reason );
+ Waf_Blocklog_Manager::write_blocklog( $rule_id, $reason );
error_log( "Jetpack WAF Blocked Request\t$action\t$rule_id\t$status_code\t$reason" );
header( "X-JetpackWAF-Blocked: $status_code - rule $rule_id" );
if ( defined( 'JETPACK_WAF_MODE' ) && 'normal' === JETPACK_WAF_MODE ) {
@@ -281,106 +281,6 @@ public function block( $action, $rule_id, $reason, $status_code = 403 ) {
}
}
- /**
- * Get the headers for logging purposes.
- */
- public function get_request_headers() {
- $all_headers = getallheaders();
- $exclude_headers = array( 'Authorization', 'Cookie', 'Proxy-Authorization', 'Set-Cookie' );
-
- foreach ( $exclude_headers as $header ) {
- unset( $all_headers[ $header ] );
- }
-
- return $all_headers;
- }
-
- /**
- * Write block logs. We won't write to the file if it exceeds 100 mb.
- *
- * @param string $rule_id Rule id.
- * @param string $reason Block reason.
- */
- public function write_blocklog( $rule_id, $reason ) {
- $log_data = array();
- $log_data['rule_id'] = $rule_id;
- $log_data['reason'] = $reason;
- $log_data['timestamp'] = gmdate( 'Y-m-d H:i:s' );
- $log_data['request_uri'] = isset( $_SERVER['REQUEST_URI'] ) ? \stripslashes( $_SERVER['REQUEST_URI'] ) : ''; // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.MissingUnslash
- $log_data['user_agent'] = isset( $_SERVER['HTTP_USER_AGENT'] ) ? \stripslashes( $_SERVER['HTTP_USER_AGENT'] ) : ''; // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.MissingUnslash
- $log_data['referer'] = isset( $_SERVER['HTTP_REFERER'] ) ? \stripslashes( $_SERVER['HTTP_REFERER'] ) : ''; // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.MissingUnslash
- $log_data['content_type'] = isset( $_SERVER['CONTENT_TYPE'] ) ? \stripslashes( $_SERVER['CONTENT_TYPE'] ) : ''; // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.MissingUnslash
- $log_data['get_params'] = json_encode( $_GET );
-
- if ( defined( 'JETPACK_WAF_SHARE_DEBUG_DATA' ) && JETPACK_WAF_SHARE_DEBUG_DATA ) {
- $log_data['post_params'] = json_encode( $_POST );
- $log_data['headers'] = $this->get_request_headers();
- }
-
- if ( defined( 'JETPACK_WAF_SHARE_DATA' ) && JETPACK_WAF_SHARE_DATA ) {
- $file_path = JETPACK_WAF_DIR . '/waf-blocklog';
- $file_exists = file_exists( $file_path );
-
- if ( ! $file_exists || filesize( $file_path ) < ( 100 * 1024 * 1024 ) ) {
- $fp = fopen( $file_path, 'a+' );
-
- if ( $fp ) {
- try {
- fwrite( $fp, json_encode( $log_data ) . "\n" );
- } finally {
- fclose( $fp );
- }
- }
- }
- }
-
- $this->write_blocklog_row( $log_data );
- }
-
- /**
- * Write block logs to database.
- *
- * @param array $log_data Log data.
- */
- private function write_blocklog_row( $log_data ) {
- $conn = $this->connect_to_wordpress_db();
-
- if ( ! $conn ) {
- return;
- }
-
- global $table_prefix;
-
- $statement = $conn->prepare( "INSERT INTO {$table_prefix}jetpack_waf_blocklog(reason,rule_id, timestamp) VALUES (?, ?, ?)" );
- if ( false !== $statement ) {
- $statement->bind_param( 'sis', $log_data['reason'], $log_data['rule_id'], $log_data['timestamp'] );
- $statement->execute();
-
- if ( $conn->insert_id > 100 ) {
- $conn->query( "DELETE FROM {$table_prefix}jetpack_waf_blocklog ORDER BY log_id LIMIT 1" );
- }
- }
- }
-
- /**
- * Connect to WordPress database.
- */
- private function connect_to_wordpress_db() {
- if ( ! file_exists( JETPACK_WAF_WPCONFIG ) ) {
- return;
- }
-
- require_once JETPACK_WAF_WPCONFIG;
- $conn = new \mysqli( DB_HOST, DB_USER, DB_PASSWORD, DB_NAME ); // phpcs:ignore WordPress.DB.RestrictedClasses.mysql__mysqli
-
- if ( $conn->connect_error ) {
- error_log( 'Could not connect to the database:' . $conn->connect_error );
- return null;
- }
-
- return $conn;
- }
-
/**
* Redirect.
*
diff --git a/projects/packages/waf/src/class-waf-stats.php b/projects/packages/waf/src/class-waf-stats.php
index f3b4122a2402d..5f5cb4e081424 100644
--- a/projects/packages/waf/src/class-waf-stats.php
+++ b/projects/packages/waf/src/class-waf-stats.php
@@ -7,53 +7,29 @@
namespace Automattic\Jetpack\Waf;
-use Automattic\Jetpack\IP\Utils as IP_Utils;
-
/**
* Retrieves WAF stats.
*/
class Waf_Stats {
/**
- * The global stats cache
- *
- * @var array|null
- */
- public static $global_stats = null;
-
- /**
- * Get IP allow list count
+ * Retrieve blocked requests from database
*
- * @return int The number of valid IP addresses in the allow list
+ * @return array
*/
- public static function get_ip_allow_list_count() {
- $ip_allow_list = get_option( Waf_Rules_Manager::IP_ALLOW_LIST_OPTION_NAME );
-
- if ( ! $ip_allow_list ) {
- return 0;
- }
-
- $results = IP_Utils::get_ip_addresses_from_string( $ip_allow_list );
-
- return count( $results );
+ public static function get_blocked_requests() {
+ return array(
+ 'current_day' => Waf_Blocklog_Manager::get_current_day_block_count(),
+ 'thirty_days' => Waf_Blocklog_Manager::get_thirty_days_block_counts(),
+ 'all_time' => Waf_Blocklog_Manager::get_all_time_block_count(),
+ );
}
- /**
- * Get IP block list count
+ /** The global stats cache
*
- * @return int The number of valid IP addresses in the block list
+ * @var array|null
*/
- public static function get_ip_block_list_count() {
- $ip_block_list = get_option( Waf_Rules_Manager::IP_BLOCK_LIST_OPTION_NAME );
-
- if ( ! $ip_block_list ) {
- return 0;
- }
-
- $results = IP_Utils::get_ip_addresses_from_string( $ip_block_list );
-
- return count( $results );
- }
+ public static $global_stats = null;
/**
* Get Rules version
diff --git a/projects/packages/waf/src/functions.php b/projects/packages/waf/src/functions.php
index 7d60cd28f88fe..e7391c62cc575 100644
--- a/projects/packages/waf/src/functions.php
+++ b/projects/packages/waf/src/functions.php
@@ -70,3 +70,51 @@ function flatten_array( $array, $key_prefix = '', $dot_notation = null ) {
}
return $return;
}
+
+/**
+ * Polyfill for getallheaders, which is not available in all PHP environments.
+ *
+ * @link https://github.com/ralouphie/getallheaders
+ */
+if ( ! function_exists( 'getallheaders' ) ) {
+ /**
+ * Get all HTTP header key/values as an associative array for the current request.
+ *
+ * @return array The HTTP header key/value pairs.
+ */
+ function getallheaders() {
+ // phpcs:disable WordPress.Security.ValidatedSanitizedInput
+ $headers = array();
+
+ $copy_server = array(
+ 'CONTENT_TYPE' => 'Content-Type',
+ 'CONTENT_LENGTH' => 'Content-Length',
+ 'CONTENT_MD5' => 'Content-Md5',
+ );
+
+ foreach ( $_SERVER as $key => $value ) {
+ if ( substr( $key, 0, 5 ) === 'HTTP_' ) {
+ $key = substr( $key, 5 );
+ if ( ! isset( $copy_server[ $key ] ) || ! isset( $_SERVER[ $key ] ) ) {
+ $key = str_replace( ' ', '-', ucwords( strtolower( str_replace( '_', ' ', $key ) ) ) );
+ $headers[ $key ] = $value;
+ }
+ } elseif ( isset( $copy_server[ $key ] ) ) {
+ $headers[ $copy_server[ $key ] ] = $value;
+ }
+ }
+
+ if ( ! isset( $headers['Authorization'] ) ) {
+ if ( isset( $_SERVER['REDIRECT_HTTP_AUTHORIZATION'] ) ) {
+ $headers['Authorization'] = $_SERVER['REDIRECT_HTTP_AUTHORIZATION'];
+ } elseif ( isset( $_SERVER['PHP_AUTH_USER'] ) ) {
+ $basic_pass = $_SERVER['PHP_AUTH_PW'] ?? '';
+ $headers['Authorization'] = 'Basic ' . base64_encode( $_SERVER['PHP_AUTH_USER'] . ':' . $basic_pass );
+ } elseif ( isset( $_SERVER['PHP_AUTH_DIGEST'] ) ) {
+ $headers['Authorization'] = $_SERVER['PHP_AUTH_DIGEST'];
+ }
+ }
+
+ return $headers;
+ }
+}
diff --git a/projects/packages/waf/tests/php/unit/test-waf-blocklog-manager.php b/projects/packages/waf/tests/php/unit/test-waf-blocklog-manager.php
new file mode 100644
index 0000000000000..e1835501adee3
--- /dev/null
+++ b/projects/packages/waf/tests/php/unit/test-waf-blocklog-manager.php
@@ -0,0 +1,76 @@
+assertTrue( file_exists( $waf_log_path ) );
+ $this->assertFalse( strpos( $file_content, '{"rule_id":"1337","reason":"test block"' ) === false );
+
+ unlink( $waf_log_path );
+ }
+
+ /**
+ * Test incrementing the daily summary stats.
+ */
+ public function testIncrementDailySummary() {
+ $today = gmdate( 'Y-m-d' );
+
+ $value = array();
+ $result = Waf_Blocklog_Manager::increment_daily_summary( $value );
+ $this->assertSame( 1, $result[ $today ] );
+
+ $value = array(
+ '1999-01-01' => 0,
+ '1999-01-02' => 123,
+ $today => 1,
+ );
+ $result = Waf_Blocklog_Manager::increment_daily_summary( $value );
+ $this->assertEquals( 2, $result[ $today ] );
+ }
+
+ /**
+ * Test filtering of the daily summary stats.
+ */
+ public function testFilterLast30Days() {
+ // Generate stats data with dates from 35 days ago to 5 days in the future
+ $stats = array();
+ for ( $i = -35; $i <= 5; $i++ ) {
+ $date = gmdate( 'Y-m-d', strtotime( "$i days" ) );
+ $stats[ $date ] = "data for $date";
+ }
+
+ // Generate expected data with dates from 30 days ago to today
+ $expected_stats = array();
+ for ( $i = -30; $i <= 0; $i++ ) {
+ $date = gmdate( 'Y-m-d', strtotime( "$i days" ) );
+ $expected_stats[ $date ] = "data for $date";
+ }
+
+ $filtered_stats = Waf_Blocklog_Manager::filter_last_30_days( $stats );
+ $this->assertEquals( $expected_stats, $filtered_stats );
+ }
+}
diff --git a/projects/packages/waf/tests/php/unit/test-waf-runtime.php b/projects/packages/waf/tests/php/unit/test-waf-runtime.php
index cc88628f7b443..e7823fe8fe19f 100644
--- a/projects/packages/waf/tests/php/unit/test-waf-runtime.php
+++ b/projects/packages/waf/tests/php/unit/test-waf-runtime.php
@@ -279,28 +279,6 @@ public function testVars() {
$this->assertSame( '', $this->runtime->get_var( 'abc' ) );
}
- /**
- * Test calling the log function and check if a file is written.
- *
- * @runInSeparateProcess
- */
- public function testWriteBlocklog() {
- $tmp_dir = sys_get_temp_dir();
- $waf_log_path = $tmp_dir . '/waf-blocklog';
-
- define( 'JETPACK_WAF_DIR', $tmp_dir );
- define( 'JETPACK_WAF_WPCONFIG', $tmp_dir . '/wp-config.php' );
- define( 'JETPACK_WAF_SHARE_DATA', true );
-
- $this->runtime->write_blocklog( 1337, 'test block' );
- $file_content = file_get_contents( $waf_log_path );
-
- $this->assertTrue( file_exists( $waf_log_path ) );
- $this->assertTrue( strpos( $file_content, '{"rule_id":1337,"reason":"test block"' ) !== true );
-
- unlink( $waf_log_path );
- }
-
/**
* Test the sanitize output method catches odd cases
*/
diff --git a/projects/plugins/jetpack/changelog/add-protect-firewall-stat-cards b/projects/plugins/jetpack/changelog/add-protect-firewall-stat-cards
new file mode 100644
index 0000000000000..a1c1831fa1ef7
--- /dev/null
+++ b/projects/plugins/jetpack/changelog/add-protect-firewall-stat-cards
@@ -0,0 +1,5 @@
+Significance: patch
+Type: other
+Comment: Updated composer.lock.
+
+
diff --git a/projects/plugins/protect/changelog/add-protect-firewall-stat-cards b/projects/plugins/protect/changelog/add-protect-firewall-stat-cards
new file mode 100644
index 0000000000000..244645fa6ebd6
--- /dev/null
+++ b/projects/plugins/protect/changelog/add-protect-firewall-stat-cards
@@ -0,0 +1,4 @@
+Significance: minor
+Type: added
+
+Added WAF StatCards for blocked requests
diff --git a/projects/plugins/protect/changelog/add-protect-firewall-stat-cards#2 b/projects/plugins/protect/changelog/add-protect-firewall-stat-cards#2
new file mode 100644
index 0000000000000..9aa70e3ec1f75
--- /dev/null
+++ b/projects/plugins/protect/changelog/add-protect-firewall-stat-cards#2
@@ -0,0 +1,5 @@
+Significance: patch
+Type: changed
+Comment: Updated composer.lock.
+
+
diff --git a/projects/plugins/protect/src/class-jetpack-protect.php b/projects/plugins/protect/src/class-jetpack-protect.php
index fad23ee804cb4..293ccdaeb3ce7 100644
--- a/projects/plugins/protect/src/class-jetpack-protect.php
+++ b/projects/plugins/protect/src/class-jetpack-protect.php
@@ -457,8 +457,7 @@ public static function get_waf_stats() {
}
return array(
- 'ipAllowListCount' => Waf_Stats::get_ip_allow_list_count(),
- 'ipBlockListCount' => Waf_Stats::get_ip_block_list_count(),
+ 'blockedRequests' => Plan::has_required_plan() ? Waf_Stats::get_blocked_requests() : false,
'automaticRulesLastUpdated' => Waf_Stats::get_automatic_rules_last_updated(),
);
}
diff --git a/projects/plugins/protect/src/js/components/firewall-header/firewall-statcards.jsx b/projects/plugins/protect/src/js/components/firewall-header/firewall-statcards.jsx
new file mode 100644
index 0000000000000..b5ff8c38851d9
--- /dev/null
+++ b/projects/plugins/protect/src/js/components/firewall-header/firewall-statcards.jsx
@@ -0,0 +1,86 @@
+import { Text, useBreakpointMatch, StatCard } from '@automattic/jetpack-components';
+import { __, sprintf } from '@wordpress/i18n';
+import { Icon, shield, chartBar } from '@wordpress/icons';
+import { useCallback, useMemo } from 'react';
+import styles from './styles.module.scss';
+
+const FirewallStatCards = ( { status, hasPlan, currentDayStats, thirtyDaysStats } ) => {
+ const [ isSmall ] = useBreakpointMatch( [ 'sm', 'lg' ], [ null, '<' ] );
+
+ const defaultArgs = useMemo(
+ () => ( {
+ className: status !== 'on' || ! hasPlan ? styles.disabled : styles.active,
+ variant: isSmall ? 'horizontal' : 'square',
+ } ),
+ [ status, isSmall, hasPlan ]
+ );
+
+ const getIcon = useCallback(
+ icon => (
+
+
+
+ { sprintf(
+ /* translators: Translates to Last %1$d: Number of units. %2$s: Unit of time (hours, days, etc) */
+ __( 'Last %1$d %2$s', 'jetpack-protect' ),
+ period,
+ units
+ ) }
+
+
+ ),
+ [ isSmall ]
+ );
+
+ const currentDayArgs = useMemo(
+ () => ( {
+ ...defaultArgs,
+ icon: getIcon( shield ),
+ label: getLabel( 24, 'hours' ),
+ value: status !== 'on' || ! hasPlan ? 0 : currentDayStats,
+ } ),
+ [ defaultArgs, getIcon, getLabel, status, hasPlan, currentDayStats ]
+ );
+
+ const thirtyDaysArgs = useMemo(
+ () => ( {
+ ...defaultArgs,
+ icon: getIcon( chartBar ),
+ label: getLabel( 30, 'days' ),
+ value: status !== 'on' || ! hasPlan ? 0 : thirtyDaysStats,
+ } ),
+ [ defaultArgs, getIcon, getLabel, status, hasPlan, thirtyDaysStats ]
+ );
+
+ return (
+