Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
Significance: minor
Type: changed

Updated threat components
86 changes: 86 additions & 0 deletions projects/js-packages/scan/src/actions/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
import { __ } from '@wordpress/i18n';
import { Threat } from '../types/threats.js';
import { getFixerDescription } from '../utils/index.js';

/**
* Threat Action IDs.
*/
export const THREAT_ACTION_VIEW = 'view';
export const THREAT_ACTION_FIX = 'fix';
export const THREAT_ACTION_IGNORE = 'ignore';
export const THREAT_ACTION_UNIGNORE = 'unignore';

export type ThreatAction = {
/** Unique ID of the action. */
id:
| typeof THREAT_ACTION_VIEW
| typeof THREAT_ACTION_FIX
| typeof THREAT_ACTION_IGNORE
| typeof THREAT_ACTION_UNIGNORE;

/** Label of the action. */
label: string | ( ( threats: Threat[] ) => string );

/** Description of the action. */
description?: string | ( ( threats: Threat[] ) => string );

/** Callback function which determines whether the action is eligible to run for a given threat. */
isEligible?: ( threat: Threat ) => boolean;

/** Callback function to run the action. */
callback: (
threats: Threat[],
{ onActionPerformed }?: { onActionPerformed?: ( items: Threat[] ) => void }
) => void;

/** Whether the action should use a confirmation flow. */
requiresConfirmation?: boolean;

/** Whether the action requires a Jetpack connection. */
requiresConnection?: boolean;

/** Wether the action requires site credentials. */
requiresCredentials?: boolean;
};

type ThreatActionBase = Omit< ThreatAction, 'callback' >;

type ThreatActionsObject = Partial< Record< ThreatAction[ 'id' ], ThreatActionBase > >;

/**
* Threat Actions.
*
* Threat action properties keyed by action ID.
*/
export const THREAT_ACTIONS: ThreatActionsObject = {
[ THREAT_ACTION_FIX ]: {
id: THREAT_ACTION_FIX,
label: __( 'Show Auto-Fix', 'jetpack-scan' ),
description: ( threats: Threat[] ) => {
return getFixerDescription( threats[ 0 ] );
},
isEligible( threat ) {
return !! threat.fixable;
},
requiresConfirmation: true,
requiresConnection: true,
requiresCredentials: true,
},
[ THREAT_ACTION_IGNORE ]: {
id: THREAT_ACTION_IGNORE,
label: __( 'Ignore', 'jetpack-scan' ),
isEligible( threat ) {
return threat.status === 'current';
},
requiresConfirmation: true,
requiresConnection: true,
},
[ THREAT_ACTION_UNIGNORE ]: {
id: THREAT_ACTION_UNIGNORE,
label: __( 'Un-ignore', 'jetpack-scan' ),
isEligible( threat ) {
return threat.status === 'ignored';
},
requiresConnection: true,
},
};
4 changes: 3 additions & 1 deletion projects/js-packages/scan/src/components/index.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
export { default as ScanReport } from './scan-report/index.js';
export { default as ShieldIcon } from './shield-icon/index.js';
export { default as ThreatFixerButton } from './threat-fixer-button/index.js';
export { default as ThreatModal } from './threat-modal/index.js';
export * from './threat-modals/index.js';
export { default as ThreatSeverityBadge } from './threat-severity-badge/index.js';
export { default as ThreatsDataViews } from './threats-data-views/index.js';
36 changes: 36 additions & 0 deletions projects/js-packages/scan/src/components/scan-report/constants.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import { __ } from '@wordpress/i18n';
import {
code as fileIcon,
color as themeIcon,
plugins as pluginIcon,
shield as shieldIcon,
wordpress as coreIcon,
} from '@wordpress/icons';

export const STATUS_TYPES = [
{ value: 'checked', label: __( 'Checked', 'jetpack-scan' ) },
{ value: 'unchecked', label: __( 'Unchecked', 'jetpack-scan' ) },
{ value: 'threat', label: __( 'Threat', 'jetpack-scan' ) },
];

export const TYPES = [
{ value: 'core', label: __( 'WordPress', 'jetpack-scan' ) },
{ value: 'plugins', label: __( 'Plugin', 'jetpack-scan' ) },
{ value: 'themes', label: __( 'Theme', 'jetpack-scan' ) },
{ value: 'files', label: __( 'Files', 'jetpack-scan' ) },
];

export const ICONS = {
plugins: pluginIcon,
themes: themeIcon,
core: coreIcon,
files: fileIcon,
default: shieldIcon,
};

