From 6f15e109b84b34678d23e8d5051da8ca5cc3632a Mon Sep 17 00:00:00 2001 From: dkmyta <43220201+dkmyta@users.noreply.github.com> Date: Tue, 6 Aug 2024 11:17:47 -0700 Subject: [PATCH] Protect: Add unignore threat capabilities (#38094) * 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 * Fix lint errors --------- Co-authored-by: Nate Weller Co-authored-by: Nate Weller --- .../protect/src/class-rest-controller.php | 33 ++++++++++ .../plugins/protect/src/class-threats.php | 11 ++++ .../components/ignore-threat-modal/index.jsx | 2 +- .../protect/src/js/components/modal/index.jsx | 2 + .../js/components/paid-accordion/index.jsx | 8 +++ .../js/components/threats-list/paid-list.jsx | 39 +++++++++-- .../unignore-threat-modal/index.jsx | 66 +++++++++++++++++++ .../unignore-threat-modal/styles.module.scss | 40 +++++++++++ .../plugins/protect/src/js/state/actions.js | 36 ++++++++++ 9 files changed, 229 insertions(+), 8 deletions(-) create mode 100644 projects/plugins/protect/src/js/components/unignore-threat-modal/index.jsx create mode 100644 projects/plugins/protect/src/js/components/unignore-threat-modal/styles.module.scss diff --git a/projects/plugins/protect/src/class-rest-controller.php b/projects/plugins/protect/src/class-rest-controller.php index 6da77263e808c..a1ae1b67ba654 100644 --- a/projects/plugins/protect/src/class-rest-controller.php +++ b/projects/plugins/protect/src/class-rest-controller.php @@ -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', @@ -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 * diff --git a/projects/plugins/protect/src/class-threats.php b/projects/plugins/protect/src/class-threats.php index 63a20f9ef3c89..f2a80999f65f9 100644 --- a/projects/plugins/protect/src/class-threats.php +++ b/projects/plugins/protect/src/class-threats.php @@ -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 * 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 e20974d9cd8c2..a2e222867d2e9 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 @@ -52,7 +52,7 @@ const IgnoreThreatModal = ( { id, title, label, icon, severity } ) => { { 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 Codeable.', + '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 Codeable.', 'jetpack-protect' ), { diff --git a/projects/plugins/protect/src/js/components/modal/index.jsx b/projects/plugins/protect/src/js/components/modal/index.jsx index c21f39babe9d8..1f629fc955989 100644 --- a/projects/plugins/protect/src/js/components/modal/index.jsx +++ b/projects/plugins/protect/src/js/components/modal/index.jsx @@ -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, diff --git a/projects/plugins/protect/src/js/components/paid-accordion/index.jsx b/projects/plugins/protect/src/js/components/paid-accordion/index.jsx index b922738dd6c83..adfc8ddad213c 100644 --- a/projects/plugins/protect/src/js/components/paid-accordion/index.jsx +++ b/projects/plugins/protect/src/js/components/paid-accordion/index.jsx @@ -33,6 +33,14 @@ const ScanHistoryDetails = ( { detectedAt, fixedOn, status } ) => { ) } + { 'ignored' === status && ( + <> + + + { __( 'Threat ignored', 'jetpack-protect' ) } + + + ) } ) } { ( 'fixed' === status || 'ignored' === status ) && ( 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 index c25695ee67412..adfcc75153bd9 100644 --- a/projects/plugins/protect/src/js/components/threats-list/paid-list.jsx +++ b/projects/plugins/protect/src/js/components/threats-list/paid-list.jsx @@ -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(); @@ -126,15 +136,30 @@ const ThreatAccordionItem = ( { ) } { ! description &&
{ learnMoreButton }
}
- { status !== 'ignored' && status !== 'fixed' && ( - ) } - { fixable && status !== 'fixed' && ( - + { 'current' === status && ( + <> + + { fixable && ( + + ) } + ) }
diff --git a/projects/plugins/protect/src/js/components/unignore-threat-modal/index.jsx b/projects/plugins/protect/src/js/components/unignore-threat-modal/index.jsx new file mode 100644 index 0000000000000..ab7612e23351d --- /dev/null +++ b/projects/plugins/protect/src/js/components/unignore-threat-modal/index.jsx @@ -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 ( + + + { __( 'Do you really want to unignore this threat?', 'jetpack-protect' ) } + + { __( 'Jetpack will unignore the threat:', 'jetpack-protect' ) } + +
+ +
+ + { label } + + { title } +
+
+ +
+
+ +
+ + +
+
+ ); +}; + +export default UnignoreThreatModal; diff --git a/projects/plugins/protect/src/js/components/unignore-threat-modal/styles.module.scss b/projects/plugins/protect/src/js/components/unignore-threat-modal/styles.module.scss new file mode 100644 index 0000000000000..d936392c844e5 --- /dev/null +++ b/projects/plugins/protect/src/js/components/unignore-threat-modal/styles.module.scss @@ -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; +} diff --git a/projects/plugins/protect/src/js/state/actions.js b/projects/plugins/protect/src/js/state/actions.js index eb01caa8173f3..67d0893d6046f 100644 --- a/projects/plugins/protect/src/js/state/actions.js +++ b/projects/plugins/protect/src/js/state/actions.js @@ -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 } ) => { @@ -455,6 +490,7 @@ const actions = { setwpVersion, setJetpackScan, ignoreThreat, + unignoreThreat, setModal, setNotice, clearNotice,