/******************************************************************************** * Copyright (c) 2026 Contributors to the Eclipse Foundation * * See the NOTICE file(s) distributed with this work for additional * information regarding copyright ownership. * * This program and the accompanying materials are made available under the * terms of the Eclipse Public License 2.0 which is available at * https://www.eclipse.org/legal/epl-2.0 * * SPDX-License-Identifier: EPL-2.0 ********************************************************************************/ import { FunctionComponent, useMemo } from 'react'; import { Dialog, DialogTitle, DialogContent, DialogActions, Button, Typography, List, ListItem, ListItemText, Box, Tooltip } from '@mui/material'; import { Info as InfoIcon } from '@mui/icons-material'; import { useDialogs } from '../../../hooks/scan-admin'; import { useTheme } from '@mui/material/styles'; import { ScanResult } from '../../../context/scan-admin'; /** * Get only the enforced threats from a scan. */ const getEnforcedThreats = (scan: ScanResult) => { return scan.threats?.filter(threat => threat.enforcedFlag) || []; }; /** * Get only the unenforced threats from a scan. */ const getUnenforcedThreats = (scan: ScanResult) => { return scan.threats?.filter(threat => !threat.enforcedFlag) || []; }; /** * Get actionable threats - enforced threats that have file information. * Only these can be added to allow/block lists. */ const getActionableThreats = (scan: ScanResult) => { return scan.threats?.filter(threat => threat.enforcedFlag && threat.fileName) || []; }; /** * Get threats without file information (not actionable for allow/block). * These are from scanners that don't report individual files. */ const getThreatsWithoutFileInfo = (scan: ScanResult) => { return scan.threats?.filter(threat => !threat.fileName) || []; }; /** * Confirmation dialog for extension-level allow/block actions. * Uses the useDialogs hook to consume context. * * Only files from ENFORCED threats are actionable. * Unenforced threats are informational only and * should not have allow/block decisions made on them. */ export const QuarantineDialog: FunctionComponent = () => { const theme = useTheme(); const { confirmDialog } = useDialogs(); // Check if any extension has unenforced threats (to show info message) const hasAnyUnenforcedThreats = useMemo(() => { return confirmDialog.selectedExtensions.some((scan: ScanResult) => getUnenforcedThreats(scan).length > 0); }, [confirmDialog.selectedExtensions]); // Check if any extension has threats without file info (not actionable) const hasThreatsWithoutFileInfo = useMemo(() => { return confirmDialog.selectedExtensions.some((scan: ScanResult) => getThreatsWithoutFileInfo(scan).length > 0); }, [confirmDialog.selectedExtensions]); // Calculate total actionable files across all selected extensions // Only counts enforced threats that have file information const totalActionableFiles = useMemo(() => { return confirmDialog.selectedExtensions.reduce( (total: number, scan: ScanResult) => total + getActionableThreats(scan).length, 0 ); }, [confirmDialog.selectedExtensions]); return ( {confirmDialog.action === 'allow' ? 'Confirm Allow' : 'Confirm Block'} Are you sure you want to {confirmDialog.action} {totalActionableFiles} file{totalActionableFiles !== 1 ? 's' : ''} from {confirmDialog.selectedExtensions.length !== 1 ? 'these' : 'this'} {confirmDialog.selectedExtensions.length} extension{confirmDialog.selectedExtensions.length !== 1 ? 's' : ''}? {/* Info message about unenforced threats */} {hasAnyUnenforcedThreats && ( Some extensions have unenforced threats. These are informational only and will not be added to the {confirmDialog.action === 'allow' ? 'allow' : 'block'} list. )} {/* Info message about threats without file info */} {hasThreatsWithoutFileInfo && ( Some threats do not have file-level information (e.g., from scanners that analyze the whole package). These cannot be added to allow/block lists. )} {confirmDialog.selectedExtensions.map((scan: ScanResult) => { const actionableThreats = getActionableThreats(scan); const enforcedThreats = getEnforcedThreats(scan); const unenforcedThreats = getUnenforcedThreats(scan); return ( {scan.displayName} } secondary={ {scan.namespace}.{scan.extensionName} Publisher: {scan.publisher} Version: {scan.version} } /> 0 ? ( Files to {confirmDialog.action}: {actionableThreats.map((threat, index) => (
{threat.fileName}
))}
) : ( 'No actionable files (no file-level info)' ) } arrow > {actionableThreats.length} file{actionableThreats.length !== 1 ? 's' : ''} {(enforcedThreats.length - actionableThreats.length) > 0 && ( +{enforcedThreats.length - actionableThreats.length} without file info )} {unenforcedThreats.length > 0 && ( +{unenforcedThreats.length} unenforced )}
); })}
); };