From 922fef5779e4581249df03ed993e7bd50fa9ff51 Mon Sep 17 00:00:00 2001 From: Nate Weller Date: Wed, 6 Nov 2024 13:36:15 -0700 Subject: [PATCH] Protect: Integrate ThreatsDataViews changelog --- pnpm-lock.yaml | 3 + .../protect/changelog/add-threats-data-views | 5 ++ projects/plugins/protect/package.json | 1 + .../protect/src/class-scan-history.php | 28 +--------- .../js/components/fix-threat-modal/index.jsx | 9 +-- .../components/ignore-threat-modal/index.jsx | 12 ++-- .../src/js/components/pricing-table/index.jsx | 4 +- .../js/components/threat-fix-header/index.jsx | 3 +- .../unignore-threat-modal/index.jsx | 18 +++--- .../src/js/data/scan/use-scan-status-query.ts | 2 +- .../src/js/data/use-connection-mutation.ts | 2 +- projects/plugins/protect/src/js/index.tsx | 7 +-- .../protect/src/js/routes/scan/index.jsx | 51 +++++++++++------ .../routes/scan/scan-admin-section-hero.tsx | 13 ++--- .../js/routes/scan/scan-results-data-view.tsx | 56 +++++++++++++++++++ .../src/js/routes/scan/styles.module.scss | 9 ++- .../plugins/protect/src/js/types/global.d.ts | 2 +- .../plugins/protect/src/js/types/threats.ts | 8 +++ projects/plugins/protect/webpack.config.js | 13 ++++- 19 files changed, 164 insertions(+), 82 deletions(-) create mode 100644 projects/plugins/protect/changelog/add-threats-data-views create mode 100644 projects/plugins/protect/src/js/routes/scan/scan-results-data-view.tsx diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 3cfbb06b35a04..79e9105160e67 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -4219,6 +4219,9 @@ importers: '@automattic/jetpack-connection': specifier: workspace:* version: link:../../js-packages/connection + '@automattic/jetpack-scan': + specifier: workspace:* + version: link:../../js-packages/scan '@tanstack/react-query': specifier: 5.20.5 version: 5.20.5(react@18.3.1) diff --git a/projects/plugins/protect/changelog/add-threats-data-views b/projects/plugins/protect/changelog/add-threats-data-views new file mode 100644 index 0000000000000..e15bd6a461a71 --- /dev/null +++ b/projects/plugins/protect/changelog/add-threats-data-views @@ -0,0 +1,5 @@ +Significance: minor +Type: changed + +Added DataViews component for viewing scan results. + diff --git a/projects/plugins/protect/package.json b/projects/plugins/protect/package.json index 6d6ad821bb3be..52366fa826ce8 100644 --- a/projects/plugins/protect/package.json +++ b/projects/plugins/protect/package.json @@ -29,6 +29,7 @@ "@automattic/jetpack-base-styles": "workspace:*", "@automattic/jetpack-components": "workspace:*", "@automattic/jetpack-connection": "workspace:*", + "@automattic/jetpack-scan": "workspace:*", "@tanstack/react-query": "5.20.5", "@tanstack/react-query-devtools": "5.20.5", "@wordpress/api-fetch": "7.11.0", diff --git a/projects/plugins/protect/src/class-scan-history.php b/projects/plugins/protect/src/class-scan-history.php index bd034c375caf9..ecabfd35d5e8c 100644 --- a/projects/plugins/protect/src/class-scan-history.php +++ b/projects/plugins/protect/src/class-scan-history.php @@ -213,37 +213,15 @@ public static function fetch_from_api() { * @return History_Model */ private static function normalize_api_data( $scan_data ) { - $history = new History_Model(); - $history->num_threats = 0; - $history->num_core_threats = 0; - $history->num_plugins_threats = 0; - $history->num_themes_threats = 0; - + $history = new History_Model(); $history->last_checked = $scan_data->last_checked; if ( empty( $scan_data->threats ) || ! is_array( $scan_data->threats ) ) { return $history; } - foreach ( $scan_data->threats as $threat ) { - if ( isset( $threat->extension->type ) ) { - if ( 'plugin' === $threat->extension->type ) { - self::handle_extension_threats( $threat, $history, 'plugin' ); - continue; - } - - if ( 'theme' === $threat->extension->type ) { - self::handle_extension_threats( $threat, $history, 'theme' ); - continue; - } - } - - if ( 'Vulnerable.WP.Core' === $threat->signature ) { - self::handle_core_threats( $threat, $history ); - continue; - } - - self::handle_additional_threats( $threat, $history ); + foreach ( $scan_data->threats as $source_threat ) { + $history->threats[] = new Threat_Model( $source_threat ); } return $history; diff --git a/projects/plugins/protect/src/js/components/fix-threat-modal/index.jsx b/projects/plugins/protect/src/js/components/fix-threat-modal/index.jsx index e1274e8e29a17..cbb49498c353f 100644 --- a/projects/plugins/protect/src/js/components/fix-threat-modal/index.jsx +++ b/projects/plugins/protect/src/js/components/fix-threat-modal/index.jsx @@ -7,7 +7,7 @@ import ThreatFixHeader from '../threat-fix-header'; import UserConnectionGate from '../user-connection-gate'; import styles from './styles.module.scss'; -const FixThreatModal = ( { id, fixable, label, icon, severity } ) => { +const FixThreatModal = ( { threat } ) => { const { setModal } = useModal(); const { fixThreats, isLoading: isFixersLoading } = useFixers(); @@ -21,7 +21,7 @@ const FixThreatModal = ( { id, fixable, label, icon, severity } ) => { const handleFixClick = () => { return async event => { event.preventDefault(); - await fixThreats( [ id ] ); + await fixThreats( [ threat.id ] ); setModal( { type: null } ); }; }; @@ -37,10 +37,7 @@ const FixThreatModal = ( { id, fixable, label, icon, severity } ) => {
- +
diff --git a/projects/plugins/protect/src/js/components/ignore-threat-modal/index.jsx b/projects/plugins/protect/src/js/components/ignore-threat-modal/index.jsx index 7e8113b6f38ab..591e90c6146b6 100644 --- a/projects/plugins/protect/src/js/components/ignore-threat-modal/index.jsx +++ b/projects/plugins/protect/src/js/components/ignore-threat-modal/index.jsx @@ -1,4 +1,5 @@ import { Button, getRedirectUrl, Text, ThreatSeverityBadge } from '@automattic/jetpack-components'; +import { getThreatIcon, getThreatSubtitle } from '@automattic/jetpack-scan'; import { createInterpolateElement, useState } from '@wordpress/element'; import { __ } from '@wordpress/i18n'; import { Icon } from '@wordpress/icons'; @@ -7,10 +8,11 @@ import useModal from '../../hooks/use-modal'; import UserConnectionGate from '../user-connection-gate'; import styles from './styles.module.scss'; -const IgnoreThreatModal = ( { id, title, label, icon, severity } ) => { +const IgnoreThreatModal = ( { threat } ) => { const { setModal } = useModal(); const ignoreThreatMutation = useIgnoreThreatMutation(); const codeableURL = getRedirectUrl( 'jetpack-protect-codeable-referral' ); + const icon = getThreatIcon( threat ); const [ isIgnoring, setIsIgnoring ] = useState( false ); @@ -25,7 +27,7 @@ const IgnoreThreatModal = ( { id, title, label, icon, severity } ) => { return async event => { event.preventDefault(); setIsIgnoring( true ); - await ignoreThreatMutation.mutateAsync( id ); + await ignoreThreatMutation.mutateAsync( threat.id ); setModal( { type: null } ); setIsIgnoring( false ); }; @@ -42,12 +44,12 @@ const IgnoreThreatModal = ( { id, title, label, icon, severity } ) => {
- { label } + { getThreatSubtitle( threat ) } - { title } + { threat.title }
- +
diff --git a/projects/plugins/protect/src/js/components/pricing-table/index.jsx b/projects/plugins/protect/src/js/components/pricing-table/index.jsx index 3edd7911a0b6a..0f857430d92d8 100644 --- a/projects/plugins/protect/src/js/components/pricing-table/index.jsx +++ b/projects/plugins/protect/src/js/components/pricing-table/index.jsx @@ -11,9 +11,9 @@ import { __ } from '@wordpress/i18n'; import React, { useCallback } from 'react'; import { useNavigate } from 'react-router-dom'; import useConnectSiteMutation from '../../data/use-connection-mutation'; +import useProductDataQuery from '../../data/use-product-data-query'; import useAnalyticsTracks from '../../hooks/use-analytics-tracks'; import usePlan from '../../hooks/use-plan'; -import useProtectData from '../../hooks/use-protect-data'; /** * Product Detail component. @@ -30,7 +30,7 @@ const ConnectedPricingTable = () => { } ); // Access paid protect product data - const { jetpackScan } = useProtectData(); + const { data: jetpackScan } = useProductDataQuery(); const { pricingForUi } = jetpackScan; const { introductoryOffer, currencyCode: currency = 'USD' } = pricingForUi; diff --git a/projects/plugins/protect/src/js/components/threat-fix-header/index.jsx b/projects/plugins/protect/src/js/components/threat-fix-header/index.jsx index bc5e0107cea80..d1cfd7dadd15a 100644 --- a/projects/plugins/protect/src/js/components/threat-fix-header/index.jsx +++ b/projects/plugins/protect/src/js/components/threat-fix-header/index.jsx @@ -1,4 +1,5 @@ import { Text, ThreatSeverityBadge } from '@automattic/jetpack-components'; +import { getThreatSubtitle } from '@automattic/jetpack-scan'; import { __, sprintf } from '@wordpress/i18n'; import { Icon } from '@wordpress/icons'; import React, { useState, useCallback } from 'react'; @@ -68,7 +69,7 @@ export default function ThreatFixHeader( { threat, fixAllDialog, onCheckFix } )
- { threat.label } + { getThreatSubtitle( threat ) } { getFixerMessage( threat.fixable ) } diff --git a/projects/plugins/protect/src/js/components/unignore-threat-modal/index.jsx b/projects/plugins/protect/src/js/components/unignore-threat-modal/index.jsx index 81f1eabb27d5b..7f1ef3652bb85 100644 --- a/projects/plugins/protect/src/js/components/unignore-threat-modal/index.jsx +++ b/projects/plugins/protect/src/js/components/unignore-threat-modal/index.jsx @@ -1,4 +1,5 @@ import { Button, Text, ThreatSeverityBadge } from '@automattic/jetpack-components'; +import { getThreatIcon, getThreatSubtitle } from '@automattic/jetpack-scan'; import { __ } from '@wordpress/i18n'; import { Icon } from '@wordpress/icons'; import { useState } from 'react'; @@ -7,9 +8,14 @@ import useModal from '../../hooks/use-modal'; import UserConnectionGate from '../user-connection-gate'; import styles from './styles.module.scss'; -const UnignoreThreatModal = ( { id, title, label, icon, severity } ) => { +const UnignoreThreatModal = ( { threat } ) => { const { setModal } = useModal(); + + const icon = getThreatIcon( threat ); + + const [ isUnignoring, setIsUnignoring ] = useState( false ); const unignoreThreatMutation = useUnIgnoreThreatMutation(); + const handleCancelClick = () => { return event => { event.preventDefault(); @@ -17,13 +23,11 @@ const UnignoreThreatModal = ( { id, title, label, icon, severity } ) => { }; }; - const [ isUnignoring, setIsUnignoring ] = useState( false ); - const handleUnignoreClick = () => { return async event => { event.preventDefault(); setIsUnignoring( true ); - await unignoreThreatMutation.mutateAsync( id ); + await unignoreThreatMutation.mutateAsync( threat.id ); setModal( { type: null } ); setIsUnignoring( false ); }; @@ -40,12 +44,12 @@ const UnignoreThreatModal = ( { id, title, label, icon, severity } ) => {
- { label } + { getThreatSubtitle( threat ) } - { title } + { threat.title }
- +
diff --git a/projects/plugins/protect/src/js/data/scan/use-scan-status-query.ts b/projects/plugins/protect/src/js/data/scan/use-scan-status-query.ts index 848b4395e6baf..22df11b48dd71 100644 --- a/projects/plugins/protect/src/js/data/scan/use-scan-status-query.ts +++ b/projects/plugins/protect/src/js/data/scan/use-scan-status-query.ts @@ -1,4 +1,5 @@ import { useConnection } from '@automattic/jetpack-connection'; +import { ScanStatus } from '@automattic/jetpack-scan'; import { useQuery, UseQueryResult, useQueryClient } from '@tanstack/react-query'; import camelize from 'camelize'; import API from '../../api'; @@ -7,7 +8,6 @@ import { SCAN_STATUS_IDLE, SCAN_STATUS_UNAVAILABLE, } from '../../constants'; -import { ScanStatus } from '../../types/scans'; import { QUERY_SCAN_STATUS_KEY } from './../../constants'; export const isRequestedScanNotStarted = ( status: ScanStatus ) => { diff --git a/projects/plugins/protect/src/js/data/use-connection-mutation.ts b/projects/plugins/protect/src/js/data/use-connection-mutation.ts index 90d8481f65edd..e8f40011336d0 100644 --- a/projects/plugins/protect/src/js/data/use-connection-mutation.ts +++ b/projects/plugins/protect/src/js/data/use-connection-mutation.ts @@ -1,4 +1,5 @@ import { useConnection } from '@automattic/jetpack-connection'; +import { ScanStatus } from '@automattic/jetpack-scan'; import { useMutation, UseMutationResult, useQueryClient } from '@tanstack/react-query'; import { __ } from '@wordpress/i18n'; import { @@ -10,7 +11,6 @@ import { SCAN_STATUS_OPTIMISTICALLY_SCANNING, } from '../constants'; import useNotices from '../hooks/use-notices'; -import { ScanStatus } from '../types/scans'; /** * Connect Site Mutation diff --git a/projects/plugins/protect/src/js/index.tsx b/projects/plugins/protect/src/js/index.tsx index b8983d65bb836..2b91f4b090b92 100644 --- a/projects/plugins/protect/src/js/index.tsx +++ b/projects/plugins/protect/src/js/index.tsx @@ -2,7 +2,7 @@ import { ThemeProvider } from '@automattic/jetpack-components'; import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; import { ReactQueryDevtools } from '@tanstack/react-query-devtools'; import * as WPElement from '@wordpress/element'; -import React, { useEffect } from 'react'; +import { useEffect } from 'react'; import { HashRouter, Routes, Route, useLocation, Navigate } from 'react-router-dom'; import Modal from './components/modal'; import PaidPlanGate from './components/paid-plan-gate'; @@ -12,7 +12,6 @@ import { OnboardingRenderedContextProvider } from './hooks/use-onboarding'; import { CheckoutProvider } from './hooks/use-plan'; import FirewallRoute from './routes/firewall'; import ScanRoute from './routes/scan'; -import ScanHistoryRoute from './routes/scan/history'; import SetupRoute from './routes/setup'; import './styles.module.scss'; @@ -62,7 +61,7 @@ function render() { path="/scan/history" element={ - + } /> @@ -70,7 +69,7 @@ function render() { path="/scan/history/:filter" element={ - + } /> diff --git a/projects/plugins/protect/src/js/routes/scan/index.jsx b/projects/plugins/protect/src/js/routes/scan/index.jsx index 1f3cdfdd7520f..95edd0acf78a6 100644 --- a/projects/plugins/protect/src/js/routes/scan/index.jsx +++ b/projects/plugins/protect/src/js/routes/scan/index.jsx @@ -1,14 +1,15 @@ import { AdminSection, Container, Col } from '@automattic/jetpack-components'; +import { useMemo } from 'react'; +import { useLocation, useParams } from 'react-router-dom'; import AdminPage from '../../components/admin-page'; -import ThreatsList from '../../components/threats-list'; import useScanStatusQuery from '../../data/scan/use-scan-status-query'; import useAnalyticsTracks from '../../hooks/use-analytics-tracks'; import { OnboardingContext } from '../../hooks/use-onboarding'; import usePlan from '../../hooks/use-plan'; -import useProtectData from '../../hooks/use-protect-data'; import onboardingSteps from './onboarding-steps'; import ScanAdminSectionHero from './scan-admin-section-hero'; import ScanFooter from './scan-footer'; +import ScanResultsDataView from './scan-results-data-view'; /** * Scan Page @@ -19,23 +20,39 @@ import ScanFooter from './scan-footer'; */ const ScanPage = () => { const { hasPlan } = usePlan(); - const { - counts: { - current: { threats: numThreats }, - }, - lastChecked, - } = useProtectData(); + const location = useLocation(); + const { filter } = useParams(); const { data: status } = useScanStatusQuery( { usePolling: true } ); let currentScanStatus; if ( status.error ) { currentScanStatus = 'error'; - } else if ( ! lastChecked ) { + } else if ( ! status.lastChecked ) { currentScanStatus = 'in_progress'; } else { currentScanStatus = 'active'; } + const filters = useMemo( () => { + if ( location.pathname.includes( '/scan/history' ) ) { + return [ + { + field: 'status', + value: filter ? [ filter ] : [ 'fixed', 'ignored' ], + operator: 'isAny', + }, + ]; + } + + return [ + { + field: 'status', + value: 'current', + operator: 'is', + }, + ]; + }, [ filter, location.pathname ] ); + // Track view for Protect admin page. useAnalyticsTracks( { pageViewEventName: 'protect_admin', @@ -49,15 +66,13 @@ const ScanPage = () => { - { ( ! status.error || numThreats ) && ( - - - - - - - - ) } + + + + + + + diff --git a/projects/plugins/protect/src/js/routes/scan/scan-admin-section-hero.tsx b/projects/plugins/protect/src/js/routes/scan/scan-admin-section-hero.tsx index 60d484cd4a16f..44babe871b0e2 100644 --- a/projects/plugins/protect/src/js/routes/scan/scan-admin-section-hero.tsx +++ b/projects/plugins/protect/src/js/routes/scan/scan-admin-section-hero.tsx @@ -8,28 +8,23 @@ import OnboardingPopover from '../../components/onboarding-popover'; import ScanNavigation from '../../components/scan-navigation'; import useScanStatusQuery, { isScanInProgress } from '../../data/scan/use-scan-status-query'; import usePlan from '../../hooks/use-plan'; -import useProtectData from '../../hooks/use-protect-data'; import ScanningAdminSectionHero from './scanning-admin-section-hero'; import styles from './styles.module.scss'; const ScanAdminSectionHero: React.FC = () => { const { hasPlan } = usePlan(); const [ isSm ] = useBreakpointMatch( 'sm' ); - const { - counts: { - current: { threats: numThreats }, - }, - lastChecked, - } = useProtectData(); + const { data: status } = useScanStatusQuery(); + const numThreats = status.threats.length; // Popover anchor const [ dailyScansPopoverAnchor, setDailyScansPopoverAnchor ] = useState( null ); let lastCheckedLocalTimestamp = null; - if ( lastChecked ) { + if ( status.lastChecked ) { // Convert the lastChecked UTC date to a local timestamp - lastCheckedLocalTimestamp = new Date( lastChecked + ' UTC' ).getTime(); + lastCheckedLocalTimestamp = new Date( status.lastChecked + ' UTC' ).getTime(); } if ( isScanInProgress( status ) ) { diff --git a/projects/plugins/protect/src/js/routes/scan/scan-results-data-view.tsx b/projects/plugins/protect/src/js/routes/scan/scan-results-data-view.tsx new file mode 100644 index 0000000000000..67dddc09e3243 --- /dev/null +++ b/projects/plugins/protect/src/js/routes/scan/scan-results-data-view.tsx @@ -0,0 +1,56 @@ +import { ThreatsDataViews } from '@automattic/jetpack-components'; +import { Threat } from '@automattic/jetpack-scan'; +import { useCallback } from 'react'; +import useHistoryQuery from '../../data/scan/use-history-query'; +import useScanStatusQuery from '../../data/scan/use-scan-status-query'; +import useModal from '../../hooks/use-modal'; + +/** + * Scan Results Data View + * + * @param {object} props - Component props. + * @param {Array} props.filters - Default filters to apply to the data view. + * + * @return {JSX.Element} ScanResultDataView component. + */ +export default function ScanResultsDataView( { + filters = [], +}: { + filters: React.ComponentProps< typeof ThreatsDataViews >[ 'filters' ]; +} ) { + const { setModal } = useModal(); + + const { data: scanStatus } = useScanStatusQuery(); + const { data: history } = useHistoryQuery(); + + const onFixThreats = useCallback( + ( threats: Threat[] ) => { + setModal( { type: 'FIX_THREAT', props: { threat: threats[ 0 ] } } ); + }, + [ setModal ] + ); + + const onIgnoreThreats = useCallback( + ( threats: Threat[] ) => { + setModal( { type: 'IGNORE_THREAT', props: { threat: threats[ 0 ] } } ); + }, + [ setModal ] + ); + + const onUnignoreThreats = useCallback( + ( threats: Threat[] ) => { + setModal( { type: 'UNIGNORE_THREAT', props: { threat: threats[ 0 ] } } ); + }, + [ setModal ] + ); + + return ( + + ); +} diff --git a/projects/plugins/protect/src/js/routes/scan/styles.module.scss b/projects/plugins/protect/src/js/routes/scan/styles.module.scss index 8651420159fa1..678b7a953f45e 100644 --- a/projects/plugins/protect/src/js/routes/scan/styles.module.scss +++ b/projects/plugins/protect/src/js/routes/scan/styles.module.scss @@ -9,4 +9,11 @@ .scan-navigation { margin-top: calc( var( --spacing-base ) * 3 ); // 24px -} \ No newline at end of file +} + +:global { + .dataviews-wrapper { + margin-left: -48px; + margin-right: -48px; + } +} diff --git a/projects/plugins/protect/src/js/types/global.d.ts b/projects/plugins/protect/src/js/types/global.d.ts index d844820616d06..2b30631ad540f 100644 --- a/projects/plugins/protect/src/js/types/global.d.ts +++ b/projects/plugins/protect/src/js/types/global.d.ts @@ -1,6 +1,6 @@ +import { ScanStatus } from '@automattic/jetpack-scan'; import { PluginData, ThemeData } from './installed-extensions'; import { ProductData } from './products'; -import { ScanStatus } from './scans'; import { WafStatus } from './waf'; declare module '*.scss'; diff --git a/projects/plugins/protect/src/js/types/threats.ts b/projects/plugins/protect/src/js/types/threats.ts index 757503972fa0c..b1c796df603b6 100644 --- a/projects/plugins/protect/src/js/types/threats.ts +++ b/projects/plugins/protect/src/js/types/threats.ts @@ -56,4 +56,12 @@ export type Threat = { /** The diff showing the threat's modified file contents. */ diff?: string; + + /** The affected plugin or theme. */ + extension?: { + name: string; + slug: string; + type: 'plugin' | 'theme'; + version: string; + }; }; diff --git a/projects/plugins/protect/webpack.config.js b/projects/plugins/protect/webpack.config.js index 2f6a45721b100..2f8b35ff00332 100644 --- a/projects/plugins/protect/webpack.config.js +++ b/projects/plugins/protect/webpack.config.js @@ -19,7 +19,18 @@ module.exports = [ ...jetpackWebpackConfig.resolve, }, node: false, - plugins: [ ...jetpackWebpackConfig.StandardPlugins() ], + plugins: [ + // Bundle @wordpress/components in the main bundle, to avoid conflicts with @wordpress/dataviews. + ...jetpackWebpackConfig.StandardPlugins( { + DependencyExtractionPlugin: { + requestToExternal( request ) { + if ( request === '@wordpress/components' ) { + return undefined; + } + }, + }, + } ), + ], module: { strictExportPresence: true, rules: [