Skip to content

Commit

Permalink
Protect: Add unignore threat capabilities (#38094)
Browse files Browse the repository at this point in the history
* Add Scan_History class

* changelog

* Update changelog

* Merge trunk, fix versions

* Add initial UI for scan history

* Add firstDetected and fixedOn details to threat cards

* Fix ignored message

* Fix console error

* Adjust wording and styles

* Optimize

* Fix firstDetected and fixedOn property retrieval issues, add todos

* Remove unneeded ref

* Consolidate scan history UI to existing components

* Remove todos

* Return false for scanHistory when upgrade is missing

* Manage ScanPage states when viewing history

* Move source data logic to useProtectData hook

* Improve Summary component Button logic

* ScanPage refactoring

* Reapply missing scanIsUnavailable check

* Add comments

* Hide ignore when viewing threats, and clear history cache when updating or fixing

* Add unignore actions and conditional handling to the UI

* Add null coalescing to core threat prop assignment

* Fix core threat normalization

* Optimizations and refactoring

* Revert standarization, and use conditionals

* Add dummy arg to avoid bad translation minification

* Changlog entry

* Move viewingScanHistory out of initial state

* Use threat status over lack of fixedOn to display unignore button

* Add foundation for unignore request

* Remove need for signature_id

* Add TypeScript support to Jetpack Protect

* Downgrade typescript to 5.0.4, to match other monorepo projects

* Add TypeScript support to Jetpack Protect

* Init feature branch

* Protect: Add Scan_History class (#37903)

* Protect: Add Scan History UI (#37988)

* Init feature branch

* Init feature branch

* Init feature branch

* Use react router for threat history, separate scan and history root components, minor UI adjustments

* changelog

* Add dummy args to avoid bad minification

* Hide scan section navigation when user has no plan

* Wrap ScanButton with forwardRef

* Add onboarding popover to Scan Now button in empty state

* Fix filtering of core threat history

* Redirect to /scan from /scan/history when user has no plan

* Fix animation glitch in onboarding popover

* Remove unnecessary additions

* Further removals after failed base merge

* More removals

* Add todos

* Use status to conditional render unignore action button

* Move ignore/unignore actions to dedicated modals

* Add TypeScript support to Jetpack Protect

* Downgrade typescript to 5.0.4, to match other monorepo projects

* Add TypeScript support to Jetpack Protect

* Init feature branch

* Protect: Add Scan_History class (#37903)

* Protect: Add Scan History UI (#37988)

* changelog

* Protect: Separate scanner and history views via React Router and UI adjustments (#38325)

* Run ./tools/fixup-project-versions.sh

* Restore project versions

* Run ./tools/fixup-project-versions.sh

* Remove todo comment (#38628)

* Remove changelog

* Display fix threat button only for current threats

* Further unification

* Refresh scan history after unignore

* Update projects/plugins/protect/src/js/components/unignore-threat-modal/index.jsx

Co-authored-by: Nate Weller <[email protected]>

* Fix lint errors

---------

Co-authored-by: Nate Weller <[email protected]>
Co-authored-by: Nate Weller <[email protected]>
  • Loading branch information
3 people committed Aug 6, 2024
1 parent 3d1ae41 commit a91bb6e
Show file tree
Hide file tree
Showing 9 changed files with 229 additions and 8 deletions.
33 changes: 33 additions & 0 deletions projects/plugins/protect/src/class-rest-controller.php
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,18 @@ public static function register_rest_endpoints() {
)
);

register_rest_route(
'jetpack-protect/v1',
'unignore-threat',
array(
'methods' => \WP_REST_Server::EDITABLE,
'callback' => __CLASS__ . '::api_unignore_threat',
'permission_callback' => function () {
return current_user_can( 'manage_options' );
},
)
);

register_rest_route(
'jetpack-protect/v1',
'fix-threats',
Expand Down Expand Up @@ -233,6 +245,27 @@ public static function api_ignore_threat( $request ) {
return new WP_REST_Response( 'Threat ignored.' );
}

/**
* Unignores a threat for the API endpoint
*
* @param WP_REST_Request $request The request object.
*
* @return WP_REST_Response
*/
public static function api_unignore_threat( $request ) {
if ( ! $request['threat_id'] ) {
return new WP_REST_Response( 'Missing threat ID.', 400 );
}

$threat_ignored = Threats::unignore_threat( $request['threat_id'] );

if ( ! $threat_ignored ) {
return new WP_REST_Response( 'An error occured while attempting to unignore the threat.', 500 );
}

return new WP_REST_Response( 'Threat unignored.' );
}

/**
* Fixes threats for the API endpoint
*
Expand Down
11 changes: 11 additions & 0 deletions projects/plugins/protect/src/class-threats.php
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,17 @@ public static function ignore_threat( $threat_id ) {
return self::update_threat( $threat_id, array( 'ignore' => true ) );
}

/**
* Unignore Threat
*
* @param string $threat_id The threat ID.
*
* @return bool
*/
public static function unignore_threat( $threat_id ) {
return self::update_threat( $threat_id, array( 'unignore' => true ) );
}

/**
* Fix Threats
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ const IgnoreThreatModal = ( { id, title, label, icon, severity } ) => {
<Text mb={ 4 }>
{ createInterpolateElement(
__(
'By ignoring this threat you confirm that you have reviewed the detected code and assume the risks of keeping a potentially malicious or vulnerable file on your site. If you are unsure please request an estimate with <codeableLink>Codeable</codeableLink>.',
'By choosing to ignore this threat, you acknowledge that you have reviewed the detected code. You are accepting the risks of maintaining a potentially malicious or vulnerable file on your site. If you are unsure, please request an estimate with <codeableLink>Codeable</codeableLink>.',
'jetpack-protect'
),
{
Expand Down
2 changes: 2 additions & 0 deletions projects/plugins/protect/src/js/components/modal/index.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,12 @@ import FixAllThreatsModal from '../fix-all-threats-modal';
import FixThreatModal from '../fix-threat-modal';
import IgnoreThreatModal from '../ignore-threat-modal';
import StandaloneModeModal from '../standalone-mode-modal';
import UnignoreThreatModal from '../unignore-threat-modal';
import styles from './styles.module.scss';

const MODAL_COMPONENTS = {
IGNORE_THREAT: IgnoreThreatModal,
UNIGNORE_THREAT: UnignoreThreatModal,
FIX_THREAT: FixThreatModal,
FIX_ALL_THREATS: FixAllThreatsModal,
CREDENTIALS_NEEDED: CredentialsNeededModal,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,14 @@ const ScanHistoryDetails = ( { detectedAt, fixedOn, status } ) => {
</span>
</>
) }
{ 'ignored' === status && (
<>
<span className={ styles[ 'accordion-header-status-separator' ] }></span>
<span className={ styles[ 'is-ignored' ] }>
{ __( 'Threat ignored', 'jetpack-protect' ) }
</span>
</>
) }
</Text>
) }
{ ( 'fixed' === status || 'ignored' === status ) && (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,16 @@ const ThreatAccordionItem = ( {
};
};

const handleUnignoreThreatClick = () => {
return event => {
event.preventDefault();
setModal( {
type: 'UNIGNORE_THREAT',
props: { id, label, title, icon, severity },
} );
};
};

const handleFixThreatClick = () => {
return event => {
event.preventDefault();
Expand Down Expand Up @@ -126,15 +136,30 @@ const ThreatAccordionItem = ( {
) }
{ ! description && <div className={ styles[ 'threat-section' ] }>{ learnMoreButton }</div> }
<div className={ styles[ 'threat-footer' ] }>
{ status !== 'ignored' && status !== 'fixed' && (
<Button isDestructive={ true } variant="secondary" onClick={ handleIgnoreThreatClick() }>
{ __( 'Ignore threat', 'jetpack-protect' ) }
{ 'ignored' === status && (
<Button
isDestructive={ true }
variant="secondary"
onClick={ handleUnignoreThreatClick() }
>
{ __( 'Unignore threat', 'jetpack-protect' ) }
</Button>
) }
{ fixable && status !== 'fixed' && (
<Button disabled={ fixerInProgress } onClick={ handleFixThreatClick() }>
{ __( 'Fix threat', 'jetpack-protect' ) }
</Button>
{ 'current' === status && (
<>
<Button
isDestructive={ true }
variant="secondary"
onClick={ handleIgnoreThreatClick() }
>
{ __( 'Ignore threat', 'jetpack-protect' ) }
</Button>
{ fixable && (
<Button disabled={ fixerInProgress } onClick={ handleFixThreatClick() }>
{ __( 'Fix threat', 'jetpack-protect' ) }
</Button>
) }
</>
) }
</div>
</PaidAccordionItem>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
import { Button, Text } from '@automattic/jetpack-components';
import { useDispatch, useSelect } from '@wordpress/data';
import { __ } from '@wordpress/i18n';
import { Icon } from '@wordpress/icons';
import { STORE_ID } from '../../state/store';
import ThreatSeverityBadge from '../severity';
import UserConnectionGate from '../user-connection-gate';
import styles from './styles.module.scss';

const UnignoreThreatModal = ( { id, title, label, icon, severity } ) => {
const { setModal, unignoreThreat } = useDispatch( STORE_ID );
const threatsUpdating = useSelect( select => select( STORE_ID ).getThreatsUpdating() );

const handleCancelClick = () => {
return event => {
event.preventDefault();
setModal( { type: null } );
};
};

const handleUnignoreClick = () => {
return async event => {
event.preventDefault();
unignoreThreat( id, () => {
setModal( { type: null } );
} );
};
};

return (
<UserConnectionGate>
<Text variant="title-medium" mb={ 2 }>
{ __( 'Do you really want to unignore this threat?', 'jetpack-protect' ) }
</Text>
<Text mb={ 3 }>{ __( 'Jetpack will unignore the threat:', 'jetpack-protect' ) }</Text>

<div className={ styles.threat }>
<Icon icon={ icon } className={ styles.threat__icon } />
<div className={ styles.threat__summary }>
<Text className={ styles.threat__summary__label } mb={ 1 }>
{ label }
</Text>
<Text className={ styles.threat__summary__title }>{ title }</Text>
</div>
<div className={ styles.threat__severity }>
<ThreatSeverityBadge severity={ severity } />
</div>
</div>

<div className={ styles.footer }>
<Button variant="secondary" onClick={ handleCancelClick() }>
{ __( 'Cancel', 'jetpack-protect' ) }
</Button>
<Button
isDestructive={ true }
isLoading={ Boolean( threatsUpdating && threatsUpdating[ id ] ) }
onClick={ handleUnignoreClick() }
>
{ __( 'Unignore threat', 'jetpack-protect' ) }
</Button>
</div>
</UserConnectionGate>
);
};

export default UnignoreThreatModal;
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
.threat {
border: 1px solid var( --jp-gray );
border-radius: var( --jp-border-radius );
padding: calc( var( --spacing-base ) * 2 ); // 16px
display: flex;
margin-bottom: calc( var( --spacing-base ) * 3 ); // 24px

&__icon {
margin-right: calc( var( --spacing-base ) * 2 ); // 16px
min-width: 24px;
}

&__summary {
width: 100%;
}

&__summary__label {
font-size: 18px;
line-height: 24px;
font-weight: 600;
margin-bottom: 0;
}

&__summary__title {
font-size: 14px;
line-height: 21px;
color: var( --jp-gray-80 );
}

&__severity {
align-self: center;
margin-left: calc( var( --spacing-base ) * 2 ); // 16px
margin-right: var( --spacing-base ); // 8px
}
}

.footer {
display: flex;
justify-content: space-between;
}
36 changes: 36 additions & 0 deletions projects/plugins/protect/src/js/state/actions.js
Original file line number Diff line number Diff line change
Expand Up @@ -242,6 +242,41 @@ const ignoreThreat =
} );
};

const unignoreThreat =
( threatId, callback = () => {} ) =>
async ( { dispatch } ) => {
dispatch( setThreatIsUpdating( threatId, true ) );
return await new Promise( () => {
return apiFetch( {
path: `jetpack-protect/v1/unignore-threat?threat_id=${ threatId }`,
method: 'POST',
} )
.then( () => {
return dispatch( refreshScanHistory() );
} )
.then( () => {
return dispatch( refreshStatus() );
} )
.then( () => {
return dispatch(
setNotice( { type: 'success', message: __( 'Threat unignored', 'jetpack-protect' ) } )
);
} )
.catch( () => {
return dispatch(
setNotice( {
type: 'error',
message: __( 'An error ocurred unignoring the threat.', 'jetpack-protect' ),
} )
);
} )
.finally( () => {
dispatch( setThreatIsUpdating( threatId, false ) );
callback();
} );
} );
};

const getFixThreatsStatus =
threatIds =>
async ( { dispatch } ) => {
Expand Down Expand Up @@ -455,6 +490,7 @@ const actions = {
setwpVersion,
setJetpackScan,
ignoreThreat,
unignoreThreat,
setModal,
setNotice,
clearNotice,
Expand Down

0 comments on commit a91bb6e

Please sign in to comment.