-
Notifications
You must be signed in to change notification settings - Fork 798
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
7c0b2a8
commit afabb70
Showing
7 changed files
with
426 additions
and
38 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
278 changes: 278 additions & 0 deletions
278
projects/js-packages/components/components/threat-details-modal/index.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,278 @@ | ||
import { Button, ThreatSeverityBadge } from '@automattic/jetpack-components'; | ||
import { type Threat } from '@automattic/jetpack-scan'; | ||
import { Modal } from '@wordpress/components'; | ||
import { __, sprintf } from '@wordpress/i18n'; | ||
import { useMemo } from 'react'; | ||
import ContextualUpgradeTrigger from '../contextual-upgrade-trigger'; | ||
import DiffViewer from '../diff-viewer'; | ||
import MarkedLines from '../marked-lines'; | ||
import Text from '../text'; | ||
import styles from './styles.module.scss'; | ||
|
||
const ThreatTechnicalDetails = ( { threat }: { threat: Threat } ) => { | ||
if ( ! threat.filename && ! threat.context && ! threat.diff ) { | ||
return null; | ||
} | ||
|
||
return ( | ||
<div className={ styles.section }> | ||
<Text variant="title-small">{ __( 'The technical details', 'jetpack' ) }</Text> | ||
{ threat.filename && ( | ||
<> | ||
<Text>{ __( 'Threat found in file:', 'jetpack' ) }</Text> | ||
<pre className={ styles.filename }>{ threat.filename }</pre> | ||
</> | ||
) } | ||
{ threat.context && <MarkedLines context={ threat.context } /> } | ||
{ threat.diff && <DiffViewer diff={ threat.diff } /> } | ||
</div> | ||
); | ||
}; | ||
|
||
const ThreatFixDetails = ( { | ||
threat, | ||
handleUpgradeClick, | ||
}: { | ||
threat: Threat; | ||
handleUpgradeClick: () => void; | ||
} ) => { | ||
const title = useMemo( () => { | ||
if ( threat.status === 'fixed' ) { | ||
return __( 'How did Jetpack fix it?', 'jetpack' ); | ||
} | ||
if ( threat.status === 'current' && threat.fixable ) { | ||
return __( 'How can Jetpack auto-fix this threat?', 'jetpack' ); | ||
} | ||
return __( 'How to fix it?', 'jetpack' ); | ||
}, [ threat ] ); | ||
|
||
const fix = useMemo( () => { | ||
// The threat has a fixed version available, but no auto-fix is available. | ||
// The user needs to update the extension to the fixed version. | ||
if ( ! threat.fixable && threat.fixedIn ) { | ||
return sprintf( | ||
/* translators: Translates to Updates to version. %1$s: Name. %2$s: Fixed version */ | ||
__( 'Update %1$s to version %2$s.', 'jetpack' ), | ||
threat.extension.name, | ||
threat.fixedIn | ||
); | ||
} | ||
|
||
// The threat has an auto-fix available. | ||
switch ( threat.fixable && threat.fixable.fixer ) { | ||
case 'delete': | ||
if ( threat.filename ) { | ||
if ( threat.filename.endsWith( '/' ) ) { | ||
return __( 'Deletes the directory that the infected file is in.', 'jetpack' ); | ||
} | ||
|
||
if ( threat.signature === 'Core.File.Modification' ) { | ||
return __( 'Deletes the unexpected file in a core WordPress directory.', 'jetpack' ); | ||
} | ||
|
||
return __( 'Deletes the infected file.', 'jetpack' ); | ||
} | ||
|
||
if ( threat.extension?.type === 'plugin' ) { | ||
return __( 'Deletes the plugin directory to fix the threat.', 'jetpack' ); | ||
} | ||
|
||
if ( threat.extension?.type === 'theme' ) { | ||
return __( 'Deletes the theme directory to fix the threat.', 'jetpack' ); | ||
} | ||
break; | ||
case 'update': | ||
if ( threat.fixedIn && threat.extension.name ) { | ||
return sprintf( | ||
/* translators: Translates to Updates to version. %1$s: Name. %2$s: Fixed version */ | ||
__( 'Updates %1$s to version %2$s', 'jetpack' ), | ||
threat.extension.name, | ||
threat.fixedIn | ||
); | ||
} | ||
return __( 'Upgrades the plugin or theme to a newer version.', 'jetpack' ); | ||
case 'replace': | ||
case 'rollback': | ||
if ( threat.filename ) { | ||
return threat.signature === 'Core.File.Modification' | ||
? __( | ||
'Replaces the modified core WordPress file with the original clean version from the WordPress source code.', | ||
'jetpack' | ||
) | ||
: __( | ||
'Replaces the infected file with a previously backed up version that is clean.', | ||
'jetpack' | ||
); | ||
} | ||
|
||
if ( threat.signature === 'php_hardening_WP_Config_NoSalts_001' ) { | ||
return __( | ||
'Replaces the default salt keys in wp-config.php with unique ones.', | ||
'jetpack' | ||
); | ||
} | ||
break; | ||
default: | ||
return __( 'Jetpack will auto-fix the threat.', 'jetpack' ); | ||
} | ||
}, [ threat ] ); | ||
|
||
if ( ! threat.fixable && ! threat.fixedIn ) { | ||
return null; | ||
} | ||
|
||
return ( | ||
<div className={ styles.section }> | ||
<Text variant="title-small">{ title }</Text> | ||
<Text>{ fix }</Text> | ||
|
||
{ !! handleUpgradeClick && ( | ||
<ContextualUpgradeTrigger | ||
description={ __( 'Looking for advanced scan results and one-click fixes?', 'jetpack' ) } | ||
cta={ __( 'Upgrade Jetpack Protect now', 'jetpack' ) } | ||
onClick={ handleUpgradeClick } | ||
/> | ||
) } | ||
</div> | ||
); | ||
}; | ||
|
||
const ThreatActions = ( { | ||
threat, | ||
handleFixThreatClick, | ||
handleIgnoreThreatClick, | ||
handleUnignoreThreatClick, | ||
isActiveFixInProgress, | ||
isStaleFixInProgress, | ||
}: { | ||
threat: Threat; | ||
handleFixThreatClick?: () => void; | ||
handleIgnoreThreatClick?: () => void; | ||
handleUnignoreThreatClick?: () => void; | ||
isActiveFixInProgress?: boolean; | ||
isStaleFixInProgress?: boolean; | ||
} ) => { | ||
if ( ! handleFixThreatClick && ! handleIgnoreThreatClick && ! handleUnignoreThreatClick ) { | ||
return null; | ||
} | ||
|
||
return ( | ||
<div className={ styles.actions }> | ||
{ 'ignored' === threat.status && !! handleUnignoreThreatClick && ( | ||
<Button isDestructive={ true } variant="secondary" onClick={ handleUnignoreThreatClick() }> | ||
{ __( 'Un-ignore', 'jetpack' ) } | ||
</Button> | ||
) } | ||
{ 'current' === threat.status && ( | ||
<> | ||
{ !! handleIgnoreThreatClick && ( | ||
<Button | ||
isDestructive={ true } | ||
variant="secondary" | ||
onClick={ handleIgnoreThreatClick() } | ||
disabled={ isActiveFixInProgress || isStaleFixInProgress } | ||
> | ||
{ __( 'Ignore', 'jetpack' ) } | ||
</Button> | ||
) } | ||
{ threat.fixable && !! handleFixThreatClick && ( | ||
<Button | ||
isPrimary | ||
disabled={ isActiveFixInProgress || isStaleFixInProgress } | ||
onClick={ handleFixThreatClick() } | ||
> | ||
{ __( 'Auto-Fix', 'jetpack' ) } | ||
</Button> | ||
) } | ||
</> | ||
) } | ||
</div> | ||
); | ||
}; | ||
|
||
/** | ||
* ThreatDetailsModal component | ||
* | ||
* @param {object} props - The props. | ||
* @param {object} props.threat - The threat. | ||
* @param {Function} props.handleUpgradeClick - The handleUpgradeClick function. | ||
* @param {boolean} props.isActiveFixInProgress - The isActiveFixInProgress flag. | ||
* @param {boolean} props.isStaleFixInProgress - The isStaleFixInProgress flag. | ||
* @param {Function} props.handleFixThreatClick - The handleFixThreatClick function. | ||
* @param {Function} props.handleIgnoreThreatClick - The handleIgnoreThreatClick function. | ||
* @param {Function} props.handleUnignoreThreatClick - The handleUnignoreThreatClick function. | ||
* | ||
* @return {JSX.Element} The threat details modal. | ||
*/ | ||
export default function ThreatDetailsModal( { | ||
threat, | ||
handleUpgradeClick, | ||
isActiveFixInProgress, | ||
isStaleFixInProgress, | ||
handleFixThreatClick, | ||
handleIgnoreThreatClick, | ||
handleUnignoreThreatClick, | ||
...modalProps | ||
}: { | ||
threat: Threat; | ||
handleUpgradeClick?: () => void; | ||
isActiveFixInProgress?: boolean; | ||
isStaleFixInProgress?: boolean; | ||
handleFixThreatClick?: () => void; | ||
handleIgnoreThreatClick?: () => void; | ||
handleUnignoreThreatClick?: () => void; | ||
[ key: string ]: unknown; | ||
} ): JSX.Element { | ||
const title = useMemo( () => { | ||
if ( threat.title ) { | ||
return threat.title; | ||
} | ||
|
||
if ( threat.status === 'fixed' ) { | ||
return __( 'What was the problem?', 'jetpack' ); | ||
} | ||
|
||
return __( 'What is the problem?', 'jetpack' ); | ||
}, [ threat ] ); | ||
|
||
return ( | ||
<Modal size="large" title={ __( 'Threat Details', 'jetpack' ) } { ...modalProps }> | ||
<div className={ styles[ 'threat-details' ] }> | ||
<div className={ styles.section }> | ||
<div className={ styles.title }> | ||
<Text variant="title-small">{ title }</Text> | ||
{ !! threat.severity && <ThreatSeverityBadge severity={ threat.severity } /> } | ||
</div> | ||
|
||
{ !! threat.description && <Text>{ threat.description }</Text> } | ||
|
||
{ !! threat.source && ( | ||
<div> | ||
<Button | ||
variant="link" | ||
isExternalLink={ true } | ||
weight="regular" | ||
href={ threat.source } | ||
> | ||
{ __( 'See more technical details of this threat', 'jetpack' ) } | ||
</Button> | ||
</div> | ||
) } | ||
</div> | ||
|
||
<ThreatFixDetails threat={ threat } handleUpgradeClick={ handleUpgradeClick } /> | ||
|
||
<ThreatTechnicalDetails threat={ threat } /> | ||
|
||
<ThreatActions | ||
threat={ threat } | ||
handleFixThreatClick={ handleFixThreatClick } | ||
handleIgnoreThreatClick={ handleIgnoreThreatClick } | ||
handleUnignoreThreatClick={ handleUnignoreThreatClick } | ||
isActiveFixInProgress={ isActiveFixInProgress } | ||
isStaleFixInProgress={ isStaleFixInProgress } | ||
/> | ||
</div> | ||
</Modal> | ||
); | ||
} |
67 changes: 67 additions & 0 deletions
67
projects/js-packages/components/components/threat-details-modal/stories/index.stories.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,67 @@ | ||
import { useCallback, useState } from 'react'; | ||
import Button from '../../button/index.js'; | ||
import ThreatDetailsModal from '../index.js'; | ||
|
||
export default { | ||
title: 'JS Packages/Components/Threat Details Modal', | ||
component: ThreatDetailsModal, | ||
}; | ||
|
||
const Base = args => { | ||
const [ isOpen, setIsOpen ] = useState( false ); | ||
const onClick = useCallback( () => setIsOpen( true ), [] ); | ||
const onRequestClose = useCallback( () => setIsOpen( false ), [] ); | ||
return ( | ||
<div> | ||
<Button onClick={ onClick }>Open Threat Details Modal</Button> | ||
{ isOpen ? <ThreatDetailsModal { ...args } onRequestClose={ onRequestClose } /> : null } | ||
</div> | ||
); | ||
}; | ||
|
||
export const ThreatResult = Base.bind( {} ); | ||
ThreatResult.args = { | ||
threat: { | ||
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, | ||
severity: 8, | ||
fixable: { fixer: 'rollback', target: 'January 26, 2024, 6:49 am', extensionStatus: '' }, | ||
fixer: { status: 'not_started' }, | ||
status: 'current', | ||
filename: '/var/www/html/wp-content/index.php', | ||
context: { | ||
'1': 'echo <<<HTML', | ||
'2': 'X5O!P%@AP[4\\PZX54(P^)7CC)7}$EICAR-STANDARD-ANTIVIRUS-TEST-FILE!$H+H*', | ||
'3': 'HTML;', | ||
marks: {}, | ||
}, | ||
}, | ||
handleFixThreatClick: () => {}, | ||
handleIgnoreThreatClick: () => {}, | ||
handleUnignoreThreatClick: () => {}, | ||
}; | ||
|
||
export const VulnerableExtension = Base.bind( {} ); | ||
VulnerableExtension.args = { | ||
threat: { | ||
id: 184847701, | ||
signature: 'Vulnerable.WP.Extension', | ||
title: 'Vulnerable Plugin: WP Super Cache (version 1.6.3)', | ||
description: | ||
'The plugin WP Super Cache (version 1.6.3) has a known vulnerability. The WP Super Cache plugin before version 1.7.2 is vulnerable to an authenticated RCE in the settings page.', | ||
fixedIn: '1.12.4', | ||
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', | ||
}, | ||
}, | ||
handleUpgradeClick: () => {}, | ||
}; |
32 changes: 32 additions & 0 deletions
32
projects/js-packages/components/components/threat-details-modal/styles.module.scss
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,32 @@ | ||
|
||
.threat-details { | ||
display: flex; | ||
flex-direction: column; | ||
gap: calc( var( --spacing-base ) * 3 ); // 24px | ||
} | ||
|
||
.section { | ||
display: flex; | ||
flex-direction: column; | ||
gap: calc( var( --spacing-base ) * 2 ); // 16px | ||
} | ||
|
||
.title { | ||
display: flex; | ||
align-items: center; | ||
gap: calc( var( --spacing-base ) * 1.5 ); // 12px | ||
} | ||
|
||
.filename { | ||
background-color: var( --jp-gray-0 ); | ||
padding: calc( var( --spacing-base ) * 3 ); // 24px | ||
overflow-x: auto; | ||
} | ||
|
||
.actions { | ||
display: flex; | ||
gap: calc( var( --spacing-base ) * 2 ); // 16px | ||
justify-content: flex-end; | ||
padding-top: calc( var( --spacing-base ) * 3 ); // 24px | ||
border-top: 1px solid var( --jp-gray-0 ); | ||
} |
Oops, something went wrong.