Skip to content

Protect Dashboard: Add the Scan section - #53195

Merged
enejb merged 33 commits into
trunkfrom
add/protect-dashboard-scan
Oct 8, 2026
Merged

enejb merged 33 commits into
trunkfrom
add/protect-dashboard-scan

Conversation

@enejb

@enejb enejb commented Oct 6, 2026 •

Copy link
Copy Markdown
Member

Fixes JETPACK-2932

Part of JETPACK-2881, JETPACK-2880

Proposed changes

Adds the Scan section to the flag-gated Protect dashboard (jetpack-protect-dashboard flag, protect-dashboard module), in packages/protect.

Scan card (Overview)

  • Threats in a table: severity, the threat with the plugin's WordPress.org icon (or the theme's screenshot), when it was found, and actions. Plugins show their installed name rather than the slug Scan reports.
  • Active / Ignored tabs. Ignored threats come from Scan history (cached for 5 minutes) and need a Scan plan.
  • Row actions: Auto-fix when Scan has a fixer, and a ⋯ menu with View details, Update plugin/theme/WordPress, Deactivate plugin or Switch theme, View on WordPress.org, and Ignore/Unignore. The update and deactivate links go to core's own screens with nonces, only for users who can do that.
  • Scan now / Scan again now; while a scan runs the card shows only a progress line (Scan's reported percentage) and what is being checked. Polling backs off (5s, then 15s), pauses in background tabs, and offers "Check again" after about 10 minutes.
  • A safe state when nothing is found, and the free daily check's 24h gate on "Scan now" without a plan.

Threat details sidebar

  • Clicking a threat (its icon, title, or chevron) opens the route's inspector via ?threat=<id>: what Jetpack found, the file and flagged lines or the affected plugin, how to resolve it, and update/deactivate/WordPress.org links.
  • With a Scan plan: Ignore it / Auto-fix it, or Unignore it for an ignored threat. Fixes are followed until WordPress.com reports them done.
  • Ignore, unignore and fix progress and results show as core/notices snackbars; ignore and unignore offer Undo.

PHP

  • src/sections/class-scan.php: GET/POST jetpack/v4/protect-dashboard/scan (report, start a scan at most once a minute), POST …/threats/<id>/ignore|unignore, GET|POST …/threats/<id>/fix (start, then status), and GET …/scan/ignored. Fix and ignore routes need a Scan plan.
  • src/class-dashboard-threats.php adds the installed name, icon, admin action links, code context and vulnerability links to each threat, reading plugins and update checks once per list.
  • Sections can now declare an inspector ({ param, Panel }); Scan is the first.

Settings card: Scan has no settings here, so the card explains what runs and links to Scan management or the upgrade page.

Related product discussion/links

Does this pull request change what data or activity we track or use?

No.

Testing instructions

  1. On a connected site running this branch:
    • wp companion feature-flag enable jetpack-protect-dashboard (or the jetpack_feature_flag_enabled_jetpack-protect-dashboard filter)
    • wp jetpack module activate protect-dashboard
  2. Add threats: install, but don't activate, an old vulnerable plugin (e.g. Contact Form 7 5.3.1). With a Scan plan, the Jetpack threat tester plugin adds EICAR test files (deactivate it afterwards: its REST routes are unauthenticated). Run a scan.
  3. Go to Jetpack › Protect.
    • The table lists the threats with icons, real plugin names, severity and detection dates.
    • Click a threat's icon or title: the sidebar opens with its details. Close it with ×.
    • ⋯ → Update plugin goes to core's updater for that plugin; View on WordPress.org opens its directory page.
  4. With a Scan plan:
    • In the sidebar, Ignore it: an "Ignoring threat…" snackbar, then "Threat ignored." with Undo; the sidebar marks the threat Ignored and offers Unignore it; the threat moves to the Ignored tab. Undo moves it back.
    • Auto-fix it on a fixable threat (e.g. Contact Form 7, which Scan updates): "Fixing threat…", then "Threat fixed." and the threat leaves the list. Note that Scan's fixer for some vulnerable plugins and themes is delete.
    • Scan now: the card switches to the scanning state straight away, with a progress line, until the scan finishes.
  5. Without a Scan plan: the badge reads "Vulnerability checks only", there is no Ignored tab, no Auto-fix, no Ignore/Unignore in the ⋯ menu, and no Ignore/Auto-fix footer in the sidebar. "Scan now" is disabled while the free daily report is under 24h old, with a popover saying when the next check can run.
  6. Error path: block requests to public-api.wordpress.com (e.g. a pre_http_request filter returning a WP_Error) and reload: the card shows "We couldn’t check your site…" with "Try again". Ignore/fix failures show as error snackbars.
  7. Settings tab: the Scan card explains what runs and links to Scan management or the upgrade page.

Tip: to try the free path on a site that has a plan, a temporary mu-plugin with a pre_option_jetpack_active_plan filter returning a free plan works; delete it afterwards and run wp eval 'Automattic\Jetpack\Current_Plan::refresh_from_wpcom();' to restore the cached plan.

Review follow-ups

Fixed in the latest round:

  • The once-a-minute scan request lock is claimed before calling WordPress.com (atomically via wp_cache_add() when there is a persistent object cache) and released if the request fails.
  • "Scan now" switches to the scanning state immediately from the plan the card already has, and reverts if the request fails.
  • Ignore/Unignore (and Auto-fix) only appear in the row menu when the site has a Scan plan; free reports can't be ignored.
  • Dashboard_Test clears the real sections Dashboard::init() now registers.
  • PHP 8.5: no null array offset when a threat has no plugin slug.
  • New tests: the Scan section's admin-only gate, its request lock (a held lock refuses a second claim; a released one allows a retry), how a fix status is read from WordPress.com's response, and the row titles.

Coverage: the rest of class-scan.php proxies WordPress.com (scan, fix, ignore, history) and the rest of the new JS is React views; both were exercised on a live Scan-plan site (screenshots and recording below) rather than mocked in unit tests, hence Covered by non-unit tests.

Deferred:

  • A local "Delete file" fix for file threats Scan has no fixer for.
  • Moving the WordPress.com alerts and history calls into protect-status, shared with the Protect plugin.
  • Page render can still wait on a WordPress.com request when the report cache is cold: same behaviour as the Protect plugin's own page.

Screenshots

Captured on a Jurassic Ninja site with Jetpack Scan at Jetpack › Protect, 1440px wide. Threats come from the Jetpack threat tester, a few known-vulnerable plugins, and a malicious wp_options row.

Screen recording: ignore a threat and undo it, then auto-fix a vulnerable theme.

scan-recording.mov

Scan card: threats in a table with WordPress.org plugin icons, Active / Ignored views, and a row menu (view, update, deactivate, ignore).

Scan card with eight threats in a table

Details sidebar for a file threat: what Jetpack found, the file and the flagged lines, and how to resolve it.

Threat details sidebar for malicious code in a file

Details sidebar for a vulnerable plugin: links to update the plugin or view it on WordPress.org, plus Ignore it and Auto-fix it.

Threat details sidebar for a vulnerable plugin

After ignoring a threat: the sidebar marks it Ignored and offers Unignore it, and a snackbar offers Undo.

Ignored threat in the sidebar with an Undo snackbar

Ignored view:

Ignored tab listing one ignored threat

While a scan runs: Scan's reported progress, and what is being checked.

Scanning state with a progress bar

Settings → Scan card (unchanged):

Scan settings card

@enejb enejb self-assigned this Oct 6, 2026
@github-actions

github-actions Bot commented Oct 6, 2026 •

Copy link
Copy Markdown
Contributor

Are you an Automattician? Please test your changes on all WordPress.com environments to help mitigate accidental explosions.

  • To test on WoA, go to the Plugins menu on a WoA dev site. Click on the "Upload" button and follow the upgrade flow to be able to upload, install, and activate the Jetpack Beta plugin. Once the plugin is active, go to Jetpack > Jetpack Beta, select your plugin (Jetpack or WordPress.com Site Helper), and enable the add/protect-dashboard-scan branch.
  • To test on Simple, run the following command on your sandbox:
bin/jetpack-downloader test jetpack add/protect-dashboard-scan
bin/jetpack-downloader test jetpack-mu-wpcom-plugin add/protect-dashboard-scan

Interested in more tips and information?

  • In your local development environment, use the jetpack rsync command to sync your changes to a WoA dev blog.
  • Read more about our development workflow here: PCYsg-eg0-p2
  • Figure out when your changes will be shipped to customers here: PCYsg-eg5-p2

@github-actions github-actions Bot added [Feature] Protect Dashboard [Plugin] Jetpack Issues about the Jetpack plugin. https://wordpress.org/plugins/jetpack/ labels Oct 6, 2026
@github-actions

github-actions Bot commented Oct 6, 2026 •

Copy link
Copy Markdown
Contributor

Thank you for your PR!

When contributing to Jetpack, we have a few suggestions that can help us test and review your patch:

  • ✅ Include a description of your PR changes.
  • ✅ Add a "[Status]" label (In Progress, Needs Review, ...).
  • ✅ Add testing instructions.
  • ✅ Specify whether this PR includes any changes to data or privacy.
  • ✅ Add changelog entries to affected projects

This comment will be updated as you work on your PR and make changes. If you think that some of those checks are not needed for your PR, please explain why you think so. Thanks for cooperation 🤖


Follow this PR Review Process:

  1. Ensure all required checks appearing at the bottom of this PR are passing.
  2. Make sure to test your changes on all platforms that it applies to. You're responsible for the quality of the code you ship.
  3. You can use GitHub's Reviewers functionality to request a review.
  4. When it's reviewed and merged, you will be pinged in Slack to deploy the changes to WordPress.com simple once the build is done.

If you have questions about anything, reach out in #jetpack-developers for guidance!


Jetpack plugin:

No scheduled milestone found for this plugin.

If you have any questions about the release process, please ask in the #jetpack-releases channel on Slack.

@jp-launch-control

jp-launch-control Bot commented Oct 6, 2026 •

Copy link
Copy Markdown

Code Coverage Summary

Coverage changed in 3 files.

File Coverage Δ% Δ Uncovered
projects/packages/protect/src/class-dashboard-threats.php 90/118 (76.27%) -16.32% 26 💔
projects/packages/protect/routes/dashboard/sections/scan/index.tsx 2/2 (100.00%) 0.00% 0 💚
projects/packages/protect/routes/dashboard/components/overview.tsx 5/6 (83.33%) 16.67% -1 💚

17 files are newly checked for coverage. Only the first 5 are listed here.

File Coverage
projects/packages/protect/routes/dashboard/inspector.tsx 0/3 (0.00%) 💔
projects/packages/protect/routes/dashboard/route.tsx 0/2 (0.00%) 💔
projects/packages/protect/routes/dashboard/sections/inspector-params.ts 0/1 (0.00%) 💔
projects/packages/protect/routes/dashboard/sections/scan/inspector.tsx 0/6 (0.00%) 💔
projects/packages/protect/routes/dashboard/sections/scan/scanning-state.tsx 0/3 (0.00%) 💔

Full summary · PHP report · JS report

Coverage check overridden by Covered by non-unit tests Use to ignore the Code coverage requirement check when E2Es or other non-unit tests cover the code .

@enejb
enejb force-pushed the add/protect-dashboard-scan branch from aabb392 to f445192 Compare October 6, 2026 01:11
With the protect-dashboard module active and no standalone plugin, the link went to protect-details or Jetpack Cloud instead of the Protect page.
… retry

A failed save no longer resets the IP list field to the saved value, and a
failed settings load now clears its error on retry and offers a Try again
button. Section types take an optional state type, the tab search param is
narrowed at runtime, and the settings hook returns a stable object.
ProtectCard and CardRow keep their props and now render through Card from
the WordPress UI package, so border, radius, background and header spacing
come from the design system.
… section registry

Adds routes/ to the client Jest roots so route tests run.
The base branch added a Jetpack_Protect_Dashboard_Test class of its own, so
the section registry tests now live in that class and the threats test moves
into the same suite directory.
My Jetpack no longer depends on the feature flags package, so the Jetpack plugin now reports the flag through jetpack_my_jetpack_protect_in_jetpack.
ProtectCard now wraps its children in Card.Content, so the stats grid
and the threat list picked up the card's inline padding: their dividers
stopped short of the card edges and the stats were padded twice. Render
both as CardRow so they bleed like every other row, and pad the stats
with the card's own padding so they line up with the header.
Pass ScanState to ProtectSection and DashboardContext now that they
take a state type, and drop the casts.
@enejb
enejb force-pushed the add/protect-dashboard-foundation branch 3 times, most recently from b531356 to 6567e4b Compare October 8, 2026 14:23
Base automatically changed from add/protect-dashboard-foundation to trunk October 8, 2026 15:34
…d-scan

# Conflicts:
#	projects/packages/my-jetpack/src/class-main-features.php
#	projects/packages/my-jetpack/src/products/class-protect.php
#	projects/packages/my-jetpack/tests/php/Protect_Product_Test.php
#	projects/plugins/jetpack/_inc/lib/class-jetpack-protect-dashboard-feature-flags.php
#	projects/plugins/jetpack/composer.json
#	projects/plugins/jetpack/composer.lock
#	projects/plugins/jetpack/modules/protect-dashboard.php
#	projects/plugins/jetpack/tests/php/_inc/lib/Jetpack_Protect_Dashboard_Feature_Flags_Test.php
@enejb
enejb marked this pull request as ready for review October 8, 2026 19:50
@enejb
enejb requested review from kraftbj and a balanced review from Copilot October 8, 2026 19:50

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

Scan startup, free-plan action gating, request throttling, and the documented disclosure behavior have unresolved defects.

4 open findings
What changed in this PR

Adds the Scan section to the feature-gated Protect dashboard, including reporting, scan controls, and threat management.

Changes:

  • Adds PHP Scan REST endpoints and threat formatting.
  • Adds Scan overview/settings UI, polling, threat details, and actions.
  • Adds dependencies, tests, styles, and changelogs.
File Description
projects/​plugins/​jetpack/​changelog/​add-protect-dashboard-scan Records the Jetpack-facing change.
projects/​packages/​protect/​tests/​php/​Dashboard_Threats_Test.php Tests threat formatting and icons.
projects/​packages/​protect/​src/​sections/​class-scan.php Implements Scan state and REST endpoints.
projects/​packages/​protect/​src/​class-dashboard-threats.php Enriches formatted threat data.
projects/​packages/​protect/​routes/​dashboard/​sections/​types.ts Adds inspector support to sections.
projects/​packages/​protect/​routes/​dashboard/​sections/​scan/​types.ts Defines Scan state and threat types.
projects/​packages/​protect/​routes/​dashboard/​sections/​scan/​threats-list.tsx Renders threat tables and actions.
projects/​packages/​protect/​routes/​dashboard/​sections/​scan/​threat-media.tsx Renders threat icons and images.
projects/​packages/​protect/​routes/​dashboard/​sections/​scan/​threat-details.tsx Implements threat details and controls.
projects/​packages/​protect/​routes/​dashboard/​sections/​scan/​threat-actions.ts Handles fix and ignore operations.
projects/​packages/​protect/​routes/​dashboard/​sections/​scan/​test/​threat-actions.test.ts Tests ignore and undo behavior.
projects/​packages/​protect/​routes/​dashboard/​sections/​scan/​style.scss Styles Scan cards and threat UI.
projects/​packages/​protect/​routes/​dashboard/​sections/​scan/​store.ts Shares Scan and inspector state.
projects/​packages/​protect/​routes/​dashboard/​sections/​scan/​settings-card.tsx Adds the Scan settings card.
projects/​packages/​protect/​routes/​dashboard/​sections/​scan/​scanning-state.tsx Displays scanning progress.
projects/​packages/​protect/​routes/​dashboard/​sections/​scan/​scan-card.tsx Implements the overview Scan card.
projects/​packages/​protect/​routes/​dashboard/​sections/​scan/​scan-button.tsx Controls scan availability and explanation.
projects/​packages/​protect/​routes/​dashboard/​sections/​scan/​safe-state.tsx Displays the no-threat state.
projects/​packages/​protect/​routes/​dashboard/​sections/​scan/​labels.ts Provides localized Scan labels.
projects/​packages/​protect/​routes/​dashboard/​sections/​scan/​inspector.tsx Selects inspector threat details.
projects/​packages/​protect/​routes/​dashboard/​sections/​scan/​index.tsx Registers Scan dashboard components.
projects/​packages/​protect/​routes/​dashboard/​sections/​inspector-params.ts Lists inspector URL parameters.
projects/​packages/​protect/​routes/​dashboard/​route.tsx Activates the route inspector.
projects/​packages/​protect/​routes/​dashboard/​package.json Adds data and notices dependencies.
projects/​packages/​protect/​routes/​dashboard/​inspector.tsx Renders the selected section inspector.
projects/​packages/​protect/​package.json Declares package dependencies.
projects/​packages/​protect/​changelog/​add-protect-dashboard-scan Records the package feature.
pnpm-lock.yaml Locks the added dependencies.
Files not reviewed (1)
  • pnpm-lock.yaml: Generated file

🧠 Review effort: Balanced


💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment on lines +361 to +365
if ( $has_plan && ! get_transient( self::REQUEST_LOCK ) ) {
$api_url = Scan_Status::get_api_url();
$response = is_wp_error( $api_url )
? $api_url
: Client::wpcom_json_api_request_as_blog( $api_url . '/enqueue', '2', array( 'method' => 'POST' ), null, 'wpcom' );

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 216aa3d: the lock is claimed before the WordPress.com request and released if it fails. With a persistent object cache it uses wp_cache_add(), which is atomic; without one, transients can't be made atomic, but claiming first shrinks the race from a WordPress.com round trip to a few microseconds.

Comment on lines +99 to +115
const startScan = useCallback( () => {
const requested = Date.now();
setIsStarting( true );
setStartError( null );
apiFetch< ScanState >( { path: SCAN_PATH, method: 'POST' } )
.then( next => {
if ( next.hasPlan && ! next.error ) {
setRequestedAt( requested );
}
setPolls( 0 );
mergeScan( next );
} )
.catch( ( e: { message?: string } ) =>
setStartError( e?.message || __( 'The scan couldn’t be started.', 'jetpack-protect-pkg' ) )
)
.finally( () => setIsStarting( false ) );
}, [] );

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 216aa3d: "Scan now" sets the requested-at time from the plan the card already has, before the request, and clears it if the request fails.

Comment on lines +191 to +196
<CardRow className="jp-protect-card__threats">
<ThreatsList
threats={ threats }
ignored={ ignored }
empty={ <SafeState scan={ scan } isStarting={ isStarting } onScan={ startScan } /> }
/>

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The disclosure was removed on purpose: the threats now always show in a table, with a details sidebar. The PR description was stale, and I've updated it to match.

Comment on lines +111 to +118
<Menu.Separator />
<Menu.Item onClick={ onToggleIgnore }>
<Menu.ItemLabel>
{ isIgnored
? __( 'Unignore threat', 'jetpack-protect-pkg' )
: __( 'Ignore threat', 'jetpack-protect-pkg' ) }
</Menu.ItemLabel>
</Menu.Item>

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 216aa3d: ThreatsList takes canAct (the Scan plan), and the separator plus Ignore/Unignore item, and the row's Auto-fix button, only render when it's true.

@enejb enejb added the Covered by non-unit tests Use to ignore the Code coverage requirement check when E2Es or other non-unit tests cover the code label Oct 8, 2026
@enejb
enejb merged commit 7545a4e into trunk Oct 8, 2026
122 checks passed
@enejb
enejb deleted the add/protect-dashboard-scan branch October 8, 2026 21:46
@github-actions github-actions Bot added this to the jetpack/16.4 milestone Oct 8, 2026
@github-actions github-actions Bot added [Status] UI Changes Add this to PRs that change the UI so documentation can be updated. and removed [Status] In Progress labels Oct 8, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Covered by non-unit tests Use to ignore the Code coverage requirement check when E2Es or other non-unit tests cover the code [Feature] Protect Dashboard [Package] My Jetpack [Package] Protect [Plugin] Jetpack Issues about the Jetpack plugin. https://wordpress.org/plugins/jetpack/ [Status] UI Changes Add this to PRs that change the UI so documentation can be updated. [Tests] Includes Tests

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants