Skip to content

Commit

Permalink
Protect: Add global WAF stats (#38388)
Browse files Browse the repository at this point in the history
* Add global statistics to initial state (with caching), and fix hardcoded total vuln counts in the UI

* changelog

* Fix phan errors

* Add types for get_global_stats

* Revert prior change

* Address phan errors

* Fixes and improvements

* Fix versions
  • Loading branch information
dkmyta authored Jul 29, 2024
1 parent d4771e5 commit eb5cdb0
Show file tree
Hide file tree
Showing 25 changed files with 198 additions and 28 deletions.
4 changes: 4 additions & 0 deletions projects/packages/waf/changelog/add-protect-global-waf-stats
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
Significance: minor
Type: added

Adds global statistics
2 changes: 1 addition & 1 deletion projects/packages/waf/composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@
"link-template": "https://github.com/Automattic/jetpack-waf/compare/v${old}...v${new}"
},
"branch-alias": {
"dev-trunk": "0.17.x-dev"
"dev-trunk": "0.18.x-dev"
}
},
"config": {
Expand Down
88 changes: 88 additions & 0 deletions projects/packages/waf/src/class-waf-stats.php
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,13 @@
*/
class Waf_Stats {

/**
* The global stats cache
*
* @var array|null
*/
public static $global_stats = null;

/**
* Get IP allow list count
*
Expand Down Expand Up @@ -69,4 +76,85 @@ public static function get_rules_version() {
public static function get_automatic_rules_last_updated() {
return get_option( Waf_Rules_Manager::AUTOMATIC_RULES_LAST_UPDATED_OPTION_NAME );
}

/**
* Checks if the current cached global stats is expired and should be renewed
*
* @return boolean
*/
public static function is_global_stats_cache_expired() {
$option_timestamp = get_option( 'jetpack_protect_global_stats_timestamp' );

if ( ! $option_timestamp ) {
return true;
}

return time() > (int) $option_timestamp;
}

/**
* Checks if we should consider the stored cache or bypass it
*
* @return boolean
*/
public static function should_use_global_stats_cache() {
return ! ( defined( 'JETPACK_PROTECT_DEV__BYPASS_CACHE' ) && JETPACK_PROTECT_DEV__BYPASS_CACHE );
}

/**
* Get the global stats from the API endpoint
*/
public static function fetch_global_stats_from_api() {
$url = esc_url_raw( 'https://public-api.wordpress.com/wpcom/v2/jetpack-protect-global-stats' );
$response = wp_remote_get( $url );

$response_code = wp_remote_retrieve_response_code( $response );

if ( is_wp_error( $response ) || 200 !== $response_code || empty( $response['body'] ) ) {
return new \WP_Error( 'failed_fetching_global_stats', 'Failed to fetch global stats data from the server', array( 'status' => $response_code ) );
}

$body = json_decode( wp_remote_retrieve_body( $response ) );

update_option( 'jetpack_protect_global_stats', maybe_serialize( $body ) );
update_option( 'jetpack_protect_global_stats_timestamp', time() + 86400 ); // Caches expires after 24 hours.

return $body;
}

/**
* Gets the current cached global stats
*
* @return bool|array False if value is not found. Array with values if cache is found.
*/
public static function get_global_stats_from_options() {
return maybe_unserialize( get_option( 'jetpack_protect_global_stats' ) );
}

/**
* Get the global stats
* If the cache is expired, it will fetch the data from the API
* If the cache is not expired, it will return the cached data
*
* @param bool $refresh_from_wpcom Whether to refresh the data from the API.
* @return array|\WP_Error
*/
public static function get_global_stats( $refresh_from_wpcom = false ) {
if ( self::$global_stats !== null ) {
return self::$global_stats;
}

if ( $refresh_from_wpcom || ! self::should_use_global_stats_cache() || self::is_global_stats_cache_expired() ) {
$global_stats = self::fetch_global_stats_from_api();
} else {
$global_stats = self::get_global_stats_from_options();
}

// Ensure that $global_stats is the correct type
if ( ! is_array( $global_stats ) && ! ( $global_stats instanceof \WP_Error ) ) {
return new \WP_Error( 'unexpected_type', 'Unexpected type or null returned for global stats' );
}

return $global_stats;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
Significance: patch
Type: changed
Comment: Updated composer.lock.


4 changes: 2 additions & 2 deletions projects/plugins/backup/composer.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 5 additions & 0 deletions projects/plugins/boost/changelog/add-protect-global-waf-stats
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
Significance: patch
Type: changed
Comment: Updated composer.lock.


4 changes: 2 additions & 2 deletions projects/plugins/boost/composer.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
Significance: patch
Type: other
Comment: Updated composer.lock.


4 changes: 2 additions & 2 deletions projects/plugins/jetpack/composer.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
Significance: patch
Type: changed
Comment: Updated composer.lock.


4 changes: 2 additions & 2 deletions projects/plugins/migration/composer.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
Significance: minor
Type: added

Adds global statistics
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
Significance: patch
Type: changed
Comment: Updated composer.lock.


4 changes: 2 additions & 2 deletions projects/plugins/protect/composer.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions projects/plugins/protect/src/class-jetpack-protect.php
Original file line number Diff line number Diff line change
Expand Up @@ -453,6 +453,7 @@ public static function get_waf_stats() {
'ipAllowListCount' => Waf_Stats::get_ip_allow_list_count(),
'ipBlockListCount' => Waf_Stats::get_ip_block_list_count(),
'automaticRulesLastUpdated' => Waf_Stats::get_automatic_rules_last_updated(),
'globalStats' => Waf_Stats::get_global_stats(),
);
}
}
27 changes: 22 additions & 5 deletions projects/plugins/protect/src/js/components/scan-footer/index.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,12 @@ import {
Container,
} from '@automattic/jetpack-components';
import { useProductCheckoutWorkflow } from '@automattic/jetpack-connection';
import { __ } from '@wordpress/i18n';
import { __, sprintf } from '@wordpress/i18n';
import React from 'react';
import { JETPACK_SCAN_SLUG } from '../../constants';
import useAnalyticsTracks from '../../hooks/use-analytics-tracks';
import useProtectData from '../../hooks/use-protect-data';
import useWafData from '../../hooks/use-waf-data';
import SeventyFiveLayout from '../seventy-five-layout';
import styles from './styles.module.scss';

Expand Down Expand Up @@ -74,6 +75,12 @@ const ProductPromotion = () => {

const FooterInfo = () => {
const { hasRequiredPlan } = useProtectData();
const { stats } = useWafData();
const { globalStats } = stats;
const totalVulnerabilities = parseInt( globalStats?.totalVulnerabilities );
const totalVulnerabilitiesFormatted = isNaN( totalVulnerabilities )
? '50,000'
: totalVulnerabilities.toLocaleString();

if ( hasRequiredPlan ) {
const learnMoreScanUrl = getRedirectUrl( 'protect-footer-learn-more-scan' );
Expand All @@ -98,11 +105,21 @@ const FooterInfo = () => {

return (
<div className={ styles[ 'info-section' ] }>
<Title>{ __( 'Over 22,000 listed vulnerabilities', 'jetpack-protect' ) }</Title>
<Title>
{ sprintf(
// translators: placeholder is the number of total vulnerabilities i.e. "22,000".
__( 'Over %s listed vulnerabilities', 'jetpack-protect' ),
totalVulnerabilitiesFormatted
) }
</Title>
<Text mb={ 3 }>
{ __(
'Every day we check your plugin, theme, and WordPress versions against our 22,000 listed vulnerabilities powered by WPScan, an Automattic brand.',
'jetpack-protect'
{ sprintf(
// translators: placeholder is the number of total vulnerabilities i.e. "22,000".
__(
'Every day we check your plugin, theme, and WordPress versions against our %s listed vulnerabilities powered by WPScan, an Automattic brand.',
'jetpack-protect'
),
totalVulnerabilitiesFormatted
) }
</Text>

Expand Down
19 changes: 15 additions & 4 deletions projects/plugins/protect/src/js/routes/scan/index.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { AdminSectionHero, Container, Col, H3, Text } from '@automattic/jetpack-
import { useConnectionErrorNotice, ConnectionError } from '@automattic/jetpack-connection';
import { Spinner } from '@wordpress/components';
import { useSelect, useDispatch } from '@wordpress/data';
import { __ } from '@wordpress/i18n';
import { __, sprintf } from '@wordpress/i18n';
import React, { useEffect } from 'react';
import AdminPage from '../../components/admin-page';
import AlertSVGIcon from '../../components/alert-icon';
Expand All @@ -14,6 +14,7 @@ import ThreatsList from '../../components/threats-list';
import useAnalyticsTracks from '../../hooks/use-analytics-tracks';
import { OnboardingContext } from '../../hooks/use-onboarding';
import useProtectData from '../../hooks/use-protect-data';
import useWafData from '../../hooks/use-waf-data';
import { STORE_ID } from '../../state/store';
import inProgressImage from './in-progress.png';
import onboardingSteps from './onboarding-steps';
Expand All @@ -23,6 +24,12 @@ import useStatusPolling from './use-status-polling';

const ScanPage = () => {
const { lastChecked, currentStatus, errorCode, errorMessage, hasRequiredPlan } = useProtectData();
const { stats } = useWafData();
const { globalStats } = stats;
const totalVulnerabilities = parseInt( globalStats?.totalVulnerabilities );
const totalVulnerabilitiesFormatted = isNaN( totalVulnerabilities )
? '50,000'
: totalVulnerabilities.toLocaleString();
const { hasConnectionError } = useConnectionErrorNotice();
const { refreshStatus } = useDispatch( STORE_ID );
const { statusIsFetching, scanIsUnavailable, status } = useSelect( select => ( {
Expand Down Expand Up @@ -140,9 +147,13 @@ const ScanPage = () => {
<ProgressBar value={ currentProgress } />
) }
<Text>
{ __(
'We are scanning for security threats from our more than 22,000 listed vulnerabilities, powered by WPScan. This could take a minute or two.',
'jetpack-protect'
{ sprintf(
// translators: placeholder is the number of total vulnerabilities i.e. "22,000".
__(
'We are scanning for security threats from our more than %s listed vulnerabilities, powered by WPScan. This could take a minute or two.',
'jetpack-protect'
),
totalVulnerabilitiesFormatted
) }
</Text>
</Col>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
Significance: patch
Type: changed
Comment: Updated composer.lock.


4 changes: 2 additions & 2 deletions projects/plugins/search/composer.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
Significance: patch
Type: changed
Comment: Updated composer.lock.


4 changes: 2 additions & 2 deletions projects/plugins/social/composer.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading

0 comments on commit eb5cdb0

Please sign in to comment.