diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index deb4ef0436cf1..106bd39765d56 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1172,6 +1172,9 @@ importers: '@wordpress/i18n': specifier: 5.9.0 version: 5.9.0 + '@wordpress/icons': + specifier: 10.9.0 + version: 10.9.0(react@18.3.1) '@wordpress/url': specifier: 4.9.0 version: 4.9.0 @@ -4204,6 +4207,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) @@ -7830,12 +7836,6 @@ packages: peerDependencies: react: ^18.0.0 - '@wordpress/dataviews@4.4.4': - resolution: {integrity: sha512-b+2DTP8uPznxpnD0khRHDUeuj3U5Cy32amr3vwiN9xqV9hl51fzSe+ELAUTHrFKlMaQNkH/0c8cH81fU0JIeuw==} - engines: {node: '>=18.12.0', npm: '>=8.19.2'} - peerDependencies: - react: ^18.0.0 - '@wordpress/dataviews@4.5.0': resolution: {integrity: sha512-3vZN6jFR6gFDvuAitpS/0D80ByWYkhRfuTAAmzptq6rC9CkC4VNRbIJZbxMsKEt2qh44T7TVYVR6yVm2p+8+oQ==} engines: {node: '>=18.12.0', npm: '>=8.19.2'} @@ -18786,28 +18786,6 @@ snapshots: rememo: 4.0.2 use-memo-one: 1.1.3(react@18.3.1) - '@wordpress/dataviews@4.4.4(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': - dependencies: - '@ariakit/react': 0.4.12(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@babel/runtime': 7.24.7 - '@wordpress/components': 28.9.0(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@wordpress/compose': 7.9.0(react@18.3.1) - '@wordpress/data': 10.9.0(react@18.3.1) - '@wordpress/element': 6.9.0 - '@wordpress/i18n': 5.9.0 - '@wordpress/icons': 10.9.0(react@18.3.1) - '@wordpress/primitives': 4.9.0(react@18.3.1) - '@wordpress/private-apis': 1.9.0 - '@wordpress/warning': 3.9.0 - clsx: 2.1.1 - react: 18.3.1 - remove-accents: 0.5.0 - transitivePeerDependencies: - - '@emotion/is-prop-valid' - - '@types/react' - - react-dom - - supports-color - '@wordpress/dataviews@4.5.0(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: '@ariakit/react': 0.4.12(react-dom@18.3.1(react@18.3.1))(react@18.3.1) @@ -19411,9 +19389,9 @@ snapshots: '@wordpress/icons@10.9.0(react@18.3.1)': dependencies: - '@babel/runtime': 7.24.7 + '@babel/runtime': 7.25.7 '@wordpress/element': 6.9.0 - '@wordpress/primitives': 4.9.0(react@18.3.1) + '@wordpress/primitives': 4.10.0(react@18.3.1) react: 18.3.1 '@wordpress/interactivity-router@2.9.0': diff --git a/projects/js-packages/components/components/threats-data-view/constants.ts b/projects/js-packages/components/components/threats-data-view/constants.ts deleted file mode 100644 index e9b2fea3e1db5..0000000000000 --- a/projects/js-packages/components/components/threats-data-view/constants.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { __ } from '@wordpress/i18n'; - -export const PAID_PLUGIN_SUPPORT_URL = 'https://jetpack.com/contact-support/?rel=support'; - -export const THREAT_STATUSES = [ - { value: 'current', label: __( 'Active', 'jetpack' ) }, - { value: 'fixed', label: __( 'Fixed', 'jetpack' ) }, - { value: 'ignored', label: __( 'Ignored', 'jetpack' ) }, -]; - -export const THREAT_TYPES = [ - { value: 'plugin', label: __( 'Plugin', 'jetpack' ) }, - { value: 'theme', label: __( 'Theme', 'jetpack' ) }, - { value: 'core', label: __( 'WordPress', 'jetpack' ) }, - { value: 'file', label: __( 'File', 'jetpack' ) }, - { value: 'database', label: __( 'Database', 'jetpack' ) }, -]; diff --git a/projects/js-packages/components/components/threats-data-view/fixer-status.tsx b/projects/js-packages/components/components/threats-data-view/fixer-status.tsx deleted file mode 100644 index 89c68bc56b4cf..0000000000000 --- a/projects/js-packages/components/components/threats-data-view/fixer-status.tsx +++ /dev/null @@ -1,156 +0,0 @@ -import { ExternalLink, Spinner } from '@wordpress/components'; -import { View } from '@wordpress/dataviews'; -import { createInterpolateElement } from '@wordpress/element'; -import { __ } from '@wordpress/i18n'; -import { Icon } from '@wordpress/icons'; -import { check, info } from '@wordpress/icons'; -import { PAID_PLUGIN_SUPPORT_URL } from './constants'; -import IconTooltip from './icon-tooltip'; -import styles from './styles.module.scss'; -import { ThreatFixStatus } from './types'; -import { fixerStatusIsStale } from './utils'; - -/** - * Fixer Status component. - * - * @param {object} props - Component props. - * @param {boolean} props.fixer - The fixer status. - * @param {number} props.size - The size of the icon. - * - * @return {JSX.Element} The component. - */ -export default function FixerStatusIcon( { - fixer, - size = 24, -}: { - fixer?: ThreatFixStatus; - size?: number; -} ): JSX.Element { - if ( fixer && fixerStatusIsStale( fixer ) ) { - return ( - contact support.', - 'jetpack' - ), - { - supportLink: ( - - ), - } - ) } - /> - ); - } - - if ( fixer && 'error' in fixer && fixer.error ) { - return ( - contact support.', - 'jetpack' - ), - { - supportLink: ( - - ), - } - ) } - /> - ); - } - - if ( fixer && 'status' in fixer && fixer.status === 'in_progress' ) { - return ( -
- -
- ); - } - - return ; -} - -/** - * FixerStatusText component. - * @param {object} props - Component props. - * @param {boolean} props.fixer - The fixer status. - * @return {string} The component. - */ -function FixerStatusText( { fixer }: { fixer?: ThreatFixStatus } ): JSX.Element { - if ( fixer && fixerStatusIsStale( fixer ) ) { - return ( - - { __( 'Fixer is taking longer than expected', 'jetpack' ) } - - ); - } - - if ( fixer && 'error' in fixer && fixer.error ) { - return ( - - { __( 'An error occurred auto-fixing this threat', 'jetpack' ) } - - ); - } - - if ( fixer && 'status' in fixer && fixer.status === 'in_progress' ) { - return { __( 'Auto-fixing', 'jetpack' ) }; - } - - return { __( 'Auto-fixable', 'jetpack' ) }; -} - -/** - * FixerStatusBadge component. - * @param {object} props - Component props. - * @param {boolean} props.fixer - The fixer status. - * @return {string} The component. - */ -export function FixerStatusBadge( { fixer }: { fixer?: ThreatFixStatus } ): JSX.Element { - return ( -
- - -
- ); -} - -/** - * DataViewFixerStatus component. - * @param {object} props - Component props. - * @param {boolean} props.fixer - The fixer status. - * @param {object} props.view - The view. - * @return {string} The component. - */ -export function DataViewFixerStatus( { - fixer, - view, -}: { - fixer?: ThreatFixStatus; - view: View; -} ): JSX.Element { - if ( view.type === 'table' ) { - return ( -
- -
- ); - } - - return ; -} diff --git a/projects/js-packages/components/components/threats-data-view/icon-tooltip.tsx b/projects/js-packages/components/components/threats-data-view/icon-tooltip.tsx deleted file mode 100644 index 32699631c9807..0000000000000 --- a/projects/js-packages/components/components/threats-data-view/icon-tooltip.tsx +++ /dev/null @@ -1,52 +0,0 @@ -import { Text } from '@automattic/jetpack-components'; -import { Popover } from '@wordpress/components'; -import { Icon } from '@wordpress/icons'; -import React, { useCallback, useState } from 'react'; -import styles from './styles.module.scss'; - -const IconTooltip = ( { icon, iconClassName, iconSize, popoverPosition = 'top', text } ) => { - const [ showPopover, setShowPopover ] = useState( false ); - const [ timeoutId, setTimeoutId ] = useState( null ); - - const handleEnter = useCallback( () => { - // Clear any existing timeout if user hovers back quickly - if ( timeoutId ) { - clearTimeout( timeoutId ); - setTimeoutId( null ); - } - setShowPopover( true ); - }, [ timeoutId ] ); - - const handleOut = useCallback( () => { - // Set a timeout to delay the hiding of the popover - const id = setTimeout( () => { - setShowPopover( false ); - setTimeoutId( null ); // Clear the timeout ID after the popover is hidden - }, 100 ); - - setTimeoutId( id ); - }, [] ); - - return ( -
- - { showPopover && ( - - - { text } - - - ) } -
- ); -}; - -export default IconTooltip; diff --git a/projects/js-packages/components/components/threats-data-view/index.tsx b/projects/js-packages/components/components/threats-data-view/index.tsx deleted file mode 100644 index 2377e7a3a12cd..0000000000000 --- a/projects/js-packages/components/components/threats-data-view/index.tsx +++ /dev/null @@ -1,446 +0,0 @@ -import { Icon } from '@wordpress/components'; -import { - Action, - DataViews, - Field, - Filter, - filterSortAndPaginate, - SortDirection, - SupportedLayouts, - type View, -} from '@wordpress/dataviews'; -import { __, _x } from '@wordpress/i18n'; -import { useCallback, useMemo, useState } from 'react'; -import Badge from '../badge'; -import { THREAT_STATUSES, THREAT_TYPES } from './constants'; -import { DataViewFixerStatus } from './fixer-status'; -import styles from './styles.module.scss'; -import { DataViewThreat, ThreatsDataViewActionCallback } from './types'; -import { getThreatIcon, getThreatSubtitle, getThreatType } from './utils'; - -/** - * DataView component for displaying security threats. - * - * @param {object} props - Component props. - * @param {Array} props.data - Threats data. - * @param {Array} props.filters - Initial DataView filters. - * @param {Function} props.onChangeSelection - Callback function run when an item is selected. - * @param {Function} props.onFixThreat - Threat fix action callback. - * @param {Function} props.onIgnoreThreat - Threat ignore action callback. - * @param {Function} props.onUnignoreThreat - Threat unignore action callback. - * @param {Function} props.isThreatEligibleForFix - Function to determine if a threat is eligible for fixing. - * @param {Function} props.isThreatEligibleForIgnore - Function to determine if a threat is eligible for ignoring. - * @param {Function} props.isThreatEligibleForUnignore - Function to determine if a threat is eligible for unignoring. - * @return {JSX.Element} The component. - */ -export default function ThreatsDataView( { - data, - filters, - onChangeSelection, - isThreatEligibleForFix, - isThreatEligibleForIgnore, - isThreatEligibleForUnignore, - onFixThreat, - onIgnoreThreat, - onUnignoreThreat, -}: { - data: DataViewThreat[]; - filters?: Filter[]; - onChangeSelection?: ( selectedItemIds: string[] ) => void; - isThreatEligibleForFix?: ( threat: DataViewThreat ) => boolean; - isThreatEligibleForIgnore?: ( threat: DataViewThreat ) => boolean; - isThreatEligibleForUnignore?: ( threat: DataViewThreat ) => boolean; - onFixThreat?: ThreatsDataViewActionCallback; - onIgnoreThreat?: ThreatsDataViewActionCallback; - onUnignoreThreat?: ThreatsDataViewActionCallback; -} ): JSX.Element { - const baseView = { - sort: { - field: 'severity', - direction: 'desc' as SortDirection, - }, - search: '', - filters: filters || [], - page: 1, - perPage: 25, - }; - - /** - * DataView default layouts. - * - * This property provides layout information about the view types that are active. If empty, enables all layout types (see “Layout Types”) with empty layout data. - * - * @see https://developer.wordpress.org/block-editor/reference-guides/packages/packages-dataviews/#defaultlayouts-record-string-view - */ - const defaultLayouts: SupportedLayouts = { - table: { - ...baseView, - fields: [ 'severity', 'threat', 'auto-fix' ], - layout: { - primaryField: 'severity', - combinedFields: [ - { - id: 'threat', - label: __( 'Threat', 'jetpack' ), - children: [ 'subtitle', 'title', 'description' ], - direction: 'vertical', - }, - ], - }, - }, - list: { - ...baseView, - fields: [ 'severity', 'subtitle', 'signature', 'auto-fix' ], - layout: { - primaryField: 'title', - mediaField: 'icon', - }, - }, - }; - - /** - * DataView view object - configures how the dataset is visible to the user. - * - * @see https://developer.wordpress.org/block-editor/reference-guides/packages/packages-dataviews/#view-object - */ - const [ view, setView ] = useState< View >( { - type: 'table', - ...defaultLayouts.table, - } ); - - /** - * Compute values based on the threats data. - * - * @member {object} extensions - List of unique threat extensions. - * @member {object} signatures - List of unique threat signatures. - * @member {Array} dataFields - List of unique fields. - */ - const { extensions, signatures, dataFields } = useMemo( () => { - return data.reduce( - ( acc, threat ) => { - // Extensions - if ( threat?.extension ) { - if ( ! acc.extensions.find( ( { value } ) => value === threat.extension.slug ) ) { - acc.extensions.push( { value: threat.extension.slug, label: threat.extension.name } ); - } - } - - // Signatures - if ( threat?.signature ) { - if ( ! acc.signatures.find( ( { value } ) => value === threat.signature ) ) { - acc.signatures.push( { value: threat.signature, label: threat.signature } ); - } - } - - // Fields - const fields = Object.keys( threat ); - fields.forEach( field => { - if ( - ! acc.dataFields.includes( field ) && - threat[ field ] !== null && - threat[ field ] !== undefined - ) { - acc.dataFields.push( field ); - } - } ); - - return acc; - }, - { - extensions: [] as { value: string; label: string }[], - signatures: [] as { value: string; label: string }[], - dataFields: [] as string[], - } - ); - }, [ data ] ); - - /** - * DataView fields - describes the visible items for each record in the dataset. - * - * @see https://developer.wordpress.org/block-editor/reference-guides/packages/packages-dataviews/#fields-object - */ - const fields = useMemo( () => { - const result: Field< DataViewThreat >[] = [ - { - id: 'title', - label: __( 'Title', 'jetpack' ), - enableGlobalSearch: true, - enableHiding: false, - render( { item }: { item: DataViewThreat } ) { - if ( view.type === 'list' ) { - return item.title; - } - return { item.title }; - }, - }, - { - id: 'description', - label: __( 'Description', 'jetpack' ), - enableGlobalSearch: true, - enableHiding: false, - render( { item }: { item: DataViewThreat } ) { - return { item.description }; - }, - }, - { - id: 'icon', - label: __( 'Icon', 'jetpack' ), - getValue( { item }: { item: DataViewThreat } ) { - return getThreatType( item ); - }, - render( { item }: { item: DataViewThreat } ) { - return ( -
- -
- ); - }, - enableHiding: false, - }, - { - id: 'status', - label: __( 'Status', 'jetpack' ), - elements: THREAT_STATUSES, - getValue( { item }: { item: DataViewThreat } ) { - if ( ! item.status ) { - return 'current'; - } - return ( - THREAT_STATUSES.find( ( { value } ) => value === item.status )?.value ?? item.status - ); - }, - }, - { - id: 'extension', - label: __( 'Extension', 'jetpack' ), - enableGlobalSearch: true, - elements: extensions, - getValue( { item }: { item: DataViewThreat } ) { - return item.extension ? item.extension.slug : ''; - }, - }, - { - id: 'type', - label: __( 'Category', 'jetpack' ), - elements: THREAT_TYPES, - getValue( { item }: { item: DataViewThreat } ) { - if ( 'signature' in item && item.signature === 'Vulnerable.WP.Core' ) { - return 'core'; - } - if ( 'extension' in item && item.extension ) { - return item.extension.type; - } - if ( 'filename' in item && item.filename ) { - return 'file'; - } - if ( 'table' in item && item.table ) { - return 'database'; - } - - return 'uncategorized'; - }, - }, - { - id: 'subtitle', - label: __( 'Affected Item', 'jetpack' ), - getValue( { item }: { item: DataViewThreat } ) { - return getThreatSubtitle( item ); - }, - render( { item }: { item: DataViewThreat } ) { - if ( view.type === 'table' ) { - return ( -
- - { getThreatSubtitle( item ) } -
- ); - } - - return getThreatSubtitle( item ); - }, - }, - ...( dataFields.includes( 'signature' ) - ? [ - { - id: 'signature', - label: __( 'Signature', 'jetpack' ), - elements: signatures, - enableGlobalSearch: true, - getValue( { item }: { item: DataViewThreat } ) { - return item.signature || ''; - }, - }, - ] - : [] ), - ...( dataFields.includes( 'severity' ) - ? [ - { - id: 'severity', - label: __( 'Severity', 'jetpack' ), - getValue( { item }: { item: DataViewThreat } ) { - return item.severity ?? 0; - }, - render( { item }: { item: DataViewThreat } ) { - let text = _x( 'Low', 'Severity label for issues rated below 3.', 'jetpack' ); - let variant: 'danger' | 'warning' | undefined; - - if ( item.severity >= 5 ) { - text = _x( - 'Critical', - 'Severity label for issues rated 5 or higher.', - 'jetpack' - ); - variant = 'danger'; - } else if ( item.severity >= 3 && item.severity < 5 ) { - text = _x( - 'High', - 'Severity label for issues rated between 3 and 5.', - 'jetpack' - ); - variant = 'warning'; - } - - return { text }; - }, - }, - ] - : [] ), - ...( dataFields.includes( 'fixable' ) - ? [ - { - id: 'auto-fix', - label: __( 'Auto-fix', 'jetpack' ), - enableHiding: false, - getValue( { item }: { item: DataViewThreat } ) { - return item.fixable ? 'Yes' : ''; - }, - render( { item }: { item: DataViewThreat } ) { - return item.fixable ? ( - - ) : null; - }, - }, - ] - : [] ), - ]; - - return result; - }, [ extensions, signatures, dataFields, view ] ); - - /** - * DataView actions - collection of operations that can be performed upon each record. - * - * @see https://developer.wordpress.org/block-editor/reference-guides/packages/packages-dataviews/#actions-object - */ - const actions = useMemo( () => { - const result: Action< DataViewThreat >[] = []; - - if ( dataFields.includes( 'fixable' ) ) { - result.push( { - id: 'fix', - label: __( 'Auto-Fix', 'jetpack' ), - isPrimary: true, - callback: onFixThreat, - isEligible( item ) { - if ( ! onFixThreat ) { - return false; - } - if ( isThreatEligibleForFix ) { - return isThreatEligibleForFix( item ); - } - return !! item.fixable; - }, - icon: 'check', - } ); - } - - if ( dataFields.includes( 'status' ) ) { - result.push( { - id: 'ignore', - label: __( 'Ignore', 'jetpack' ), - isPrimary: true, - isDestructive: true, - callback: onIgnoreThreat, - isEligible( item ) { - if ( ! onIgnoreThreat ) { - return false; - } - if ( isThreatEligibleForIgnore ) { - return isThreatEligibleForIgnore( item ); - } - return item.status === 'current'; - }, - icon: 'unseen', - } ); - } - - if ( dataFields.includes( 'status' ) ) { - result.push( { - id: 'un-ignore', - label: __( 'Unignore', 'jetpack' ), - isPrimary: true, - isDestructive: true, - callback: onUnignoreThreat, - isEligible( item ) { - if ( ! onUnignoreThreat ) { - return false; - } - if ( isThreatEligibleForUnignore ) { - return isThreatEligibleForUnignore( item ); - } - return item.status === 'ignored'; - }, - icon: 'seen', - } ); - } - - return result; - }, [ - dataFields, - onFixThreat, - onIgnoreThreat, - onUnignoreThreat, - isThreatEligibleForFix, - isThreatEligibleForIgnore, - isThreatEligibleForUnignore, - ] ); - - /** - * Apply the view settings (i.e. filters, sorting, pagination) to the dataset. - * - * @see https://github.com/WordPress/gutenberg/blob/trunk/packages/dataviews/src/filter-and-sort-data-view.ts - */ - const { data: processedData, paginationInfo } = useMemo( () => { - return filterSortAndPaginate( data, view, fields ); - }, [ data, view, fields ] ); - - /** - * Callback function to update the view state. - * - * @see https://developer.wordpress.org/block-editor/reference-guides/packages/packages-dataviews/#onchangeview-function - */ - const onChangeView = useCallback( ( newView: View ) => { - setView( newView ); - }, [] ); - - /** - * DataView getItemId function - returns the unique ID for each record in the dataset. - * - * @see https://developer.wordpress.org/block-editor/reference-guides/packages/packages-dataviews/#getitemid-function - */ - const getItemId = useCallback( ( item: DataViewThreat ) => item.id.toString(), [] ); - - return ( - - ); -} diff --git a/projects/js-packages/components/components/threats-data-view/stories/index.stories.tsx b/projects/js-packages/components/components/threats-data-view/stories/index.stories.tsx deleted file mode 100644 index 676debe4cf6c8..0000000000000 --- a/projects/js-packages/components/components/threats-data-view/stories/index.stories.tsx +++ /dev/null @@ -1,364 +0,0 @@ -import ThreatsDataView from '..'; - -export default { - title: 'JS Packages/Components/Threats Data View', - component: ThreatsDataView, - parameters: { - backgrounds: { - default: 'light', - values: [ { name: 'light', value: 'white' } ], - }, - }, - decorators: [ - Story => ( -
- -
- ), - ], -}; - -export const Default = args => ; -Default.args = { - data: [ - { - id: 185869885, - signature: 'EICAR_AV_Test', - title: 'Malicious code found in file: index.php', - description: - "This is the standard EICAR antivirus test code, and not a real infection. If your site contains this code when you don't expect it to, contact Jetpack support for some help.", - firstDetected: '2024-10-07T20:45:06.000Z', - fixedIn: null, - fixedOn: null, - severity: 8, - fixable: { fixer: 'rollback', target: 'January 26, 2024, 6:49 am', extensionStatus: '' }, - fixer: { status: 'in_progress', last_updated: new Date().toISOString() }, - status: 'current', - filename: '/var/www/html/wp-content/index.php', - context: { - '1': 'echo << - alert( 'Threat fix action callback triggered! This is handled by the component consumer.' ), // eslint-disable-line no-alert - onIgnoreThreat: () => - alert( 'Ignore threat action callback triggered! This is handled by the component consumer.' ), // eslint-disable-line no-alert - onUnignoreThreat: () => - // eslint-disable-next-line no-alert - alert( - 'Unignore threat action callback triggered! This is handled by the component consumer.' - ), -}; - -export const FixerStatuses = args => ; -FixerStatuses.args = { - data: [ - { - id: 13216959, - signature: 'Vulnerable.WP.Core', - title: 'Vulnerable WordPress Version (6.4.3)', - description: 'This threat has an auto-fixer available. ', - firstDetected: '2024-07-15T21:56:50.000Z', - severity: 4, - fixer: null, - fixedOn: '2024-07-15T22:01:42.000Z', - status: 'fixed', - fixable: { fixer: 'update', target: '6.4.4', extensionStatus: 'inactive' }, - version: '6.4.3', - source: '', - }, - { - id: 12345678910, - signature: 'Vulnerable.WP.Extension', - title: 'Vulnerable Plugin: Example Plugin (version 1.2.3)', - description: 'This threat has an in-progress auto-fixer.', - firstDetected: '2024-10-02T17:34:59.000Z', - fixedIn: '1.2.4', - fixedOn: null, - severity: 3, - fixable: { fixer: 'update', target: '1.12.4', extensionStatus: 'inactive' }, - fixer: { status: 'in_progress', last_updated: new Date().toISOString() }, - status: 'current', - filename: null, - context: null, - source: 'https://wpscan.com/vulnerability/733d8a02-0d44-4b78-bbb2-37e447acd2f3', - extension: { - name: 'Example Plugin', - slug: 'example-plugin', - version: '1.2.3', - type: 'plugin', - }, - }, - { - id: 12345678911, - signature: 'Vulnerable.WP.Extension', - title: 'Vulnerable Theme: Example Theme (version 2.2.2)', - description: 'This threat has an in-progress auto-fixer that is taking too long.', - firstDetected: '2024-10-02T17:34:59.000Z', - fixedIn: '2.22.22', - fixedOn: null, - severity: 3, - fixable: { fixer: 'update', target: '1.12.4', extensionStatus: 'inactive' }, - fixer: { status: 'in_progress', last_updated: new Date( '1999-01-01' ).toISOString() }, - status: 'current', - filename: null, - context: null, - source: 'https://wpscan.com/vulnerability/733d8a02-0d44-4b78-bbb2-37e447acd2f3', - extension: { - name: 'Example Theme', - slug: 'example-theme', - version: '2.2.2', - type: 'theme', - }, - }, - { - id: 12345678912, - signature: 'Vulnerable.WP.Extension', - title: 'Vulnerable Theme: Example Theme II (version 3.3.3)', - description: 'This threat has a fixer with an error status.', - firstDetected: '2024-10-02T17:34:59.000Z', - fixedIn: '3.4.5', - fixedOn: null, - severity: 3, - fixable: { fixer: 'update', target: '1.12.4', extensionStatus: 'inactive' }, - fixer: { status: 'error', error: 'error' }, - status: 'current', - filename: null, - context: null, - source: 'https://wpscan.com/vulnerability/733d8a02-0d44-4b78-bbb2-37e447acd2f3', - extension: { - name: 'Example Theme II', - slug: 'example-theme-2', - version: '3.3.3', - type: 'theme', - }, - }, - { - id: 185868972, - signature: 'EICAR_AV_Test_Suspicious', - title: 'Malicious code found in file: jptt_eicar.php', - description: 'This threat has no auto-fixer available.', - firstDetected: '2024-10-07T20:40:15.000Z', - fixedIn: null, - fixedOn: null, - severity: 1, - fixable: false, - status: 'current', - filename: '/var/www/html/wp-content/uploads/jptt_eicar.php', - context: { - '6': 'echo << ; -FreeResults.args = { - data: [ - { - id: '1d0470df-4671-47ac-8d87-a165e8f7d502', - title: 'WooCommerce <= 3.2.3 - Authenticated PHP Object Injection', - description: - 'Versions 3.2.3 and earlier are affected by an issue where cached queries within shortcodes could lead to object injection. This is related to the recent WordPress 4.8.3 security release.This issue can only be exploited by users who can edit content and add shortcodes, but we still recommend all users running WooCommerce 3.x upgrade to 3.2 to mitigate this issue.', - firstDetected: null, - fixedIn: '3.2.4', - fixedOn: null, - severity: null, - fixable: null, - status: null, - filename: null, - context: null, - signature: null, - source: 'https://wpscan.com/vulnerability/1d0470df-4671-47ac-8d87-a165e8f7d502', - extension: { - name: 'WooCommerce', - slug: 'woocommerce', - version: '3.2.3', - type: 'plugin', - }, - }, - { - id: '7275a176-d579-471a-8492-df8edbdf27de', - signature: null, - subtitle: 'WooCommerce 3.4.5', - title: 'WooCommerce <= 3.4.5 - Authenticated Stored XSS', - description: - 'The WooCommerce WordPress plugin was affected by an Authenticated Stored XSS security vulnerability.', - firstDetected: null, - fixedIn: '3.4.6', - fixedOn: null, - severity: null, - fixable: null, - status: null, - filename: null, - context: null, - source: 'https://wpscan.com/vulnerability/7275a176-d579-471a-8492-df8edbdf27de', - extension: { - name: 'WooCommerce', - slug: 'woocommerce', - version: '3.4.5', - type: 'plugin', - }, - }, - { - id: '733d8a02-0d44-4b78-bbb2-37e447acd2f3', - signature: null, - title: 'WP Super Cache < 1.7.2 - Authenticated Remote Code Execution (RCE)', - description: - 'The plugin was affected by an authenticated (admin+) RCE in the settings page due to input validation failure and weak $cache_path check in the WP Super Cache Settings -> Cache Location option. Direct access to the wp-cache-config.php file is not prohibited, so this vulnerability can be exploited for a web shell injection.\r\n\r\nAnother possible attack vector: from XSS (via another plugin affected by XSS) to RCE.', - firstDetected: null, - fixedIn: '1.7.2', - fixedOn: null, - severity: null, - fixable: null, - status: null, - filename: null, - context: null, - source: 'https://wpscan.com/vulnerability/733d8a02-0d44-4b78-bbb2-37e447acd2f3', - extension: { - name: 'WP Super Cache', - slug: 'wp-super-cache', - version: '1.6.3', - type: 'plugin', - }, - }, - ], -}; diff --git a/projects/js-packages/components/components/threats-data-view/styles.module.scss b/projects/js-packages/components/components/threats-data-view/styles.module.scss deleted file mode 100644 index 5cf22e93b8a4e..0000000000000 --- a/projects/js-packages/components/components/threats-data-view/styles.module.scss +++ /dev/null @@ -1,122 +0,0 @@ -@import '@wordpress/dataviews/build-style/style.css'; - -:global { - .dataviews-view-table tbody .dataviews-view-table__cell-content-wrapper { - min-height: 0; - } - - .dataviews-view-table td, .dataviews-view-table th { - white-space: initial; - } - - .dataviews-view-table td .components-flex { - gap: 4px; - } - - .dataviews-view-list .dataviews-views-list__fields { - align-items: center; - } -} - -.media { - width: 100%; - height: 100%; - display: flex; - align-items: center; - justify-content: center; - color: black; - background-color: #EDFFEE; - border-color: #EDFFEE; - - svg { - fill: currentColor; - } -} - -.icon-check { - fill: var( --jp-green-40 ); -} - -.icon-info { - fill: var( --jp-red ); -} - -.support-link { - color: inherit; - - &:focus, - &:hover { - color: inherit; - box-shadow: none; - } -} - -.fixer-status { - display: flex; - align-items: center; - line-height: 0; - - .icon-spinner { - margin-left: 1px; - } - - .icon-info { - margin-left: -3px; - } - - .icon-check { - margin-left: -6px; - } -} - -.threat__primary { - display: flex; - align-items: center; - gap: 8px; -} - -.threat__subtitle { - display: flex; - align-items: center; - gap: 6px; - font-size: 12px; - color: var( --jp-gray-80 ); - margin-bottom: 4px; - - > svg { - color: currentColor; - } -} - -.threat__title { - color: var( --jp-gray-80 ); - font-weight: 510; -} - -.threat__description { - color: var( --jp-gray-80 ); - font-size: 12px; -} - -.icon-spinner { - svg { - margin: 0; - } -} - -.spinner-spacer { - margin-left: 8px; -} - -.info-spacer { - margin-left: 4px; -} - -.check-spacer { - margin-left: -2px; -} - -.threat__fixer { - min-width: 54px; - text-align: center; -} diff --git a/projects/js-packages/components/components/threats-data-view/test/index.test.tsx b/projects/js-packages/components/components/threats-data-view/test/index.test.tsx deleted file mode 100644 index 0cada3e4c063a..0000000000000 --- a/projects/js-packages/components/components/threats-data-view/test/index.test.tsx +++ /dev/null @@ -1,34 +0,0 @@ -import { render, screen } from '@testing-library/react'; -import ThreatsDataView from '..'; -import { DataViewThreat } from '../types'; - -const data = [ - { - id: 185869885, - signature: 'EICAR_AV_Test', - title: 'Malicious code found in file: index.php', - description: - "This is the standard EICAR antivirus test code, and not a real infection. If your site contains this code when you don't expect it to, contact Jetpack support for some help.", - firstDetected: '2024-10-07T20:45:06.000Z', - fixedIn: null, - fixedOn: null, - severity: 8, - fixable: { fixer: 'rollback', target: 'January 26, 2024, 6:49 am', extensionStatus: '' }, - status: 'current', - filename: '/var/www/html/wp-content/index.php', - context: { - '1': 'echo << { - it( 'renders threat data', () => { - render( ); - expect( screen.getByText( 'Malicious code found in file: index.php' ) ).toBeInTheDocument(); - } ); -} ); diff --git a/projects/js-packages/components/components/threats-data-view/types.d.ts b/projects/js-packages/components/components/threats-data-view/types.d.ts deleted file mode 100644 index 3f75a9f7d0b9d..0000000000000 --- a/projects/js-packages/components/components/threats-data-view/types.d.ts +++ /dev/null @@ -1,93 +0,0 @@ -export type ThreatStatus = 'fixed' | 'ignored' | 'current'; - -export type ThreatFixType = 'replace' | 'delete' | 'update' | string; - -export type DataViewThreat = { - /** The threat's unique ID. */ - id: number; - - /** The threat's signature. */ - signature: string; - - /** The threat's title. */ - title: string; - - /** The threat's description. */ - description: string; - - /** The threat's current status. */ - status: ThreatStatus; - - /** The threat's severity level (0-10). */ - severity: number; - - /** The date the threat was first detected on the site, in YYYY-MM-DDTHH:MM:SS.000Z format. */ - firstDetected: string; - - /** The version the threat is fixed in. */ - fixedIn?: string | null; - - /** The date the threat was fixed, in YYYY-MM-DDTHH:MM:SS.000Z format. */ - fixedOn?: string | null; - - /** The fixable details. */ - fixable: - | { - fixer: ThreatFixType; - target?: string | null; - extensionStatus?: string | null; - } - | false; - - /** If available, the threat's latest fixer status. */ - fixer?: ThreatFixStatus; - - /** The threat's source. */ - source?: string; - - /** The threat's affected extension. */ - extension?: { - name: string; - slug: string; - type: 'plugin' | 'theme' | 'core'; - version: string; - }; - - /** The threat's context. */ - context?: Record< string, unknown > | null; - - /** The name of the affected file. */ - filename: string | null; - - /** The rows affected by the database threat. */ - rows?: unknown; - - /** The table name of the database threat. */ - table?: string; - - /** The diff showing the threat's modified file contents. */ - diff?: string; -}; - -export type ThreatsDataViewActionCallback = ( - items: Threat[], - context: { registry: unknown; onActionPerformed?: ( threats: DataViewThreat[] ) => void } -) => void; - -export type FixerStatus = 'not_started' | 'in_progress' | 'fixed' | 'not_fixed'; - -/** - * Threat Fix Status - * - * Individual fixer status for a threat. - */ -export type ThreatFixStatusError = { - error: string; -}; - -export type ThreatFixStatusSuccess = { - status: FixerStatus; - last_updated: string; -}; - -export type ThreatFixStatus = ThreatFixStatusError | ThreatFixStatusSuccess; diff --git a/projects/js-packages/scan/package.json b/projects/js-packages/scan/package.json index 3215bb8366e35..a16304cd73364 100644 --- a/projects/js-packages/scan/package.json +++ b/projects/js-packages/scan/package.json @@ -55,6 +55,7 @@ "@wordpress/api-fetch": "7.9.0", "@wordpress/element": "6.9.0", "@wordpress/i18n": "5.9.0", + "@wordpress/icons": "10.9.0", "@wordpress/url": "4.9.0", "debug": "4.3.4", "react": "^18.2.0", diff --git a/projects/js-packages/scan/src/index.ts b/projects/js-packages/scan/src/index.ts index 314a00ec1f4fb..651a2434ada6b 100644 --- a/projects/js-packages/scan/src/index.ts +++ b/projects/js-packages/scan/src/index.ts @@ -1 +1,2 @@ export * from './types/index.js'; +export * from './utils.js'; diff --git a/projects/js-packages/scan/src/types/fixers.d.ts b/projects/js-packages/scan/src/types/fixers.d.ts index 6ba9433122dbb..b99a93def29a4 100644 --- a/projects/js-packages/scan/src/types/fixers.d.ts +++ b/projects/js-packages/scan/src/types/fixers.d.ts @@ -15,3 +15,26 @@ export type ThreatFixStatusSuccess = { }; export type ThreatFixStatus = ThreatFixStatusError | ThreatFixStatusSuccess; + +/** + * Fixers Status + * + * Overall status of all fixers. + */ +type FixersStatusBase = { + ok: boolean; // Discriminator for overall success +}; + +export type FixersStatusError = FixersStatusBase & { + ok: false; + error: string; +}; + +export type FixersStatusSuccess = FixersStatusBase & { + ok: true; + threats: { + [ key: number ]: ThreatFixStatus; + }; +}; + +export type FixersStatus = FixersStatusSuccess | FixersStatusError; diff --git a/projects/js-packages/scan/src/types/threat.d.ts b/projects/js-packages/scan/src/types/threat.d.ts index 757503972fa0c..09bf2a43f6082 100644 --- a/projects/js-packages/scan/src/types/threat.d.ts +++ b/projects/js-packages/scan/src/types/threat.d.ts @@ -56,4 +56,12 @@ export type Threat = { /** The diff showing the threat's modified file contents. */ diff?: string; + + /** The affected extension. */ + extension?: { + slug: string; + name: string; + version: string; + type: 'plugin' | 'theme' | 'core'; + }; }; diff --git a/projects/js-packages/components/components/threats-data-view/utils.ts b/projects/js-packages/scan/src/utils.ts similarity index 85% rename from projects/js-packages/components/components/threats-data-view/utils.ts rename to projects/js-packages/scan/src/utils.ts index 3899b66d2f747..16c222a6d510a 100644 --- a/projects/js-packages/components/components/threats-data-view/utils.ts +++ b/projects/js-packages/scan/src/utils.ts @@ -1,7 +1,8 @@ import { code, color, grid, plugins, shield, wordpress } from '@wordpress/icons'; -import { DataViewThreat, ThreatFixStatus } from './types'; +import { ThreatFixStatus } from './types/fixers.js'; +import { Threat } from './types/threat.js'; -export const getThreatIcon = ( threat: DataViewThreat ) => { +export const getThreatIcon = ( threat: Threat ) => { const type = getThreatType( threat ); switch ( type ) { @@ -20,7 +21,7 @@ export const getThreatIcon = ( threat: DataViewThreat ) => { } }; -export const getThreatType = ( threat: DataViewThreat ) => { +export const getThreatType = ( threat: Threat ) => { if ( threat.signature === 'Vulnerable.WP.Core' ) { return 'core'; } @@ -37,7 +38,7 @@ export const getThreatType = ( threat: DataViewThreat ) => { return null; }; -export const getThreatSubtitle = ( threat: DataViewThreat ) => { +export const getThreatSubtitle = ( threat: Threat ) => { const type = getThreatType( threat ); switch ( type ) { diff --git a/projects/packages/my-jetpack/_inc/components/product-cards-section/protect-card/scan-threats-status.tsx b/projects/packages/my-jetpack/_inc/components/product-cards-section/protect-card/scan-threats-status.tsx index 4349419dc95f3..ddb14a3915fe5 100644 --- a/projects/packages/my-jetpack/_inc/components/product-cards-section/protect-card/scan-threats-status.tsx +++ b/projects/packages/my-jetpack/_inc/components/product-cards-section/protect-card/scan-threats-status.tsx @@ -24,28 +24,14 @@ export const ScanAndThreatStatus = () => { const { protect: { scanData }, } = getMyJetpackWindowInitialState(); - const { plugins, themes, num_threats: numThreats = 0 } = scanData || {}; + const numThreats = scanData.threats.length; const criticalScanThreatCount = useMemo( () => { - const { core, database, files, num_plugins_threats, num_themes_threats } = scanData || {}; - const pluginsThreats = num_plugins_threats - ? plugins.reduce( ( accum, plugin ) => accum.concat( plugin.threats ), [] ) - : []; - const themesThreats = num_themes_threats - ? themes.reduce( ( accum, theme ) => accum.concat( theme.threats ), [] ) - : []; - const allThreats = [ - ...pluginsThreats, - ...themesThreats, - ...( core?.threats ?? [] ), - ...database, - ...files, - ]; - return allThreats.reduce( + return scanData.threats.reduce( ( accum, threat ) => ( threat.severity >= 5 ? ( accum += 1 ) : accum ), 0 ); - }, [ plugins, themes, scanData ] ); + }, [ scanData.threats ] ); if ( isPluginActive && isSiteConnected ) { if ( hasProtectPaidPlan ) { diff --git a/projects/packages/my-jetpack/_inc/components/product-cards-section/protect-card/use-last-scan-text.ts b/projects/packages/my-jetpack/_inc/components/product-cards-section/protect-card/use-last-scan-text.ts index 1cf61f6ce0edf..0eb498144465a 100644 --- a/projects/packages/my-jetpack/_inc/components/product-cards-section/protect-card/use-last-scan-text.ts +++ b/projects/packages/my-jetpack/_inc/components/product-cards-section/protect-card/use-last-scan-text.ts @@ -13,14 +13,10 @@ export const useLastScanText = () => { themes, protect: { scanData }, } = getMyJetpackWindowInitialState(); - const { - plugins: fromScanPlugins, - themes: fromScanThemes, - last_checked: lastScanTime = null, - } = scanData || {}; + const { last_checked: lastScanTime = null } = scanData || {}; - const pluginsCount = fromScanPlugins.length || Object.keys( plugins ).length; - const themesCount = fromScanThemes.length || Object.keys( themes ).length; + const pluginsCount = Object.keys( plugins ).length; + const themesCount = Object.keys( themes ).length; const timeSinceLastScan = lastScanTime ? timeSince( Date.parse( lastScanTime ) ) : false; diff --git a/projects/packages/my-jetpack/_inc/components/product-cards-section/protect-card/use-protect-tooltip-copy.ts b/projects/packages/my-jetpack/_inc/components/product-cards-section/protect-card/use-protect-tooltip-copy.ts index 6f95e251ea099..f2e2b48fd5901 100644 --- a/projects/packages/my-jetpack/_inc/components/product-cards-section/protect-card/use-protect-tooltip-copy.ts +++ b/projects/packages/my-jetpack/_inc/components/product-cards-section/protect-card/use-protect-tooltip-copy.ts @@ -35,19 +35,15 @@ export function useProtectTooltipCopy(): TooltipContent { themes, protect: { scanData, wafConfig: wafData }, } = getMyJetpackWindowInitialState(); - const { - plugins: fromScanPlugins, - themes: fromScanThemes, - num_threats: numThreats = 0, - } = scanData || {}; + const numThreats = scanData.threats.length; const { jetpack_waf_automatic_rules: isAutoFirewallEnabled, blocked_logins: blockedLoginsCount, brute_force_protection: hasBruteForceProtection, } = wafData || {}; - const pluginsCount = fromScanPlugins.length || Object.keys( plugins ).length; - const themesCount = fromScanThemes.length || Object.keys( themes ).length; + const pluginsCount = Object.keys( plugins ).length; + const themesCount = Object.keys( themes ).length; const settingsLink = useMemo( () => { if ( isProtectPluginActive ) { diff --git a/projects/packages/my-jetpack/changelog/protect-status-compat b/projects/packages/my-jetpack/changelog/protect-status-compat new file mode 100644 index 0000000000000..14eba53e0fcfc --- /dev/null +++ b/projects/packages/my-jetpack/changelog/protect-status-compat @@ -0,0 +1,5 @@ +Significance: patch +Type: changed +Comment: Package compatibility updates, no functional changes. + + diff --git a/projects/packages/my-jetpack/global.d.ts b/projects/packages/my-jetpack/global.d.ts index f302d8567e765..cb8794181c659 100644 --- a/projects/packages/my-jetpack/global.d.ts +++ b/projects/packages/my-jetpack/global.d.ts @@ -48,6 +48,12 @@ type ThreatItem = { fixed_in: string; description: string | null; source: string | null; + extension: { + slug: string; + name: string; + version: string; + type: 'plugin' | 'theme' | 'core'; + }; // Scan API properties (paid plan) context: string | null; filename: string | null; @@ -58,15 +64,6 @@ type ThreatItem = { status: number | null; }; -type ScanItem = { - checked: boolean; - name: string; - slug: string; - threats: ThreatItem[]; - type: string; - version: string; -}; - interface Window { myJetpackInitialState?: { siteSuffix: string; @@ -211,22 +208,15 @@ interface Window { }; protect: { scanData: { - core: ScanItem; + threats: ThreatItem[]; current_progress?: string; data_source: string; - database: string[]; error: boolean; error_code?: string; error_message?: string; - files: string[]; has_unchecked_items: boolean; last_checked: string; - num_plugins_threats: number; - num_themes_threats: number; - num_threats: number; - plugins: ScanItem[]; status: string; - themes: ScanItem[]; }; wafConfig: { automatic_rules_available: boolean; diff --git a/projects/packages/protect-models/changelog/update-protect-threats-data b/projects/packages/protect-models/changelog/update-protect-threats-data new file mode 100644 index 0000000000000..1cb2d3079cb4d --- /dev/null +++ b/projects/packages/protect-models/changelog/update-protect-threats-data @@ -0,0 +1,4 @@ +Significance: major +Type: changed + +Changed the formatting of threat data. diff --git a/projects/packages/protect-models/src/class-extension-model.php b/projects/packages/protect-models/src/class-extension-model.php index 95a49c8e5b7c3..60185c973b4ed 100644 --- a/projects/packages/protect-models/src/class-extension-model.php +++ b/projects/packages/protect-models/src/class-extension-model.php @@ -33,13 +33,6 @@ class Extension_Model { */ public $version; - /** - * A collection of threats related to this version of the extension. - * - * @var array - */ - public $threats = array(); - /** * Whether the extension has been checked for threats. * @@ -77,34 +70,4 @@ public function __construct( $extension = array() ) { } } } - - /** - * Set Threats - * - * @param array $threats An array of threat data to add to the extension. - */ - public function set_threats( $threats ) { - if ( ! is_array( $threats ) ) { - $this->threats = array(); - return; - } - - // convert each provided threat item into an instance of Threat_Model - $threats = array_map( - function ( $threat ) { - if ( is_a( $threat, 'Threat_Model' ) ) { - return $threat; - } - - if ( is_object( $threat ) ) { - $threat = (array) $threat; - } - - return new Threat_Model( $threat ); - }, - $threats - ); - - $this->threats = $threats; - } } diff --git a/projects/packages/protect-models/src/class-history-model.php b/projects/packages/protect-models/src/class-history-model.php index ff10ae4bf468b..1de243a3a22f2 100644 --- a/projects/packages/protect-models/src/class-history-model.php +++ b/projects/packages/protect-models/src/class-history-model.php @@ -18,68 +18,12 @@ class History_Model { */ public $last_checked; - /** - * The number of threats. - * - * @var int - */ - public $num_threats; - - /** - * The number of core threats. - * - * @var int - */ - public $num_core_threats; - - /** - * The number of plugin threats. - * - * @var int - */ - public $num_plugins_threats; - - /** - * The number of theme threats. - * - * @var int - */ - public $num_themes_threats; - - /** - * WordPress core. - * - * @var array - */ - public $core = array(); - - /** - * Status themes. - * - * @var array - */ - public $themes = array(); - - /** - * Status plugins. - * - * @var array - */ - public $plugins = array(); - - /** - * File threats. - * - * @var array - */ - public $files = array(); - /** * Database threats. * - * @var array + * @var array */ - public $database = array(); + public $threats = array(); /** * Whether there was an error loading the history. diff --git a/projects/packages/protect-models/src/class-status-model.php b/projects/packages/protect-models/src/class-status-model.php index 73bec9dd0f4de..a719364edddfe 100644 --- a/projects/packages/protect-models/src/class-status-model.php +++ b/projects/packages/protect-models/src/class-status-model.php @@ -25,27 +25,6 @@ class Status_Model { */ public $last_checked; - /** - * The number of threats. - * - * @var int - */ - public $num_threats; - - /** - * The number of plugin threats. - * - * @var int - */ - public $num_plugins_threats; - - /** - * The number of theme threats. - * - * @var int - */ - public $num_themes_threats; - /** * The current report status. * @@ -61,39 +40,11 @@ class Status_Model { public $fixable_threat_ids = array(); /** - * WordPress core status. - * - * @var object - */ - public $core; - - /** - * Status themes. - * - * @var array - */ - public $themes = array(); - - /** - * Status plugins. - * - * @var array - */ - public $plugins = array(); - - /** - * File threats. - * - * @var array - */ - public $files = array(); - - /** - * Database threats. + * Threats. * - * @var array + * @var array */ - public $database = array(); + public $threats = array(); /** * Whether the site includes items that have not been checked. @@ -136,9 +87,6 @@ class Status_Model { * @param array $status The status data to load into the class instance. */ public function __construct( $status = array() ) { - // set status defaults - $this->core = new \stdClass(); - foreach ( $status as $property => $value ) { if ( property_exists( $this, $property ) ) { $this->$property = $value; diff --git a/projects/packages/protect-models/src/class-threat-model.php b/projects/packages/protect-models/src/class-threat-model.php index d85e1b97cc686..5335edd058a67 100644 --- a/projects/packages/protect-models/src/class-threat-model.php +++ b/projects/packages/protect-models/src/class-threat-model.php @@ -103,6 +103,13 @@ class Threat_Model { */ public $source; + /** + * The threat's extension information. + * + * @var null|Extension_Model + */ + public $extension; + /** * Threat Constructor * @@ -114,6 +121,10 @@ public function __construct( $threat ) { } foreach ( $threat as $property => $value ) { + if ( 'extension' === $property ) { + $this->extension = new Extension_Model( $value ); + continue; + } if ( property_exists( $this, $property ) ) { $this->$property = $value; } diff --git a/projects/packages/protect-models/tests/php/test-extension-model.php b/projects/packages/protect-models/tests/php/test-extension-model.php index 8e7c37c89c937..e491a7d0281ed 100644 --- a/projects/packages/protect-models/tests/php/test-extension-model.php +++ b/projects/packages/protect-models/tests/php/test-extension-model.php @@ -10,55 +10,20 @@ * @package automattic/jetpack-protect */ class Test_Extension_Model extends BaseTestCase { - - /** - * Get a sample threat - * - * @param int|string $id The sample threat's unique identifier. - * @return array - */ - private static function get_sample_threat( $id = 0 ) { - return array( - 'id' => "test-threat-$id", - 'signature' => 'Test.Threat', - 'title' => "Test Threat $id", - 'description' => 'This is a test threat.', - ); - } - /** * Tests for extension model's __construct() method. */ public function test_extension_model_construct() { - $test_data = array( - 'name' => 'Test Extension', - 'slug' => 'test-extension', + $test_data = array( + 'slug' => 'test-extension-1', + 'name' => 'Test Extension 1', 'version' => '1.0.0', - 'threats' => array( - self::get_sample_threat( 0 ), - self::get_sample_threat( 1 ), - self::get_sample_threat( 2 ), - ), - 'checked' => true, - 'type' => 'plugins', - ); - - // Initialize multiple instances of Extension_Model to test varying initial params - $test_extensions = array( - new Extension_Model( $test_data ), - new Extension_Model( (object) $test_data ), + 'type' => 'plugin', ); + $test_extension = new Extension_Model( $test_data ); - foreach ( $test_extensions as $extension ) { - foreach ( $extension->threats as $loop_index => $threat ) { - // Validate the threat data is converted into Threat_Models - $this->assertSame( 'Automattic\Jetpack\Protect_Models\Threat_Model', get_class( $threat ) ); - - // Validate the threat data is set properly - foreach ( self::get_sample_threat( $loop_index ) as $key => $value ) { - $this->assertSame( $value, $threat->{ $key } ); - } - } + foreach ( $test_data as $key => $value ) { + $this->assertSame( $value, $test_extension->$key ); } } } diff --git a/projects/packages/protect-models/tests/php/test-threat-model.php b/projects/packages/protect-models/tests/php/test-threat-model.php index 02cd1face2f66..b972ccea5f790 100644 --- a/projects/packages/protect-models/tests/php/test-threat-model.php +++ b/projects/packages/protect-models/tests/php/test-threat-model.php @@ -15,34 +15,63 @@ class Test_Threat_Model extends BaseTestCase { * Tests for threat model's __construct() method. */ public function test_threat_model_construct() { + // Initialize multiple instances of Extension_Threat to test varying initial params $test_data = array( - 'id' => 'abc-123-abc-123', - 'signature' => 'Test.Threat', - 'title' => 'Test Threat', - 'description' => 'This is a test threat.', - 'first_detected' => '2022-01-01T00:00:00.000Z', - 'fixed_in' => '1.0.1', - 'severity' => 4, - 'fixable' => (object) array( - 'fixer' => 'update', - 'target' => '1.0.1', - 'extension_status' => 'active', + array( + 'id' => 'test-threat-1', + 'signature' => 'Test.Threat', + 'title' => 'Test Threat 1', + 'description' => 'This is a test threat.', + 'extension' => array( + 'slug' => 'test-extension-1', + 'name' => 'Test Extension 1', + 'version' => '1.0.0', + 'type' => 'plugin', + ), + ), + array( + 'id' => 'test-threat-2', + 'signature' => 'Test.Threat', + 'title' => 'Test Threat 2', + 'description' => 'This is a test threat.', + 'extension' => array( + 'slug' => 'test-extension-2', + 'name' => 'Test Extension 2', + 'version' => '1.0.0', + 'type' => 'theme', + ), + ), + array( + 'id' => 'test-threat-3', + 'signature' => 'Test.Threat', + 'title' => 'Test Threat 3', + 'description' => 'This is a test threat.', ), - 'status' => 'current', - 'filename' => '/srv/htdocs/wp-content/uploads/threat.jpg.php', - 'context' => (object) array(), ); - // Initialize multiple instances of Threat_Model to test varying initial params - $test_threats = array( - new Threat_Model( $test_data ), - new Threat_Model( (object) $test_data ), + $test_threats = array_map( + function ( $threat_data ) { + return new Threat_Model( $threat_data ); + }, + $test_data ); - foreach ( $test_threats as $threat ) { + foreach ( $test_threats as $loop_index => $threat ) { + // Validate the threat data is normalized into model classes + $this->assertSame( 'Automattic\Jetpack\Protect_Models\Threat_Model', get_class( $threat ) ); + if ( isset( $threat->extension ) ) { + $this->assertSame( 'Automattic\Jetpack\Protect_Models\Extension_Model', get_class( $threat->extension ) ); + } + // Validate the threat data is set properly - foreach ( $test_data as $key => $value ) { - $this->assertSame( $value, $threat->{ $key } ); + foreach ( $test_data[ $loop_index ] as $key => $value ) { + if ( 'extension' === $key ) { + foreach ( $value as $extension_key => $extension_value ) { + $this->assertSame( $extension_value, $threat->extension->$extension_key ); + } + continue; + } + $this->assertSame( $value, $threat->$key ); } } } diff --git a/projects/packages/protect-status/changelog/update-protect-threats-data b/projects/packages/protect-status/changelog/update-protect-threats-data new file mode 100644 index 0000000000000..1cb2d3079cb4d --- /dev/null +++ b/projects/packages/protect-status/changelog/update-protect-threats-data @@ -0,0 +1,4 @@ +Significance: major +Type: changed + +Changed the formatting of threat data. diff --git a/projects/packages/protect-status/src/class-protect-status.php b/projects/packages/protect-status/src/class-protect-status.php index 832b1cde58964..bd4912c9ae7c3 100644 --- a/projects/packages/protect-status/src/class-protect-status.php +++ b/projects/packages/protect-status/src/class-protect-status.php @@ -132,130 +132,113 @@ public static function fetch_from_server() { * @return Status_Model */ protected static function normalize_protect_report_data( $report_data ) { + global $wp_version; + $status = new Status_Model(); $status->data_source = 'protect_report'; - // map report data properties directly into the Status_Model - $status->status = isset( $report_data->status ) ? $report_data->status : null; - $status->last_checked = isset( $report_data->last_checked ) ? $report_data->last_checked : null; - $status->num_threats = isset( $report_data->num_vulnerabilities ) ? $report_data->num_vulnerabilities : null; - $status->num_themes_threats = isset( $report_data->num_themes_vulnerabilities ) ? $report_data->num_themes_vulnerabilities : null; - $status->num_plugins_threats = isset( $report_data->num_plugins_vulnerabilities ) ? $report_data->num_plugins_vulnerabilities : null; + $status->status = isset( $report_data->status ) ? $report_data->status : null; + $status->last_checked = isset( $report_data->last_checked ) ? $report_data->last_checked : null; - // merge plugins from report with all installed plugins before mapping into the Status_Model + // Plugin Vulnerabilities $installed_plugins = Plugins_Installer::get_plugins(); $last_report_plugins = isset( $report_data->plugins ) ? $report_data->plugins : new \stdClass(); - $status->plugins = self::merge_installed_and_checked_lists( $installed_plugins, $last_report_plugins, array( 'type' => 'plugins' ) ); - - // merge themes from report with all installed plugins before mapping into the Status_Model - $installed_themes = Sync_Functions::get_themes(); - $last_report_themes = isset( $report_data->themes ) ? $report_data->themes : new \stdClass(); - $status->themes = self::merge_installed_and_checked_lists( $installed_themes, $last_report_themes, array( 'type' => 'themes' ) ); - - // normalize WordPress core report data and map into Status_Model - $status->core = self::normalize_core_information( isset( $report_data->core ) ? $report_data->core : new \stdClass() ); - - // check if any installed items (themes, plugins, or core) have not been checked in the report - $all_items = array_merge( $status->plugins, $status->themes, array( $status->core ) ); - $unchecked_items = array_filter( - $all_items, - function ( $item ) { - return ! isset( $item->checked ) || ! $item->checked; + foreach ( $installed_plugins as $installed_slug => $installed_plugin ) { + // Skip vulnerabilities for plugins that are not installed + if ( ! isset( $last_report_plugins->{ $installed_slug } ) ) { + continue; } - ); - $status->has_unchecked_items = ! empty( $unchecked_items ); - - return $status; - } - /** - * Merges the list of installed extensions with the list of extensions that were checked for known vulnerabilities and return a normalized list to be used in the UI - * - * @param array $installed The list of installed extensions, where each attribute key is the extension slug. - * @param object $checked The list of checked extensions. - * @param array $append Additional data to append to each result in the list. - * @return array Normalized list of extensions. - */ - protected static function merge_installed_and_checked_lists( $installed, $checked, $append ) { - $new_list = array(); - foreach ( array_keys( $installed ) as $slug ) { + $report_plugin = $last_report_plugins->{ $installed_slug }; - $checked = (object) $checked; + // Skip vulnerabilities for plugins with a mismatched version + if ( $report_plugin->version !== $installed_plugin['Version'] ) { + continue; + } - $extension = new Extension_Model( - array_merge( + foreach ( $report_plugin->vulnerabilities as $report_vulnerability ) { + $status->threats[] = new Threat_Model( array( - 'name' => $installed[ $slug ]['Name'], - 'version' => $installed[ $slug ]['Version'], - 'slug' => $slug, - 'threats' => array(), - 'checked' => false, - ), - $append - ) - ); - - if ( isset( $checked->{ $slug } ) && $checked->{ $slug }->version === $installed[ $slug ]['Version'] ) { - $extension->version = $checked->{ $slug }->version; - $extension->checked = true; - - if ( is_array( $checked->{ $slug }->vulnerabilities ) ) { - foreach ( $checked->{ $slug }->vulnerabilities as $threat ) { - $extension->threats[] = new Threat_Model( + 'id' => $report_vulnerability->id, + 'title' => $report_vulnerability->title, + 'fixed_in' => $report_vulnerability->fixed_in, + 'description' => isset( $report_vulnerability->description ) ? $report_vulnerability->description : null, + 'source' => isset( $report_vulnerability->id ) ? Redirect::get_url( 'jetpack-protect-vul-info', array( 'path' => $report_vulnerability->id ) ) : null, + 'extension' => new Extension_Model( array( - 'id' => $threat->id, - 'title' => $threat->title, - 'fixed_in' => $threat->fixed_in, - 'description' => isset( $threat->description ) ? $threat->description : null, - 'source' => isset( $threat->id ) ? Redirect::get_url( 'jetpack-protect-vul-info', array( 'path' => $threat->id ) ) : null, + 'slug' => $installed_slug, + 'name' => $installed_plugin['Name'], + 'version' => $installed_plugin['Version'], + 'type' => 'plugin', ) - ); - } - } + ), + ) + ); } - - $new_list[] = $extension; - } - $new_list = parent::sort_threats( $new_list ); + // Theme Vulnerabilities + $installed_themes = Sync_Functions::get_themes(); + $last_report_themes = isset( $report_data->themes ) ? $report_data->themes : new \stdClass(); + foreach ( $installed_themes as $installed_slug => $installed_theme ) { + // Skip vulnerabilities for themes that are not installed + if ( ! isset( $last_report_themes->{ $installed_slug } ) ) { + continue; + } - return $new_list; - } + $report_theme = $last_report_themes->{ $installed_slug }; - /** - * Check if the WordPress version that was checked matches the current installed version. - * - * @param object $core_check The object returned by Protect wpcom endpoint. - * @return object The object representing the current status of core checks. - */ - protected static function normalize_core_information( $core_check ) { - global $wp_version; + // Skip vulnerabilities for themes with a mismatched version + if ( $report_theme->version !== $installed_theme['Version'] ) { + continue; + } - $core = new Extension_Model( - array( - 'type' => 'core', - 'name' => 'WordPress', - 'version' => $wp_version, - 'checked' => false, - ) - ); + foreach ( $report_theme->vulnerabilities as $report_vulnerability ) { + $status->threats[] = new Threat_Model( + array( + 'id' => $report_vulnerability->id, + 'title' => $report_vulnerability->title, + 'fixed_in' => $report_vulnerability->fixed_in, + 'description' => isset( $report_vulnerability->description ) ? $report_vulnerability->description : null, + 'source' => isset( $report_vulnerability->id ) ? Redirect::get_url( 'jetpack-protect-vul-info', array( 'path' => $report_vulnerability->id ) ) : null, + 'extension' => new Extension_Model( + array( + 'slug' => $installed_slug, + 'name' => $installed_theme['Name'], + 'version' => $installed_theme['Version'], + 'type' => 'theme', + ) + ), + ) + ); + } + } - if ( isset( $core_check->version ) && $core_check->version === $wp_version ) { - if ( is_array( $core_check->vulnerabilities ) ) { - $core->checked = true; - $core->set_threats( + // WordPress Core Vulnerabilities + $last_report_core = isset( $report_data->core ) ? $report_data->core : new \stdClass(); + if ( isset( $last_report_core->version ) && $last_report_core->version === $wp_version ) { + if ( is_array( $last_report_core->vulnerabilities ) ) { + $core_threats = array_map( - function ( $vulnerability ) { - $vulnerability->source = isset( $vulnerability->id ) ? Redirect::get_url( 'jetpack-protect-vul-info', array( 'path' => $vulnerability->id ) ) : null; - return $vulnerability; + function ( $vulnerability ) use ( $last_report_core ) { + $threat = new Threat_Model( $vulnerability ); + $threat->source = isset( $threat->id ) ? Redirect::get_url( 'jetpack-protect-vul-info', array( 'path' => $threat->id ) ) : null; + $threat->extension = new Extension_Model( + array( + 'slug' => 'wordpress', + 'name' => 'WordPress', + 'version' => $last_report_core->version, + 'type' => 'core', + ) + ); + return $threat; }, - $core_check->vulnerabilities - ) - ); + $last_report_core->vulnerabilities + ); + $status->threats = array_merge( $status->threats, $core_threats ); } } - return $core; + return $status; } } diff --git a/projects/packages/protect-status/src/class-scan-status.php b/projects/packages/protect-status/src/class-scan-status.php index 0ed447f3b8fd3..7518db880349b 100644 --- a/projects/packages/protect-status/src/class-scan-status.php +++ b/projects/packages/protect-status/src/class-scan-status.php @@ -9,11 +9,9 @@ use Automattic\Jetpack\Connection\Client; use Automattic\Jetpack\Connection\Manager as Connection_Manager; -use Automattic\Jetpack\Plugins_Installer; use Automattic\Jetpack\Protect_Models\Extension_Model; use Automattic\Jetpack\Protect_Models\Status_Model; use Automattic\Jetpack\Protect_Models\Threat_Model; -use Automattic\Jetpack\Sync\Functions as Sync_Functions; use Jetpack_Options; use WP_Error; @@ -145,9 +143,6 @@ private static function normalize_api_data( $scan_data ) { $status = new Status_Model(); $status->data_source = 'scan_api'; $status->status = isset( $scan_data->state ) ? $scan_data->state : null; - $status->num_threats = 0; - $status->num_themes_threats = 0; - $status->num_plugins_threats = 0; $status->has_unchecked_items = false; $status->current_progress = isset( $scan_data->current->progress ) ? $scan_data->current->progress : null; @@ -158,109 +153,52 @@ private static function normalize_api_data( $scan_data ) { } } - $status->core = new Extension_Model( - array( - 'type' => 'core', - 'name' => 'WordPress', - 'version' => $wp_version, - 'checked' => true, // to do: default to false once Scan API has manifest - ) - ); - if ( isset( $scan_data->threats ) && is_array( $scan_data->threats ) ) { foreach ( $scan_data->threats as $threat ) { if ( isset( $threat->fixable ) && $threat->fixable ) { $status->fixable_threat_ids[] = $threat->id; } + // Plugin and Theme Threats if ( isset( $threat->extension->type ) ) { - if ( 'plugin' === $threat->extension->type ) { - // add the extension if it does not yet exist in the status - if ( ! isset( $status->plugins[ $threat->extension->slug ] ) ) { - $status->plugins[ $threat->extension->slug ] = new Extension_Model( - array( - 'name' => isset( $threat->extension->name ) ? $threat->extension->name : null, - 'slug' => isset( $threat->extension->slug ) ? $threat->extension->slug : null, - 'version' => isset( $threat->extension->version ) ? $threat->extension->version : null, - 'type' => 'plugin', - 'checked' => true, - 'threats' => array(), - ) - ); - } - - $status->plugins[ $threat->extension->slug ]->threats[] = new Threat_Model( - array( - 'id' => isset( $threat->id ) ? $threat->id : null, - 'signature' => isset( $threat->signature ) ? $threat->signature : null, - 'title' => isset( $threat->title ) ? $threat->title : null, - 'description' => isset( $threat->description ) ? $threat->description : null, - 'vulnerability_description' => isset( $threat->vulnerability_description ) ? $threat->vulnerability_description : null, - 'fix_description' => isset( $threat->fix_description ) ? $threat->fix_description : null, - 'payload_subtitle' => isset( $threat->payload_subtitle ) ? $threat->payload_subtitle : null, - 'payload_description' => isset( $threat->payload_description ) ? $threat->payload_description : null, - 'first_detected' => isset( $threat->first_detected ) ? $threat->first_detected : null, - 'fixed_in' => isset( $threat->fixer->fixer ) && 'update' === $threat->fixer->fixer ? $threat->fixer->target : null, - 'severity' => isset( $threat->severity ) ? $threat->severity : null, - 'fixable' => isset( $threat->fixer ) ? $threat->fixer : null, - 'status' => isset( $threat->status ) ? $threat->status : null, - 'filename' => isset( $threat->filename ) ? $threat->filename : null, - 'context' => isset( $threat->context ) ? $threat->context : null, - 'source' => isset( $threat->source ) ? $threat->source : null, - ) - ); - ++$status->num_threats; - ++$status->num_plugins_threats; - continue; - } - - if ( 'theme' === $threat->extension->type ) { - // add the extension if it does not yet exist in the status - if ( ! isset( $status->themes[ $threat->extension->slug ] ) ) { - $status->themes[ $threat->extension->slug ] = new Extension_Model( + $status->threats[] = new Threat_Model( + array( + 'id' => isset( $threat->id ) ? $threat->id : null, + 'signature' => isset( $threat->signature ) ? $threat->signature : null, + 'title' => isset( $threat->title ) ? $threat->title : null, + 'description' => isset( $threat->description ) ? $threat->description : null, + 'vulnerability_description' => isset( $threat->vulnerability_description ) ? $threat->vulnerability_description : null, + 'fix_description' => isset( $threat->fix_description ) ? $threat->fix_description : null, + 'payload_subtitle' => isset( $threat->payload_subtitle ) ? $threat->payload_subtitle : null, + 'payload_description' => isset( $threat->payload_description ) ? $threat->payload_description : null, + 'first_detected' => isset( $threat->first_detected ) ? $threat->first_detected : null, + 'fixed_in' => isset( $threat->fixer->fixer ) && 'update' === $threat->fixer->fixer ? $threat->fixer->target : null, + 'severity' => isset( $threat->severity ) ? $threat->severity : null, + 'fixable' => isset( $threat->fixer ) ? $threat->fixer : null, + 'status' => isset( $threat->status ) ? $threat->status : null, + 'filename' => isset( $threat->filename ) ? $threat->filename : null, + 'context' => isset( $threat->context ) ? $threat->context : null, + 'source' => isset( $threat->source ) ? $threat->source : null, + 'extension' => new Extension_Model( array( 'name' => isset( $threat->extension->name ) ? $threat->extension->name : null, 'slug' => isset( $threat->extension->slug ) ? $threat->extension->slug : null, 'version' => isset( $threat->extension->version ) ? $threat->extension->version : null, - 'type' => 'theme', - 'checked' => true, - 'threats' => array(), + 'type' => $threat->extension->type, ) - ); - } - - $status->themes[ $threat->extension->slug ]->threats[] = new Threat_Model( - array( - 'id' => isset( $threat->id ) ? $threat->id : null, - 'signature' => isset( $threat->signature ) ? $threat->signature : null, - 'title' => isset( $threat->title ) ? $threat->title : null, - 'description' => isset( $threat->description ) ? $threat->description : null, - 'vulnerability_description' => isset( $threat->vulnerability_description ) ? $threat->vulnerability_description : null, - 'fix_description' => isset( $threat->fix_description ) ? $threat->fix_description : null, - 'payload_subtitle' => isset( $threat->payload_subtitle ) ? $threat->payload_subtitle : null, - 'payload_description' => isset( $threat->payload_description ) ? $threat->payload_description : null, - 'first_detected' => isset( $threat->first_detected ) ? $threat->first_detected : null, - 'fixed_in' => isset( $threat->fixer->fixer ) && 'update' === $threat->fixer->fixer ? $threat->fixer->target : null, - 'severity' => isset( $threat->severity ) ? $threat->severity : null, - 'fixable' => isset( $threat->fixer ) ? $threat->fixer : null, - 'status' => isset( $threat->status ) ? $threat->status : null, - 'filename' => isset( $threat->filename ) ? $threat->filename : null, - 'context' => isset( $threat->context ) ? $threat->context : null, - 'source' => isset( $threat->source ) ? $threat->source : null, - ) - ); - ++$status->num_threats; - ++$status->num_themes_threats; - continue; - } + ), + ) + ); + continue; } + // WordPress Core Threats if ( isset( $threat->signature ) && 'Vulnerable.WP.Core' === $threat->signature ) { if ( $threat->version !== $wp_version ) { continue; } - $status->core->threats[] = new Threat_Model( + $status->threats[] = new Threat_Model( array( 'id' => $threat->id, 'signature' => $threat->signature, @@ -268,99 +206,34 @@ private static function normalize_api_data( $scan_data ) { 'description' => $threat->description, 'first_detected' => $threat->first_detected, 'severity' => $threat->severity, + 'extension' => new Extension_Model( + array( + 'name' => 'WordPress', + 'slug' => 'wordpress', + 'version' => $wp_version, + 'type' => 'core', + ) + ), ) ); - ++$status->num_threats; continue; } + // File Threats if ( ! empty( $threat->filename ) ) { - $status->files[] = new Threat_Model( $threat ); - ++$status->num_threats; + $status->threats[] = new Threat_Model( $threat ); continue; } + // Database Threats if ( ! empty( $threat->table ) ) { - $status->database[] = new Threat_Model( $threat ); - ++$status->num_threats; + $status->threats[] = new Threat_Model( $threat ); continue; } } } - $installed_plugins = Plugins_Installer::get_plugins(); - $status->plugins = self::merge_installed_and_checked_lists( $installed_plugins, $status->plugins, array( 'type' => 'plugins' ), true ); - - $installed_themes = Sync_Functions::get_themes(); - $status->themes = self::merge_installed_and_checked_lists( $installed_themes, $status->themes, array( 'type' => 'themes' ), true ); - - foreach ( array_merge( $status->themes, $status->plugins ) as $extension ) { - if ( ! $extension->checked ) { - $status->has_unchecked_items = true; - break; - } - } - return $status; } - - /** - * Merges the list of installed extensions with the list of extensions that were checked for known vulnerabilities and return a normalized list to be used in the UI - * - * @param array $installed The list of installed extensions, where each attribute key is the extension slug. - * @param object $checked The list of checked extensions. - * @param array $append Additional data to append to each result in the list. - * @return array Normalized list of extensions. - */ - protected static function merge_installed_and_checked_lists( $installed, $checked, $append ) { - $new_list = array(); - $checked = (object) $checked; - - foreach ( array_keys( $installed ) as $slug ) { - /** - * Extension Type Map - * - * @var array $extension_type_map Key value pairs of extension types and their corresponding - * identifier used by the Scan API data source. - */ - $extension_type_map = array( - 'themes' => 'r1', - 'plugins' => 'r2', - ); - - $version = $installed[ $slug ]['Version']; - $short_slug = str_replace( '.php', '', explode( '/', $slug )[0] ); - $scanifest_slug = $extension_type_map[ $append['type'] ] . ":$short_slug@$version"; - - $extension = new Extension_Model( - array_merge( - array( - 'name' => $installed[ $slug ]['Name'], - 'version' => $version, - 'slug' => $slug, - 'threats' => array(), - 'checked' => false, - ), - $append - ) - ); - - if ( ! isset( $checked->extensions ) // no extension data available from Scan API - || is_array( $checked->extensions ) && in_array( $scanifest_slug, $checked->extensions, true ) // extension data matches Scan API - ) { - $extension->checked = true; - if ( isset( $checked->{ $short_slug }->threats ) ) { - $extension->threats = $checked->{ $short_slug }->threats; - } - } - - $new_list[] = $extension; - - } - - $new_list = parent::sort_threats( $new_list ); - - return $new_list; - } } diff --git a/projects/packages/protect-status/src/class-status.php b/projects/packages/protect-status/src/class-status.php index df547b88e528b..b1a7c520fe17d 100644 --- a/projects/packages/protect-status/src/class-status.php +++ b/projects/packages/protect-status/src/class-status.php @@ -7,7 +7,6 @@ namespace Automattic\Jetpack\Protect_Status; -use Automattic\Jetpack\Protect_Models\Extension_Model; use Automattic\Jetpack\Protect_Models\Status_Model; /** @@ -163,7 +162,7 @@ public static function has_threats() { */ public static function get_total_threats() { $status = static::get_status(); - return isset( $status->num_threats ) && is_int( $status->num_threats ) ? $status->num_threats : 0; + return isset( $status->threats ) && is_array( $status->threats ) ? count( $status->threats ) : 0; } /** @@ -172,140 +171,7 @@ public static function get_total_threats() { * @return array */ public static function get_all_threats() { - return array_merge( - self::get_wordpress_threats(), - self::get_themes_threats(), - self::get_plugins_threats(), - self::get_files_threats(), - self::get_database_threats() - ); - } - - /** - * Get threats found for WordPress core - * - * @return array - */ - public static function get_wordpress_threats() { - return self::get_threats( 'core' ); - } - - /** - * Get threats found for themes - * - * @return array - */ - public static function get_themes_threats() { - return self::get_threats( 'themes' ); - } - - /** - * Get threats found for plugins - * - * @return array - */ - public static function get_plugins_threats() { - return self::get_threats( 'plugins' ); - } - - /** - * Get threats found for files - * - * @return array - */ - public static function get_files_threats() { - return self::get_threats( 'files' ); - } - - /** - * Get threats found for plugins - * - * @return array - */ - public static function get_database_threats() { - return self::get_threats( 'database' ); - } - - /** - * Get the threats for one type of extension or core - * - * @param string $type What threats you want to get. Possible values are 'core', 'themes' and 'plugins'. - * - * @return array - */ - public static function get_threats( $type ) { $status = static::get_status(); - - if ( 'core' === $type ) { - return isset( $status->$type ) && ! empty( $status->$type->threats ) ? $status->$type->threats : array(); - } - - if ( 'files' === $type || 'database' === $type ) { - return isset( $status->$type ) && ! empty( $status->$type ) ? $status->$type : array(); - } - - $threats = array(); - if ( isset( $status->$type ) ) { - foreach ( (array) $status->$type as $item ) { - if ( ! empty( $item->threats ) ) { - $threats = array_merge( $threats, $item->threats ); - } - } - } - return $threats; - } - - /** - * Check if the WordPress version that was checked matches the current installed version. - * - * @param object $core_check The object returned by Protect wpcom endpoint. - * @return object The object representing the current status of core checks. - */ - protected static function normalize_core_information( $core_check ) { - global $wp_version; - - $core = new Extension_Model( - array( - 'type' => 'core', - 'name' => 'WordPress', - 'version' => $wp_version, - 'checked' => false, - ) - ); - - if ( isset( $core_check->version ) && $core_check->version === $wp_version ) { - if ( is_array( $core_check->vulnerabilities ) ) { - $core->checked = true; - $core->set_threats( $core_check->vulnerabilities ); - } - } - - return $core; - } - - /** - * Sort By Threats - * - * @param array $threats Array of threats to sort. - * - * @return array The sorted $threats array. - */ - protected static function sort_threats( $threats ) { - usort( - $threats, - function ( $a, $b ) { - // sort primarily based on the presence of threats - $ret = empty( $a->threats ) <=> empty( $b->threats ); - - // sort secondarily on whether the item has been checked - if ( ! $ret ) { - $ret = $a->checked <=> $b->checked; - } - - return $ret; - } - ); - - return $threats; + return isset( $status->threats ) && is_array( $status->threats ) ? $status->threats : array(); } } diff --git a/projects/packages/protect-status/tests/php/test-scan-status.php b/projects/packages/protect-status/tests/php/test-scan-status.php index 0777de7c7bd94..062b6430e41cf 100644 --- a/projects/packages/protect-status/tests/php/test-scan-status.php +++ b/projects/packages/protect-status/tests/php/test-scan-status.php @@ -124,8 +124,6 @@ public function get_sample_response() { * @return object */ public function get_sample_status() { - global $wp_version; - return new Status_Model( array( 'data_source' => 'scan_api', @@ -135,53 +133,7 @@ public function get_sample_status() { 'num_themes_threats' => 0, 'status' => 'idle', 'fixable_threat_ids' => array( '69353714' ), - 'plugins' => array( - new Extension_Model( - array( - 'version' => '3.0.0', - 'name' => 'Woocommerce', - 'checked' => true, - 'type' => 'plugins', - 'threats' => array( - new Threat_Model( - array( - 'id' => '71625245', - 'signature' => 'Vulnerable.WP.Extension', - 'description' => 'The plugin WooCommerce (version 3.0.0) has a known vulnerability. ', - 'first_detected' => '2022-07-27T17:22:16.000Z', - 'severity' => 3, - 'fixable' => null, - 'status' => 'current', - 'source' => 'https://wpvulndb.com/vulnerabilities/10220', - ) - ), - ), - 'slug' => 'woocommerce', - ) - ), - ), - 'themes' => array( - new Extension_Model( - array( - 'name' => 'Sample Theme', - 'slug' => 'theme-1', - 'version' => '1.0.2', - 'type' => 'themes', - 'threats' => array(), - 'checked' => true, - ) - ), - ), - 'core' => new Extension_Model( - array( - 'version' => $wp_version, - 'threats' => array(), - 'checked' => true, - 'name' => 'WordPress', - 'type' => 'core', - ) - ), - 'files' => array( + 'threats' => array( new Threat_Model( array( 'id' => 71626681, @@ -199,6 +151,26 @@ public function get_sample_status() { ), ) ), + new Threat_Model( + array( + 'id' => '71625245', + 'signature' => 'Vulnerable.WP.Extension', + 'description' => 'The plugin WooCommerce (version 3.0.0) has a known vulnerability. ', + 'first_detected' => '2022-07-27T17:22:16.000Z', + 'severity' => 3, + 'fixable' => null, + 'status' => 'current', + 'source' => 'https://wpvulndb.com/vulnerabilities/10220', + 'extension' => new Extension_Model( + array( + 'slug' => 'woocommerce', + 'version' => '3.0.0', + 'name' => 'WooCommerce', + 'type' => 'plugin', + ) + ), + ) + ), new Threat_Model( array( 'id' => 69353714, @@ -217,8 +189,6 @@ public function get_sample_status() { ) ), ), - 'database' => array(), - 'has_unchecked_items' => false, ) ); } @@ -364,72 +334,6 @@ public function test_get_total_threats() { $this->assertSame( 3, $status ); } - /** - * Test get all threats - */ - public function test_get_all_threats() { - $this->mock_connection(); - - $expected = array( - new Threat_Model( - array( - 'id' => '71625245', - 'signature' => 'Vulnerable.WP.Extension', - 'description' => 'The plugin WooCommerce (version 3.0.0) has a known vulnerability. ', - 'first_detected' => '2022-07-27T17:22:16.000Z', - 'severity' => 3, - 'fixable' => null, - 'status' => 'current', - 'source' => 'https://wpvulndb.com/vulnerabilities/10220', - ) - ), - new Threat_Model( - array( - 'id' => 71626681, - 'signature' => 'EICAR_AV_Test_Critical', - 'description' => 'This is the standard EICAR antivirus test code, and not a real infection. If your site contains this code when you don\'t expect it to, contact Jetpack support for some help.', - 'first_detected' => '2022-07-27T17 => 49 => 35.000Z', - 'severity' => 5, - 'fixer' => null, - 'status' => 'current', - 'filename' => '/var/www/html/wp-content/uploads/jptt_eicar.php', - 'context' => (object) array( - '15' => 'echo <<', - '17' => 'HTML;', - 'marks' => new \stdClass(), - ), - ) - ), - new Threat_Model( - array( - 'id' => 69353714, - 'signature' => 'Core.File.Modification', - 'description' => 'Core WordPress files are not normally changed. If you did not make these changes you should review the code.', - 'first_detected' => '2022-06-23T18:42:29.000Z', - 'severity' => 4, - 'status' => 'current', - 'fixable' => (object) array( - 'fixer' => 'replace', - 'file' => '/var/www/html/wp-admin/index.php', - 'extensionStatus' => '', - ), - 'filename' => '/var/www/html/wp-admin/index.php', - 'diff' => "--- /tmp/wordpress/6.0-en_US/wordpress/wp-admin/index.php\t2021-11-03 03:16:57.000000000 +0000\n+++ /tmp/6299071296/core-file-23271BW6i4wLCe3T7\t2022-06-23 18:42:29.087377846 +0000\n@@ -209,3 +209,4 @@\n wp_print_community_events_templates();\n \n require_once ABSPATH . 'wp-admin/admin-footer.php';\n+if ( true === false ) exit();\n\\ No newline at end of file\n", - ) - ), - ); - - add_filter( 'pre_http_request', array( $this, 'return_sample_response' ) ); - add_filter( 'all_plugins', array( $this, 'return_sample_plugins' ) ); - add_filter( 'jetpack_sync_get_themes_callable', array( $this, 'return_sample_themes' ) ); - $all_threats = Scan_Status::get_all_threats(); - remove_filter( 'pre_http_request', array( $this, 'return_sample_response' ) ); - remove_filter( 'all_plugins', array( $this, 'return_sample_plugins' ) ); - remove_filter( 'jetpack_sync_get_themes_callable', array( $this, 'return_sample_themes' ) ); - - $this->assertEquals( $expected, $all_threats ); - } - /** * Data provider for test_is_cache_expired */ diff --git a/projects/packages/protect-status/tests/php/test-status.php b/projects/packages/protect-status/tests/php/test-status.php index b437e9bc48002..e38cc5f4c3350 100644 --- a/projects/packages/protect-status/tests/php/test-status.php +++ b/projects/packages/protect-status/tests/php/test-status.php @@ -13,7 +13,6 @@ use Automattic\Jetpack\Protect_Models\Extension_Model; use Automattic\Jetpack\Protect_Models\Status_Model; use Automattic\Jetpack\Protect_Models\Threat_Model; -use Automattic\Jetpack\Redirect; use Jetpack_Options; use WorDBless\BaseTestCase; @@ -29,104 +28,6 @@ protected function set_up() { Protect_Status::$status = null; } - /** - * Get a sample checked theme result - * - * @param string $id The unique theme ID. - * @param bool $with_threats Whether the sample should include a vulnerability. - * @return object - */ - public function get_sample_theme( $id, $with_threats = true ) { - $item = (object) array( - 'version' => '1.0.2', - 'name' => 'Sample Theme', - 'checked' => true, - 'type' => 'themes', - 'threats' => array(), - 'slug' => "theme-$id", - ); - if ( $with_threats ) { - $item->threats[] = $this->get_sample_threat(); - } - return $item; - } - - /** - * Get a sample checked plugin result - * - * @param string $id The unique plugin ID. - * @param bool $with_threats Whether the sample should include a vulnerability. - * @return object - */ - public function get_sample_plugin( $id, $with_threats = true ) { - $item = (object) array( - 'version' => '1.0.2', - 'name' => 'Sample Plugin', - 'checked' => true, - 'type' => 'plugins', - 'threats' => array(), - 'slug' => "plugin-$id", - ); - if ( $with_threats ) { - $item->threats[] = $this->get_sample_threat(); - } - return $item; - } - - /** - * Get a sample checked core result - * - * @param bool $with_threats Whether the sample should include a vulnerability. - * @return object - */ - public function get_sample_core( $with_threats = true ) { - global $wp_version; - - $item = (object) array( - 'version' => $wp_version, - 'threats' => array(), - 'checked' => true, - 'name' => 'WordPress', - 'type' => 'core', - ); - if ( $with_threats ) { - $item->threats[] = $this->get_sample_threat(); - } - - return $item; - } - - /** - * Get a sample vulnerabilty - * - * @return object - */ - public function get_sample_vul() { - return (object) array( - 'id' => 'asdasdasd-123123-asdasd', - 'title' => 'Sample Vul', - 'fixed_in' => '2.0.0', - ); - } - - /** - * Get a sample threat - * - * @return object - */ - public function get_sample_threat() { - $id = 'asdasdasd-123123-asdasd'; - - return new Threat_Model( - array( - 'id' => $id, - 'title' => 'Sample Vul', - 'fixed_in' => '2.0.0', - 'source' => Redirect::get_url( 'jetpack-protect-vul-info', array( 'path' => $id ) ), - ) - ); - } - /** * Get a sample empty response * @@ -163,29 +64,37 @@ public function get_sample_response() { 'num_themes_vulnerabilities' => 1, 'num_plugins_vulnerabilities' => 1, 'themes' => (object) array( - 'theme-1' => (object) array( - 'slug' => 'theme-1', - 'name' => 'Sample Theme', + 'example-theme-1' => (object) array( + 'slug' => 'example-theme', + 'name' => 'Example Theme', 'version' => '1.0.2', 'checked' => true, 'vulnerabilities' => array( - $this->get_sample_vul(), + (object) array( + 'id' => 'example-theme-threat', + 'title' => 'Example Theme Threat', + 'fixed_in' => '2.0.0', + ), ), ), ), 'plugins' => (object) array( - 'plugin-1' => (object) array( - 'slug' => 'plugin-1', - 'name' => 'Sample Plugin', + 'example-plugin-1' => (object) array( + 'slug' => 'example-plugin', + 'name' => 'Example Plugin', 'version' => '1.0.2', 'checked' => true, 'vulnerabilities' => array( - $this->get_sample_vul(), + (object) array( + 'id' => 'example-plugin-threat', + 'title' => 'Example Plugin Threat', + 'fixed_in' => '2.0.0', + ), ), ), - 'plugin-2' => (object) array( - 'slug' => 'plugin-2', - 'name' => 'Sample Plugin', + 'example-plugin-2' => (object) array( + 'slug' => 'example-plugin-2', + 'name' => 'Example Plugin 2', 'version' => '1.0.2', 'checked' => true, 'vulnerabilities' => array(), @@ -195,7 +104,10 @@ public function get_sample_response() { 'version' => $wp_version, 'checked' => true, 'vulnerabilities' => array( - $this->get_sample_vul(), + (object) array( + 'id' => 'example-core-threat', + 'title' => 'Example Core Threat', + ), ), 'name' => 'WordPress', ), @@ -208,23 +120,61 @@ public function get_sample_response() { * @return object */ public function get_sample_status() { + global $wp_version; + return new Status_Model( array( - 'data_source' => 'protect_report', - 'plugins' => array( - new Extension_Model( $this->get_sample_plugin( '1' ) ), - new Extension_Model( $this->get_sample_plugin( '2', false ) ), - ), - 'themes' => array( - new Extension_Model( $this->get_sample_theme( '1' ) ), + 'data_source' => 'protect_report', + 'threats' => array( + new Threat_Model( + array( + 'id' => 'example-plugin-threat', + 'title' => 'Example Plugin Threat', + 'fixed_in' => '2.0.0', + 'source' => 'https://jetpack.com/redirect/?source=jetpack-protect-vul-info&site=example.org&path=example-plugin-threat', + 'extension' => new Extension_Model( + array( + 'name' => 'Example Plugin', + 'slug' => 'example-plugin-1', + 'version' => '1.0.2', + 'type' => 'plugin', + ) + ), + ) + ), + new Threat_Model( + array( + 'id' => 'example-theme-threat', + 'title' => 'Example Theme Threat', + 'fixed_in' => '2.0.0', + 'source' => 'https://jetpack.com/redirect/?source=jetpack-protect-vul-info&site=example.org&path=example-theme-threat', + 'extension' => new Extension_Model( + array( + 'name' => 'Example Theme', + 'slug' => 'example-theme-1', + 'version' => '1.0.2', + 'type' => 'theme', + ) + ), + ) + ), + new Threat_Model( + array( + 'id' => 'example-core-threat', + 'title' => 'Example Core Threat', + 'source' => 'https://jetpack.com/redirect/?source=jetpack-protect-vul-info&site=example.org&path=example-core-threat', + 'extension' => new Extension_Model( + array( + 'name' => 'WordPress', + 'slug' => 'wordpress', + 'version' => $wp_version, + 'type' => 'core', + ) + ), + ) + ), ), - 'core' => new Extension_Model( $this->get_sample_core() ), - 'wordpress' => $this->get_sample_core(), - 'last_checked' => '2003-03-03 03:03:03', - 'num_threats' => 3, - 'num_themes_threats' => 1, - 'num_plugins_threats' => 1, - 'has_unchecked_items' => false, + 'last_checked' => '2003-03-03 03:03:03', ) ); } @@ -251,12 +201,12 @@ public function return_sample_response() { */ public function return_sample_plugins() { return array( - 'plugin-1' => array( - 'Name' => 'Sample Plugin', + 'example-plugin-1' => array( + 'Name' => 'Example Plugin', 'Version' => '1.0.2', ), - 'plugin-2' => array( - 'Name' => 'Sample Plugin', + 'example-plugin-2' => array( + 'Name' => 'Example Plugin', 'Version' => '1.0.2', ), ); @@ -269,8 +219,8 @@ public function return_sample_plugins() { */ public function return_sample_themes() { return array( - 'theme-1' => array( - 'Name' => 'Sample Theme', + 'example-theme-1' => array( + 'Name' => 'Example Theme', 'Version' => '1.0.2', ), ); @@ -360,34 +310,15 @@ public function test_get_status() { public function test_get_total_threats() { $this->mock_connection(); - add_filter( 'pre_http_request', array( $this, 'return_sample_response' ) ); - $status = Protect_Status::get_total_threats(); - remove_filter( 'pre_http_request', array( $this, 'return_sample_response' ) ); - - $this->assertSame( 3, $status ); - } - - /** - * Test get all threats - */ - public function test_get_all_threats() { - $this->mock_connection(); - - $expected = array( - $this->get_sample_threat(), - $this->get_sample_threat(), - $this->get_sample_threat(), - ); - add_filter( 'pre_http_request', array( $this, 'return_sample_response' ) ); add_filter( 'all_plugins', array( $this, 'return_sample_plugins' ) ); add_filter( 'jetpack_sync_get_themes_callable', array( $this, 'return_sample_themes' ) ); - $status = Protect_Status::get_all_threats(); + $status = Protect_Status::get_total_threats(); remove_filter( 'pre_http_request', array( $this, 'return_sample_response' ) ); remove_filter( 'all_plugins', array( $this, 'return_sample_plugins' ) ); remove_filter( 'jetpack_sync_get_themes_callable', array( $this, 'return_sample_themes' ) ); - $this->assertEquals( $expected, $status ); + $this->assertSame( 3, $status ); } /** diff --git a/projects/plugins/protect/package.json b/projects/plugins/protect/package.json index 257979fa86ad8..9d9567efe68a3 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.9.0", diff --git a/projects/plugins/protect/src/class-scan-history.php b/projects/plugins/protect/src/class-scan-history.php index de88b135778cc..7046903af6f26 100644 --- a/projects/plugins/protect/src/class-scan-history.php +++ b/projects/plugins/protect/src/class-scan-history.php @@ -211,91 +211,20 @@ 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; } - /** - * Handles threats for extensions such as plugins or themes. - * - * @param object $threat The threat object. - * @param object $history The history object. - * @param string $type The type of extension ('plugin' or 'theme'). - * @return void - */ - private static function handle_extension_threats( $threat, $history, $type ) { - $extension_list = $type === 'plugin' ? 'plugins' : 'themes'; - $extensions = &$history->{ $extension_list}; - $found_index = null; - - // Check if the extension does not exist in the array - foreach ( $extensions as $index => $extension ) { - if ( $extension->slug === $threat->extension->slug ) { - $found_index = $index; - break; - } - } - - // Add the extension if it does not yet exist in the history - if ( $found_index === null ) { - $new_extension = new Extension_Model( - array( - 'name' => $threat->extension->name ?? null, - 'slug' => $threat->extension->slug ?? null, - 'version' => $threat->extension->version ?? null, - 'type' => $type, - 'checked' => true, - 'threats' => array(), - ) - ); - $extensions[] = $new_extension; - $found_index = array_key_last( $extensions ); - } - - // Add the threat to the found extension - $extensions[ $found_index ]->threats[] = new Threat_Model( $threat ); - - // Increment the threat counts - ++$history->num_threats; - if ( $type === 'plugin' ) { - ++$history->num_plugins_threats; - } elseif ( $type === 'theme' ) { - ++$history->num_themes_threats; - } - } - /** * Handles core threats * 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/free-accordion/index.jsx b/projects/plugins/protect/src/js/components/free-accordion/index.jsx deleted file mode 100644 index e801d9374fd33..0000000000000 --- a/projects/plugins/protect/src/js/components/free-accordion/index.jsx +++ /dev/null @@ -1,64 +0,0 @@ -import { Text } from '@automattic/jetpack-components'; -import { Icon, chevronDown, chevronUp } from '@wordpress/icons'; -import clsx from 'clsx'; -import React, { useState, useCallback, useContext } from 'react'; -import styles from './styles.module.scss'; - -const FreeAccordionContext = React.createContext(); - -export const FreeAccordionItem = ( { id, title, label, icon, children, onOpen } ) => { - const accordionData = useContext( FreeAccordionContext ); - const open = accordionData?.open === id; - const setOpen = accordionData?.setOpen; - - const bodyClassNames = clsx( styles[ 'accordion-body' ], { - [ styles[ 'accordion-body-open' ] ]: open, - [ styles[ 'accordion-body-close' ] ]: ! open, - } ); - - const handleClick = useCallback( () => { - if ( ! open ) { - onOpen?.(); - } - setOpen( current => { - return current === id ? null : id; - } ); - }, [ open, onOpen, setOpen, id ] ); - - return ( -
- -
- { children } -
-
- ); -}; - -const FreeAccordion = ( { children } ) => { - const [ open, setOpen ] = useState(); - - return ( - -
{ children }
-
- ); -}; - -export default FreeAccordion; diff --git a/projects/plugins/protect/src/js/components/free-accordion/stories/index.stories.jsx b/projects/plugins/protect/src/js/components/free-accordion/stories/index.stories.jsx deleted file mode 100644 index 43ad41e2501eb..0000000000000 --- a/projects/plugins/protect/src/js/components/free-accordion/stories/index.stories.jsx +++ /dev/null @@ -1,120 +0,0 @@ -import { Text } from '@automattic/jetpack-components'; -import { wordpress, plugins } from '@wordpress/icons'; -import React from 'react'; -import FreeAccordion, { FreeAccordionItem } from '..'; - -export default { - title: 'Plugins/Protect/Free Accordion', - component: FreeAccordion, - parameters: { - layout: 'centered', - }, - decorators: [ - Story => ( -
- -
- ), - ], -}; - -// eslint-disable-next-line no-unused-vars -export const Default = args => ( - - - - What is the problem? - - - Post authors are able to bypass KSES restrictions in WordPress { '>' }= 5.9 (and or - Gutenberg { '>' }= 9.8.0) due to the order filters are executed, which could allow them to - perform to Stored Cross-Site Scripting attacks - - - How to fix it? - - Update to WordPress 5.9.2 - - - - What is the problem? - - - Post authors are able to bypass KSES restrictions in WordPress { '>' }= 5.9 (and or - Gutenberg { '>' }= 9.8.0) due to the order filters are executed, which could allow them to - perform to Stored Cross-Site Scripting attacks - - - How to fix it? - - Update to WordPress 5.9.2 - - - - What is the problem? - - - Post authors are able to bypass KSES restrictions in WordPress { '>' }= 5.9 (and or - Gutenberg { '>' }= 9.8.0) due to the order filters are executed, which could allow them to - perform to Stored Cross-Site Scripting attacks - - - How to fix it? - - Update to WordPress 5.9.2 - - - - What is the problem? - - - Post authors are able to bypass KSES restrictions in WordPress { '>' }= 5.9 (and or - Gutenberg { '>' }= 9.8.0) due to the order filters are executed, which could allow them to - perform to Stored Cross-Site Scripting attacks - - - How to fix it? - - Update to WordPress 5.9.2 - - - - What is the problem? - - - Post authors are able to bypass KSES restrictions in WordPress { '>' }= 5.9 (and or - Gutenberg { '>' }= 9.8.0) due to the order filters are executed, which could allow them to - perform to Stored Cross-Site Scripting attacks - - - How to fix it? - - Update to WordPress 5.9.2 - - -); diff --git a/projects/plugins/protect/src/js/components/free-accordion/styles.module.scss b/projects/plugins/protect/src/js/components/free-accordion/styles.module.scss deleted file mode 100644 index 5278f6eff39f4..0000000000000 --- a/projects/plugins/protect/src/js/components/free-accordion/styles.module.scss +++ /dev/null @@ -1,79 +0,0 @@ -.accordion { - border-radius: var( --jp-border-radius ); - border: 1px solid var( --jp-gray ); - - & > *:not(:last-child) { - border-bottom: 1px solid var( --jp-gray ); - } -} - -.accordion-item { - background-color: var( --jp-white ); -} - -.accordion-header { - margin: 0; - display: grid; - grid-template-columns: repeat(9, 1fr); - cursor: pointer; - box-sizing: border-box; - background: none; - border: none; - width: 100%; - align-items: center; - outline-color: var( --jp-black ); - padding: calc( var( --spacing-base ) * 2) calc( var( --spacing-base ) * 3); // 16px | 24px - text-align: start; - - >:first-of-type { - grid-column: 1/8; - } - - >:last-of-type { - grid-column: 9; - } - - &:hover { - background: var( --jp-gray-0 ); - } -} - -.accordion-header-label { - display: flex; - align-items: center; - font-size: var( --font-body-small ); - font-weight: normal; -} - -.accordion-header-label-icon { - margin-right: var( --spacing-base ); // 8px -} - -.accordion-header-description { - font-weight: 600; - margin-left: calc( var( --spacing-base ) * 4 ); // 32px - margin-bottom: var( --spacing-base ); // 8px -} - -.accordion-header-button { - align-items: center; -} - -.accordion-body { - transform-origin: top center; - overflow: hidden; - - &-close { - transition: all .1s; - max-height: 0; - padding: 0; - transform: scaleY(0); - } - - &-open { - transition: max-height .3s, transform .2s; - padding: calc( var( --spacing-base ) * 4 ) calc( var( --spacing-base ) * 7 ); // 32 px | 56px - max-height: 1000px; - transform: scaleY(1); - } -} 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/paid-accordion/index.jsx b/projects/plugins/protect/src/js/components/paid-accordion/index.jsx deleted file mode 100644 index d4a6fa5d3f0ba..0000000000000 --- a/projects/plugins/protect/src/js/components/paid-accordion/index.jsx +++ /dev/null @@ -1,187 +0,0 @@ -import { - Spinner, - Text, - ThreatSeverityBadge, - useBreakpointMatch, -} from '@automattic/jetpack-components'; -import { ExternalLink } from '@wordpress/components'; -import { dateI18n } from '@wordpress/date'; -import { createInterpolateElement } from '@wordpress/element'; -import { sprintf, __ } from '@wordpress/i18n'; -import { Icon, check, chevronDown, chevronUp, info } from '@wordpress/icons'; -import clsx from 'clsx'; -import React, { useState, useCallback, useContext, useMemo } from 'react'; -import { PAID_PLUGIN_SUPPORT_URL } from '../../constants'; -import useFixers from '../../hooks/use-fixers'; -import IconTooltip from '../icon-tooltip'; -import styles from './styles.module.scss'; - -// Extract context provider for clarity and reusability -const PaidAccordionContext = React.createContext(); - -// Component for displaying threat dates -const ScanHistoryDetails = ( { firstDetected, fixedOn, status } ) => { - const statusText = useMemo( () => { - if ( status === 'fixed' ) { - return sprintf( - /* translators: %s: Fixed on date */ - __( 'Threat fixed %s', 'jetpack-protect' ), - dateI18n( 'M j, Y', fixedOn ) - ); - } - if ( status === 'ignored' ) { - return __( 'Threat ignored', 'jetpack-protect' ); - } - return null; - }, [ status, fixedOn ] ); - - return ( - firstDetected && ( - <> - - { sprintf( - /* translators: %s: First detected date */ - __( 'Threat found %s', 'jetpack-protect' ), - dateI18n( 'M j, Y', firstDetected ) - ) } - { statusText && ( - <> - - { statusText } - - ) } - - { [ 'fixed', 'ignored' ].includes( status ) && } - - ) - ); -}; - -// Badge for displaying the status (fixed or ignored) -const StatusBadge = ( { status } ) => ( -
- { status === 'fixed' - ? __( 'Fixed', 'jetpack-protect' ) - : __( 'Ignored', 'jetpack-protect', /* dummy arg to avoid bad minification */ 0 ) } -
-); - -const renderFixerStatus = ( isActiveFixInProgress, isStaleFixInProgress ) => { - if ( isStaleFixInProgress ) { - return ( - contact support.', - 'jetpack-protect' - ), - { - supportLink: ( - - ), - } - ) } - /> - ); - } - - if ( isActiveFixInProgress ) { - return ; - } - - return ; -}; - -export const PaidAccordionItem = ( { - id, - title, - label, - icon, - fixable, - severity, - children, - firstDetected, - fixedOn, - onOpen, - status, - hideAutoFixColumn = false, -} ) => { - const { open, setOpen } = useContext( PaidAccordionContext ); - const isOpen = open === id; - - const { isThreatFixInProgress, isThreatFixStale } = useFixers(); - - const handleClick = useCallback( () => { - if ( ! isOpen ) { - onOpen?.(); - } - setOpen( current => ( current === id ? null : id ) ); - }, [ isOpen, onOpen, setOpen, id ] ); - - const [ isSmall ] = useBreakpointMatch( [ 'sm', 'lg' ], [ null, '<' ] ); - - return ( -
- -
- { children } -
-
- ); -}; - -const PaidAccordion = ( { children } ) => { - const [ open, setOpen ] = useState(); - - return ( - -
{ children }
-
- ); -}; - -export default PaidAccordion; diff --git a/projects/plugins/protect/src/js/components/paid-accordion/stories/broken/index.stories.jsx b/projects/plugins/protect/src/js/components/paid-accordion/stories/broken/index.stories.jsx deleted file mode 100644 index 252f22b2bad77..0000000000000 --- a/projects/plugins/protect/src/js/components/paid-accordion/stories/broken/index.stories.jsx +++ /dev/null @@ -1,120 +0,0 @@ -import { Text } from '@automattic/jetpack-components'; -import { wordpress, plugins } from '@wordpress/icons'; -import React from 'react'; -import PaidAccordion, { PaidAccordionItem } from '..'; - -export default { - title: 'Plugins/Protect/Paid Accordion', - component: PaidAccordion, - parameters: { - layout: 'centered', - }, - decorators: [ - Story => ( -
- -
- ), - ], -}; - -// eslint-disable-next-line no-unused-vars -export const Default = args => ( - - - - What is the problem? - - - Post authors are able to bypass KSES restrictions in WordPress { '>' }= 5.9 (and or - Gutenberg { '>' }= 9.8.0) due to the order filters are executed, which could allow them to - perform to Stored Cross-Site Scripting attacks - - - How to fix it? - - Update to WordPress 5.9.2 - - - - What is the problem? - - - Post authors are able to bypass KSES restrictions in WordPress { '>' }= 5.9 (and or - Gutenberg { '>' }= 9.8.0) due to the order filters are executed, which could allow them to - perform to Stored Cross-Site Scripting attacks - - - How to fix it? - - Update to WordPress 5.9.2 - - - - What is the problem? - - - Post authors are able to bypass KSES restrictions in WordPress { '>' }= 5.9 (and or - Gutenberg { '>' }= 9.8.0) due to the order filters are executed, which could allow them to - perform to Stored Cross-Site Scripting attacks - - - How to fix it? - - Update to WordPress 5.9.2 - - - - What is the problem? - - - Post authors are able to bypass KSES restrictions in WordPress { '>' }= 5.9 (and or - Gutenberg { '>' }= 9.8.0) due to the order filters are executed, which could allow them to - perform to Stored Cross-Site Scripting attacks - - - How to fix it? - - Update to WordPress 5.9.2 - - - - What is the problem? - - - Post authors are able to bypass KSES restrictions in WordPress { '>' }= 5.9 (and or - Gutenberg { '>' }= 9.8.0) due to the order filters are executed, which could allow them to - perform to Stored Cross-Site Scripting attacks - - - How to fix it? - - Update to WordPress 5.9.2 - - -); diff --git a/projects/plugins/protect/src/js/components/paid-accordion/styles.module.scss b/projects/plugins/protect/src/js/components/paid-accordion/styles.module.scss deleted file mode 100644 index 3e88a5f9f7ccb..0000000000000 --- a/projects/plugins/protect/src/js/components/paid-accordion/styles.module.scss +++ /dev/null @@ -1,191 +0,0 @@ -.accordion { - display: inline-block; - width: 100%; - border-radius: var( --jp-border-radius ); - border: 1px solid var( --jp-gray ); - - & > *:not(:last-child) { - border-bottom: 1px solid var( --jp-gray ); - } -} - -.accordion-item { - background-color: var( --jp-white ); -} - -.accordion-header { - margin: 0; - display: grid; - grid-template-columns: repeat(9, 1fr); - cursor: pointer; - box-sizing: border-box; - background: none; - border: none; - width: 100%; - align-items: center; - outline-color: var( --jp-black ); - padding: calc( var( --spacing-base ) * 2) calc( var( --spacing-base ) * 3); // 16px | 24px - text-align: start; - - >:first-of-type { - grid-column: 1/7; - } - - >:last-of-type { - grid-column: 9; - } - - >:not( :first-child ) { - margin: auto; - } - - &:hover { - background: var( --jp-gray-0 ); - } -} - -.accordion-header-label { - display: flex; - align-items: center; - font-size: var( --font-body-small ); - font-weight: normal; -} - -.accordion-header-label-icon { - margin-right: var( --spacing-base ); // 8px -} - -.accordion-header-description { - font-weight: 600; - margin-left: calc( var( --spacing-base ) * 4 ); // 32px - margin-bottom: var( --spacing-base ); // 8px -} - -.accordion-header-status { - font-size: var( --font-body-small ); - font-weight: normal; - margin-left: calc( var( --spacing-base ) * 4 ); // 32px - margin-bottom: var( --spacing-base ); // 8px -} - -.accordion-header-status-separator { - display: inline-block; - height: 4px; - margin: 2px 12px; - width: 4px; - background-color: var( --jp-gray-50 ); -} - -.accordion-header-button { - align-items: center; -} - -.accordion-body { - transform-origin: top center; - overflow: hidden; - - &-close { - transition: all .1s; - max-height: 0; - padding: 0; - transform: scaleY(0); - } - - &-open { - transition: max-height .3s, transform .2s; - padding: calc( var( --spacing-base ) * 4 ) calc( var( --spacing-base ) * 7 ); // 32 px | 56px - max-height: 1000px; - transform: scaleY(1); - } -} - -.icon-check { - fill: var( --jp-green-40 ); -} - -.icon-info { - fill: var( --jp-red ); -} - -.status-badge { - border-radius: 32px; - flex-shrink: 0; - font-size: 12px; - font-style: normal; - font-weight: 600; - line-height: 16px; - padding: calc( var( --spacing-base ) / 2 ); // 4px - position: relative; - text-align: center; - width: 60px; - margin-left: calc( var( --spacing-base ) * 4 ); // 32px - - &.fixed { - color: var( --jp-white ); - background-color: #008a20; - } - - &.ignored { - color: var( --jp-white ); - background-color: var( --jp-gray-50 ); - } -} - -.is-fixed { - color: #008a20; -} - -.support-link { - color: inherit; - - &:focus, - &:hover { - color: inherit; - box-shadow: none; - } -} - -@media ( max-width: 599px ) { - .accordion-header { - display: grid; - grid-auto-rows: minmax( auto, auto ); - - >:first-child { - grid-column: 1/8; - grid-row: 1; - } - - >:nth-child( 2 ) { - padding-left: calc( var( --spacing-base ) * 4 ); // 32px - grid-row: 2; - } - - >:nth-child( 3 ) { - grid-row: 2; - } - - >:nth-child( 3 ) span { - position: absolute; - margin-top: var( --spacing-base ); // 8px - } - - >:last-child { - grid-column: 10; - grid-row: 1/3; - } - } - - .status-badge { - display: none; - } -} - -@media ( max-width: 1200px ) { - .accordion-header-status { - display: grid; - } - - .accordion-header-status-separator { - display: none; - } -} 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/threats-list/empty.jsx b/projects/plugins/protect/src/js/components/threats-list/empty.jsx deleted file mode 100644 index 2d493b11e64a4..0000000000000 --- a/projects/plugins/protect/src/js/components/threats-list/empty.jsx +++ /dev/null @@ -1,140 +0,0 @@ -import { H3, Text } from '@automattic/jetpack-components'; -import { createInterpolateElement } from '@wordpress/element'; -import { sprintf, __, _n } from '@wordpress/i18n'; -import { useMemo, useState } from 'react'; -import useScanStatusQuery, { isScanInProgress } from '../../data/scan/use-scan-status-query'; -import usePlan from '../../hooks/use-plan'; -import useProtectData from '../../hooks/use-protect-data'; -import OnboardingPopover from '../onboarding-popover'; -import ScanButton from '../scan-button'; -import styles from './styles.module.scss'; - -const ProtectCheck = () => ( - - - - -); - -/** - * Time Since - * - * @param {string} date - The past date to compare to the current date. - * @return {string} - A description of the amount of time between a date and now, i.e. "5 minutes ago". - */ -const timeSince = date => { - const now = new Date(); - const offset = now.getTimezoneOffset() * 60000; - - const seconds = Math.floor( ( new Date( now.getTime() + offset ).getTime() - date ) / 1000 ); - - let interval = seconds / 31536000; // 364 days - if ( interval > 1 ) { - return sprintf( - // translators: placeholder is a number amount of years i.e. "5 years ago". - _n( '%s year ago', '%s years ago', Math.floor( interval ), 'jetpack-protect' ), - Math.floor( interval ) - ); - } - - interval = seconds / 2592000; // 30 days - if ( interval > 1 ) { - return sprintf( - // translators: placeholder is a number amount of months i.e. "5 months ago". - _n( '%s month ago', '%s months ago', Math.floor( interval ), 'jetpack-protect' ), - Math.floor( interval ) - ); - } - - interval = seconds / 86400; // 1 day - if ( interval > 1 ) { - return sprintf( - // translators: placeholder is a number amount of days i.e. "5 days ago". - _n( '%s day ago', '%s days ago', Math.floor( interval ), 'jetpack-protect' ), - Math.floor( interval ) - ); - } - - interval = seconds / 3600; // 1 hour - if ( interval > 1 ) { - return sprintf( - // translators: placeholder is a number amount of hours i.e. "5 hours ago". - _n( '%s hour ago', '%s hours ago', Math.floor( interval ), 'jetpack-protect' ), - Math.floor( interval ) - ); - } - - interval = seconds / 60; // 1 minute - if ( interval > 1 ) { - return sprintf( - // translators: placeholder is a number amount of minutes i.e. "5 minutes ago". - _n( '%s minute ago', '%s minutes ago', Math.floor( interval ), 'jetpack-protect' ), - Math.floor( interval ) - ); - } - - return __( 'a few seconds ago', 'jetpack-protect' ); -}; - -const EmptyList = () => { - const { lastChecked } = useProtectData(); - const { hasPlan } = usePlan(); - const { data: status } = useScanStatusQuery(); - - const [ dailyAndManualScansPopoverAnchor, setDailyAndManualScansPopoverAnchor ] = - useState( null ); - - const timeSinceLastScan = useMemo( () => { - return lastChecked ? timeSince( Date.parse( lastChecked ) ) : null; - }, [ lastChecked ] ); - - return ( -
- -

- { __( "Don't worry about a thing", 'jetpack-protect' ) } -

- - { timeSinceLastScan - ? createInterpolateElement( - sprintf( - // translators: placeholder is the amount of time since the last scan, i.e. "5 minutes ago". - __( - 'The last Protect scan ran %s and everything looked great.', - 'jetpack-protect' - ), - timeSinceLastScan - ), - { - strong: , - } - ) - : __( 'No threats have been detected by the current scan.', 'jetpack-protect' ) } - - { hasPlan && ( - <> - - { ! isScanInProgress( status ) && ( -
- ); -}; - -export default EmptyList; diff --git a/projects/plugins/protect/src/js/components/threats-list/free-list.jsx b/projects/plugins/protect/src/js/components/threats-list/free-list.jsx deleted file mode 100644 index 88d4a92f9bac5..0000000000000 --- a/projects/plugins/protect/src/js/components/threats-list/free-list.jsx +++ /dev/null @@ -1,125 +0,0 @@ -import { Text, Button, ContextualUpgradeTrigger } from '@automattic/jetpack-components'; -import { __, sprintf } from '@wordpress/i18n'; -import React, { useCallback } from 'react'; -import useAnalyticsTracks from '../../hooks/use-analytics-tracks'; -import usePlan from '../../hooks/use-plan'; -import FreeAccordion, { FreeAccordionItem } from '../free-accordion'; -import Pagination from './pagination'; -import styles from './styles.module.scss'; - -const ThreatAccordionItem = ( { - description, - fixedIn, - icon, - id, - label, - name, - source, - title, - type, -} ) => { - const { recordEvent } = useAnalyticsTracks(); - const { upgradePlan } = usePlan(); - - const getScan = useCallback( () => { - recordEvent( 'jetpack_protect_threat_list_get_scan_link_click' ); - upgradePlan(); - }, [ recordEvent, upgradePlan ] ); - - const learnMoreButton = source ? ( - - ) : null; - - return ( - { - if ( ! [ 'core', 'plugin', 'theme' ].includes( type ) ) { - return; - } - recordEvent( `jetpack_protect_${ type }_threat_open` ); - }, [ recordEvent, type ] ) } - > - { description && ( -
- - { __( 'What is the problem?', 'jetpack-protect' ) } - - { description } - { learnMoreButton } -
- ) } - { fixedIn && ( -
- - { __( 'How to fix it?', 'jetpack-protect' ) } - - - { - /* translators: Translates to Update to. %1$s: Name. %2$s: Fixed version */ - sprintf( __( 'Update to %1$s %2$s', 'jetpack-protect' ), name, fixedIn ) - } - - -
- ) } - { ! description &&
{ learnMoreButton }
} -
- ); -}; - -const FreeList = ( { list } ) => { - return ( - - { ( { currentItems } ) => ( - - { currentItems.map( - ( { - description, - fixedIn, - icon, - id, - label, - name, - source, - table, - title, - type, - version, - } ) => ( - - ) - ) } - - ) } - - ); -}; - -export default FreeList; diff --git a/projects/plugins/protect/src/js/components/threats-list/index.jsx b/projects/plugins/protect/src/js/components/threats-list/index.jsx deleted file mode 100644 index 2823a804c1412..0000000000000 --- a/projects/plugins/protect/src/js/components/threats-list/index.jsx +++ /dev/null @@ -1,194 +0,0 @@ -import { - Container, - Col, - Title, - Button, - useBreakpointMatch, - Text, -} from '@automattic/jetpack-components'; -import { __, sprintf } from '@wordpress/i18n'; -import React, { useCallback, useMemo, useState } from 'react'; -import useScanStatusQuery, { isScanInProgress } from '../../data/scan/use-scan-status-query'; -import useFixers from '../../hooks/use-fixers'; -import useModal from '../../hooks/use-modal'; -import usePlan from '../../hooks/use-plan'; -import OnboardingPopover from '../onboarding-popover'; -import ScanButton from '../scan-button'; -import EmptyList from './empty'; -import FreeList from './free-list'; -import ThreatsNavigation from './navigation'; -import PaidList from './paid-list'; -import styles from './styles.module.scss'; -import useThreatsList from './use-threats-list'; - -const ThreatsList = () => { - const { hasPlan } = usePlan(); - const { item, list, selected, setSelected } = useThreatsList(); - const [ isSm ] = useBreakpointMatch( 'sm' ); - const { isThreatFixInProgress, isThreatFixStale } = useFixers(); - - const { data: status } = useScanStatusQuery(); - const scanning = isScanInProgress( status ); - - // List of fixable threats that do not have a fix in progress - const fixableList = useMemo( () => { - return list.filter( threat => { - const threatId = parseInt( threat.id ); - return ( - threat.fixable && ! isThreatFixInProgress( threatId ) && ! isThreatFixStale( threatId ) - ); - } ); - }, [ list, isThreatFixInProgress, isThreatFixStale ] ); - - // Popover anchors - const [ yourScanResultsPopoverAnchor, setYourScanResultsPopoverAnchor ] = useState( null ); - const [ understandSeverityPopoverAnchor, setUnderstandSeverityPopoverAnchor ] = useState( null ); - const [ showAutoFixersPopoverAnchor, setShowAutoFixersPopoverAnchor ] = useState( null ); - const [ dailyAndManualScansPopoverAnchor, setDailyAndManualScansPopoverAnchor ] = - useState( null ); - - const { setModal } = useModal(); - - const handleShowAutoFixersClick = threatList => { - return event => { - event.preventDefault(); - setModal( { - type: 'FIX_ALL_THREATS', - props: { threatList }, - } ); - }; - }; - - const getTitle = useCallback( () => { - switch ( selected ) { - case 'all': - if ( list.length === 1 ) { - return __( 'All threats', 'jetpack-protect' ); - } - return sprintf( - /* translators: placeholder is the amount of threats found on the site. */ - __( 'All %s threats', 'jetpack-protect' ), - list.length - ); - case 'core': - return sprintf( - /* translators: placeholder is the amount of WordPress threats found on the site. */ - __( '%1$s WordPress %2$s', 'jetpack-protect' ), - list.length, - list.length === 1 ? 'threat' : 'threats' - ); - case 'files': - return sprintf( - /* translators: placeholder is the amount of file threats found on the site. */ - __( '%1$s file %2$s', 'jetpack-protect' ), - list.length, - list.length === 1 ? 'threat' : 'threats' - ); - case 'database': - return sprintf( - /* translators: placeholder is the amount of database threats found on the site. */ - __( '%1$s database %2$s', 'jetpack-protect' ), - list.length, - list.length === 1 ? 'threat' : 'threats' - ); - default: - return sprintf( - /* translators: Translates to Update to. %1$s: Name. %2$s: Fixed version */ - __( '%1$s %2$s in %3$s %4$s', 'jetpack-protect' ), - list.length, - list.length === 1 ? 'threat' : 'threats', - item?.name, - item?.version - ); - } - }, [ selected, list, item ] ); - - return ( - - -
- -
- { ! scanning && ( - - ) } - - - { list?.length > 0 ? ( - <> -
- { getTitle() } - { hasPlan && ( -
- { fixableList.length > 0 && ( - <> - - { ! scanning && ( -
- ) } -
- { hasPlan ? ( - <> -
- -
- - { __( - 'If you have manually fixed any of the threats listed above, you can run a manual scan now or wait for Jetpack to scan your site later today.', - 'jetpack-protect' - ) } - - -
-
- { ! scanning && ( -
- ); -}; - -export default ThreatsList; diff --git a/projects/plugins/protect/src/js/components/threats-list/navigation.jsx b/projects/plugins/protect/src/js/components/threats-list/navigation.jsx deleted file mode 100644 index 9befe85a78612..0000000000000 --- a/projects/plugins/protect/src/js/components/threats-list/navigation.jsx +++ /dev/null @@ -1,130 +0,0 @@ -import { useBreakpointMatch } from '@automattic/jetpack-components'; -import { __ } from '@wordpress/i18n'; -import { - wordpress as coreIcon, - plugins as pluginsIcon, - warning as warningIcon, - color as themesIcon, - code as filesIcon, -} from '@wordpress/icons'; -import { useCallback, useMemo } from 'react'; -import useAnalyticsTracks from '../../hooks/use-analytics-tracks'; -import usePlan from '../../hooks/use-plan'; -import useProtectData from '../../hooks/use-protect-data'; -import Navigation, { NavigationItem, NavigationGroup } from '../navigation'; - -const ThreatsNavigation = ( { selected, onSelect, sourceType = 'scan', statusFilter = 'all' } ) => { - const { hasPlan } = usePlan(); - const { - results: { plugins, themes }, - counts: { - current: { threats: numThreats, core: numCoreThreats, files: numFilesThreats }, - }, - } = useProtectData( { sourceType, filter: { status: statusFilter } } ); - - const { recordEvent } = useAnalyticsTracks(); - const [ isSmallOrLarge ] = useBreakpointMatch( 'lg', '<' ); - - const trackNavigationClickAll = useCallback( () => { - recordEvent( 'jetpack_protect_navigation_all_click' ); - }, [ recordEvent ] ); - - const trackNavigationClickCore = useCallback( () => { - recordEvent( 'jetpack_protect_navigation_core_click' ); - }, [ recordEvent ] ); - - const trackNavigationClickPlugin = useCallback( () => { - recordEvent( 'jetpack_protect_navigation_plugin_click' ); - }, [ recordEvent ] ); - - const trackNavigationClickTheme = useCallback( () => { - recordEvent( 'jetpack_protect_navigation_theme_click' ); - }, [ recordEvent ] ); - - const trackNavigationClickFiles = useCallback( () => { - recordEvent( 'jetpack_protect_navigation_file_click' ); - }, [ recordEvent ] ); - - const allLabel = useMemo( () => { - if ( statusFilter === 'fixed' ) { - return __( 'All fixed threats', 'jetpack-protect' ); - } - if ( statusFilter === 'ignored' ) { - return __( - 'All ignored threats', - 'jetpack-protect', - /** dummy arg to avoid bad minification */ 0 - ); - } - return __( 'All threats', 'jetpack-protect' ); - }, [ statusFilter ] ); - - return ( - - - - - { plugins.map( ( { name, threats, checked } ) => ( - - ) ) } - - - { themes.map( ( { name, threats, checked } ) => ( - - ) ) } - - { hasPlan && ( - <> - - - ) } - - ); -}; - -export default ThreatsNavigation; diff --git a/projects/plugins/protect/src/js/components/threats-list/pagination.jsx b/projects/plugins/protect/src/js/components/threats-list/pagination.jsx deleted file mode 100644 index 3e17bed0eeac4..0000000000000 --- a/projects/plugins/protect/src/js/components/threats-list/pagination.jsx +++ /dev/null @@ -1,142 +0,0 @@ -import { Button, useBreakpointMatch } from '@automattic/jetpack-components'; -import { __, sprintf } from '@wordpress/i18n'; -import { chevronLeft, chevronRight } from '@wordpress/icons'; -import React, { useCallback, useState, useMemo } from 'react'; -import styles from './styles.module.scss'; - -const PaginationButton = ( { pageNumber, currentPage, onPageChange } ) => { - const isCurrentPage = useMemo( () => currentPage === pageNumber, [ currentPage, pageNumber ] ); - - const handleClick = useCallback( () => { - onPageChange( pageNumber ); - }, [ onPageChange, pageNumber ] ); - - return ( - - ); -}; - -const Pagination = ( { list, itemPerPage = 10, children } ) => { - const [ isSm ] = useBreakpointMatch( 'sm' ); - - const [ currentPage, setCurrentPage ] = useState( 1 ); - - const handlePreviousPageClick = useCallback( - () => setCurrentPage( currentPage - 1 ), - [ currentPage, setCurrentPage ] - ); - const handleNextPageClick = useCallback( - () => setCurrentPage( currentPage + 1 ), - [ currentPage, setCurrentPage ] - ); - - const totalPages = useMemo( () => Math.ceil( list.length / itemPerPage ), [ list, itemPerPage ] ); - - const currentItems = useMemo( () => { - const indexOfLastItem = currentPage * itemPerPage; - const indexOfFirstItem = indexOfLastItem - itemPerPage; - return list.slice( indexOfFirstItem, indexOfLastItem ); - }, [ currentPage, list, itemPerPage ] ); - - const pageNumbers = useMemo( () => { - if ( isSm ) { - return [ currentPage ]; - } - - const result = [ 1 ]; - if ( currentPage > 3 && totalPages > 4 ) { - result.push( '…' ); - } - - if ( currentPage === 1 ) { - // Current page is the first page. - // i.e. [ 1 ] 2 3 4 ... 10 - result.push( currentPage + 1, currentPage + 2, currentPage + 3 ); - } else if ( currentPage === 2 ) { - // Current page is the second to first page. - // i.e. 1 [ 2 ] 3 4 ... 10 - result.push( currentPage, currentPage + 1, currentPage + 2 ); - } else if ( currentPage < totalPages - 1 ) { - // Current page is positioned in the middle of the pagination. - // i.e. 1 ... 3 [ 4 ] 5 ... 10 - result.push( currentPage - 1, currentPage, currentPage + 1 ); - } else if ( currentPage === totalPages - 1 ) { - // Current page is the second to last page. - // i.e. 1 ... 7 8 [ 9 ] 10 - currentPage > 3 && result.push( currentPage - 2 ); - currentPage > 2 && result.push( currentPage - 1 ); - result.push( currentPage ); - } else if ( currentPage === totalPages ) { - // Current page is the last page. - // i.e. 1 ... 7 8 9 [ 10 ] - currentPage >= 5 && result.push( currentPage - 3 ); - currentPage >= 4 && result.push( currentPage - 2 ); - result.push( currentPage - 1 ); - } - - if ( result[ result.length - 1 ] < totalPages - 1 ) { - result.push( '…' ); - result.push( totalPages ); - } else if ( result[ result.length - 1 ] < totalPages ) { - result.push( totalPages ); - } - - return result.filter( pageNumber => pageNumber <= totalPages || isNaN( pageNumber ) ); - }, [ currentPage, isSm, totalPages ] ); - - return ( - <> - { children( { currentItems } ) } - { totalPages > 1 && ( - - ) } - - ); -}; - -export default Pagination; diff --git a/projects/plugins/protect/src/js/components/threats-list/paid-list.jsx b/projects/plugins/protect/src/js/components/threats-list/paid-list.jsx deleted file mode 100644 index baedf8dfa5184..0000000000000 --- a/projects/plugins/protect/src/js/components/threats-list/paid-list.jsx +++ /dev/null @@ -1,253 +0,0 @@ -import { - Text, - Button, - DiffViewer, - MarkedLines, - useBreakpointMatch, -} from '@automattic/jetpack-components'; -import { __, sprintf } from '@wordpress/i18n'; -import React, { useCallback } from 'react'; -import useAnalyticsTracks from '../../hooks/use-analytics-tracks'; -import useFixers from '../../hooks/use-fixers'; -import useModal from '../../hooks/use-modal'; -import PaidAccordion, { PaidAccordionItem } from '../paid-accordion'; -import Pagination from './pagination'; -import styles from './styles.module.scss'; - -const ThreatAccordionItem = ( { - context, - description, - diff, - filename, - firstDetected, - fixedIn, - fixedOn, - icon, - fixable, - id, - label, - name, - source, - title, - type, - severity, - status, - hideAutoFixColumn = false, -} ) => { - const { setModal } = useModal(); - const { recordEvent } = useAnalyticsTracks(); - - const { isThreatFixInProgress, isThreatFixStale } = useFixers(); - const isActiveFixInProgress = isThreatFixInProgress( id ); - const isStaleFixInProgress = isThreatFixStale( id ); - - const learnMoreButton = source ? ( - - ) : null; - - const handleIgnoreThreatClick = () => { - return event => { - event.preventDefault(); - setModal( { - type: 'IGNORE_THREAT', - props: { id, label, title, icon, severity }, - } ); - }; - }; - - const handleUnignoreThreatClick = () => { - return event => { - event.preventDefault(); - setModal( { - type: 'UNIGNORE_THREAT', - props: { id, label, title, icon, severity }, - } ); - }; - }; - - const handleFixThreatClick = () => { - return event => { - event.preventDefault(); - setModal( { - type: 'FIX_THREAT', - props: { id, fixable, label, icon, severity }, - } ); - }; - }; - - return ( - { - if ( ! [ 'core', 'plugin', 'theme', 'file', 'database' ].includes( type ) ) { - return; - } - recordEvent( `jetpack_protect_${ type }_threat_open` ); - }, [ recordEvent, type ] ) } - hideAutoFixColumn={ hideAutoFixColumn } - > - { description && ( -
- - { status !== 'fixed' - ? __( 'What is the problem?', 'jetpack-protect' ) - : __( - 'What was the problem?', - 'jetpack-protect', - /** dummy arg to avoid bad minification */ 0 - ) } - - { description } - { learnMoreButton } -
- ) } - { ( filename || context || diff ) && ( - - { __( 'The technical details', 'jetpack-protect' ) } - - ) } - { filename && ( - <> - - { - /* translators: filename follows in separate line; e.g. "PHP.Injection.5 in: `post.php`" */ - __( 'Threat found in file:', 'jetpack-protect' ) - } - -
{ filename }
- - ) } - { context && } - { diff && } - { fixedIn && status !== 'fixed' && ( -
- - { __( 'How to fix it?', 'jetpack-protect' ) } - - - { - /* translators: Translates to Update to. %1$s: Name. %2$s: Fixed version */ - sprintf( __( 'Update to %1$s %2$s', 'jetpack-protect' ), name, fixedIn ) - } - -
- ) } - { ! description &&
{ learnMoreButton }
} - { [ 'ignored', 'current' ].includes( status ) && ( -
- { 'ignored' === status && ( - - ) } - { 'current' === status && ( - <> - - { fixable && ( - - ) } - - ) } -
- ) } -
- ); -}; - -const PaidList = ( { list, hideAutoFixColumn = false } ) => { - const [ isSmall ] = useBreakpointMatch( [ 'sm', 'lg' ], [ null, '<' ] ); - - return ( - <> - { ! isSmall && ( -
- { __( 'Details', 'jetpack-protect' ) } - { __( 'Severity', 'jetpack-protect' ) } - { ! hideAutoFixColumn && { __( 'Auto-fix', 'jetpack-protect' ) } } - -
- ) } - - { ( { currentItems } ) => ( - - { currentItems.map( - ( { - context, - description, - diff, - filename, - firstDetected, - fixedIn, - fixedOn, - icon, - fixable, - id, - label, - name, - severity, - source, - table, - title, - type, - version, - status, - } ) => ( - - ) - ) } - - ) } - - - ); -}; - -export default PaidList; diff --git a/projects/plugins/protect/src/js/components/threats-list/styles.module.scss b/projects/plugins/protect/src/js/components/threats-list/styles.module.scss deleted file mode 100644 index 4a50d87b2562b..0000000000000 --- a/projects/plugins/protect/src/js/components/threats-list/styles.module.scss +++ /dev/null @@ -1,129 +0,0 @@ -.empty { - display: flex; - width: 100%; - height: 100%; - align-items: center; - justify-content: center; - max-height: 600px; - flex-direction: column; -} - -.threat-section + .threat-section { - margin-top: calc( var( --spacing-base ) * 5 ); // 40px -} - -.threat-filename { - background-color: var( --jp-gray-0 ); - padding: calc( var( --spacing-base ) * 3 ); // 24px - overflow-x: scroll; -} - -.threat-footer { - display: flex; - justify-content: flex-end; - border-top: 1px solid var( --jp-gray ); - padding-top: calc( var( --spacing-base ) * 3 ); // 24px - margin-top: calc( var( --spacing-base ) * 3 ); // 24px -} -.threat-item-cta { - margin-top: calc( var( --spacing-base ) * 4 ); // 36px -} - -.list-header { - display: flex; - align-items: flex-end; - margin-bottom: calc( var( --spacing-base ) * 2.25 ); // 18px -} - -.list-title { - flex: 1; - margin-bottom: 0; -} - -.list-header__controls { - display: flex; - gap: calc( var( --spacing-base ) * 2 ); // 16px -} - -.threat-footer { - width: 100%; - display: flex; - justify-content: right; - padding-top: calc( var( --spacing-base ) * 4 ); // 32px - border-top: 1px solid var( --jp-gray ); - - > :last-child { - margin-left: calc( var( --spacing-base ) * 2 ); // 16px - } -} - -.accordion-header { - display: grid; - grid-template-columns: repeat( 9, 1fr ); - background-color: white; - padding: calc( var( --spacing-base ) * 2 ) calc( var( --spacing-base ) * 3 ); // 16px | 24px - border: 1px solid var( --jp-gray ); - border-bottom: none; - color: var( --jp-gray-50 ); - width: 100%; - - > span:first-child { - grid-column: 1 / 7; - } - - > span:not( :first-child ) { - text-align: center; - } -} - -.manual-scan { - margin: calc( var( --spacing-base ) * 4 ) calc( var( --spacing-base ) * 8 ); // 32px | 64px - text-align: center; -} - -@media ( max-width: 599px ) { - - .list-header { - margin-bottom: calc( var( --spacing-base ) * 3 ); // 24px - } - - .list-title { - display: none; - } - - .threat-footer { - justify-content: center; - - > * { - width: 50%; - } - } -} - -.pagination-container { - display: flex; - justify-content: center; - align-items: center; - gap: 4px; - margin-top: calc( var( --spacing-base ) * 4 ); // 24px - margin-bottom: calc(var(--spacing-base) * 2); // 16px - - button { - font-size: var( --font-body ); - width: auto; - height: auto; - padding: 0 var( --spacing-base ); // 0 | 8px - line-height: 32px; - min-width: 32px; - - &.unfocused { - color: var( --jp-black ); - background: none; - - &:hover:not(:disabled) { - color: var( --jp-black ); - background: none; - } - } - } -} diff --git a/projects/plugins/protect/src/js/components/threats-list/use-threats-list.js b/projects/plugins/protect/src/js/components/threats-list/use-threats-list.js deleted file mode 100644 index de000288251ae..0000000000000 --- a/projects/plugins/protect/src/js/components/threats-list/use-threats-list.js +++ /dev/null @@ -1,158 +0,0 @@ -import { - plugins as pluginsIcon, - wordpress as coreIcon, - color as themesIcon, - code as filesIcon, - grid as databaseIcon, -} from '@wordpress/icons'; -import { useEffect, useMemo, useState } from 'react'; -import useProtectData from '../../hooks/use-protect-data'; - -const sortThreats = ( a, b ) => b.severity - a.severity; - -/** - * Flatten threats data - * - * Merges threat category data with each threat it contains, plus any additional data provided. - * - * @param {object} data - The threat category data, i.e. "core", "plugins", "themes", etc. - * @param {object} newData - Additional data to add to each threat. - * @return {object[]} Array of threats with additional properties from the threat category and function argument. - */ -const flattenThreats = ( data, newData ) => { - // If "data" is an empty object - if ( typeof data === 'object' && Object.keys( data ).length === 0 ) { - return []; - } - - // If "data" has multiple entries, recursively flatten each one. - if ( Array.isArray( data ) ) { - return data.map( extension => flattenThreats( extension, newData ) ).flat(); - } - - // Merge the threat category data with each threat it contains, plus any additional data provided. - return data?.threats.map( threat => ( { - ...threat, - ...data, - ...newData, - } ) ); -}; - -/** - * Threats List Hook - * - * @param {object} args - Arguments for the hook. - * @param {string} args.source - "scan" or "history". - * @param {string} args.status - "all", "fixed", or "ignored". - * --- - * @typedef {object} UseThreatsList - * @property {object} item - The selected threat category. - * @property {object[]} list - The list of threats to display. - * @property {string} selected - The selected threat category. - * @property {Function} setSelected - Sets the selected threat category. - * --- - * @return {UseThreatsList} useThreatsList hook. - */ -const useThreatsList = ( { source, status } = { source: 'scan', status: 'all' } ) => { - const [ selected, setSelected ] = useState( 'all' ); - const { - results: { plugins, themes, core, files, database }, - } = useProtectData( { - sourceType: source, - filter: { status, key: selected }, - } ); - - const { unsortedList, item } = useMemo( () => { - // If a specific threat category is selected, filter for and flatten the category's threats. - if ( selected && selected !== 'all' ) { - // Core, files, and database data threats are already grouped together, - // so we just need to flatten them and add the appropriate icon. - switch ( selected ) { - case 'core': - return { - unsortedList: flattenThreats( core, { icon: coreIcon } ), - item: core, - }; - case 'files': - return { - unsortedList: flattenThreats( { threats: files }, { icon: filesIcon } ), - item: files, - }; - case 'database': - return { - unsortedList: flattenThreats( { threats: database }, { icon: databaseIcon } ), - item: database, - }; - default: - break; - } - - // Extensions (i.e. plugins and themes) have entries for each individual extension, - // so we need to check for a matching threat in each extension. - const selectedPlugin = plugins.find( plugin => plugin?.name === selected ); - if ( selectedPlugin ) { - return { - unsortedList: flattenThreats( selectedPlugin, { icon: pluginsIcon } ), - item: selectedPlugin, - }; - } - const selectedTheme = themes.find( theme => theme?.name === selected ); - if ( selectedTheme ) { - return { - unsortedList: flattenThreats( selectedTheme, { icon: themesIcon } ), - item: selectedTheme, - }; - } - } - - // Otherwise, return all threats. - return { - unsortedList: [ - ...flattenThreats( core, { icon: coreIcon } ), - ...flattenThreats( plugins, { icon: pluginsIcon } ), - ...flattenThreats( themes, { icon: themesIcon } ), - ...flattenThreats( { threats: files }, { icon: filesIcon } ), - ...flattenThreats( { threats: database }, { icon: databaseIcon } ), - ], - item: null, - }; - }, [ core, database, files, plugins, selected, themes ] ); - - const getLabel = threat => { - if ( threat.name && threat.version ) { - // Extension threat i.e. "Woocommerce (3.0.0)" - return `${ threat.name } (${ threat.version })`; - } - - if ( threat.filename ) { - // File threat i.e. "index.php" - return threat.filename.split( '/' ).pop(); - } - - if ( threat.table ) { - // Database threat i.e. "wp_posts" - return threat.table; - } - }; - - const list = useMemo( () => { - return unsortedList - .sort( sortThreats ) - .map( threat => ( { label: getLabel( threat ), ...threat } ) ); - }, [ unsortedList ] ); - - useEffect( () => { - if ( selected !== 'all' && status !== 'all' && list.length === 0 ) { - setSelected( 'all' ); - } - }, [ selected, status, item, list ] ); - - return { - item, - list, - selected, - setSelected, - }; -}; - -export default useThreatsList; 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/hooks/use-protect-data/index.ts b/projects/plugins/protect/src/js/hooks/use-protect-data/index.ts deleted file mode 100644 index d560cff20ef86..0000000000000 --- a/projects/plugins/protect/src/js/hooks/use-protect-data/index.ts +++ /dev/null @@ -1,174 +0,0 @@ -import { __ } from '@wordpress/i18n'; -import { useMemo } from 'react'; -import useHistoryQuery from '../../data/scan/use-history-query'; -import useScanStatusQuery from '../../data/scan/use-scan-status-query'; -import useProductDataQuery from '../../data/use-product-data-query'; -import { ExtensionStatus } from '../../types/scans'; -import { Threat, ThreatStatus } from '../../types/threats'; - -type ThreatFilterKey = 'all' | 'core' | 'files' | 'database' | string; - -type Filter = { key: ThreatFilterKey; status: ThreatStatus | 'all' }; - -// Valid "key" values for filtering. -const KEY_FILTERS = [ 'all', 'core', 'plugins', 'themes', 'files', 'database' ]; - -/** - * Filter Extension Threats - * - * @param {Array} threats - The threats to filter. - * @param {object} filter - The filter to apply to the data. - * @param {string} filter.status - The status to filter: 'all', 'current', 'fixed', or 'ignored'. - * @param {string} filter.key - The key to filter: 'all', 'core', 'files', 'database', or an extension name. - * @param {string} key - The threat's key: 'all', 'core', 'files', 'database', or an extension name. - * - * @return {Array} The filtered threats. - */ -const filterThreats = ( threats: Threat[], filter: Filter, key: ThreatFilterKey ): Threat[] => { - if ( ! Array.isArray( threats ) ) { - return []; - } - - return threats.filter( threat => { - if ( filter.status && filter.status !== 'all' && threat.status !== filter.status ) { - return false; - } - if ( filter.key && filter.key !== 'all' && filter.key !== key ) { - return false; - } - return true; - } ); -}; - -/** - * Get parsed data from the initial state - * - * @param {object} options - The options to use when getting the data. - * @param {string} options.sourceType - 'scan' or 'history'. - * @param {object} options.filter - The filter to apply to the data. - * _param {string} options.filter.status - 'all', 'fixed', or 'ignored'. - * _param {string} options.filter.key - 'all', 'core', 'files', 'database', or an extension name. - * - * @return {object} The information available in Protect's initial state. - */ -export default function useProtectData( - { sourceType, filter } = { - sourceType: 'scan', - filter: { status: null, key: null }, - } -) { - const { data: status } = useScanStatusQuery(); - const { data: scanHistory } = useHistoryQuery(); - const { data: jetpackScan } = useProductDataQuery(); - - const { counts, results, error, lastChecked, hasUncheckedItems } = useMemo( () => { - // This hook can provide data from two sources: the current scan or the scan history. - const data = sourceType === 'history' ? { ...scanHistory } : { ...status }; - - // Prepare the result object. - const result = { - results: { - core: [], - plugins: [], - themes: [], - files: [], - database: [], - }, - counts: { - all: { - threats: 0, - core: 0, - plugins: 0, - themes: 0, - files: 0, - database: 0, - }, - current: { - threats: 0, - core: 0, - plugins: 0, - themes: 0, - files: 0, - database: 0, - }, - }, - error: null, - lastChecked: data.lastChecked || null, - hasUncheckedItems: data.hasUncheckedItems || false, - }; - - // Loop through the provided extensions, and update the result object. - const processExtensions = ( extensions: Array< ExtensionStatus >, key: ThreatFilterKey ) => { - if ( ! Array.isArray( extensions ) ) { - return []; - } - extensions.forEach( extension => { - // Update the total counts. - result.counts.all[ key ] += extension?.threats?.length || 0; - result.counts.all.threats += extension?.threats?.length || 0; - - // Filter the extension's threats based on the current filters. - const filteredThreats = filterThreats( - extension?.threats || [], - filter, - KEY_FILTERS.includes( filter.key ) ? key : extension?.name - ); - - // Update the result object with the extension and its filtered threats. - result.results[ key ].push( { ...extension, threats: filteredThreats } ); - - // Update the current counts. - result.counts.current[ key ] += filteredThreats.length; - result.counts.current.threats += filteredThreats.length; - } ); - }; - - // Loop through the provided threats, and update the result object. - const processThreats = ( threatsToProcess: Threat[], key: ThreatFilterKey ) => { - if ( ! Array.isArray( threatsToProcess ) ) { - return []; - } - - result.counts.all[ key ] += threatsToProcess.length; - result.counts.all.threats += threatsToProcess.length; - - const filteredThreats = filterThreats( threatsToProcess, filter, key ); - - result.results[ key ] = [ ...result.results[ key ], ...filteredThreats ]; - result.counts.current[ key ] += filteredThreats.length; - result.counts.current.threats += filteredThreats.length; - }; - - // Core data may be either a single object or an array of multiple objects. - let cores = Array.isArray( data.core ) ? data.core : []; - if ( data?.core?.threats ) { - cores = [ data.core ]; - } - - // Process the data - processExtensions( cores, 'core' ); - processExtensions( data?.plugins, 'plugins' ); - processExtensions( data?.themes, 'themes' ); - processThreats( data?.files, 'files' ); - processThreats( data?.database, 'database' ); - - // Handle errors - if ( data.error ) { - result.error = { - message: data.errorMessage || __( 'An error occurred.', 'jetpack-protect' ), - code: data.errorCode || 500, - }; - } - - return result; - }, [ scanHistory, sourceType, status, filter ] ); - - return { - results, - counts, - error, - lastChecked, - hasUncheckedItems, - jetpackScan, - }; -} diff --git a/projects/plugins/protect/src/js/routes/scan/history/history-admin-section-hero.tsx b/projects/plugins/protect/src/js/routes/scan/history/history-admin-section-hero.tsx index 9c8f30b7b8067..435fcfe895c5b 100644 --- a/projects/plugins/protect/src/js/routes/scan/history/history-admin-section-hero.tsx +++ b/projects/plugins/protect/src/js/routes/scan/history/history-admin-section-hero.tsx @@ -2,44 +2,37 @@ import { Status, Text } from '@automattic/jetpack-components'; import { dateI18n } from '@wordpress/date'; import { __, sprintf } from '@wordpress/i18n'; import { useMemo } from 'react'; -import { useParams } from 'react-router-dom'; +// import { useParams } from 'react-router-dom'; import AdminSectionHero from '../../../components/admin-section-hero'; import ErrorAdminSectionHero from '../../../components/error-admin-section-hero'; import ScanNavigation from '../../../components/scan-navigation'; -import useThreatsList from '../../../components/threats-list/use-threats-list'; -import useProtectData from '../../../hooks/use-protect-data'; +import useHistoryQuery from '../../../data/scan/use-history-query'; import styles from './styles.module.scss'; const HistoryAdminSectionHero: React.FC = () => { - const { filter = 'all' } = useParams(); - const { list } = useThreatsList( { - source: 'history', - status: filter, - } ); - const { counts, error } = useProtectData( { - sourceType: 'history', - filter: { status: filter }, - } ); - const { threats: numAllThreats } = counts.all; + // const { filter = 'all' } = useParams(); // to do: apply filter to history query + const { data: history } = useHistoryQuery(); + + const numAllThreats = history ? history.threats.length : 0; const oldestFirstDetected = useMemo( () => { - if ( ! list.length ) { + if ( ! history || ! history.threats.length ) { return null; } - return list.reduce( ( oldest, current ) => { + return history.threats.reduce( ( oldest, current ) => { return new Date( current.firstDetected ) < new Date( oldest.firstDetected ) ? current : oldest; } ).firstDetected; - }, [ list ] ); + }, [ history ] ); - if ( error ) { + if ( history && history.error ) { return ( ); } diff --git a/projects/plugins/protect/src/js/routes/scan/history/index.jsx b/projects/plugins/protect/src/js/routes/scan/history/index.jsx index f65cb0633f251..3b6513a28a776 100644 --- a/projects/plugins/protect/src/js/routes/scan/history/index.jsx +++ b/projects/plugins/protect/src/js/routes/scan/history/index.jsx @@ -1,298 +1,33 @@ -import { AdminSection, Container, Col, H3, Text, Title } from '@automattic/jetpack-components'; -import { __, _n, sprintf } from '@wordpress/i18n'; -import { useCallback } from 'react'; -import { Navigate, useParams } from 'react-router-dom'; +import { AdminSection, Container, Col } from '@automattic/jetpack-components'; +import { Navigate } from 'react-router-dom'; import AdminPage from '../../../components/admin-page'; -import ProtectCheck from '../../../components/protect-check-icon'; -import ThreatsNavigation from '../../../components/threats-list/navigation'; -import PaidList from '../../../components/threats-list/paid-list'; -import useThreatsList from '../../../components/threats-list/use-threats-list'; import useAnalyticsTracks from '../../../hooks/use-analytics-tracks'; import usePlan from '../../../hooks/use-plan'; -import useProtectData from '../../../hooks/use-protect-data'; import ScanFooter from '../scan-footer'; import HistoryAdminSectionHero from './history-admin-section-hero'; -import StatusFilters from './status-filters'; -import styles from './styles.module.scss'; +import ScanHistoryDataView from './scan-history-data-view'; const ScanHistoryRoute = () => { // Track page view. useAnalyticsTracks( { pageViewEventName: 'protect_scan_history' } ); const { hasPlan } = usePlan(); - const { filter = 'all' } = useParams(); - - const { item, list, selected, setSelected } = useThreatsList( { - source: 'history', - status: filter, - } ); - - const { counts, error } = useProtectData( { - sourceType: 'history', - filter: { status: filter }, - } ); - const { threats: numAllThreats } = counts.all; - - const { counts: fixedCounts } = useProtectData( { - sourceType: 'history', - filter: { status: 'fixed', key: selected }, - } ); - const { threats: numFixed } = fixedCounts.current; - - const { counts: ignoredCounts } = useProtectData( { - sourceType: 'history', - filter: { status: 'ignored', key: selected }, - } ); - const { threats: numIgnored } = ignoredCounts.current; - - /** - * Get the title for the threats list based on the selected filters and the amount of threats. - */ - const getTitle = useCallback( () => { - switch ( selected ) { - case 'all': - if ( list.length === 1 ) { - switch ( filter ) { - case 'fixed': - return __( 'All fixed threats', 'jetpack-protect' ); - case 'ignored': - return __( - 'All ignored threats', - 'jetpack-protect', - /** dummy arg to avoid bad minification */ 0 - ); - default: - return __( 'All threats', 'jetpack-protect' ); - } - } - switch ( filter ) { - case 'fixed': - return sprintf( - /* translators: placeholder is the amount of fixed threats found on the site. */ - __( 'All %s fixed threats', 'jetpack-protect' ), - list.length - ); - case 'ignored': - return sprintf( - /* translators: placeholder is the amount of ignored threats found on the site. */ - __( 'All %s ignored threats', 'jetpack-protect' ), - list.length - ); - default: - return sprintf( - /* translators: placeholder is the amount of threats found on the site. */ - __( 'All %s threats', 'jetpack-protect' ), - list.length - ); - } - case 'core': - switch ( filter ) { - case 'fixed': - return sprintf( - /* translators: placeholder is the amount of fixed WordPress threats found on the site. */ - _n( - '%1$s fixed WordPress threat', - '%1$s fixed WordPress threats', - list.length, - 'jetpack-protect' - ), - list.length - ); - case 'ignored': - return sprintf( - /* translators: placeholder is the amount of ignored WordPress threats found on the site. */ - _n( - '%1$s ignored WordPress threat', - '%1$s ignored WordPress threats', - list.length, - 'jetpack-protect' - ), - list.length - ); - default: - return sprintf( - /* translators: placeholder is the amount of WordPress threats found on the site. */ - _n( - '%1$s WordPress threat', - '%1$s WordPress threats', - list.length, - 'jetpack-protect' - ), - list.length - ); - } - case 'files': - switch ( filter ) { - case 'fixed': - return sprintf( - /* translators: placeholder is the amount of fixed file threats found on the site. */ - _n( - '%1$s fixed file threat', - '%1$s fixed file threats', - list.length, - 'jetpack-protect' - ), - list.length - ); - case 'ignored': - return sprintf( - /* translators: placeholder is the amount of ignored file threats found on the site. */ - _n( - '%1$s ignored file threat', - '%1$s ignored file threats', - list.length, - 'jetpack-protect' - ), - list.length - ); - default: - return sprintf( - /* translators: placeholder is the amount of file threats found on the site. */ - _n( '%1$s file threat', '%1$s file threats', list.length, 'jetpack-protect' ), - list.length - ); - } - case 'database': - switch ( filter ) { - case 'fixed': - return sprintf( - /* translators: placeholder is the amount of fixed database threats found on the site. */ - _n( - '%1$s fixed database threat', - '%1$s fixed database threats', - list.length, - 'jetpack-protect' - ), - list.length - ); - case 'ignored': - return sprintf( - /* translators: placeholder is the amount of ignored database threats found on the site. */ - _n( - '%1$s ignored database threat', - '%1$s ignored database threats', - list.length, - 'jetpack-protect' - ), - list.length - ); - default: - return sprintf( - /* translators: placeholder is the amount of database threats found on the site. */ - _n( '%1$s database threat', '%1$s database threats', list.length, 'jetpack-protect' ), - list.length - ); - } - default: - switch ( filter ) { - case 'fixed': - return sprintf( - /* translators: Translates to "123 fixed threats in Example Plugin (1.2.3)" */ - _n( - '%1$s fixed threat in %2$s %3$s', - '%1$s fixed threats in %2$s %3$s', - list.length, - 'jetpack-protect' - ), - list.length, - item?.name, - item?.version - ); - case 'ignored': - return sprintf( - /* translators: Translates to "123 ignored threats in Example Plugin (1.2.3)" */ - _n( - '%1$s ignored threat in %2$s %3$s', - '%1$s ignored threats in %2$s %3$s', - list.length, - 'jetpack-protect' - ), - list.length, - item?.name, - item?.version - ); - default: - return sprintf( - /* translators: Translates to "123 threats in Example Plugin (1.2.3)" */ - _n( - '%1$s threat in %2$s %3$s', - '%1$s threats in %2$s %3$s', - list.length, - 'jetpack-protect' - ), - list.length, - item?.name, - item?.version - ); - } - } - }, [ selected, list.length, filter, item?.name, item?.version ] ); // Threat history is only available for paid plans. if ( ! hasPlan ) { return ; } - // Remove the filter if there are no threats to show. - if ( list.length === 0 && filter !== 'all' ) { - return ; - } - return ( - { ( ! error || numAllThreats ) && ( - - - - - - - - - { list.length > 0 ? ( -
-
- { getTitle() } -
- -
-
- -
- ) : ( - <> -
-
- -
-
-
- -

- { __( "Don't worry about a thing", 'jetpack-protect' ) } -

- - { sprintf( - /* translators: %s: Filter type */ - __( 'There are no%sthreats in your scan history.', 'jetpack-protect' ), - 'all' === filter ? ' ' : ` ${ filter } ` - ) } - -
- - ) } - -
- -
-
- ) } + + + + + + +
); diff --git a/projects/plugins/protect/src/js/routes/scan/history/scan-history-data-view.tsx b/projects/plugins/protect/src/js/routes/scan/history/scan-history-data-view.tsx new file mode 100644 index 0000000000000..d8574aaf63aef --- /dev/null +++ b/projects/plugins/protect/src/js/routes/scan/history/scan-history-data-view.tsx @@ -0,0 +1,27 @@ +import { ThreatsDataView } from '@automattic/jetpack-components'; +import useHistoryQuery from '../../../data/scan/use-history-query'; + +/** + * Scan History Data View + * + * @return {JSX.Element} ScanResultDataView component. + */ +export default function ScanHistoryDataView() { + const { data } = useHistoryQuery(); + + // const onFixThreat = useCallback( ( ) + + // Return early when scan history is unavailable (i.e. user does not have the required plan) + if ( ! data ) { + return null; + } + + return ( + + ); +} diff --git a/projects/plugins/protect/src/js/routes/scan/index.jsx b/projects/plugins/protect/src/js/routes/scan/index.jsx index 1f3cdfdd7520f..e573b9cb3cddf 100644 --- a/projects/plugins/protect/src/js/routes/scan/index.jsx +++ b/projects/plugins/protect/src/js/routes/scan/index.jsx @@ -1,14 +1,13 @@ import { AdminSection, Container, Col } from '@automattic/jetpack-components'; 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,18 +18,12 @@ import ScanFooter from './scan-footer'; */ const ScanPage = () => { const { hasPlan } = usePlan(); - const { - counts: { - current: { threats: numThreats }, - }, - lastChecked, - } = useProtectData(); 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'; @@ -49,15 +42,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..367129a87b2a3 --- /dev/null +++ b/projects/plugins/protect/src/js/routes/scan/scan-results-data-view.tsx @@ -0,0 +1,55 @@ +import { ThreatsDataView } from '@automattic/jetpack-components'; +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'; +import { Threat } from '../../types/threats'; + +/** + * Scan Results Data View + * + * @return {JSX.Element} ScanResultDataView component. + */ +export default function ScanResultsDataView() { + const { data: scanStatus } = useScanStatusQuery(); + const { data: history } = useHistoryQuery(); + + const { setModal } = useModal(); + + const onFixThreat = useCallback( + ( items: Threat[] ) => { + setModal( { type: 'FIX_THREAT', props: { threat: items[ 0 ] } } ); + }, + [ setModal ] + ); + + const onIgnoreThreat = useCallback( + ( items: Threat[] ) => { + setModal( { type: 'IGNORE_THREAT', props: { threat: items[ 0 ] } } ); + }, + [ setModal ] + ); + + const onUnignoreThreat = useCallback( + ( items: Threat[] ) => { + setModal( { type: 'UNIGNORE_THREAT', props: { threat: items[ 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 0b5e90e21e7ae..e737b3a738e3a 100644 --- a/projects/plugins/protect/src/js/routes/scan/styles.module.scss +++ b/projects/plugins/protect/src/js/routes/scan/styles.module.scss @@ -20,4 +20,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/scans.ts b/projects/plugins/protect/src/js/types/scans.ts index 98cf04de3fb3a..fe60eec485402 100644 --- a/projects/plugins/protect/src/js/types/scans.ts +++ b/projects/plugins/protect/src/js/types/scans.ts @@ -39,14 +39,8 @@ export type ScanStatus = { /** The time the last scan was checked, in YYYY-MM-DD HH:MM:SS format. */ lastChecked: string | null; - /** The number of plugin threats found in the latest status. */ - numPluginsThreats: number; - - /** The number of theme threats found in the latest status. */ - numThemesThreats: number; - - /** The total number of threats found in the latest status. */ - numThreats: number; + /** The security threats identified in the latest scan. */ + threats: Threat[]; /** Whether there was an error in the scan results. */ error: boolean | null; @@ -56,26 +50,4 @@ export type ScanStatus = { /** The error message. */ errorMessage: string | null; - - /** WordPress Core Status */ - core: { - checked: boolean; - name: string; - slug: string; - threats: Threat[]; - type: 'core'; - version: string; - } | null; - - /** Plugins Status */ - plugins: ExtensionStatus[]; - - /** Themes Status */ - themes: ExtensionStatus[]; - - /** File Threats */ - files: Threat[]; - - /** Database Threats */ - database: Threat[]; }; 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; + }; };