export const FIELD_ICON = 'icon';
export const FIELD_TYPE = 'type';
export const FIELD_NAME = 'name';
export const FIELD_STATUS = 'status';
export const FIELD_UPDATE = 'update';
export const FIELD_VERSION = 'version';
245 changes: 245 additions & 0 deletions projects/js-packages/scan/src/components/scan-report/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,245 @@
import { Tooltip } from '@wordpress/components';
import {
type SupportedLayouts,
type View,
type Field,
DataViews,
filterSortAndPaginate,
} from '@wordpress/dataviews';
import { __, _n } from '@wordpress/i18n';
import { Icon } from '@wordpress/icons';
import { useCallback, useMemo, useState } from 'react';
import { type ScanReportExtension } from '@automattic/jetpack-scan';
import ShieldIcon from '../shield-icon/index.js';
import {
FIELD_NAME,
FIELD_VERSION,
FIELD_ICON,
FIELD_STATUS,
FIELD_TYPE,
STATUS_TYPES,
TYPES,
ICONS,
} from './constants.js';
import styles from './styles.module.scss';

/**
* DataViews component for displaying a scan report.
*
* @param {object} props - Component props.
* @param {string} props.dataSource - Data source.
* @param {Array} props.data - Scan report data.
* @param {Function} props.onChangeSelection - Callback function run when an item is selected.
*
* @return {JSX.Element} The ScanReport component.
*/
export default function ScanReport( {
dataSource,
data,
onChangeSelection,
}: {
dataSource: string;
data: ScanReportExtension[];
onChangeSelection: ( selected: string[] ) => void;
} ): JSX.Element {
const baseView = {
search: '',
filters: [],
page: 1,
perPage: 20,
};

/**
* 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: [ FIELD_TYPE, FIELD_NAME, FIELD_VERSION ],
titleField: FIELD_STATUS,
showMedia: false,
},
list: {
...baseView,
fields: [ FIELD_STATUS, FIELD_VERSION, FIELD_TYPE ],
titleField: FIELD_NAME,
mediaField: FIELD_ICON,
showMedia: true,
},
};

/**
* 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,
} );

/**
* 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 iconHeight = 20;
const result: Field< ScanReportExtension >[] = [
{
id: FIELD_STATUS,
elements: STATUS_TYPES,
label: __( 'Status', 'jetpack-scan' ),
enableHiding: false,
getValue( { item } ) {
if ( item.checked ) {
if ( item.threats.length > 0 ) {
return 'threat';
}
return 'checked';
}
return 'unchecked';
},
render( { item }: { item: ScanReportExtension } ) {
const scanApi = 'scan_api' === dataSource;
let variant: 'info' | 'error' | 'success' = 'info';
let text = __(
'This item was added to your site after the most recent scan. We will check for threats during the next scheduled one.',
'jetpack-scan'
);

if ( item.checked ) {
if ( item.threats.length > 0 ) {
variant = 'error';
text = _n(
'Vulnerability detected.',
'Vulnerabilities detected.',
item.threats.length,
'jetpack-scan'
);

if ( scanApi ) {
text = _n(
'Threat detected.',
'Threats detected.',
item.threats.length,
'jetpack-scan'
);
}
} else {
variant = 'success';
text = __(
'No known vulnerabilities found that affect this version.',
'jetpack-scan'
);

if ( scanApi ) {
text = __( 'No known threats found that affect this version.', 'jetpack-scan' );
}
}
}

return (
<Tooltip className={ styles.tooltip } text={ text }>
<div className={ styles.icon }>
<ShieldIcon variant={ variant } height={ iconHeight } />
</div>
</Tooltip>
);
},
},
{
id: FIELD_TYPE,
label: __( 'Type', 'jetpack-scan' ),
elements: TYPES,
enableHiding: false,
},
{
id: FIELD_NAME,
label: __( 'Name', 'jetpack-scan' ),
enableHiding: false,
enableGlobalSearch: true,
getValue( { item }: { item: ScanReportExtension } ) {
return item.name ? item.name : '';
},
},
{
id: FIELD_VERSION,
label: __( 'Version', 'jetpack-scan' ),
enableHiding: false,
enableSorting: false,
enableGlobalSearch: true,
getValue( { item }: { item: ScanReportExtension } ) {
return item.version ? item.version : '';
},
},
...( view.type === 'list'
? [
{
id: FIELD_ICON,
label: __( 'Icon', 'jetpack-scan' ),
enableSorting: false,
enableHiding: false,
getValue( { item }: { item: ScanReportExtension } ) {
return ICONS[ item.type ] || '';
},
render( { item }: { item: ScanReportExtension } ) {
return (
<div className={ styles.threat__media }>
<Icon icon={ ICONS[ item.type ] } />
</div>
);
},
},
]
: [] ),
];

return result;
}, [ view, dataSource ] );

/**
* 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: ScanReportExtension ) => `${ item.type }_${ item.slug }_${ item.version }`,
[]
);

return (
<DataViews
data={ processedData }
defaultLayouts={ defaultLayouts }
fields={ fields }
getItemId={ getItemId }
onChangeSelection={ onChangeSelection }
onChangeView={ onChangeView }
paginationInfo={ paginationInfo }
view={ view }
/>
);
}
Loading
Loading