Skip to content

Commit

Permalink
Components: add ThreatDetailsModal
Browse files Browse the repository at this point in the history
  • Loading branch information
nateweller committed Nov 9, 2024
1 parent 7c0b2a8 commit afabb70
Show file tree
Hide file tree
Showing 7 changed files with 426 additions and 38 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
display: flex;
font-family: "Courier 10 Pitch", Courier, monospace;
flex-direction: row;
overflow-x: scroll;
overflow-x: auto;
white-space: pre;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
font-family: monospace;
display: flex;
flex-direction: row;
overflow-x: scroll;
overflow-x: auto;
}

.marked-lines__marked-line {
Expand Down
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>
);
}
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: () => {},
};
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 );
}
Loading

0 comments on commit afabb70

Please sign in to comment.