diff --git a/MythicReactUI/src/components/App.js b/MythicReactUI/src/components/App.js index f7e34c24..129953e4 100644 --- a/MythicReactUI/src/components/App.js +++ b/MythicReactUI/src/components/App.js @@ -238,6 +238,16 @@ const getModernThemeAdditions = (themeMode, preferences = operatorSettingDefault }, }, }, + MuiDialogContentText: { + styleOverrides: { + root: { + color: textSecondary, + fontSize: "0.88rem", + lineHeight: 1.45, + margin: "0 0 10px", + }, + }, + }, MuiDialogActions: { styleOverrides: { root: { @@ -294,6 +304,28 @@ const getModernThemeAdditions = (themeMode, preferences = operatorSettingDefault }, }, }, + MuiFormControlLabel: { + styleOverrides: { + root: { + marginLeft: 0, + marginRight: 0, + }, + label: { + color: textPrimary, + fontSize: "0.86rem", + }, + }, + }, + MuiFormHelperText: { + styleOverrides: { + root: { + color: textSecondary, + fontSize: "0.74rem", + lineHeight: 1.35, + marginLeft: 0, + }, + }, + }, MuiTabs: { styleOverrides: { root: { @@ -363,7 +395,10 @@ const getModernThemeAdditions = (themeMode, preferences = operatorSettingDefault styleOverrides: { root: { backgroundColor: backgroundPaper, + border: `1px solid ${borderColor}`, borderRadius: 6, + minHeight: 0, + overflow: "auto", }, }, }, @@ -373,6 +408,16 @@ const getModernThemeAdditions = (themeMode, preferences = operatorSettingDefault backgroundColor: backgroundPaper, borderCollapse: "separate", borderSpacing: 0, + width: "100%", + }, + }, + }, + MuiTableHead: { + styleOverrides: { + root: { + "& .MuiTableRow-root": { + backgroundColor: tableHeaderColor, + }, }, }, }, @@ -381,10 +426,17 @@ const getModernThemeAdditions = (themeMode, preferences = operatorSettingDefault root: { borderBottom: `1px solid ${tableBorderSoft}`, color: textPrimary, + fontVariantNumeric: "tabular-nums", fontSize: "0.86rem", lineHeight: 1.35, padding: "6px 10px", verticalAlign: "middle", + "&:first-of-type": { + paddingLeft: 12, + }, + "&:last-of-type": { + paddingRight: 12, + }, }, head: { backgroundColor: tableHeaderColor, @@ -397,6 +449,11 @@ const getModernThemeAdditions = (themeMode, preferences = operatorSettingDefault textTransform: "uppercase", whiteSpace: "nowrap", }, + paddingCheckbox: { + paddingLeft: 6, + paddingRight: 6, + width: 36, + }, stickyHeader: { backgroundColor: tableHeaderColor, zIndex: 3, @@ -406,6 +463,10 @@ const getModernThemeAdditions = (themeMode, preferences = operatorSettingDefault MuiTableRow: { styleOverrides: { root: { + backgroundColor: backgroundPaper, + ".MuiTableBody-root &:nth-of-type(even):not(.Mui-selected):not(.selectedCallback):not(.selectedCallbackHierarchy)": { + backgroundColor: tableRowStripeColor, + }, "&:last-child .MuiTableCell-root": { borderBottom: 0, }, @@ -421,6 +482,24 @@ const getModernThemeAdditions = (themeMode, preferences = operatorSettingDefault }, }, }, + MuiTablePagination: { + styleOverrides: { + root: { + color: textSecondary, + }, + selectLabel: { + color: textSecondary, + fontSize: "0.78rem", + }, + displayedRows: { + color: textSecondary, + fontSize: "0.78rem", + }, + toolbar: { + minHeight: 34, + }, + }, + }, MuiTableSortLabel: { styleOverrides: { root: { @@ -437,6 +516,76 @@ const getModernThemeAdditions = (themeMode, preferences = operatorSettingDefault }, }, }, + MuiPagination: { + styleOverrides: { + root: { + display: "flex", + }, + ul: { + gap: 4, + }, + }, + }, + MuiPaginationItem: { + styleOverrides: { + root: { + minWidth: 28, + height: 28, + borderRadius: 6, + color: textSecondary, + fontSize: "0.78rem", + fontWeight: 650, + "&.Mui-selected": { + backgroundColor: withAlpha(primary, isDark ? "44" : "1F"), + borderColor: primary, + color: textPrimary, + "&:hover": { + backgroundColor: withAlpha(primary, isDark ? "55" : "2B"), + }, + }, + "&:hover": { + backgroundColor: tableRowHoverColor, + color: textPrimary, + }, + }, + outlined: { + borderColor: tableBorderSoft, + }, + icon: { + fontSize: "1rem", + }, + }, + }, + MuiToggleButton: { + styleOverrides: { + root: { + borderColor: tableBorderSoft, + borderRadius: 6, + color: textSecondary, + fontSize: "0.78rem", + fontWeight: 650, + gap: 6, + minHeight: 32, + padding: "5px 9px", + textTransform: "none", + "&:hover": { + backgroundColor: tableRowHoverColor, + color: textPrimary, + }, + "&.Mui-selected": { + backgroundColor: withAlpha(primary, isDark ? "44" : "1F"), + borderColor: primary, + color: textPrimary, + "&:hover": { + backgroundColor: withAlpha(primary, isDark ? "55" : "2B"), + }, + }, + }, + sizeSmall: { + minHeight: 30, + }, + }, + }, MuiDataGrid: { styleOverrides: { root: { @@ -457,7 +606,6 @@ const getModernThemeAdditions = (themeMode, preferences = operatorSettingDefault "& .MuiDataGrid-columnHeaders": { backgroundColor: tableHeaderColor, borderBottom: `1px solid ${borderColor}`, - minHeight: 34, }, "& .MuiDataGrid-columnHeader": { backgroundColor: tableHeaderColor, @@ -476,11 +624,12 @@ const getModernThemeAdditions = (themeMode, preferences = operatorSettingDefault "& .MuiDataGrid-cell": { borderBottom: `1px solid ${tableBorderSoft}`, outline: "none", + fontVariantNumeric: "tabular-nums", }, "& .MuiDataGrid-cell:focus, & .MuiDataGrid-cell:focus-within, & .MuiDataGrid-columnHeader:focus, & .MuiDataGrid-columnHeader:focus-within": { outline: "none", }, - "& .MuiDataGrid-row:nth-of-type(even)": { + "& .MuiDataGrid-row:nth-of-type(even):not(.Mui-selected)": { backgroundColor: tableRowStripeColor, }, "& .MuiDataGrid-row:hover": { @@ -612,7 +761,8 @@ export function App(props) { preferences?.palette?.pageHeader?.light || operatorSettingDefaults.palette.pageHeader.light, }, pageHeaderText: { - main: 'white', + main: themeMode === 'dark' ? preferences?.palette?.text?.dark || operatorSettingDefaults.palette.text.dark : + preferences?.palette?.text?.light || operatorSettingDefaults.palette.text.light, }, topAppBarColor: themeMode === 'dark' ? (preferences?.palette?.navBarColor?.dark || operatorSettingDefaults.palette.navBarColor.dark) : (preferences?.palette?.navBarColor?.light || operatorSettingDefaults.palette.navBarColor.light), @@ -723,7 +873,8 @@ export function App(props) { operatorSettingDefaults.palette.pageHeader.light, }, pageHeaderText: { - main: 'white', + main: themeMode === 'dark' ? operatorSettingDefaults.palette.text.dark : + operatorSettingDefaults.palette.text.light, }, topAppBarColor: themeMode === 'dark' ? operatorSettingDefaults.palette.navBarColor.dark : operatorSettingDefaults.palette.navBarColor.light, diff --git a/MythicReactUI/src/components/MythicComponents/MythicConfirmDialog.js b/MythicReactUI/src/components/MythicComponents/MythicConfirmDialog.js index a9169cb2..59fbdc6c 100644 --- a/MythicReactUI/src/components/MythicComponents/MythicConfirmDialog.js +++ b/MythicReactUI/src/components/MythicComponents/MythicConfirmDialog.js @@ -1,10 +1,9 @@ import React from 'react'; -import Button from '@mui/material/Button'; -import DialogActions from '@mui/material/DialogActions'; import DialogTitle from '@mui/material/DialogTitle'; import {MythicDialog} from './MythicDialog'; import DialogContentText from '@mui/material/DialogContentText'; import DialogContent from '@mui/material/DialogContent'; +import {MythicDialogButton, MythicDialogFooter} from "./MythicDialogLayout"; export function MythicConfirmDialog(props) { const handleSubmit = () => { @@ -14,6 +13,10 @@ export function MythicConfirmDialog(props) { } props.onClose(); } + const acceptIntent = props.acceptColor === "success" ? "primary" : + props.acceptColor === "warning" ? "warning" : + props.acceptColor === "info" ? "info" : + "destructive"; return ( {props.onClose()}} innerDialog={ @@ -26,14 +29,14 @@ export function MythicConfirmDialog(props) { )} - - - - + + } /> ); diff --git a/MythicReactUI/src/components/MythicComponents/MythicDataGrid.js b/MythicReactUI/src/components/MythicComponents/MythicDataGrid.js index e8f2638f..b54588a8 100644 --- a/MythicReactUI/src/components/MythicComponents/MythicDataGrid.js +++ b/MythicReactUI/src/components/MythicComponents/MythicDataGrid.js @@ -1,8 +1,36 @@ import React from 'react'; import Box from '@mui/material/Box'; import { DataGrid } from '@mui/x-data-grid'; +import {MythicLoadingState, MythicSearchEmptyState} from "./MythicStateDisplay"; -export const MythicDataGrid = ({containerSx = {}, sx = {}, density = "compact", autoPageSize = true, pageSizeOptions = [10, 25, 50, 100], ...props}) => { +const DefaultNoRowsOverlay = () => ( + +); + +const DefaultLoadingOverlay = () => ( + +); + +export const MythicDataGrid = ({ + containerSx = {}, + sx = {}, + slots = {}, + density = "compact", + autoPageSize = true, + pageSizeOptions = [10, 25, 50, 100], + columnHeaderHeight = 34, + rowHeight = 34, + ...props +}) => { return ( { + if(!props.open){ + return undefined; + } + mythicDialogStack = mythicDialogStack.filter((id) => id !== dialogIdRef.current); + mythicDialogStack.push(dialogIdRef.current); + return () => { + mythicDialogStack = mythicDialogStack.filter((id) => id !== dialogIdRef.current); + }; + }, [props.open]); + const isTopDialog = () => { + return mythicDialogStack[mythicDialogStack.length - 1] === dialogIdRef.current; + }; React.useEffect(() => { if (props.open) { const { current: descriptionElement } = descriptionElementRef; @@ -50,20 +69,30 @@ export function MythicDialog(props) { } }, [props.open]); const dialogOnClick = (e) => { - //e.preventDefault(); - //e.stopPropagation(); - if(e.target.classList.length > 0 && e.target.classList.contains("MuiDialog-container")){ - props.onClose(); + if(e.target.classList?.contains("MuiDialog-container")){ + e.stopPropagation(); + if(!isTopDialog()){ + return; + } + if(draggedState.hideBackdrop){ + props.onClose?.(); + } } } const dialogOnContextMenu = (e) => { e.stopPropagation(); } const handleOnClose = (event, reason) => { + if(reason === "backdropClick" || reason === "escapeKeyDown"){ + event?.stopPropagation?.(); + if(!isTopDialog()){ + return; + } + } if(reason === "backdropClick" && draggedState.hideBackdrop){ return; } - props.onClose(); + props.onClose?.(); } const onStart = (e) => { if(e){ @@ -110,7 +139,6 @@ export function MythicDialog(props) { { (props.onClose || props.onSubmit) && - + {props.onClose && - + } {props.onSubmit && - + } - + } ); @@ -345,11 +373,11 @@ export function MythicViewJSONAsTableDialog(props) { - - - + + ); } @@ -393,11 +421,11 @@ export function MythicViewObjectPropertiesAsTableDialog(props) { - - - + + ); } diff --git a/MythicReactUI/src/components/MythicComponents/MythicDialogLayout.js b/MythicReactUI/src/components/MythicComponents/MythicDialogLayout.js new file mode 100644 index 00000000..5c376f4f --- /dev/null +++ b/MythicReactUI/src/components/MythicComponents/MythicDialogLayout.js @@ -0,0 +1,168 @@ +import React from 'react'; +import Box from '@mui/material/Box'; +import Button from '@mui/material/Button'; +import DialogActions from '@mui/material/DialogActions'; +import Typography from '@mui/material/Typography'; + +const joinClasses = (...classes) => classes.filter(Boolean).join(" "); + +export function MythicDialogBody({children, className, compact = false, ...props}) { + return ( + + {children} + + ); +} + +export function MythicDialogSection({title, description, actions, children, className, ...props}) { + return ( + + {(title || description || actions) && + + + {title && + + {title} + + } + {description && + + {description} + + } + + {actions && + + {actions} + + } + + } + {children} + + ); +} + +export function MythicDialogGrid({children, className, minWidth = "16rem", sx = {}, ...props}) { + return ( + + {children} + + ); +} + +export function MythicDialogChoiceRow({children, className, ...props}) { + return ( + + {children} + + ); +} + +export function MythicDialogChoiceDivider({children = "OR", className, ...props}) { + return ( + + {children} + + ); +} + +export function MythicDialogFooter({children, className, ...props}) { + return ( + + {children} + + ); +} + +export function MythicDialogButton({children, className, intent = "secondary", ...props}) { + return ( + + ); +} + +export function MythicForm({children, className, ...props}) { + return ( + + {children} + + ); +} + +export function MythicFormGrid({children, className, minWidth = "16rem", sx = {}, ...props}) { + return ( + + {children} + + ); +} + +export function MythicFormField({children, className, description, label, required = false, ...props}) { + return ( + + {(label || description) && + + {label && + + {label}{required && *} + + } + {description && + + {description} + + } + + } + + {children} + + + ); +} + +export function MythicFormNote({children, className, ...props}) { + return ( + + {children} + + ); +} + +export function MythicFormSwitchRow({control, label, description, className, ...props}) { + return ( + + + + {label} + + {description && + + {description} + + } + + + {control} + + + ); +} diff --git a/MythicReactUI/src/components/MythicComponents/MythicPageHeader.js b/MythicReactUI/src/components/MythicComponents/MythicPageHeader.js index f2c44a73..cd931c8e 100644 --- a/MythicReactUI/src/components/MythicComponents/MythicPageHeader.js +++ b/MythicReactUI/src/components/MythicComponents/MythicPageHeader.js @@ -1,42 +1,281 @@ -import {useTheme} from '@mui/material/styles'; +import Box from '@mui/material/Box'; +import Chip from '@mui/material/Chip'; import Paper from '@mui/material/Paper'; -import {Typography} from "@mui/material"; +import Stack from '@mui/material/Stack'; +import Typography from '@mui/material/Typography'; +import {alpha, useTheme} from '@mui/material/styles'; -export const MythicPageHeader = ({children, title, style={}, headerVariant="h6"}) => { +export const MythicPageHeader = ({ + actions, + children, + className = "", + dense = false, + icon, + meta, + style = {}, + subtitle, + sx = {}, + title, + headerVariant = "h6", +}) => { const theme = useTheme(); - const headerTextColor = theme.palette.getContrastText(theme.pageHeader.main); + const headerTextColor = theme.pageHeaderText?.main || theme.palette.text.primary; + const supportingTextColor = alpha(headerTextColor, 0.78); + const hasActionContent = actions || children; + const actionSurface = alpha(headerTextColor, 0.1); + const actionSurfaceHover = alpha(headerTextColor, 0.16); + const actionSurfaceActive = alpha(headerTextColor, 0.23); + const actionBorder = alpha(headerTextColor, 0.24); + const actionBorderHover = alpha(headerTextColor, 0.42); + const getIntentHoverStyles = (paletteName) => { + const paletteColor = theme.palette[paletteName]?.main || headerTextColor; + return { + backgroundColor: `${alpha(paletteColor, theme.palette.mode === "dark" ? 0.28 : 0.16)} !important`, + borderColor: `${alpha(paletteColor, theme.palette.mode === "dark" ? 0.78 : 0.55)} !important`, + color: `${paletteColor} !important`, + }; + }; return ( - - - {title} - -
- {children} -
+ display: "flex", + flex: "0 0 auto", + flexDirection: {xs: "column", md: "row"}, + flexWrap: {xs: "nowrap", md: "wrap"}, + gap: dense ? 0.75 : 1, + height: "auto", + justifyContent: "space-between", + maxHeight: "none", + minHeight: dense ? 44 : 58, + minWidth: 0, + overflow: "visible", + p: dense ? "0.5rem 0.75rem" : "0.7rem 0.85rem", + width: "100%", + "& .MuiButton-root, & .MuiToggleButton-root": { + alignItems: "center", + backgroundColor: `${actionSurface} !important`, + border: `1px solid ${actionBorder} !important`, + borderRadius: `${theme.shape.borderRadius}px`, + boxShadow: "none !important", + color: `${headerTextColor} !important`, + fontSize: "0.76rem", + fontWeight: 750, + gap: 0.35, + justifyContent: "center", + letterSpacing: 0, + lineHeight: 1.2, + minHeight: 32, + minWidth: {xs: "auto", sm: "7rem"}, + px: 1.05, + textTransform: "none", + whiteSpace: "nowrap", + "&:hover": { + backgroundColor: `${actionSurfaceHover} !important`, + borderColor: `${actionBorderHover} !important`, + boxShadow: `inset 0 1px 0 ${alpha(headerTextColor, 0.12)}`, + }, + }, + "& .MuiButton-root.Mui-focusVisible, & .MuiToggleButton-root.Mui-focusVisible": { + outline: `2px solid ${alpha(headerTextColor, 0.38)}`, + outlineOffset: 2, + }, + "& .MuiButton-root.Mui-disabled, & .MuiToggleButton-root.Mui-disabled": { + backgroundColor: `${alpha(headerTextColor, 0.04)} !important`, + borderColor: `${alpha(headerTextColor, 0.1)} !important`, + color: `${alpha(headerTextColor, 0.38)} !important`, + }, + "& .MuiToggleButton-root.Mui-selected": { + backgroundColor: `${actionSurfaceActive} !important`, + borderColor: `${alpha(headerTextColor, 0.48)} !important`, + color: `${headerTextColor} !important`, + boxShadow: `inset 0 1px 0 ${alpha(headerTextColor, 0.16)}`, + "&:hover": { + backgroundColor: `${alpha(headerTextColor, 0.28)} !important`, + }, + }, + "& .MuiButton-startIcon, & .MuiButton-endIcon": { + color: "inherit", + marginLeft: 0, + marginRight: 0, + }, + "& .MuiButton-startIcon": { + paddingRight: "0.25rem", + }, + "& .MuiButton-endIcon": { + paddingLeft: "0.25rem", + }, + "& .MuiIconButton-root": { + backgroundColor: `${actionSurface} !important`, + border: `1px solid ${actionBorder} !important`, + borderRadius: `${theme.shape.borderRadius}px`, + color: `${headerTextColor} !important`, + height: 32, + width: 32, + "&:hover": { + backgroundColor: `${actionSurfaceHover} !important`, + borderColor: `${actionBorderHover} !important`, + }, + "&.Mui-disabled": { + backgroundColor: `${alpha(headerTextColor, 0.04)} !important`, + borderColor: `${alpha(headerTextColor, 0.1)} !important`, + color: `${alpha(headerTextColor, 0.38)} !important`, + }, + }, + "& .MuiButton-root.mythic-table-row-action-hover-info:hover, & .MuiToggleButton-root.mythic-table-row-action-hover-info:hover, & .MuiIconButton-root.mythic-table-row-icon-action-hover-info:hover": getIntentHoverStyles("info"), + "& .MuiButton-root.mythic-table-row-action-hover-success:hover, & .MuiToggleButton-root.mythic-table-row-action-hover-success:hover, & .MuiIconButton-root.mythic-table-row-icon-action-hover-success:hover": getIntentHoverStyles("success"), + "& .MuiButton-root.mythic-table-row-action-hover-warning:hover, & .MuiToggleButton-root.mythic-table-row-action-hover-warning:hover, & .MuiIconButton-root.mythic-table-row-icon-action-hover-warning:hover": getIntentHoverStyles("warning"), + "& .MuiButton-root.mythic-table-row-action-hover-danger:hover, & .MuiToggleButton-root.mythic-table-row-action-hover-danger:hover, & .MuiIconButton-root.mythic-table-row-icon-action-hover-danger:hover": getIntentHoverStyles("error"), + ...sx, + }} + > + + {icon && + + {icon} + + } + + + {title} + + {subtitle && + + {subtitle} + + } + {meta && + + {meta} + + } + + + {hasActionContent && + + {actions} + {children} + + }
- ) - + ); } + +export const MythicPageHeaderChip = ({status, sx = {}, ...props}) => { + const theme = useTheme(); + const headerTextColor = theme.pageHeaderText?.main || theme.palette.text.primary; + const statusColor = status ? theme.palette[status]?.main : null; + const chipColor = statusColor || alpha(headerTextColor, 0.88); + return ( + + ); +}; + +export const MythicSectionHeader = ({ + actions, + dense = false, + subtitle, + sx = {}, + title, +}) => { + const theme = useTheme(); + const headerTextColor = theme.pageHeaderText?.main || theme.palette.text.primary; + const sectionAccentColor = theme.palette.primary.main; + const sectionOverlayStart = alpha(sectionAccentColor, theme.palette.mode === "dark" ? 0.28 : 0.18); + const sectionOverlayMiddle = alpha(sectionAccentColor, theme.palette.mode === "dark" ? 0.12 : 0.08); + const sectionOverlayEnd = alpha(headerTextColor, theme.palette.mode === "dark" ? 0.055 : 0.04); + return ( + + ); +}; diff --git a/MythicReactUI/src/components/MythicComponents/MythicResizableGrid/Cell.js b/MythicReactUI/src/components/MythicComponents/MythicResizableGrid/Cell.js index 64e138a9..a0625953 100644 --- a/MythicReactUI/src/components/MythicComponents/MythicResizableGrid/Cell.js +++ b/MythicReactUI/src/components/MythicComponents/MythicResizableGrid/Cell.js @@ -126,6 +126,7 @@ export const ContextMenu = ({openContextMenu, dropdownAnchorRef, contextMenuOpti handleMenuItemClick(event, option.click)} > {option.icon}{option.name} @@ -136,11 +137,12 @@ export const ContextMenu = ({openContextMenu, dropdownAnchorRef, contextMenuOpti disabled={option.disabled} menu={ option.menuItems.map((menuOption, indx) => ( - handleMenuItemClick(event, menuOption.click)} - > + handleMenuItemClick(event, menuOption.click)} + > {menuOption.icon}{menuOption.name} )) @@ -183,4 +185,4 @@ export default Cell; )} - */ \ No newline at end of file + */ diff --git a/MythicReactUI/src/components/MythicComponents/MythicStateDisplay.js b/MythicReactUI/src/components/MythicComponents/MythicStateDisplay.js new file mode 100644 index 00000000..eb50a842 --- /dev/null +++ b/MythicReactUI/src/components/MythicComponents/MythicStateDisplay.js @@ -0,0 +1,126 @@ +import React from 'react'; +import Box from '@mui/material/Box'; +import Button from '@mui/material/Button'; +import CircularProgress from '@mui/material/CircularProgress'; +import TableCell from '@mui/material/TableCell'; +import TableRow from '@mui/material/TableRow'; +import Typography from '@mui/material/Typography'; +import {alpha, useTheme} from '@mui/material/styles'; +import ErrorOutlineOutlinedIcon from '@mui/icons-material/ErrorOutlineOutlined'; +import HourglassEmptyOutlinedIcon from '@mui/icons-material/HourglassEmptyOutlined'; +import InboxOutlinedIcon from '@mui/icons-material/InboxOutlined'; +import SearchOffOutlinedIcon from '@mui/icons-material/SearchOffOutlined'; + +const stateIcons = { + empty: , + error: , + loading: , + search: , +}; + +export function MythicStateDisplay({ + action, + actionLabel, + compact = false, + description, + icon, + loading = false, + minHeight, + onAction, + severity = "empty", + sx = {}, + title, +}) { + const theme = useTheme(); + const defaultIcon = stateIcons[severity] || stateIcons.empty; + const iconNode = loading ? ( + + ) : React.cloneElement(icon || defaultIcon, {fontSize: compact ? "small" : "medium"}); + const resolvedTitle = title || (loading ? "Loading" : "Nothing to show"); + const resolvedMinHeight = minHeight || (compact ? 112 : 176); + return ( + + + + {iconNode} + + + {resolvedTitle} + + {description && + + {description} + + } + {action || (actionLabel && onAction) ? ( + action || ( + + ) + ) : null} + + + ); +} + +export function MythicEmptyState(props) { + return ; +} + +export function MythicSearchEmptyState(props) { + return ; +} + +export function MythicLoadingState(props) { + return ; +} + +export function MythicErrorState(props) { + return ; +} + +export function MythicTableEmptyState({colSpan, ...props}) { + return ( + + + + + + ); +} diff --git a/MythicReactUI/src/components/MythicComponents/MythicStatusChip.js b/MythicReactUI/src/components/MythicComponents/MythicStatusChip.js new file mode 100644 index 00000000..7cfa1444 --- /dev/null +++ b/MythicReactUI/src/components/MythicComponents/MythicStatusChip.js @@ -0,0 +1,111 @@ +import React from 'react'; +import Chip from '@mui/material/Chip'; +import {alpha, useTheme} from '@mui/material/styles'; +import CheckCircleOutlineIcon from '@mui/icons-material/CheckCircleOutline'; +import ErrorOutlineIcon from '@mui/icons-material/ErrorOutline'; +import WarningAmberIcon from '@mui/icons-material/WarningAmber'; +import InfoOutlinedIcon from '@mui/icons-material/InfoOutlined'; +import RadioButtonCheckedIcon from '@mui/icons-material/RadioButtonChecked'; +import RadioButtonUncheckedIcon from '@mui/icons-material/RadioButtonUnchecked'; +import DeleteOutlineIcon from '@mui/icons-material/DeleteOutline'; +import LockOutlinedIcon from '@mui/icons-material/LockOutlined'; +import TimelapseIcon from '@mui/icons-material/Timelapse'; +import DoneAllIcon from '@mui/icons-material/DoneAll'; +import BlockIcon from '@mui/icons-material/Block'; +import HideSourceIcon from '@mui/icons-material/HideSource'; + +const statusConfig = { + success: {palette: "success", icon: , label: "Success"}, + error: {palette: "error", icon: , label: "Error"}, + warning: {palette: "warning", icon: , label: "Warning"}, + info: {palette: "info", icon: , label: "Info"}, + active: {palette: "success", icon: , label: "Active"}, + inactive: {palette: "error", icon: , label: "Inactive"}, + deleted: {palette: "error", icon: , label: "Deleted"}, + locked: {palette: "warning", icon: , label: "Locked"}, + building: {palette: "info", icon: , label: "Building"}, + completed: {palette: "success", icon: , label: "Completed"}, + blocked: {palette: "error", icon: , label: "Blocked"}, + skipped: {palette: "secondary", icon: , label: "Skipped"}, + neutral: {palette: "secondary", icon: , label: "Status"}, +}; + +export function getMythicStatusConfig(status) { + return statusConfig[status] || statusConfig.neutral; +} + +export function getMythicStatusFromTaskStatus(status) { + const normalized = `${status || ""}`.toLowerCase(); + if(normalized.includes("error") || normalized.includes("failed")){ + return "error"; + } + if(normalized.includes("warning")){ + return "warning"; + } + if(normalized.includes("success") || normalized.includes("completed")){ + return "success"; + } + if(normalized.includes("processing") || normalized.includes("building")){ + return "building"; + } + return "info"; +} + +export function MythicStatusChip({ + label, + status = "neutral", + icon, + showIcon = true, + size = "small", + variant = "soft", + sx = {}, + ...props +}) { + const theme = useTheme(); + const config = getMythicStatusConfig(status); + const paletteColor = theme.palette[config.palette]?.main || theme.palette.text.secondary; + const textColor = theme.palette.mode === "dark" ? theme.palette.text.primary : paletteColor; + const backgroundColor = variant === "outlined" ? "transparent" : alpha(paletteColor, theme.palette.mode === "dark" ? 0.18 : 0.11); + const borderColor = alpha(paletteColor, theme.palette.mode === "dark" ? 0.55 : 0.35); + const chipIcon = showIcon ? (icon || config.icon) : undefined; + + return ( + + ); +} diff --git a/MythicReactUI/src/components/MythicComponents/MythicTabPanel.js b/MythicReactUI/src/components/MythicComponents/MythicTabPanel.js index fea86bde..750b632f 100644 --- a/MythicReactUI/src/components/MythicComponents/MythicTabPanel.js +++ b/MythicReactUI/src/components/MythicComponents/MythicTabPanel.js @@ -23,6 +23,7 @@ export function MythicTabPanel(props) { display: value === index ? 'flex' : 'none', flexDirection: 'column', flexGrow: 1, + minHeight: 0, width: '100%', maxWidth: '100%', overflowY: "auto", diff --git a/MythicReactUI/src/components/MythicComponents/MythicTablePagination.js b/MythicReactUI/src/components/MythicComponents/MythicTablePagination.js new file mode 100644 index 00000000..217fee56 --- /dev/null +++ b/MythicReactUI/src/components/MythicComponents/MythicTablePagination.js @@ -0,0 +1,42 @@ +import React from 'react'; +import Box from '@mui/material/Box'; +import Pagination from '@mui/material/Pagination'; +import Typography from '@mui/material/Typography'; + +export const MythicTablePagination = ({ + count, + totalCount, + fetchLimit, + onChange, + page, + color = "primary", + label = "Total Results", + className = "", + containerStyle = {}, + paginationStyle = {}, + ...paginationProps +}) => { + const pageCount = count ?? Math.ceil((totalCount || 0) / (fetchLimit || 1)); + const controlledPage = page === undefined ? {} : {page}; + + return ( + + + {totalCount !== undefined && ( + {label}: {totalCount} + )} + + ); +}; diff --git a/MythicReactUI/src/components/MythicComponents/MythicTableToolbar.js b/MythicReactUI/src/components/MythicComponents/MythicTableToolbar.js new file mode 100644 index 00000000..d88fe0f0 --- /dev/null +++ b/MythicReactUI/src/components/MythicComponents/MythicTableToolbar.js @@ -0,0 +1,126 @@ +import React from 'react'; +import Box from '@mui/material/Box'; +import Button from '@mui/material/Button'; +import IconButton from '@mui/material/IconButton'; +import MenuItem from '@mui/material/MenuItem'; +import Select from '@mui/material/Select'; +import ToggleButton from '@mui/material/ToggleButton'; +import Tooltip from '@mui/material/Tooltip'; +import SearchIcon from '@mui/icons-material/Search'; +import MythicTextField from './MythicTextField'; + +export const MythicTableToolbar = ({children, className = "", style = {}}) => { + return ( + + {children} + + ); +}; + +export const MythicTableToolbarGroup = ({children, grow = false, className = "", style = {}}) => { + return ( + + {children} + + ); +}; + +export const MythicToolbarSelect = ({children, className = "", style = {}, ...props}) => { + return ( + + ); +}; + +export const MythicToolbarMenuItem = MenuItem; + +export const MythicSearchField = ({ + value, + onChange, + onEnter, + onSearch, + disabled = false, + placeholder = "Search...", + name = "Search", + showLabel = false, + autoFocus, + inputProps = {}, + endAdornment = null, +}) => { + return ( + + {endAdornment} + + + + + + + + + ) : endAdornment, + style: {padding: 0}, + ...inputProps, + }} + /> + ); +}; + +export const MythicToolbarButton = ({children, className = "", ...props}) => { + return ( + + ); +}; + +export const MythicToolbarToggle = ({ + checked, + onClick, + label, + activeLabel, + inactiveLabel, + activeIcon, + inactiveIcon, + value = "toggle", + className = "", + ...props +}) => { + return ( + + {checked ? activeIcon : inactiveIcon} + {checked ? (activeLabel || label) : (inactiveLabel || label)} + + ); +}; diff --git a/MythicReactUI/src/components/MythicComponents/MythicTag.js b/MythicReactUI/src/components/MythicComponents/MythicTag.js index cdef7507..0beac342 100644 --- a/MythicReactUI/src/components/MythicComponents/MythicTag.js +++ b/MythicReactUI/src/components/MythicComponents/MythicTag.js @@ -530,7 +530,7 @@ return ( ) )} {selectedTag.id && - {setOpenDeleteDialog(true);}} color="error" variant="contained"> + {setOpenDeleteDialog(true);}}> } {openDelete && {setOpenDeleteDialog(false);}} onSubmit={onAcceptDelete} open={openDelete}/> @@ -752,4 +752,4 @@ export const ViewEditTags = ({target_object, target_object_id}) => { ) -} \ No newline at end of file +} diff --git a/MythicReactUI/src/components/MythicComponents/MythicTransferList.js b/MythicReactUI/src/components/MythicComponents/MythicTransferList.js index 39e97ed5..a7a01afd 100644 --- a/MythicReactUI/src/components/MythicComponents/MythicTransferList.js +++ b/MythicReactUI/src/components/MythicComponents/MythicTransferList.js @@ -9,8 +9,6 @@ import ListItem from '@mui/material/ListItem'; import ListItemIcon from '@mui/material/ListItemIcon'; import ListItemText from '@mui/material/ListItemText'; import Checkbox from '@mui/material/Checkbox'; -import { CardContent } from '@mui/material'; -import CardHeader from '@mui/material/CardHeader'; import Divider from '@mui/material/Divider'; const PREFIX = 'MythicTransferList'; @@ -61,10 +59,9 @@ const CustomListElement = ({value, onClick, itemKey}) => { const CustomList = ({title, items, left, onClick, itemKey}) => { return ( - <> - - - +
+
{title}
+
{items.map((value, index) => (
@@ -80,8 +77,8 @@ const CustomList = ({title, items, left, onClick, itemKey}) => { ))} - - +
+
); } const CustomTransferList = ({leftTitle, rightTitle, initialData, parentLeftData, parentRightData, itemKey}) => { @@ -148,7 +145,7 @@ const CustomTransferList = ({leftTitle, rightTitle, initialData, parentLeftData,
-
+
); } - diff --git a/MythicReactUI/src/components/TopAppBarVertical.js b/MythicReactUI/src/components/TopAppBarVertical.js index a7d587cf..5d7ee1c8 100644 --- a/MythicReactUI/src/components/TopAppBarVertical.js +++ b/MythicReactUI/src/components/TopAppBarVertical.js @@ -57,17 +57,11 @@ import AddCircleIcon from '@mui/icons-material/AddCircle'; import ManageSearchIcon from '@mui/icons-material/ManageSearch'; import MoreHorizIcon from '@mui/icons-material/MoreHoriz'; import Button from '@mui/material/Button'; -import DialogActions from '@mui/material/DialogActions'; +import DialogContent from '@mui/material/DialogContent'; import DialogTitle from '@mui/material/DialogTitle'; -import TableRow from '@mui/material/TableRow'; -import Table from '@mui/material/Table'; -import TableBody from '@mui/material/TableBody'; -import {TableContainer, TableHead, Paper} from '@mui/material'; import Select from '@mui/material/Select'; -import Input from '@mui/material/Input'; import DeleteIcon from '@mui/icons-material/Delete'; import DragHandleIcon from '@mui/icons-material/DragHandle'; -import MythicStyledTableCell from "./MythicComponents/MythicTableCell"; import {snackActions} from "./utilities/Snackbar"; import {Dropdown, DropdownMenuItem} from "./MythicComponents/MythicNestedMenus"; import ClickAwayListener from '@mui/material/ClickAwayListener'; @@ -80,6 +74,12 @@ import {reorder} from "./MythicComponents/MythicDraggableList"; import { useNavigate } from 'react-router-dom'; import LogoutIcon from '@mui/icons-material/Logout'; import TuneIcon from '@mui/icons-material/Tune'; +import { + MythicDialogBody, + MythicDialogButton, + MythicDialogFooter, + MythicDialogSection, +} from "./MythicComponents/MythicDialogLayout"; const PREFIX = 'TopAppBarVertical'; @@ -97,13 +97,13 @@ const openedMixin = (theme) => ({ }); const closedMixin = (theme) => ({ overflowX: 'hidden', - width: `calc(${theme.spacing(4)} + 1px)`, + width: "60px", borderRadius: "0 !important", border: "0px !important", background: `${theme.navigation.background} !important`, backgroundColor: `${theme.navigation.backgroundColor} !important`, [theme.breakpoints.up('sm')]: { - width: `calc(${theme.spacing(5)} + 1px)`, + width: "60px", borderRadius: "0 !important", border: "0px !important", }, @@ -129,6 +129,9 @@ const Drawer = styled(MuiDrawer, { shouldForwardProp: (prop) => prop !== 'open' }, [`& .${classes.listSubHeader}`]: { backgroundColor: "transparent !important", + boxSizing: "border-box", + maxWidth: "calc(100% - 8px)", + width: "auto", }, [`& .${classes.listSubHeader}:hover`]: { color: `${theme.navigation.text} !important`, @@ -151,7 +154,7 @@ const Drawer = styled(MuiDrawer, { shouldForwardProp: (prop) => prop !== 'open' backgroundColor: "transparent !important", border: "0 !important", borderRadius: "0 !important", - } + }, }, }, }, @@ -171,7 +174,18 @@ const Drawer = styled(MuiDrawer, { shouldForwardProp: (prop) => prop !== 'open' backgroundColor: "transparent !important", border: "0 !important", borderRadius: "0 !important", - } + }, + '& .MuiListItemText-root': { + display: "none", + margin: "0 !important", + maxWidth: 0, + minWidth: 0, + overflow: "hidden", + width: 0, + }, + '& .MuiListItem-root > .MuiSvgIcon-root': { + display: "none", + }, }, }, }, @@ -189,6 +203,13 @@ export const StyledListItem = styled(ListItem)( borderRadius: theme.shape.borderRadius, color: theme.navigation.text, backgroundColor: "transparent !important", + boxSizing: "border-box", + maxWidth: "calc(100% - 8px)", + width: "auto", + "& .MuiListItemText-root": { + marginLeft: "0.35rem", + minWidth: 0, + }, "& .MuiListItemText-primary": { fontSize: theme.typography.pxToRem(12.5), fontWeight: 600, @@ -203,10 +224,12 @@ export const StyledListItemIcon = styled(ListItemIcon)( paddingTop:0, marginTop: 0, paddingBottom: 0, - minWidth: "32px", + minWidth: "36px", + width: "36px", justifyContent: "center", color: theme.navigation.icon, backgroundColor: "transparent !important", + overflow: "visible", }), ); @@ -569,10 +592,9 @@ const AllSettingOptions = [ ].sort(); const TopAppBarVerticalAdjustShortcutsDialog = ({onClose}) => { - const theme = useTheme(); const sideShortcuts = useGetMythicSetting({setting_name: "sideShortcuts", default_value: defaultShortcuts}) const [currentShortcuts, setCurrentShortcuts] = React.useState(sideShortcuts); - const [updateSetting, _] = useSetMythicSetting(); + const [updateSetting] = useSetMythicSetting(); const reset = () => { setCurrentShortcuts(defaultShortcuts); } @@ -611,75 +633,84 @@ const TopAppBarVerticalAdjustShortcutsDialog = ({onClose}) => { return ( Configure Side Shortcuts -
- - + + + } + sx={{display: "flex", flexDirection: "column", flex: "1 1 auto", minHeight: 0}} > - Shortcut - - - - - - - - - - - - {(provided) => ( - +
{currentShortcuts.map((c, i) => ( - + {(provided2, snapshot) => ( - - - - - - removeShortcut(i)}> - - - - + + + +
} > {AllSettingOptions.map((opt) => ( {opt} ))} - - )} +
+
+ removeShortcut(i)} + > + + +
+
+ )} ))} -
)} + {provided.placeholder} + + )}
-
-
-
- - - - - + + + Save + +
) } @@ -796,17 +827,27 @@ export function TopAppBarVertical(props) { - - - navigate('/new')} width={"40px"} height={"35px"} alt="Mythic" style={{cursor: "pointer"}}/> - + + + navigate('/new')} width={"32px"} height={"28px"} alt="Mythic" style={{cursor: "pointer"}}/> + Mythic: v{serverVersion}
UI: v{mythicUIVersion}
- + {theme.palette.mode === 'light' && } @@ -1051,10 +1092,10 @@ function TopBarRightShortcutsVertical({me, menuOpen, serverName}){ - + @@ -1070,10 +1111,10 @@ function TopBarRightShortcutsVertical({me, menuOpen, serverName}){ - + ) diff --git a/MythicReactUI/src/components/pages/BrowserScripts/BrowserScriptsTable.js b/MythicReactUI/src/components/pages/BrowserScripts/BrowserScriptsTable.js index 836b7e81..bafb71d7 100644 --- a/MythicReactUI/src/components/pages/BrowserScripts/BrowserScriptsTable.js +++ b/MythicReactUI/src/components/pages/BrowserScripts/BrowserScriptsTable.js @@ -5,35 +5,46 @@ import TableCell from '@mui/material/TableCell'; import TableContainer from '@mui/material/TableContainer'; import TableHead from '@mui/material/TableHead'; import TableRow from '@mui/material/TableRow'; -import Paper from '@mui/material/Paper'; -import Typography from '@mui/material/Typography'; import { BrowserScriptsTableRow } from './BrowserScriptsTableRow'; -import {useTheme} from '@mui/material/styles'; import AddCircleIcon from '@mui/icons-material/AddCircle'; -import Button from '@mui/material/Button'; import { MythicDialog } from '../../MythicComponents/MythicDialog'; import {EditScriptDialog} from './EditScriptDialog'; -import {MythicPageHeader} from "../../MythicComponents/MythicPageHeader"; +import {MythicPageHeader, MythicPageHeaderChip} from "../../MythicComponents/MythicPageHeader"; +import {MythicToolbarButton} from "../../MythicComponents/MythicTableToolbar"; +import {MythicTableEmptyState} from "../../MythicComponents/MythicStateDisplay"; export function BrowserScriptsTable(props){ - const theme = useTheme(); const [openNewScriptDialog, setOpenNewScriptDialog] = React.useState(false); + const scriptCountLabel = props.browserscripts.length === 1 ? "1 script" : `${props.browserscripts.length} scripts`; + const activeCount = props.browserscripts.filter((script) => script.active).length; + const activeCountLabel = activeCount === 1 ? "1 active" : `${activeCount} active`; + const modifiedCount = props.browserscripts.filter((script) => script.user_modified).length; return ( <> - - - {openNewScriptDialog && - {setOpenNewScriptDialog(false);}} - innerDialog={ - {setOpenNewScriptDialog(false);}} title="Create New Browser Script" new={true} onSubmitEdit={props.onSubmitNew} /> - } /> + + + + {modifiedCount > 0 && } + } - + actions={ + setOpenNewScriptDialog(true)} startIcon={}> + Script + + } + /> + {openNewScriptDialog && + {setOpenNewScriptDialog(false);}} + innerDialog={ + {setOpenNewScriptDialog(false);}} title="Create New Browser Script" new={true} onSubmitEdit={props.onSubmitNew} /> + } /> + } @@ -47,6 +58,14 @@ export function BrowserScriptsTable(props){ + {props.browserscripts.length === 0 && + + } {props.browserscripts.map( (op) => ( - {setOpenEdit(true);}} color="info" variant="contained"> + {setOpenEdit(true);}}> + + ) } - diff --git a/MythicReactUI/src/components/pages/Callbacks/AddRemoveCallbackCommandsDialog.js b/MythicReactUI/src/components/pages/Callbacks/AddRemoveCallbackCommandsDialog.js index cd1591fd..b73c8879 100644 --- a/MythicReactUI/src/components/pages/Callbacks/AddRemoveCallbackCommandsDialog.js +++ b/MythicReactUI/src/components/pages/Callbacks/AddRemoveCallbackCommandsDialog.js @@ -1,6 +1,4 @@ -import React, {useEffect} from 'react'; -import Button from '@mui/material/Button'; -import DialogActions from '@mui/material/DialogActions'; +import React from 'react'; import DialogContent from '@mui/material/DialogContent'; import DialogTitle from '@mui/material/DialogTitle'; import List from '@mui/material/List'; @@ -8,11 +6,14 @@ import ListItem from '@mui/material/ListItem'; import ListItemIcon from '@mui/material/ListItemIcon'; import ListItemText from '@mui/material/ListItemText'; import Checkbox from '@mui/material/Checkbox'; -import CardHeader from '@mui/material/CardHeader'; import {gql, useQuery} from '@apollo/client'; -import { CardContent } from '@mui/material'; -import {classes, StyledButton, StyledDivider} from '../../MythicComponents/MythicTransferList'; -import {MythicDraggableDialogTitle} from "../../MythicComponents/MythicDraggableDialogTitle"; +import {classes, StyledButton} from '../../MythicComponents/MythicTransferList'; +import { + MythicDialogButton, + MythicDialogBody, + MythicDialogFooter, + MythicDialogSection +} from '../../MythicComponents/MythicDialogLayout'; const getCommandsQuery = gql` query GetCallbackDetails($callback_id: Int!) { @@ -68,15 +69,14 @@ const CustomList = ({title, items, handleToggle}) => { )) }, [items, handleToggle]); return ( - <> - - - +
+
{title}
+
{renderedList} - - +
+
) }; export function AddRemoveCallbackCommandsDialog(props) { @@ -187,67 +187,71 @@ export function AddRemoveCallbackCommandsDialog(props) { Add or Remove Commands for Callback {props.display_id} - This will add or remove commands associated with this callback from Mythic's perspective. - This does NOT add or remove commands within the payload itself that's beaconing out to Mythic. -
-
- -
-
- - >> - - - > - - - < - - - << - - -
-
- -
-
+ + +
+
+ +
+
+ + >> + + + > + + + < + + + << + + +
+
+ +
+
+
+
- - - - + +
); } - diff --git a/MythicReactUI/src/components/pages/Callbacks/C2PathDialog.js b/MythicReactUI/src/components/pages/Callbacks/C2PathDialog.js index 987078cb..9375b9aa 100644 --- a/MythicReactUI/src/components/pages/Callbacks/C2PathDialog.js +++ b/MythicReactUI/src/components/pages/Callbacks/C2PathDialog.js @@ -517,7 +517,7 @@ const getTaskWidth = (node) => { return Math.max(325, (nodeText.length * 8) + 10) } const getEventNodeWidth = (node) => { - return (node.maxNameLength * 8) + 10 + 100; + return Math.max(190, (node.maxNameLength * 8) + 120); } const getBrowserscriptWidth = (node) => { let nodeText = " "; @@ -534,7 +534,7 @@ const getHeight = (node) => { return 0; } if(node.type === "eventNode"){ - return 20; + return 88; } return 80; } diff --git a/MythicReactUI/src/components/pages/Callbacks/CallbacksTableColumnsReorderDialog.js b/MythicReactUI/src/components/pages/Callbacks/CallbacksTableColumnsReorderDialog.js index 1bf47dbc..389ac6ba 100644 --- a/MythicReactUI/src/components/pages/Callbacks/CallbacksTableColumnsReorderDialog.js +++ b/MythicReactUI/src/components/pages/Callbacks/CallbacksTableColumnsReorderDialog.js @@ -1,24 +1,25 @@ import React from 'react'; -import Button from '@mui/material/Button'; -import DialogActions from '@mui/material/DialogActions'; import DialogContent from '@mui/material/DialogContent'; import DialogTitle from '@mui/material/DialogTitle'; import IconButton from '@mui/material/IconButton'; -import { useTheme } from '@mui/material/styles'; import {reorder} from "../../MythicComponents/MythicDraggableList"; import { Draggable, DragDropContext, Droppable, } from "@hello-pangea/dnd"; -import { List, ListItem, ListItemText, Paper } from '@mui/material'; import DragHandleIcon from '@mui/icons-material/DragHandle'; import VisibilityOffIcon from '@mui/icons-material/VisibilityOff'; import VisibilityIcon from '@mui/icons-material/Visibility'; +import { + MythicDialogBody, + MythicDialogButton, + MythicDialogFooter, + MythicDialogSection, +} from "../../MythicComponents/MythicDialogLayout"; export function CallbacksTableColumnsReorderDialog({initialItems, onSubmit, onClose, onReset, visible, hidden}) { const [items, setItems] = React.useState(initialItems); - const theme = useTheme(); const onDragEnd = ({ destination, source }) => { // dropped outside the list if (!destination) return; @@ -26,13 +27,12 @@ export function CallbacksTableColumnsReorderDialog({initialItems, onSubmit, onCl setItems(newItems); }; React.useEffect( () => { - const newItems = items.map( c => { + setItems((currentItems) => currentItems.map( c => { if(visible.includes(c.name)){ return {...c, visible: true}; } return {...c, visible: false}; - }) - setItems(newItems); + })); }, [visible, hidden]); const onToggleVisibility = (i) => { const newItems = items.map( (c, index) => { @@ -49,21 +49,28 @@ export function CallbacksTableColumnsReorderDialog({initialItems, onSubmit, onCl return ( - Drag and Drop to Adjust the Order and Toggle Visibility - - + Column Layout + + + + + + - - - - - + + + Save + + ); } @@ -73,46 +80,48 @@ export const DraggableList = ({ items, onDragEnd, onToggleVisibility }) => { {(provided) => ( - +
{items.map((item, index) => ( ))} {provided.placeholder} - +
)}
); }; export const DraggableListItem = ({ item, index, onToggleVisibility }) => { - const theme = useTheme(); return ( {(provided, snapshot) => ( - - - - onToggleVisibility(index)} style={{float: "right", margin: 0, padding: 0}}> - {item.visible ? ( - - ) : ( - - ) - - } - - + + + +
+ {item.name} +
+
+ onToggleVisibility(index)} + > + {item.visible ? ( + + ) : ( + + )} + +
+ )}
); }; - diff --git a/MythicReactUI/src/components/pages/Callbacks/CallbacksTabsCustomFileBasedBrowserTable.js b/MythicReactUI/src/components/pages/Callbacks/CallbacksTabsCustomFileBasedBrowserTable.js index f39d6f82..ede013fa 100644 --- a/MythicReactUI/src/components/pages/Callbacks/CallbacksTabsCustomFileBasedBrowserTable.js +++ b/MythicReactUI/src/components/pages/Callbacks/CallbacksTabsCustomFileBasedBrowserTable.js @@ -476,7 +476,8 @@ export const CallbacksTabsCustomFileBasedBrowserTable = (props) => { { name: removeDisplay, type: "item", disabled: removeCommand === undefined, - icon: , + danger: true, + icon: , click: ({event}) => { event.stopPropagation(); props.onTaskRowAction({ @@ -546,7 +547,8 @@ export const CallbacksTabsCustomFileBasedBrowserTable = (props) => { { name: `Remove All Selected`, type: "item", disabled: removeCommand === undefined, - icon: , + danger: true, + icon: , click: ({event}) => { event.stopPropagation(); let newTasks = []; @@ -973,4 +975,4 @@ export const TableRowSizeCell = ({ cellData, rowData }) => { } }; return getStringSize(cellData); -}; \ No newline at end of file +}; diff --git a/MythicReactUI/src/components/pages/Callbacks/CallbacksTabsFileBrowserTable.js b/MythicReactUI/src/components/pages/Callbacks/CallbacksTabsFileBrowserTable.js index 41da8b0a..4965da3b 100644 --- a/MythicReactUI/src/components/pages/Callbacks/CallbacksTabsFileBrowserTable.js +++ b/MythicReactUI/src/components/pages/Callbacks/CallbacksTabsFileBrowserTable.js @@ -539,7 +539,8 @@ export const CallbacksTabsFileBrowserTable = (props) => { }, { name: removeDisplay, type: "item", disabled: removeCommand === undefined, - icon: , + danger: true, + icon: , click: ({event}) => { event.stopPropagation(); props.onTaskRowAction({ @@ -582,7 +583,8 @@ export const CallbacksTabsFileBrowserTable = (props) => { }, { name: `Remove All Selected`, type: "item", - icon: , + danger: true, + icon: , click: ({event}) => { event.stopPropagation(); let newTasks = []; @@ -1016,6 +1018,7 @@ const FileBrowserTableRowActionCell = ({ rowData, cellData, onTaskRowAction, tre handleMenuItemClick(event, option.click)} > {option.icon} {option.name} @@ -1029,6 +1032,7 @@ const FileBrowserTableRowActionCell = ({ rowData, cellData, onTaskRowAction, tre handleMenuItemClick(event, menuOption.click)} > {menuOption.icon}{menuOption.name} diff --git a/MythicReactUI/src/components/pages/Callbacks/DetailedCallbackTable.js b/MythicReactUI/src/components/pages/Callbacks/DetailedCallbackTable.js index 072cbc7f..24a4905f 100644 --- a/MythicReactUI/src/components/pages/Callbacks/DetailedCallbackTable.js +++ b/MythicReactUI/src/components/pages/Callbacks/DetailedCallbackTable.js @@ -8,8 +8,6 @@ import TableHead from '@mui/material/TableHead'; import Typography from '@mui/material/Typography'; import {useQuery, gql, useMutation} from '@apollo/client'; import LinearProgress from '@mui/material/LinearProgress'; -import Paper from '@mui/material/Paper'; -import {useTheme} from '@mui/material/styles'; import DialogActions from '@mui/material/DialogActions'; import DialogContent from '@mui/material/DialogContent'; import DialogTitle from '@mui/material/DialogTitle'; @@ -34,6 +32,7 @@ import {HostFileDialog} from "../Payloads/HostFileDialog"; import PublicIcon from '@mui/icons-material/Public'; import {payloadsCallbackAllowed} from "../Payloads/Payloads"; import Switch from '@mui/material/Switch'; +import {MythicSectionHeader} from "../../MythicComponents/MythicPageHeader"; const GET_Payload_Details = gql` query GetCallbackDetails($callback_id: Int!) { @@ -184,7 +183,6 @@ mutation removeLoadedCommand($id: Int!){ } `; export function DetailedCallbackTable(props){ - const theme = useTheme(); const me = useReactiveVar(meState); const [openHostDialog, setOpenHostDialog] = React.useState(false); const [openDetailedView, setOpenDetailedView] = React.useState(false); @@ -335,18 +333,10 @@ export function DetailedCallbackTable(props){ Callback Configuration - - - Callback Information - - + - - - Payload Information - - +
@@ -454,11 +444,7 @@ export function DetailedCallbackTable(props){
- - - Build Parameters - - + @@ -496,11 +482,7 @@ export function DetailedCallbackTable(props){
- - - Build Steps - - + @@ -530,11 +512,7 @@ export function DetailedCallbackTable(props){ { c2Profiles.map( (c2) => ( - - - {c2.c2_profile} - - +
@@ -565,13 +543,12 @@ export function DetailedCallbackTable(props){ ))} - - - Loaded Commands - - - - + {setOpenAddRemoveCommandsDialog(true)}}>Add/Remove Commands + } + />
@@ -594,10 +571,16 @@ export function DetailedCallbackTable(props){ {cmd.mythic} {cmd.payload} - - - + + + + + )) diff --git a/MythicReactUI/src/components/pages/Callbacks/ImportCallbackConfigDialog.js b/MythicReactUI/src/components/pages/Callbacks/ImportCallbackConfigDialog.js index e999d1fb..52fb0d60 100644 --- a/MythicReactUI/src/components/pages/Callbacks/ImportCallbackConfigDialog.js +++ b/MythicReactUI/src/components/pages/Callbacks/ImportCallbackConfigDialog.js @@ -1,10 +1,15 @@ import React from 'react'; -import Button from '@mui/material/Button'; -import DialogActions from '@mui/material/DialogActions'; import DialogContent from '@mui/material/DialogContent'; import DialogTitle from '@mui/material/DialogTitle'; import {gql, useMutation} from '@apollo/client'; import { snackActions } from '../../utilities/Snackbar'; +import {DragAndDropFileUpload} from './TaskParametersDialogRow'; +import { + MythicDialogBody, + MythicDialogButton, + MythicDialogFooter, + MythicDialogSection +} from '../../MythicComponents/MythicDialogLayout'; const import_callback = gql` mutation importCallbackConfigMutation($config: jsonb!) { @@ -35,35 +40,36 @@ export function ImportCallbackConfigDialog(props) { snackActions.error("Failed to parse configuration") } } - const onFileChange = (evt) => { + const onFileChange = (newFile) => { const reader = new FileReader(); reader.onload = (e) => { const contents = e.target.result; - setFileValue({name: evt.target.files[0].name, contents: contents}); + setFileValue({name: newFile.name, contents: contents}); } - reader.readAsBinaryString(evt.target.files[0]); + reader.readAsBinaryString(newFile); } return ( Import Callback Config From Other Mythic Server - Export a callback config from another Mythic server and import it here to interact with that callback from this server. -
- + + + + +
- - - - + +
); } - diff --git a/MythicReactUI/src/components/pages/Callbacks/ModifyCallbackMythicTreeGroupsDialog.js b/MythicReactUI/src/components/pages/Callbacks/ModifyCallbackMythicTreeGroupsDialog.js index 323d9fdd..3e72d4fa 100644 --- a/MythicReactUI/src/components/pages/Callbacks/ModifyCallbackMythicTreeGroupsDialog.js +++ b/MythicReactUI/src/components/pages/Callbacks/ModifyCallbackMythicTreeGroupsDialog.js @@ -151,7 +151,7 @@ export function ModifyCallbackMythicTreeGroupsDialog(props){ {groups.map( (a, i) => ( - {removeArrayValue(i)}} size="large"> + {removeArrayValue(i)}}> 0} diff --git a/MythicReactUI/src/components/pages/Callbacks/ResponseDisplay.js b/MythicReactUI/src/components/pages/Callbacks/ResponseDisplay.js index 432c1cb4..c8dd7cf6 100644 --- a/MythicReactUI/src/components/pages/Callbacks/ResponseDisplay.js +++ b/MythicReactUI/src/components/pages/Callbacks/ResponseDisplay.js @@ -18,6 +18,7 @@ import {GetMythicSetting} from "../../MythicComponents/MythicSavedUserSetting"; import {ResponseDisplayGraph} from "./ResponseDisplayGraph"; import {operatorSettingDefaults} from "../../../cache"; import {ResponseDisplayTabs} from "./ResponseDisplayTabs"; +import {MythicEmptyState, MythicLoadingState, MythicSearchEmptyState} from "../../MythicComponents/MythicStateDisplay"; const subResponsesStream = gql` subscription subResponsesStream($task_id: Int!){ @@ -588,12 +589,47 @@ const ResponseDisplayComponent = ({rawResponses, viewBrowserScript, output, comm fetchScripts({variables: {command_id: command_id}}); } }, [command_id, task.id]); + const stateSx = { + backgroundColor: "transparent", + color: "inherit", + height: expand ? "100%" : undefined, + }; if(loadingBrowserScript){ - return null + return ( + + ) + } + if(rawResponses.length === 0 && output.length === 0){ + return search ? ( + + ) : ( + + ) } const shouldUseBrowserScript = viewBrowserScript && localViewBrowserScript; if(shouldUseBrowserScript && browserScriptData === undefined){ - return null + return ( + + ) } return ( shouldUseBrowserScript ? ( diff --git a/MythicReactUI/src/components/pages/Callbacks/ResponseDisplayInteractive.js b/MythicReactUI/src/components/pages/Callbacks/ResponseDisplayInteractive.js index aed4f9ac..ae51c72d 100644 --- a/MythicReactUI/src/components/pages/Callbacks/ResponseDisplayInteractive.js +++ b/MythicReactUI/src/components/pages/Callbacks/ResponseDisplayInteractive.js @@ -23,6 +23,7 @@ import FormControl from "@mui/material/FormControl"; import WrapTextIcon from '@mui/icons-material/WrapText'; import {useTheme} from '@mui/material/styles'; import HeightIcon from '@mui/icons-material/Height'; +import {MythicEmptyState} from "../../MythicComponents/MythicStateDisplay"; const getInteractiveTaskingQuery = gql` ${taskingDataFragment} @@ -297,6 +298,7 @@ const InteractiveTerminalDisplay = ({data, useASNIColor, showTaskStatus, wrapTex const terminalRef = React.useRef(null); const fitAddonRef = React.useRef(null); const resizeObserverRef = React.useRef(null); + const fitAnimationFrameRef = React.useRef(null); const replayStateRef = React.useRef({settingsKey: "", signatures: []}); const autoScrollRef = React.useRef(autoScroll); const wrapTextRef = React.useRef(wrapText); @@ -333,6 +335,15 @@ const InteractiveTerminalDisplay = ({data, useASNIColor, showTaskStatus, wrapTex console.error(error); } }, []); + const scheduleFitTerminal = React.useCallback( () => { + if(fitAnimationFrameRef.current !== null){ + return; + } + fitAnimationFrameRef.current = window.requestAnimationFrame(() => { + fitAnimationFrameRef.current = null; + fitTerminal(); + }); + }, [fitTerminal]); React.useEffect( () => { autoScrollRef.current = autoScroll; if(autoScroll){ @@ -341,16 +352,16 @@ const InteractiveTerminalDisplay = ({data, useASNIColor, showTaskStatus, wrapTex }, [autoScroll]); React.useEffect( () => { wrapTextRef.current = wrapText; - fitTerminal(); - }, [wrapText, fitTerminal]); + scheduleFitTerminal(); + }, [wrapText, scheduleFitTerminal]); React.useEffect( () => { if(wrapText){ unwrappedColumnCountRef.current = 0; } else { unwrappedColumnCountRef.current = Math.min(Math.max(longestLineLength + 2, 1), INTERACTIVE_TERMINAL_MAX_UNWRAPPED_COLS); } - fitTerminal(); - }, [fitTerminal, longestLineLength, wrapText]); + scheduleFitTerminal(); + }, [longestLineLength, scheduleFitTerminal, wrapText]); React.useEffect( () => { if(!terminalElementRef.current){ return; @@ -370,15 +381,20 @@ const InteractiveTerminalDisplay = ({data, useASNIColor, showTaskStatus, wrapTex terminal.open(terminalElementRef.current); terminalRef.current = terminal; fitAddonRef.current = fitAddon; - resizeObserverRef.current = new ResizeObserver(() => fitTerminal()); + resizeObserverRef.current = new ResizeObserver(() => scheduleFitTerminal()); if(terminalScrollContainerRef.current){ resizeObserverRef.current.observe(terminalScrollContainerRef.current); } - window.requestAnimationFrame(() => { + fitAnimationFrameRef.current = window.requestAnimationFrame(() => { + fitAnimationFrameRef.current = null; fitTerminal(); setTerminalReady(true); }); return () => { + if(fitAnimationFrameRef.current !== null){ + window.cancelAnimationFrame(fitAnimationFrameRef.current); + fitAnimationFrameRef.current = null; + } resizeObserverRef.current?.disconnect(); resizeObserverRef.current = null; fitAddonRef.current = null; @@ -396,7 +412,7 @@ const InteractiveTerminalDisplay = ({data, useASNIColor, showTaskStatus, wrapTex return; } const terminal = terminalRef.current; - fitTerminal(); + scheduleFitTerminal(); const nextSignatures = data.map(getInteractiveTerminalEntrySignature); const settingsKey = `${useASNIColor}:${showTaskStatus}:${wrapText}`; const previousReplayState = replayStateRef.current; @@ -429,7 +445,7 @@ const InteractiveTerminalDisplay = ({data, useASNIColor, showTaskStatus, wrapTex } else { afterWrite(); } - }, [data, showTaskStatus, terminalReady, useASNIColor, wrapText]); + }, [data, scheduleFitTerminal, showTaskStatus, terminalReady, useASNIColor, wrapText]); return (
{ }, 2000); return () => clearTimeout(timeoutID); }, [backdropOpen]); + const panelHeight = props.expand ? "100%" : "clamp(260px, 36vh, 500px)"; + const outputMinHeight = props.expand ? 0 : "160px"; return (
{ }
- + {visibleOutput.length === 0 && !backdropOpen ? ( + + ) : ( + + )}
{!props.task?.is_interactive_task &&
diff --git a/MythicReactUI/src/components/pages/Callbacks/TaskDisplay.js b/MythicReactUI/src/components/pages/Callbacks/TaskDisplay.js index 362cd84e..80ef745f 100644 --- a/MythicReactUI/src/components/pages/Callbacks/TaskDisplay.js +++ b/MythicReactUI/src/components/pages/Callbacks/TaskDisplay.js @@ -1,5 +1,5 @@ import React, {useEffect, useLayoutEffect, useRef} from 'react'; -import { styled } from '@mui/material/styles'; +import { alpha, styled } from '@mui/material/styles'; import {IconButton, Link} from '@mui/material'; import Paper from '@mui/material/Paper'; import Typography from '@mui/material/Typography'; @@ -9,7 +9,6 @@ import AccordionSummary from '@mui/material/AccordionSummary'; import ExpandMoreIcon from '@mui/icons-material/ExpandMore'; import ExpandLessIcon from '@mui/icons-material/ExpandLess'; import ChatOutlinedIcon from '@mui/icons-material/ChatOutlined'; -import Badge from '@mui/material/Badge'; import {useTheme} from '@mui/material/styles'; import {gql, useSubscription } from '@apollo/client'; import {TaskDisplayContainer, TaskDisplayContainerConsole} from './TaskDisplayContainer'; @@ -31,6 +30,7 @@ import ComputerIcon from '@mui/icons-material/Computer'; import PublicIcon from '@mui/icons-material/Public'; import WarningAmberIcon from '@mui/icons-material/WarningAmber'; import NumbersIcon from '@mui/icons-material/Numbers'; +import {getMythicStatusFromTaskStatus, MythicStatusChip} from '../../MythicComponents/MythicStatusChip'; const PREFIX = 'TaskDisplay'; @@ -43,7 +43,22 @@ export const classes = { secondaryHeadingExpanded: `${PREFIX}-secondaryHeadingExpanded`, icon: `${PREFIX}-icon`, details: `${PREFIX}-details`, - column: `${PREFIX}-column` + column: `${PREFIX}-column`, + taskHeaderBody: `${PREFIX}-taskHeaderBody`, + taskMetaRow: `${PREFIX}-taskMetaRow`, + taskMetaItem: `${PREFIX}-taskMetaItem`, + taskMetaIcon: `${PREFIX}-taskMetaIcon`, + taskHeaderBodyCompact: `${PREFIX}-taskHeaderBodyCompact`, + taskHeaderActions: `${PREFIX}-taskHeaderActions`, + taskIconButton: `${PREFIX}-taskIconButton`, + taskCommandRow: `${PREFIX}-taskCommandRow`, + taskCommandText: `${PREFIX}-taskCommandText`, + taskCommandTextCompact: `${PREFIX}-taskCommandTextCompact`, + taskCommandName: `${PREFIX}-taskCommandName`, + taskCommandParams: `${PREFIX}-taskCommandParams`, + taskChildToggle: `${PREFIX}-taskChildToggle`, + taskCommentBlock: `${PREFIX}-taskCommentBlock`, + consolePrompt: `${PREFIX}-consolePrompt` }; export const accordionClasses = { root: `${ACCORDION_PREFIX}-root`, @@ -68,6 +83,11 @@ export const StyledPaper = styled(Paper)(( backgroundColor: theme.palette.background.paper, border: `1px solid ${theme.borderColor}`, borderRadius: theme.shape.borderRadius, + transition: "background-color 120ms ease, border-color 120ms ease, box-shadow 120ms ease", + "&:hover": { + borderColor: theme.palette.mode === "dark" ? alpha(theme.palette.common.white, 0.22) : alpha(theme.palette.common.black, 0.18), + backgroundColor: theme.palette.mode === "dark" ? alpha(theme.palette.common.white, 0.035) : alpha(theme.palette.common.black, 0.015), + }, }, [`& .${classes.heading}`]: { @@ -123,6 +143,137 @@ export const StyledPaper = styled(Paper)(( display: "inline-block", margin: 0, height: "auto", + }, + [`& .${classes.taskHeaderBody}`]: { + display: "flex", + flexDirection: "column", + gap: 5, + minWidth: 0, + width: "100%", + }, + [`& .${classes.taskHeaderBodyCompact}`]: { + gap: 4, + }, + [`& .${classes.taskMetaRow}`]: { + alignItems: "center", + color: theme.palette.text.secondary, + display: "flex", + flexWrap: "wrap", + gap: 5, + minWidth: 0, + }, + [`& .${classes.taskHeaderBodyCompact} .${classes.taskMetaRow}`]: { + flexWrap: "nowrap", + maxHeight: 24, + overflow: "hidden", + }, + [`& .${classes.taskMetaItem}`]: { + alignItems: "center", + backgroundColor: theme.palette.mode === "dark" ? alpha(theme.palette.common.white, 0.05) : alpha(theme.palette.common.black, 0.035), + border: `1px solid ${theme.palette.mode === "dark" ? alpha(theme.palette.common.white, 0.08) : alpha(theme.palette.common.black, 0.07)}`, + borderRadius: 5, + color: theme.palette.text.secondary, + display: "inline-flex", + fontSize: theme.typography.pxToRem(11.5), + fontWeight: 600, + gap: 4, + lineHeight: 1.2, + maxWidth: "18rem", + minHeight: 22, + minWidth: 0, + padding: "2px 6px", + whiteSpace: "nowrap", + "& > span:last-child": { + minWidth: 0, + overflow: "hidden", + textOverflow: "ellipsis", + }, + "& a": { + color: "inherit", + fontWeight: 700, + }, + }, + [`& .${classes.taskHeaderBodyCompact} .${classes.taskMetaItem}`]: { + flex: "0 0 auto", + }, + [`& .${classes.taskMetaIcon}`]: { + flexShrink: 0, + fontSize: "0.86rem", + height: "0.86rem", + width: "0.86rem", + }, + [`& .${classes.taskHeaderActions}`]: { + alignItems: "center", + display: "inline-flex", + flexWrap: "wrap", + gap: 4, + minWidth: 0, + }, + [`& .${classes.taskHeaderBodyCompact} .${classes.taskHeaderActions}`]: { + flex: "0 0 auto", + flexWrap: "nowrap", + }, + [`& .${classes.taskIconButton}`]: { + border: `1px solid ${theme.palette.mode === "dark" ? alpha(theme.palette.common.white, 0.08) : alpha(theme.palette.common.black, 0.08)}`, + borderRadius: 5, + color: theme.palette.text.secondary, + height: 24, + padding: 0, + width: 24, + "& svg": { + fontSize: "1rem", + }, + }, + [`& .${classes.taskCommandRow}`]: { + alignItems: "flex-start", + display: "flex", + gap: 6, + minWidth: 0, + width: "100%", + }, + [`& .${classes.taskCommandText}`]: { + color: theme.taskPromptCommandTextColor, + flex: "1 1 auto", + fontSize: theme.typography.pxToRem(14), + lineHeight: 1.35, + minWidth: 0, + }, + [`& .${classes.taskCommandTextCompact}`]: { + display: "-webkit-box", + overflow: "hidden", + WebkitBoxOrient: "vertical", + WebkitLineClamp: "2", + }, + [`& .${classes.taskCommandName}`]: { + color: theme.taskPromptCommandTextColor, + fontWeight: 800, + }, + [`& .${classes.taskCommandParams}`]: { + color: theme.palette.text.primary, + overflowWrap: "anywhere", + }, + [`& .${classes.taskChildToggle}`]: { + color: theme.palette.text.secondary, + height: 24, + marginTop: -2, + padding: 0, + width: 24, + }, + [`& .${classes.taskCommentBlock}`]: { + backgroundColor: theme.palette.mode === "dark" ? alpha(theme.palette.common.white, 0.045) : alpha(theme.palette.common.black, 0.025), + border: `1px solid ${theme.palette.mode === "dark" ? alpha(theme.palette.common.white, 0.08) : alpha(theme.palette.common.black, 0.07)}`, + borderRadius: 5, + color: theme.palette.text.primary, + fontSize: theme.typography.pxToRem(12.5), + lineHeight: 1.35, + padding: "6px 8px", + }, + [`& .${classes.consolePrompt}`]: { + color: theme.palette.text.secondary, + flexShrink: 0, + fontFamily: 'Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace', + fontSize: theme.typography.pxToRem(13), + fontWeight: 800, } })); @@ -190,77 +341,131 @@ function TaskDisplayConsolePreMemo({task, me, filterOptions, newlyIssuedTasks}){ ); } export const TaskDisplayConsole = React.memo(TaskDisplayConsolePreMemo); -const TaskStatusDisplay = ({task, theme}) => { +const TaskStatusDisplay = ({task}) => { + const chipSx = { + display: "inline-flex", + height: 22, + mx: 0.5, + verticalAlign: "middle", + "& .MuiChip-icon": { + fontSize: "0.86rem", + }, + "& .MuiChip-label": { + px: 0.7, + }, + }; if(task.status.toLowerCase().includes("error")){ - return ({task.status.toLowerCase()}) + return }else if(task.status === "cleared"){ - return (cleared) + return }else if(task.status === "completed" || task.status === "success"){ return null//return (completed) - }else if(task.status === "submitted"){ - return ({task.status.toLowerCase()}) - }else if(task.status.toLowerCase().includes("processing")){ - return ({task.status.toLowerCase()}) }else if(task.opsec_pre_blocked && !task.opsec_pre_bypassed){ - return (OPSEC BLOCKED (PRE)) + return }else if(task.opsec_post_blocked && !task.opsec_post_bypassed){ - return (OPSEC BLOCKED (POST)) + return }else{ - return ({task.status.toLowerCase()}) + return ( + + ) } } +const getTaskAccentColor = (task, theme) => { + if(task.status.toLowerCase().includes("error")){ + return theme.palette.error.main; + } + if(task.status.toLowerCase() === "cleared"){ + return theme.palette.warning.main; + } + if(task.status === "submitted"){ + return theme.palette.info.main; + } + if(task.opsec_pre_blocked && !task.opsec_pre_bypassed){ + return theme.palette.warning.main; + } + if(task.opsec_post_blocked && !task.opsec_post_bypassed){ + return theme.palette.warning.main; + } + if(task.status.toLowerCase().includes("processing")){ + return theme.palette.warning.main; + } + if(task.status === "completed" || task.completed){ + return theme.palette.success.main; + } + return theme.palette.info.main; +} +const getCallbackPrimaryIP = (task) => { + try{ + return JSON.parse(task.callback.ip)[0] || ""; + }catch(error){ + return ""; + } +} +const TaskMetaItem = ({children, icon, title, style}) => { + const item = ( + + {icon && React.cloneElement(icon, { + className: classes.taskMetaIcon, + fontSize: "inherit", + })} + {children} + + ); + if(title){ + return ( + + {item} + + ) + } + return item; +} +const TaskHeaderAction = ({title, children, ...props}) => ( + + + {children} + + +) const TaskTagDisplay = ({task}) => { return ( ) } const ColoredTaskDisplay = ({task, theme, children, expanded}) => { - const [themeColor, setThemeColor] = React.useState(theme.palette.info.main); - useEffect( () => { - if(task.status.toLowerCase().includes("error")){ - setThemeColor(theme.palette.error.main); - }else if(task.status.toLowerCase() === "cleared"){ - setThemeColor(theme.palette.warning.main); - }else if(task.status === "submitted"){ - setThemeColor(theme.palette.info.main); - }else if(task.opsec_pre_blocked && !task.opsec_pre_bypassed){ - setThemeColor(theme.palette.warning.main); - }else if(task.opsec_post_blocked && !task.opsec_post_bypassed){ - setThemeColor(theme.palette.warning.main); - }else if(task.status.toLowerCase().includes("processing")){ - setThemeColor(theme.palette.warning.main); - }else if(task.status === "completed" || task.completed){ - setThemeColor(theme.palette.success.main); - }else{ - setThemeColor(theme.palette.info.main); - } - }, [task.status, task.completed]) - return( - - {children} - - ) + const themeColor = getTaskAccentColor(task, theme); + return( + + {children} + + ) } -const GetOperatorDisplay = ({initialHideUsernameValue, task}) => { - if(initialHideUsernameValue){ - return ''; - } - return " / " + task.operator.username; -} -export const ColoredTaskLabel = ({task, theme, me, taskDivID, onClick, displayChildren, toggleDisplayChildren, expanded }) => { +export const ColoredTaskLabel = ({task, theme, me, taskDivID, onClick, displayChildren, toggleDisplayChildren, expanded, compact=false }) => { const [displayComment, setDisplayComment] = React.useState(false); - const [alertBadges, setAlertBadges] = React.useState(0); const initialHideUsernameValue = GetMythicSetting({setting_name: "hideUsernames", default_value: operatorSettingDefaults.hideUsernames}); const initialShowIPValue = GetMythicSetting({setting_name: "showIP", default_value: operatorSettingDefaults.showIP}); - const ipValue = JSON.parse(task.callback.ip)[0]; + const ipValue = getCallbackPrimaryIP(task); const initialShowHostnameValue = GetMythicSetting({setting_name: "showHostname", default_value: operatorSettingDefaults.showHostname}); const initialShowCallbackGroupsValue = GetMythicSetting({setting_name: "showCallbackGroups", default_value: operatorSettingDefaults.showCallbackGroups}); const initialTaskTimestampDisplayField = GetMythicSetting({setting_name: "taskTimestampDisplayField", default_value: operatorSettingDefaults.taskTimestampDisplayField}); const initialShowOPSECBypassUsername = GetMythicSetting({setting_name: "showOPSECBypassUsername", default_value: operatorSettingDefaults.showOPSECBypassUsername}); const displayTimestamp = task[initialTaskTimestampDisplayField] ? task[initialTaskTimestampDisplayField] : task.timestamp; const [openKillTaskButton, setOpenKillTaskButton] = React.useState({open: false}); + const command = task?.command?.cmd || task.command_name; + const commandLine = command + " " + task.display_params; + const callbackGroups = task.callback.mythictree_groups?.join(', ') || ""; + const opsecBypassUsers = [ + task?.opsec_pre_bypass_user?.username, + task?.opsec_post_bypass_user?.username, + ].filter(Boolean).join(", "); const toggleDisplayComment = (evt) => { evt.stopPropagation(); setDisplayComment(!displayComment); @@ -282,117 +487,111 @@ export const ColoredTaskLabel = ({task, theme, me, taskDivID, onClick, displayCh } return ( -
+
{displayComment && ( - - {task.commentOperator.username}
- {task.comment} -
+
+ + {task.commentOperator?.username || "comment"} + + + {task.comment} + +
)} -
- - [{toLocalTime(displayTimestamp, me?.user?.view_utc_time || false)}] - {" / "} +
+ }> + {toLocalTime(displayTimestamp, me?.user?.view_utc_time || false)} + + }> + T-{task.display_id} + + {!initialHideUsernameValue && + }> + {task.operator.username} + + } + + C-{task.callback.display_id} + + {initialShowHostnameValue && + }> + {task.callback.host} + + } + {initialShowIPValue && ipValue !== "" && + }> + {ipValue} + + } + {initialShowCallbackGroupsValue && callbackGroups !== "" && + + {callbackGroups} + + } + {task?.command?.payloadtype?.name && + + {task.command.payloadtype.name} + + } + {initialShowOPSECBypassUsername && opsecBypassUsers !== "" && + }> + {opsecBypassUsers} + + } + {task.comment.length > 0 && + + {task.comment} + + } + {task.has_intercepted_response && - - - - - - - {" / "} - + + + } - {task?.eventstepinstance !== null && - - - - - - - - {" / "} - + {task?.eventstepinstance && + + + } - - - T-{task.display_id} - - {!task.completed && task.status_timestamp_processing && - - - onClickKillIcon(e, true)} - style={{cursor: "pointer", height: "12px", marginLeft: "5px"}} /> - - - } - - - {" / "} - - C-{task.callback.display_id} - - {initialShowHostnameValue ? ` / ${task.callback.host} ` : ''} - {initialShowIPValue ? `/ ${ipValue} ` : ''} - {initialShowCallbackGroupsValue ? `/ ${task.callback.mythictree_groups.join(', ')} ` : ''} - {" / "} - {task?.command?.payloadtype?.name} - {initialShowOPSECBypassUsername && <> - {(task?.opsec_pre_bypass_user?.username || task?.opsec_post_bypass_user?.username) && - {" /"} - - - - {task?.opsec_pre_bypass_user?.username} - {task?.opsec_pre_bypass_user?.username && task?.opsec_post_bypass_user?.username && ", "} - {task?.opsec_post_bypass_user?.username} - } - } - {" "} - + {!task.completed && task.status_timestamp_processing && + onClickKillIcon(e, true)}> + + + } + {task.comment.length > 0 && - - {" / "} - - + + + } - {task.comment} + +
+
+ {task.tasks.length > 0 && + + {displayChildren ? : } + + } + + + {command} + {task.display_params !== "" && + {task.display_params} + } - -
-
- -
- - {task.tasks.length > 0 && !displayChildren && - - } - {task.tasks.length > 0 && displayChildren && - - } - - - {(task?.command?.cmd || task.command_name) + " " + task.display_params} - - - -
+
+
{openKillTaskButton.open && } @@ -815,6 +1021,7 @@ export const TaskLabelFlat = ({task, me, showOnSelectTask, onSelectTask, graphVi > ) @@ -822,10 +1029,11 @@ export const TaskLabelFlat = ({task, me, showOnSelectTask, onSelectTask, graphVi const ColoredTaskDisplayConsole = ({task, theme, children, expanded}) => { + const themeColor = getTaskAccentColor(task, theme); return( - + {children} ) @@ -953,6 +1161,9 @@ export const ColoredTaskLabelConsole = ({task, theme, me, taskDivID, onClick, di const initialShowHostnameValue = GetMythicSetting({setting_name: "showHostname", default_value: operatorSettingDefaults.showHostname}); const initialShowIPValue = GetMythicSetting({setting_name: "showIP", default_value: operatorSettingDefaults.showIP}); const [openKillTaskButton, setOpenKillTaskButton] = React.useState({open: false}); + const ipValue = getCallbackPrimaryIP(task); + const command = task?.command?.cmd || task.command_name; + const commandLine = command + " " + task.display_params; const preventPropagation = (e) => { e.stopPropagation(); //e.preventDefault(); @@ -963,102 +1174,66 @@ export const ColoredTaskLabelConsole = ({task, theme, me, taskDivID, onClick, di } setOpenKillTaskButton({open: open}); } - const [themeColor, setThemeColor] = React.useState(theme.palette.info.main); - useEffect( () => { - if(task.status.toLowerCase().includes("error")){ - setThemeColor(theme.palette.error.main); - }else if(task.status.toLowerCase() === "cleared"){ - setThemeColor(theme.palette.warning.main); - }else if(task.status === "submitted"){ - setThemeColor(theme.palette.info.main); - }else if(task.opsec_pre_blocked && !task.opsec_pre_bypassed){ - setThemeColor(theme.palette.warning.main); - }else if(task.opsec_post_blocked && !task.opsec_post_bypassed){ - setThemeColor(theme.palette.warning.main); - }else if(task.status.toLowerCase().includes("processing")){ - setThemeColor(theme.palette.warning.main); - }else if(task.status === "completed" || (task.status === "success" && task.completed)){ - setThemeColor(theme.palette.success.main); - }else{ - setThemeColor(theme.palette.info.main); - } - }, [task.status, task.completed]) return ( -
- - ┌──[ - - {toLocalTimeShort(displayTimestamp, me?.user?.view_utc_time || false)} - {"]"} - {!initialHideUsernameValue && - <> - {"-["} - - {task.operator.username} - {"]"} - +
+
+ }> + {toLocalTimeShort(displayTimestamp, me?.user?.view_utc_time || false)} + + {!initialHideUsernameValue && + }> + {task.operator.username} + + } + }> + T-{task.display_id} + + {initialShowHostnameValue && + }> + {task.callback.host} + + } + {initialShowIPValue && ipValue !== "" && + }> + {ipValue} + + } + {(task.opsec_pre_blocked && !task.opsec_pre_bypassed) && + }> + OPSEC pre + + } + {(task.opsec_post_blocked && !task.opsec_post_bypassed) && + }> + OPSEC post + + } + + {!task.completed && task.status_timestamp_processing && + onClickKillIcon(e, true)}> + + + } + + +
+ + + {">"} + + {command} + {task.display_params !== "" && + {task.display_params} } - {"-["} - - - T-{task.display_id} - - {!task.completed && task.status_timestamp_processing && - <> - - onClickKillIcon(e, true)} - style={{cursor: "pointer", height: "15px", marginLeft: "5px"}} /> - - - } - {"]"} - {initialShowHostnameValue ? ( - <> - {"-["} - - {task.callback.host} - {"]"} - ) : null} - {initialShowIPValue ? ( - <> - {"-["} - - {JSON.parse(task.callback.ip)[0]} - {"]"} - ) : null} - {(task.opsec_pre_blocked && !task.opsec_pre_bypassed) ? ( - <> - {"-["} - - {"OPSEC BLOCKED (PRE)]"} - ) : null} - {(task.opsec_post_blocked && !task.opsec_post_bypassed) ? ( - <> - {"-["} - - {"OPSEC BLOCKED (POST)]"} - ) : null} - {")"} - - - └─ - {">_"}{" "} - - - - - {(task?.command?.cmd || task.command_name)} {task.display_params} - - - + + +
{openKillTaskButton.open && { } return ( - + -
); } diff --git a/MythicReactUI/src/components/pages/Callbacks/TaskDisplayContainer.js b/MythicReactUI/src/components/pages/Callbacks/TaskDisplayContainer.js index 5a2fafba..6851effb 100644 --- a/MythicReactUI/src/components/pages/Callbacks/TaskDisplayContainer.js +++ b/MythicReactUI/src/components/pages/Callbacks/TaskDisplayContainer.js @@ -26,15 +26,18 @@ import {FontAwesomeIcon} from '@fortawesome/react-fontawesome'; import {faExclamationTriangle} from '@fortawesome/free-solid-svg-icons'; import { faExternalLinkAlt, faExpandArrowsAlt } from '@fortawesome/free-solid-svg-icons'; import SearchIcon from '@mui/icons-material/Search'; -import SpeedDial from '@mui/material/SpeedDial'; -import SpeedDialAction from '@mui/material/SpeedDialAction'; -import { Backdrop } from '@mui/material'; +import Button from '@mui/material/Button'; +import Menu from '@mui/material/Menu'; +import MenuItem from '@mui/material/MenuItem'; +import ListItemIcon from '@mui/material/ListItemIcon'; +import ListItemText from '@mui/material/ListItemText'; +import Divider from '@mui/material/Divider'; import {downloadFileFromMemory} from '../../utilities/Clipboard'; import InsertPhotoIcon from '@mui/icons-material/InsertPhoto'; import html2canvas from 'html2canvas'; import CloseFullscreenIcon from '@mui/icons-material/CloseFullscreen'; import CodeOffIcon from '@mui/icons-material/CodeOff'; -import SettingsTwoToneIcon from '@mui/icons-material/SettingsTwoTone'; +import MoreHorizIcon from '@mui/icons-material/MoreHoriz'; import {b64DecodeUnicode} from './ResponseDisplay'; import {MythicStyledTooltip} from "../../MythicComponents/MythicStyledTooltip"; import IconButton from '@mui/material/IconButton'; @@ -88,17 +91,16 @@ export const TaskDisplayContainer = ({task, me}) => { return ( <> - + + ) } -const SpeedDialDisplayGeneric = ({toggleViewBrowserScript, toggleSelectAllOutput, +const TaskActionsToolbarGeneric = ({toggleViewBrowserScript, toggleSelectAllOutput, toggleOpenSearch, taskData, viewAllOutput, me, - responseRef, style, fabStyle, viewBrowserScript}) => { + responseRef, viewBrowserScript}) => { const theme = useTheme(); const [task, setTask] = React.useState(taskData || {}); const eventingDataRef = React.useRef({name: "", value: 0}); const [openEventingDialog, setOpenEventingDialog] = React.useState(false); - const [openSpeedDial, setOpenSpeedDial] = React.useState(false); + const [actionsMenuAnchor, setActionsMenuAnchor] = React.useState(null); const [openTaskTagDialog, setOpenTaskTagDialog] = React.useState(false); const [openCommentDialog, setOpenCommentDialog] = React.useState(false); const [openParametersDialog, setOpenParametersDialog] = React.useState(false); @@ -478,9 +480,15 @@ const SpeedDialDisplayGeneric = ({toggleViewBrowserScript, toggleSelectAllOutput React.useEffect( () => { setTask(taskData); }, [taskData.id, taskData.token, taskData.original_params, taskData.opsec_pre_blocked, taskData.opsec_pre_bypassed, taskData.opsec_post_blocked, taskData.opsec_post_bypassed]) + const openActionsMenu = (event) => { + setActionsMenuAnchor(event.currentTarget); + }; + const closeActionsMenu = () => { + setActionsMenuAnchor(null); + }; const onDownloadResponses = () => { downloadResponses({variables: {task_id: task.id}}); - setOpenSpeedDial(false); + closeActionsMenu(); }; const copyToClipboard = () => { let command = task?.command?.cmd || task.command_name; @@ -490,7 +498,7 @@ const SpeedDialDisplayGeneric = ({toggleViewBrowserScript, toggleSelectAllOutput }else{ snackActions.error("Failed to copy text"); } - setOpenSpeedDial(false); + closeActionsMenu(); }; const [reissueTask] = useMutation(ReissueTaskMutationGQL, { onCompleted: data => { @@ -540,12 +548,15 @@ const SpeedDialDisplayGeneric = ({toggleViewBrowserScript, toggleSelectAllOutput fakeLink.remove(); })(); + closeActionsMenu(); }; const onReissueTask = () => { reissueTask({variables: {task_id: task.id}}); + closeActionsMenu(); } const onReissueTaskHandler = () => { reissueTaskHandler({variables: {task_id: task.id}}); + closeActionsMenu(); } const onTriggerEventing = () => { eventingDataRef.current = { @@ -553,10 +564,10 @@ const SpeedDialDisplayGeneric = ({toggleViewBrowserScript, toggleSelectAllOutput value: task.id }; setOpenEventingDialog(true); + closeActionsMenu(); } return ( - {setOpenSpeedDial(false);}} style={{zIndex: 2, position: "absolute"}}/> {openTaskTagDialog && {setOpenTaskTagDialog(false);}} innerDialog={{setOpenTaskTagDialog(false);}} />} @@ -601,165 +612,128 @@ const SpeedDialDisplayGeneric = ({toggleViewBrowserScript, toggleSelectAllOutput } /> } - } - style={{...style}} - onClick={()=>{setOpenSpeedDial(!openSpeedDial)}} - FabProps={{...fabStyle, color: "secondary", size: "small", sx: {minHeight: "30px", height: "30px", width: "30px"}}} - open={openSpeedDial} - direction="right" - > - : } - arrow - tooltipPlacement={"top"} - tooltipTitle={"Toggle BrowserScript"} - onClick={() => {toggleViewBrowserScript();setOpenSpeedDial(false);}} - /> - : } - arrow - tooltipPlacement={"top"} - tooltipTitle={viewAllOutput ? "View Paginated Output" : "View All Output"} - onClick={() => {toggleSelectAllOutput();setOpenSpeedDial(false);}} - /> - } - arrow - tooltipPlacement={"top"} - tooltipTitle={"Search Output"} - onClick={() => {toggleOpenSearch();setOpenSpeedDial(false);}} - /> - } - arrow - tooltipPlacement={"top"} - tooltipTitle={"Download output"} - onClick={onDownloadResponses} - /> - } - arrow - tooltipPlacement={"top"} - tooltipTitle={"Download screenshot of output"} - onClick={onDownloadImageClickPng} - /> - } - arrow - tooltipPlacement={"top"} - tooltipTitle={"Edit Tags"} - onClick={()=>{setOpenTaskTagDialog(true);setOpenSpeedDial(false);}} - /> - } - arrow - tooltipPlacement={"top"} - tooltipTitle={"Open Task in New Window"} - onClick={()=> {window.open('/new/task/' + task.display_id, "_blank")}} - /> - } - arrow - tooltipPlacement={"top"} - tooltipTitle={"Copy original params to clipboard"} - onClick={copyToClipboard} - /> - } - arrow - tooltipPlacement={"top"} - tooltipTitle={"Edit Comment"} - onClick={()=>{setOpenCommentDialog(true);setOpenSpeedDial(false);}} - /> - } - arrow - tooltipPlacement={"top"} - tooltipTitle={"View All Parameters And Timestamps"} - onClick={()=>{setOpenParametersDialog(true);setOpenSpeedDial(false);}} - /> - } - arrow - tooltipPlacement={"top"} - tooltipTitle={"View Stdout/Stderr of Task"} - onClick={()=>{setOpenStdoutStderrDialog(true);setOpenSpeedDial(false);}} - /> - } - arrow - tooltipPlacement={"top"} - tooltipTitle={"Trigger Eventing Based on Task"} - onClick={()=>{onTriggerEventing();setOpenSpeedDial(false);}} - /> - {task.opsec_pre_blocked === null ? null : ( task.opsec_pre_bypassed === false ? ( - } - arrow - tooltipPlacement={"top"} - tooltipTitle={"Submit OPSEC PreCheck Bypass Request"} - onClick={()=>{setOpenOpsecDialog({open: true, view: "pre"});setOpenSpeedDial(false);}} - /> - ): ( - } - arrow - tooltipPlacement={"top"} - tooltipTitle={"View OPSEC PreCheck Data"} - onClick={()=>{setOpenOpsecDialog({open: true, view: "pre"});setOpenSpeedDial(false);}} - /> - ) - ) - } - {task.opsec_post_blocked === null ? null : ( task.opsec_post_bypassed === false ? ( - } - arrow - tooltipPlacement={"top"} - tooltipTitle={"Submit OPSEC PostCheck Bypass Request"} - onClick={()=>{setOpenOpsecDialog({open: true, view: "post"});setOpenSpeedDial(false);}} - /> - ): ( - } - arrow - tooltipPlacement={"top"} - tooltipTitle={"View OPSEC PostCheck Data"} - onClick={()=>{setOpenOpsecDialog({open: true, view: "post"});setOpenSpeedDial(false);}} - /> - ) - ) - } - {task.token === null ? null : ( - } - arrow - tooltipPlacement={"top"} - tooltipTitle={"View Token Information"} - onClick={()=>{setOpenTokenDialog(true);setOpenSpeedDial(false);}} - /> - )} - {task.status.toLowerCase().includes("error: container") ? ( - } - arrow - tooltipPlacement={"top"} - tooltipTitle={"Resubmit Tasking"} - onClick={onReissueTask} - /> - ) : null} - {task.status.toLowerCase().includes("error: task") ? ( - } - arrow - tooltipPlacement={"top"} - tooltipTitle={"Resubmit Task Handler"} - onClick={onReissueTaskHandler} - /> - ):null} - + + + + {viewBrowserScript ? : } + + + + + {viewAllOutput ? : } + + + + + + + + + + + + + + + + + + + + {setOpenTaskTagDialog(true);closeActionsMenu();}}> + + Edit Tags + + {window.open('/new/task/' + task.display_id, "_blank");closeActionsMenu();}}> + + Open Task in New Window + + + + Copy original params + + {setOpenCommentDialog(true);closeActionsMenu();}}> + + Edit Comment + + {setOpenParametersDialog(true);closeActionsMenu();}}> + + View Parameters And Timestamps + + {setOpenStdoutStderrDialog(true);closeActionsMenu();}}> + + View Stdout/Stderr + + + + Trigger Eventing Based on Task + + {(task.opsec_pre_blocked !== null || task.opsec_post_blocked !== null || task.token !== null || + task.status.toLowerCase().includes("error: container") || task.status.toLowerCase().includes("error: task")) && + + } + {task.opsec_pre_blocked !== null && ( + {setOpenOpsecDialog({open: true, view: "pre"});closeActionsMenu();}}> + + {task.opsec_pre_bypassed === false ? + : + + } + + {task.opsec_pre_bypassed === false ? "Submit OPSEC PreCheck Bypass Request" : "View OPSEC PreCheck Data"} + + )} + {task.opsec_post_blocked !== null && ( + {setOpenOpsecDialog({open: true, view: "post"});closeActionsMenu();}}> + + {task.opsec_post_bypassed === false ? + : + + } + + {task.opsec_post_bypassed === false ? "Submit OPSEC PostCheck Bypass Request" : "View OPSEC PostCheck Data"} + + )} + {task.token !== null && ( + {setOpenTokenDialog(true);closeActionsMenu();}}> + + View Token Information + + )} + {task.status.toLowerCase().includes("error: container") && ( + + + Resubmit Tasking + + )} + {task.status.toLowerCase().includes("error: task") && ( + + + Resubmit Task Handler + + )} + + ) diff --git a/MythicReactUI/src/components/pages/Callbacks/TaskParametersDialogRow.js b/MythicReactUI/src/components/pages/Callbacks/TaskParametersDialogRow.js index 6563c58a..4771d50a 100644 --- a/MythicReactUI/src/components/pages/Callbacks/TaskParametersDialogRow.js +++ b/MythicReactUI/src/components/pages/Callbacks/TaskParametersDialogRow.js @@ -818,8 +818,8 @@ export function TaskParametersDialogRow(props){ - {removeArrayValue(i)}} color="error"> - + {removeArrayValue(i)}}> + @@ -862,7 +862,7 @@ export function TaskParametersDialogRow(props){ {typedArrayValue.map( (a, i) => ( - {removeTypedArrayValue(i)}} size="large"> + {removeTypedArrayValue(i)}}>
@@ -1082,8 +1082,8 @@ export function TaskParametersDialogRow(props){ - + @@ -1172,12 +1172,9 @@ export function TaskParametersDialogRow(props){ borderRadius: "10px", marginRight: "5px" }}/> Credential - + }}>Credential ) diff --git a/MythicReactUI/src/components/pages/CreatePayload/CreatePayloadNavigationButtons.js b/MythicReactUI/src/components/pages/CreatePayload/CreatePayloadNavigationButtons.js index 80bf99b9..90bdf182 100644 --- a/MythicReactUI/src/components/pages/CreatePayload/CreatePayloadNavigationButtons.js +++ b/MythicReactUI/src/components/pages/CreatePayload/CreatePayloadNavigationButtons.js @@ -16,17 +16,17 @@ export function CreatePayloadNavigationButtons(props){
{props.showExtraOptions && } size="small" value={opt.name} onChange={(e) => onChangeDictKey(e, i)}> value} size="small" value={opt.value} onChange={(e) => onChangeDictVal(e, i)}>
@@ -671,7 +671,7 @@ export function CreatePayloadParameter({onChange, parameter_type, default_value, )} {dictSelectOptions.length > 0 ? (
- + -
@@ -656,8 +660,8 @@ const C2ProfileTabs = ({includedC2Profiles, onChange, os, onCloseTab, onChangeCr {c.name} - onCloseTabLocal(e, index)} > - + onCloseTabLocal(e, index)} > +
} {...a11yProps(index)} style={{flexShrink: 0}} /> diff --git a/MythicReactUI/src/components/pages/CreatePayload/Step5Build.js b/MythicReactUI/src/components/pages/CreatePayload/Step5Build.js index ddf8972b..f11e08ac 100644 --- a/MythicReactUI/src/components/pages/CreatePayload/Step5Build.js +++ b/MythicReactUI/src/components/pages/CreatePayload/Step5Build.js @@ -226,7 +226,7 @@ export function Step5Build(props){ OS: {props.buildOptions[1].os} - props.moveToStep(0)}> + props.moveToStep(0)}> @@ -275,7 +275,7 @@ export function Step5Build(props){ 2. Build Parameter Configuration - props.moveToStep(1)}> + props.moveToStep(1)}> @@ -297,7 +297,7 @@ export function Step5Build(props){ 3. Command Selection - props.moveToStep(2)}> + props.moveToStep(2)}> @@ -305,10 +305,10 @@ export function Step5Build(props){ {props.buildOptions[2]?.map(c => (
{c} - e.stopPropagation()} + e.stopPropagation()} href={"/docs/agents/" + props.buildOptions[1].payload_type + "/commands/" + c} style={{marginLeft: "10px", float: "right"}} target="_blank"> - +
))} @@ -329,7 +329,7 @@ export function Step5Build(props){ 4. C2 Configuration - props.moveToStep(3)}> + props.moveToStep(3)}> @@ -378,4 +378,4 @@ export function Step5Build(props){ }
- */ \ No newline at end of file + */ diff --git a/MythicReactUI/src/components/pages/CreateWrapper/CreatePayloadNavigationButtons.js b/MythicReactUI/src/components/pages/CreateWrapper/CreatePayloadNavigationButtons.js index 3f26da54..8fa665f1 100644 --- a/MythicReactUI/src/components/pages/CreateWrapper/CreatePayloadNavigationButtons.js +++ b/MythicReactUI/src/components/pages/CreateWrapper/CreatePayloadNavigationButtons.js @@ -15,17 +15,17 @@ export function CreatePayloadNavigationButtons(props){ return (
{props.showExtraOptions && + {toLocalTime(props.payload.creation_time, me.user.view_utc_time)} {b64DecodeUnicode(props.payload.filemetum.filename_text)} +
+
{props.payload.description} - setOpenDetailedView(true)}> - + setOpenDetailedView(true)}> + diff --git a/MythicReactUI/src/components/pages/CreateWrapper/Step5Build.js b/MythicReactUI/src/components/pages/CreateWrapper/Step5Build.js index dead9783..d3d68d39 100644 --- a/MythicReactUI/src/components/pages/CreateWrapper/Step5Build.js +++ b/MythicReactUI/src/components/pages/CreateWrapper/Step5Build.js @@ -182,7 +182,7 @@ export function Step5Build(props){ OS: {props.buildOptions[1].os} - props.moveToStep(0)}> + props.moveToStep(0)}> @@ -231,7 +231,7 @@ export function Step5Build(props){ 2. Build Parameter Configuration - props.moveToStep(1)}> + props.moveToStep(1)}> @@ -253,7 +253,7 @@ export function Step5Build(props){ 3. Embedded Payload Configuration - props.moveToStep(2)}> + props.moveToStep(2)}> diff --git a/MythicReactUI/src/components/pages/EventFeed/EventFeedTable.js b/MythicReactUI/src/components/pages/EventFeed/EventFeedTable.js index 7085c028..53de4de1 100644 --- a/MythicReactUI/src/components/pages/EventFeed/EventFeedTable.js +++ b/MythicReactUI/src/components/pages/EventFeed/EventFeedTable.js @@ -1,36 +1,38 @@ import React from 'react'; import { EventFeedTableEvents } from './EventFeedTableEvents'; -import Typography from '@mui/material/Typography'; -import Paper from '@mui/material/Paper'; -import {useTheme} from '@mui/material/styles'; -import Pagination from '@mui/material/Pagination'; -import MythicTextField from "../../MythicComponents/MythicTextField"; -import SearchIcon from '@mui/icons-material/Search'; -import Tooltip from '@mui/material/Tooltip'; -import IconButton from '@mui/material/IconButton'; import AutoFixHighIcon from '@mui/icons-material/AutoFixHigh'; import HealingIcon from '@mui/icons-material/Healing'; import MenuItem from '@mui/material/MenuItem'; -import Select from '@mui/material/Select'; -import Grid from '@mui/material/Grid'; import {alertCount} from "../../../cache"; import {levelOptions} from "./EventFeed"; import {MythicPageBody} from "../../MythicComponents/MythicPageBody"; +import {MythicPageHeader, MythicPageHeaderChip} from "../../MythicComponents/MythicPageHeader"; +import {MythicTablePagination} from "../../MythicComponents/MythicTablePagination"; +import {MythicSearchField, MythicTableToolbar, MythicTableToolbarGroup, MythicToolbarButton, MythicToolbarSelect} from "../../MythicComponents/MythicTableToolbar"; +import {MythicEmptyState} from "../../MythicComponents/MythicStateDisplay"; const EventList = ({onUpdateLevel, onUpdateResolution, operationeventlog}) => { return (
- {operationeventlog.map( o => )} + {operationeventlog.length === 0 ? ( + + ) : ( + operationeventlog.map( o => ) + )}
) }; export function EventFeedTable(props){ - const theme = useTheme(); const [search, setSearch] = React.useState(""); const [level, setLevel] = React.useState("info"); @@ -51,30 +53,37 @@ export function EventFeedTable(props){ props.onLevelChange("warning (unresolved)"); } }, []); + const visibleEventsLabel = props.operationeventlog.length === 1 ? "1 shown" : `${props.operationeventlog.length} shown`; + const totalEventsLabel = props.pageData.totalCount === 1 ? "1 total" : `${props.pageData.totalCount} total`; return ( - - - - - - - - - - - - - , - style: {padding: 0} - }}/> - - - - - + + +
-
- - Total Results: {props.pageData.totalCount} -
+
); -} \ No newline at end of file +} diff --git a/MythicReactUI/src/components/pages/EventFeed/EventFeedTableEvents.js b/MythicReactUI/src/components/pages/EventFeed/EventFeedTableEvents.js index 14385fb9..89e442d8 100644 --- a/MythicReactUI/src/components/pages/EventFeed/EventFeedTableEvents.js +++ b/MythicReactUI/src/components/pages/EventFeed/EventFeedTableEvents.js @@ -6,8 +6,8 @@ import { meState } from '../../../cache'; import {useReactiveVar} from '@apollo/client'; import ListItem from '@mui/material/ListItem'; import ListItemText from '@mui/material/ListItemText'; -import {useTheme} from '@mui/material/styles'; import {EventFeedTableEventsActions} from './EventFeedTableEventsActions'; +import {MythicStatusChip} from '../../MythicComponents/MythicStatusChip'; const PREFIX = 'EventFeedTableEvents'; @@ -30,35 +30,23 @@ const StyledListItem = styled(ListItem)(( } })); -const GetPreAdornment = ({message}) => { - const theme = useTheme(); - const getColor = React.useCallback(() => { - if(message.warning){ - if(message.resolved){ - return theme.palette.success.main; - } else { - return theme.palette.error.main; - } - } - return theme.palette.info.main; - }, [theme, message.level, message.resolved, message.warning]); - const getSymbol = React.useCallback(() => { - if(message.warning){ - if(message.resolved){ - return "+"; - } else { - return "!"; - } - } - return "*"; - }, [theme, message.level, message.resolved, message.warning]); +const GetEventStatusChip = ({message}) => { + if(message.warning){ + return ( + + ); + } return ( - <> -

[

-

{getSymbol()}

-

]

- - ) + + ); } export function EventFeedTableEvents(props){ @@ -89,8 +77,8 @@ export function EventFeedTableEvents(props){ } secondary={ -
- +
+
                         {props.message}
                     
@@ -105,4 +93,3 @@ export function EventFeedTableEvents(props){ ); } - diff --git a/MythicReactUI/src/components/pages/EventFeed/EventFeedTableEventsActions.js b/MythicReactUI/src/components/pages/EventFeed/EventFeedTableEventsActions.js index b5a9cf29..edf33830 100644 --- a/MythicReactUI/src/components/pages/EventFeed/EventFeedTableEventsActions.js +++ b/MythicReactUI/src/components/pages/EventFeed/EventFeedTableEventsActions.js @@ -27,8 +27,9 @@ export function EventFeedTableEventsActions(props){ + size="small"> { @@ -45,4 +46,3 @@ export function EventFeedTableEventsActions(props){ ); } - diff --git a/MythicReactUI/src/components/pages/Eventing/CreateEventWorkflowDialog.js b/MythicReactUI/src/components/pages/Eventing/CreateEventWorkflowDialog.js index 6ba091fa..c62951fc 100644 --- a/MythicReactUI/src/components/pages/Eventing/CreateEventWorkflowDialog.js +++ b/MythicReactUI/src/components/pages/Eventing/CreateEventWorkflowDialog.js @@ -85,19 +85,24 @@ export function TestEventGroupFileDialog({onClose, initialWorkflow}){ } return ( - - Create and Verify Eventing Workflow - - - - - - - setOpenCreateEventingStepper(true)}> - - - + +
+
Create and verify eventing workflow
+
Edit workflow source and validate it before upload.
+
+
+ + + + + + + setOpenCreateEventingStepper(true)}> + + + +
{openEventStepRender.open && { @@ -115,20 +120,20 @@ export function TestEventGroupFileDialog({onClose, initialWorkflow}){ /> }
- + - - - -
) -} \ No newline at end of file +} diff --git a/MythicReactUI/src/components/pages/Eventing/CreateEventingStepper.js b/MythicReactUI/src/components/pages/Eventing/CreateEventingStepper.js index eb2c03c4..cd1c4ca3 100644 --- a/MythicReactUI/src/components/pages/Eventing/CreateEventingStepper.js +++ b/MythicReactUI/src/components/pages/Eventing/CreateEventingStepper.js @@ -1,14 +1,11 @@ import React from 'react'; -import Stepper from '@mui/material/Stepper'; -import Step from '@mui/material/Step'; -import StepLabel from '@mui/material/StepLabel'; import Button from '@mui/material/Button'; import DialogActions from '@mui/material/DialogActions'; import DialogContent from '@mui/material/DialogContent'; import DeleteIcon from '@mui/icons-material/Delete'; import { useReactiveVar } from '@apollo/client'; import {meState} from "../../../cache"; -import {Table, TableBody, TableContainer, TableHead, TableRow, Typography, IconButton, Paper, Switch} from '@mui/material'; +import {Table, TableBody, TableHead, TableRow, Typography, IconButton, Switch} from '@mui/material'; import FormControl from '@mui/material/FormControl'; import MenuItem from '@mui/material/MenuItem'; import TextField from '@mui/material/TextField'; @@ -31,6 +28,20 @@ import AddCircleIcon from '@mui/icons-material/AddCircle'; function getSteps(){ return ['Trigger Metadata', 'Steps', 'Confirm'] } +const stepDetails = [ + { + title: "Trigger metadata", + subtitle: "Name the workflow and define when it can run.", + }, + { + title: "Workflow steps", + subtitle: "Build the ordered actions, inputs, outputs, and dependencies.", + }, + { + title: "Review and create", + subtitle: "Validate the generated workflow before uploading it.", + }, +]; const debounceDelay = 500; const taskFields = [ "id", "agent_task_id", "display_id", "operation_id", "command_id", "command_name", "params", @@ -66,19 +77,19 @@ function CreateEventingStepperNavigationButtons(props){ const disabledButtons = (me?.user?.current_operation_id || 0) <= 0; return ( - - + + { files.length > 0 && - - {files?.map((f, i) => f.name).join(", ")} - +
+ {files?.map((f, i) => ( + {f.name} + ))} +
} -
- +
) } const getRunAsDescription = ({runAs}) => { @@ -631,120 +645,109 @@ const CreateEventingStep1 = ({finished, back, first, last, cancel, prevData}) => } }, [prevData]); return ( - <> - -
- - - - - - - - - Workflow Name - - setName(value)} value={name} - /> - - - - Description - - setDescription(value)} value={description} - /> - - - - Trigger - - - - {triggerOptions.map((opt) => ( - - {opt} - +
+
+
+
+
+
Workflow identity
+
Name and describe what this workflow is meant to do.
+
+
+
+
Workflow name
+ setName(value)} value={name} marginBottom={"0px"} /> +
+
+
Description
+ setDescription(value)} value={description} marginBottom={"0px"} /> +
+
+
+
+
+
Trigger behavior
+
{triggerOptionsData[trigger]?.description}
+
+
+
Trigger
+ + + {triggerOptions.map((opt) => ( + + {opt} + + ))} + + +
+
+
Trigger data
+ {triggerOptionsData[trigger]?.trigger_data?.length > 0 ? + (
+ + + + + + + + {triggerData?.map( t => ( + ))} - - - - {triggerOptionsData[trigger]?.description} - - - - - Trigger Data - - {triggerOptionsData[trigger]?.trigger_data?.length > 0 ? - (
- - - - - - - - {triggerData?.map( t => ( - - ))} - -
) : - ( - N/A - ) - } - - - - Run As - - - - {getRunAsDescription({runAs})} - - - - - Keywords - - - - {"Keywords provide additional ways for the workflow to get executed. These are custom words you provide that can then be supplied via API calls to kick off this workflow outside of the trigger method."} - - - - - Files - - - - {"Files you upload here (or associate with the workflow later on via the paperclip icon) are available in all the steps via the workflow.filename identifier."} - - - - - Environment - - This is a free form set of key-value pairs in JSON that you can provide to make available to all your steps for easy access. You can think of this kind of like global static variables. -

- -
-
- - -
+ + ) : + (
None
) + } +
+
+
+
+
Run context
+
{getRunAsDescription({runAs})}
+
+
+
Run as
+ +
+
+
+
+
Optional inputs
+
Keywords and files can be referenced by workflow steps later.
+
+
+
Keywords
+ +
+
+
Files
+ +
+
+
+
+
Environment
+
Provide JSON key-value pairs that every step can read.
+
+
+ +
+
+
+ - + ) } @@ -761,6 +764,38 @@ const getInputTypeDescription = (t) => { } return inputOptionsData["output"].description; } +const EventingStepConfigSection = ({title, description, children, className = ""}) => ( +
+
+
{title}
+ {description && +
{description}
+ } +
+
+ {children} +
+
+) +const EventingStepFieldBlock = ({label, description, required = false, children, className = ""}) => ( +
+
+ {label} + {required && + Required + } +
+ {description && +
{description}
+ } +
+ {children} +
+
+) +const EventingStepEmptyInline = ({children}) => ( +
{children}
+) const EventingStepInputs = ({updateStep, index, localInputOptions, step1Data, prevData}) => { const theme = useTheme(); const [localInputs, setLocalInputs] = React.useState([]); @@ -816,90 +851,93 @@ const EventingStepInputs = ({updateStep, index, localInputOptions, step1Data, pr } }, [prevData]); return ( - <> - {localInputs.map( (d, i) => ( - -
- removeLocalInput(i)}> - +
+
+ {localInputs.length === 0 && + No inputs configured. + } + {localInputs.map( (d, i) => ( +
+ removeLocalInput(i)}> + - onChangeLocalInputName(i, value)} - value={localInputs[i].name} - marginBottom={"0px"}/> - - { - changeLocalInputType(e, i) - }} - > - {localInputOptions.map((opt) => ( - - {opt} - - ))} - - - {localInputs[i].type === "env" && triggerOptionsData[step1Data.trigger].env.length > 0 ? - ( -
- - 0} - value={localInputs[i].value_type} - onChange={(e) => { - onChangeLocalInputValueType(e, i) - }} - > - {triggerOptionsData[step1Data.trigger].env.map((opt) => ( - - {opt} - - ))} - - - OR - onChangeLocalInputValue(i, value)} - value={localInputs[i].value} - marginBottom={"0px"} /> -
- ) - : - ( onChangeLocalInputValue(i, value)} - value={localInputs[i].value} - marginBottom={"0px"} InputProps={{ - startAdornment: - - {getInputAdornment(localInputs[i].type)} - - }}/>) - } +
+
+ onChangeLocalInputName(i, value)} + value={localInputs[i].name} + marginBottom={"0px"}/> + + { + changeLocalInputType(e, i) + }} + > + {localInputOptions.map((opt) => ( + + {opt} + + ))} + + + {localInputs[i].type === "env" && triggerOptionsData[step1Data.trigger].env.length > 0 ? + ( +
+ + 0} + value={localInputs[i].value_type} + onChange={(e) => { + onChangeLocalInputValueType(e, i) + }} + > + {triggerOptionsData[step1Data.trigger].env.map((opt) => ( + + {opt} + + ))} + + + or +
+ onChangeLocalInputValue(i, value)} + value={localInputs[i].value} + marginBottom={"0px"} /> +
+
+ ) + : + ( onChangeLocalInputValue(i, value)} + value={localInputs[i].value} + marginBottom={"0px"} InputProps={{ + startAdornment: + + {getInputAdornment(localInputs[i].type)} + + }}/>) + } +
+
{getInputTypeDescription(localInputs[i].type)}
+
- {getInputTypeDescription(localInputs[i].type)} -
- - ))} -
+ - +
) } const EventingStepOutputs = ({updateStep, index, selectedAction, prevData}) => { @@ -943,59 +981,66 @@ const EventingStepOutputs = ({updateStep, index, selectedAction, prevData}) => { } }, [prevData]); return ( - <> - {localOutputs.map( (d, i) => ( - -
- removeLocalOutput(i)}> - +
+
+ {localOutputs.length === 0 && + No outputs configured. + } + {localOutputs.map( (d, i) => ( +
+ removeLocalOutput(i)}> + - onChangeLocalOutputName(i, value)} - value={localOutputs[i].name} - marginBottom={"0px"}/> - {outputOptionsData[selectedAction].output_fields.length > 0 && - <> - - 0} - value={localOutputs[i].type} - onChange={(e) => { - onChangeLocalOutputType(e, i) - }} - > - {outputOptionsData[selectedAction].output_fields.map((opt) => ( - - {opt} - - ))} - - - OR - - } - - onChangeLocalOutputValue(i, value)} - value={localOutputs[i].value} - marginBottom={"0px"}/> +
+
+ onChangeLocalOutputName(i, value)} + value={localOutputs[i].name} + marginBottom={"0px"}/> + {outputOptionsData[selectedAction].output_fields.length > 0 ? ( +
+ + 0} + value={localOutputs[i].type} + onChange={(e) => { + onChangeLocalOutputType(e, i) + }} + > + {outputOptionsData[selectedAction].output_fields.map((opt) => ( + + {opt} + + ))} + + + or +
+ onChangeLocalOutputValue(i, value)} + value={localOutputs[i].value} + marginBottom={"0px"}/> +
+
+ ) : ( + onChangeLocalOutputValue(i, value)} + value={localOutputs[i].value} + marginBottom={"0px"}/> + )} +
+
- - ))} -
+ - +
) } const EventingStepActionDataTaskCreate = ({allSteps, updateStep, index, prevData}) => { @@ -1307,7 +1352,6 @@ const EventingStepActionDataCustomFunction = ({allSteps, updateStep, index, prev -

@@ -1438,13 +1482,13 @@ const EventingStepActionDataConditionalCheck = ({allSteps, updateStep, index, pr alignItems: "center", marginTop: "8px" }} key={"step" + s}> - removeStep(i)}> - + removeStep(i)}> +
))} - - + + @@ -1691,7 +1735,6 @@ const EventingStepActionDataTaskIntercept = ({allSteps, updateStep, index, prevD -
@@ -1753,7 +1796,6 @@ const EventingStepActionDataResponseIntercept = ({allSteps, updateStep, index, p -
@@ -1990,7 +2032,6 @@ const EventingStepActionDataSendWebhook = ({allSteps, updateStep, index, prevDat -

@@ -2041,14 +2082,13 @@ const actionOptionsData = { } const EventingStep = ({step, allSteps, updateStep, index, step1Data}) => { - const theme = useTheme(); const [name, setName] = React.useState(step.name); const [description, setDescription] = React.useState(step.description); const [selectedAction, setSelectedAction] = React.useState(step.action); const [dependsOnOptions, setDependsOnOptions] = React.useState([]); const [dependsOn, setDependsOn] = React.useState([]); const [updatedActionOptions, setUpdatedActionOptions] = React.useState([]); - const [continueOnError, setContinueOnError] = React.useState(step.continue_on_error); + const [continueOnError, setContinueOnError] = React.useState(step.continue_on_error || false); const [localInputOptions, setLocalInputOptions] = React.useState([]); const onChangeName = (name, value, error) => { @@ -2100,7 +2140,7 @@ const EventingStep = ({step, allSteps, updateStep, index, step1Data}) => { setDependsOn(newDependsOn); updateStep(index, "depends_on", newDependsOn); } - const ActionDataElement = actionOptionsData[selectedAction].element; + const ActionDataElement = actionOptionsData[selectedAction]?.element; const onChangeContinueOnError = (evt) => { setContinueOnError(evt.target.checked) updateStep(index, "continue_on_error", evt.target.checked); @@ -2133,132 +2173,143 @@ const EventingStep = ({step, allSteps, updateStep, index, step1Data}) => { } }, [allSteps, name]); return ( - - - - - - - - - - - Name - - +
+
+
{name || `Step ${index + 1}`}
+
{description || "Describe what this step does and how it should run."}
+
+
+ {selectedAction} + +
+
+
+ +
+ + - - - - Description - - + + - - - - Action - - - - {updatedActionOptions.map((opt) => ( - - {opt} - - ))} - - - - - - - - - {actionOptionsData[selectedAction]?.description} - - - - - Inputs - - - - - - Action Data - - {ActionDataElement !== null && - + +
+
+ + + + + {updatedActionOptions.map((opt) => ( + + {opt} + + ))} + + + + +
+ + + + + + +
+ +
+ {ActionDataElement !== null && ActionDataElement !== undefined && + + } +
+
+ +
+
+ {dependsOn.length === 0 && + No dependencies configured. } - - - - Outputs - - - - - - Depends On - {dependsOn.map((d, i) => ( -
- removeDependsOn(i)}/> - - { - changeDependsOn(e, i) - }} - > - {dependsOnOptions.map((opt) => ( - - {opt} - - ))} - - +
+ removeDependsOn(i)}> + + +
+ + { + changeDependsOn(e, i) + }} + > + {dependsOnOptions.map((opt) => ( + + {opt} + + ))} + + +
))} - - - - - Continue on Error - - - - - -
-
- +
+ + + + + ) } const inputsHelp = ` @@ -2268,7 +2319,7 @@ Steps: Inputs: Inputs are a dictionary with key/value pairs. When starting a step, Mythic iterates through the inputs and checks to see if the input's "key" is available in the step's action data. If it's present, then that key is swapped out with the corresponding value. This happens for all the inputs before the final action data is passed along to the action. This allows you to provide "placeholders" in your action data for things that you don't know while you're creating this workflow (for example: the callback id for a new callback trigger). -Action Data: +Action data: Action data is the set of key/value pairs that are needed by the action to perform the required action. This is highly specific to the corresponding action and has been updated in the UI for each action that's available with required fields marked as required and descriptions for all other fields. Outputs: @@ -2277,6 +2328,8 @@ Outputs: const CreateEventingStep2 = ({finished, back, first, last, cancel, prevData, step1Data}) => { const [steps, setSteps] = React.useState(prevData ? prevData : []); const [displayHelp, setDisplayHelp] = React.useState(false); + const [activeStepIndex, setActiveStepIndex] = React.useState(0); + const stepRefs = React.useRef({}); const addStep = () => { setSteps([...steps, { name: "", @@ -2294,6 +2347,7 @@ const CreateEventingStep2 = ({finished, back, first, last, cancel, prevData, ste const newSteps = [...steps]; newSteps.splice(i, 1); setSteps(newSteps); + setActiveStepIndex((current) => Math.max(0, Math.min(current, newSteps.length - 1))); } const updateStep = (i, name, value) => { const newSteps = steps.map( (s, index) => { @@ -2307,20 +2361,57 @@ const CreateEventingStep2 = ({finished, back, first, last, cancel, prevData, ste const finishedStep2 = () => { finished(steps); } + const stepScrollRef = React.useRef(null); + const updateActiveStepFromScroll = React.useCallback(() => { + const scrollContainer = stepScrollRef.current; + if(scrollContainer === null || steps.length === 0){ + return; + } + const containerRect = scrollContainer.getBoundingClientRect(); + const marker = containerRect.top + Math.min(120, containerRect.height * 0.25); + const bestStep = steps.reduce((prev, cur, i) => { + const stepElement = stepRefs.current[i]; + if(stepElement === undefined || stepElement === null){ + return prev; + } + const stepRect = stepElement.getBoundingClientRect(); + if(stepRect.top <= marker && stepRect.bottom >= marker){ + return {index: i, distance: 0}; + } + const distance = Math.min(Math.abs(stepRect.top - marker), Math.abs(stepRect.bottom - marker)); + if(distance < prev.distance){ + return {index: i, distance}; + } + return prev; + }, {index: activeStepIndex, distance: Number.MAX_SAFE_INTEGER}); + setActiveStepIndex((current) => current === bestStep.index ? current : bestStep.index); + }, [activeStepIndex, steps]); + const scrollToStep = (i) => { + setActiveStepIndex(i); + stepRefs.current[i]?.scrollIntoView({behavior: "smooth", block: "start"}); + } React.useEffect( () => { if(prevData !== undefined){ setSteps(prevData); } }, [prevData]); return ( - <> - - +
+
+
+
{steps.length} configured {steps.length === 1 ? "step" : "steps"}
+
{step1Data?.trigger || "manual"} trigger
+
+
+ + +
+
{displayHelp && { @@ -2333,32 +2424,63 @@ const CreateEventingStep2 = ({finished, back, first, last, cancel, prevData, ste />} /> } -
- - - - - - - - - {steps.map( (s, i) => ( - - - removeStep(i)}> - - - - - - - - ))} - -
+
+ {steps.length > 0 && +
+
+
Step index
+
Jump to a step
+
+
+ {steps.map((s, i) => ( + + ))} +
+
+ } +
+ {steps.length === 0 ? ( +
+
No steps configured
+
Add a step to start building this workflow.
+
+ ) : steps.map( (s, i) => ( +
{ + if(element){ + stepRefs.current[i] = element; + } + }} + > +
+
+
Step {i + 1}
+
{s.name || "Unnamed step"}
+
+ removeStep(i)}> + + +
+ +
+ ))} +
- +
) } @@ -2495,12 +2617,13 @@ const CreateEventingStep3 = ({finished, back, first, last, cancel, prevData, ste } } return ( - <> -
-
- +
+
+
+
+ - - + {openEventStepRender.open && }
+
+
+
- +
) } export function CreateEventingStepper(props){ @@ -2567,37 +2693,26 @@ export function CreateEventingStepper(props){ const finished = () => { props.onClose(null, true); } + const activeStepDetails = stepDetails[activeStep] || stepDetails[0]; return ( - -
- - {steps.map((label, index) => ( - - {label} - - ))} - + +
+
+
+
+
Create eventing workflow
+
Build a workflow from trigger metadata through generated output.
+
+ Step {activeStep + 1} of {steps.length} +
+
+
+
+
{activeStepDetails.title}
+
{activeStepDetails.subtitle}
+
{getStepContent(activeStep)} +
); diff --git a/MythicReactUI/src/components/pages/Eventing/EventApprovalDialog.js b/MythicReactUI/src/components/pages/Eventing/EventApprovalDialog.js index a3390915..1fbc5b47 100644 --- a/MythicReactUI/src/components/pages/Eventing/EventApprovalDialog.js +++ b/MythicReactUI/src/components/pages/Eventing/EventApprovalDialog.js @@ -16,6 +16,7 @@ import {toLocalTime} from "../../utilities/Time"; import CheckCircleTwoToneIcon from '@mui/icons-material/CheckCircleTwoTone'; import CancelTwoToneIcon from '@mui/icons-material/CancelTwoTone'; import {snackActions} from "../../utilities/Snackbar"; +import {MythicStatusChip} from "../../MythicComponents/MythicStatusChip"; const updateApprovalStatusMutation = gql` mutation updateApprovalStatus($eventgroupapproval_id: Int!, $approved: Boolean!) { @@ -62,7 +63,7 @@ export function EventGroupTableRunAsDialog({eventgroupapprovals, me, onClose, se Individual users must approve workflows to run under their account and operation leads must approve bot workflows.
- Run As: {selectedEventGroup.run_as}
+ Run as: {selectedEventGroup.run_as}
{getRunAsHelp(selectedEventGroup.run_as)}
@@ -80,43 +81,40 @@ export function EventGroupTableRunAsDialog({eventgroupapprovals, me, onClose, se {e.operator.username} {e.approved ? ( - <> - - - +
) : e.created_at === e.updated_at ? ( - <> - - - +
) : ( - <> - - + } /> - +
)} @@ -132,7 +130,7 @@ export function EventGroupTableRunAsDialog({eventgroupapprovals, me, onClose, se - diff --git a/MythicReactUI/src/components/pages/Eventing/EventEditEventGroupDialog.js b/MythicReactUI/src/components/pages/Eventing/EventEditEventGroupDialog.js index a6159826..5c2484bb 100644 --- a/MythicReactUI/src/components/pages/Eventing/EventEditEventGroupDialog.js +++ b/MythicReactUI/src/components/pages/Eventing/EventEditEventGroupDialog.js @@ -114,14 +114,13 @@ export function EventGroupTableEditDialog({onClose, selectedEventGroup, includeS - - ); } - diff --git a/MythicReactUI/src/components/pages/Eventing/EventFileManageDialog.js b/MythicReactUI/src/components/pages/Eventing/EventFileManageDialog.js index ce6c44dd..630db914 100644 --- a/MythicReactUI/src/components/pages/Eventing/EventFileManageDialog.js +++ b/MythicReactUI/src/components/pages/Eventing/EventFileManageDialog.js @@ -21,7 +21,7 @@ import MythicStyledTableCell from "../../MythicComponents/MythicTableCell"; import {faPhotoVideo} from '@fortawesome/free-solid-svg-icons'; import {FontAwesomeIcon} from '@fortawesome/react-fontawesome'; import DeleteIcon from '@mui/icons-material/Delete'; -import {Link, Typography} from '@mui/material'; +import {IconButton, Link, Typography} from '@mui/material'; import {MythicConfirmDialog} from "../../MythicComponents/MythicConfirmDialog"; import CloudUploadIcon from '@mui/icons-material/CloudUpload'; @@ -38,15 +38,19 @@ export function EventFileManageDialog({onClose, selectedEventGroup}) { return ( - Add or Remove Files associated with this workflow - +
+ Add or Remove Files associated with this workflow + +
@@ -64,7 +68,7 @@ export function EventFileManageDialog({onClose, selectedEventGroup}) { - @@ -102,11 +106,15 @@ function EventFileManageDialogTableRow({eventFile}) { {openDelete && {setOpenDelete(false);}} onSubmit={onAcceptDelete} open={openDelete}/> } - {setOpenDelete(true);}}/> + + {setOpenDelete(true);}}> + + + - + + + {openPreviewMediaDialog && - diff --git a/MythicReactUI/src/components/pages/Eventing/EventGroupInstancesTable.js b/MythicReactUI/src/components/pages/Eventing/EventGroupInstancesTable.js index 92433ba1..404a9737 100644 --- a/MythicReactUI/src/components/pages/Eventing/EventGroupInstancesTable.js +++ b/MythicReactUI/src/components/pages/Eventing/EventGroupInstancesTable.js @@ -274,33 +274,33 @@ function EventGroupInstancesTableMaterialReactTablePreMemo({eventgroups, me, set return case "Status": return ( -
+
{ selectedInstanceID === 0 ? ( - {setSelectedInstance(row.id);}} > - + {setSelectedInstance(row.id);}} > + ) : ( - {setSelectedInstance(0);}} > - + {setSelectedInstance(0);}} > + ) } - {openViewInstanceLargeDialog(row)}}> - + {openViewInstanceLargeDialog(row)}}> + - onSaveToClipboard(row)}> - + onSaveToClipboard(row)}> +
@@ -344,26 +344,26 @@ function EventGroupInstancesTableMaterialReactTablePreMemo({eventgroups, me, set return case "Action": return ( -
+
{row.end_timestamp === null ? ( {onOpenCancelDialog({id: row.id});}} - color={"warning"}> - + className="mythic-table-row-icon-action mythic-table-row-icon-action-hover-warning" size="small"> + ) : row.status === "error" || row.status === "cancelled" ? ( {onOpenRetryDialog({id: row.id});}} - color={"warning"}> - + className="mythic-table-row-icon-action mythic-table-row-icon-action-hover-warning" size="small"> + ) : row.status === "success" ? ( {onOpenRunAgainDialog({id: row.id});}} - color={"success"}> - + className="mythic-table-row-icon-action mythic-table-row-icon-action-hover-success" size="small"> + ) : null} @@ -471,4 +471,4 @@ function EventGroupInstancesTableMaterialReactTablePreMemo({eventgroups, me, set ) } -export const EventGroupInstancesTableMaterialReactTable = React.memo(EventGroupInstancesTableMaterialReactTablePreMemo) \ No newline at end of file +export const EventGroupInstancesTableMaterialReactTable = React.memo(EventGroupInstancesTableMaterialReactTablePreMemo) diff --git a/MythicReactUI/src/components/pages/Eventing/EventGroupTable.js b/MythicReactUI/src/components/pages/Eventing/EventGroupTable.js index d0dc626e..ea767f94 100644 --- a/MythicReactUI/src/components/pages/Eventing/EventGroupTable.js +++ b/MythicReactUI/src/components/pages/Eventing/EventGroupTable.js @@ -1,8 +1,6 @@ -import React, {useCallback} from 'react'; +import React from 'react'; import { gql, useMutation } from '@apollo/client'; import {snackActions} from '../../utilities/Snackbar'; -import {useTheme} from '@mui/material/styles'; -import Typography from '@mui/material/Typography'; import Button from '@mui/material/Button'; import {toLocalTime} from "../../utilities/Time"; import NotificationsActiveTwoToneIcon from '@mui/icons-material/NotificationsActiveTwoTone'; @@ -16,7 +14,6 @@ import { EventStepRenderFlowWithProvider } from "./EventStepRender"; import OpenInNewTwoToneIcon from '@mui/icons-material/OpenInNewTwoTone'; -import PermMediaTwoToneIcon from '@mui/icons-material/PermMediaTwoTone'; import AddCircleIcon from '@mui/icons-material/AddCircle'; import Table from '@mui/material/Table'; import TableBody from '@mui/material/TableBody'; @@ -44,6 +41,7 @@ import {EventGroupConsumingContainersDialog} from "./EventGroupConsumingContaine import CalendarMonthTwoToneIcon from '@mui/icons-material/CalendarMonthTwoTone'; import {EventGroupTableEditDialog} from "./EventEditEventGroupDialog"; import EditIcon from '@mui/icons-material/Edit'; +import {MythicPageHeader, MythicPageHeaderChip} from "../../MythicComponents/MythicPageHeader"; const updateDeleteStatusMutation = gql(` mutation updateDeleteStatusMutation($eventgroup_id: Int!, $deleted: Boolean!) { @@ -142,75 +140,73 @@ export function EventGroupTable({selectedEventGroup, me, showInstances, showGrap triggerManually({variables: {eventgroup_id: selectedEventGroup.id}}); } return ( -
+
{selectedEventGroup.id === 0 && - All Eventing Runs + } {selectedEventGroup.id !== 0 && -
- {selectedEventGroup.deleted ? ( -
- - - -
- ) : ( -
- - - - - {openDeleteDialog && - { - setOpenDeleteDialog(false); - }} onSubmit={onAcceptDelete} open={openDeleteDialog}/> - } -
- )} - {selectedEventGroup?.name} - {selectedEventGroup.active ? - ( + <> + - - - - {openActiveDialog && - { - setOpenActiveDialog(false); - }} onSubmit={onAcceptActive} open={openActiveDialog} - acceptText={"Disable"} - /> - } + : } + label={selectedEventGroup.active ? "Enabled" : "Disabled"} + status={selectedEventGroup.active ? "success" : undefined} + className={selectedEventGroup.active ? "" : "mythic-eventing-header-chip-disabled"} + /> + + + + {selectedEventGroup.deleted && } - - ) : - ( - - - - ) + } + actions={ + <> + {selectedEventGroup.deleted ? ( + + ) : ( + + )} + {selectedEventGroup.active ? ( + + ) : ( + + )} + + } + /> + {openDeleteDialog && + { + setOpenDeleteDialog(false); + }} onSubmit={onAcceptDelete} open={openDeleteDialog}/> + } + {openActiveDialog && + { + setOpenActiveDialog(false); + }} onSubmit={onAcceptActive} open={openActiveDialog} + acceptText={"Disable"} + /> } -
{selectedEventGroup?.description} @@ -220,7 +216,7 @@ export function EventGroupTable({selectedEventGroup, me, showInstances, showGrap Keywords Context Env - Run As + Run as Actions @@ -244,8 +240,8 @@ export function EventGroupTable({selectedEventGroup, me, showInstances, showGrap {selectedEventGroup.trigger_data && { setOpenTriggerDataView(true); - }} size={"small"} variant={"outlined"} color={"info"} style={{}}> - + }} className="mythic-table-row-icon-action mythic-table-row-icon-action-hover-info" size="small"> + } @@ -253,8 +249,8 @@ export function EventGroupTable({selectedEventGroup, me, showInstances, showGrap {selectedEventGroup.environment && { setOpenEnvView(true); - }} size={"small"} variant={"outlined"} color={"info"} style={{}}> - + }} className="mythic-table-row-icon-action mythic-table-row-icon-action-hover-info" size="small"> + } @@ -263,16 +259,16 @@ export function EventGroupTable({selectedEventGroup, me, showInstances, showGrap {selectedEventGroup.approved_to_run ? ( - setOpenApprovalDialog(true)}> - + setOpenApprovalDialog(true)}> + ) : ( - setOpenApprovalDialog(true)}> - + setOpenApprovalDialog(true)}> + @@ -284,39 +280,39 @@ export function EventGroupTable({selectedEventGroup, me, showInstances, showGrap +
- {setOpenEditDialog(true)}}> - + {setOpenEditDialog(true)}}> + {selectedEventGroup.trigger === "manual" && - - + + } { setOpenEventStepRender(true); - }} size={"small"} variant={"outlined"} color={"secondary"} style={{}}> - + }} className="mythic-table-row-icon-action mythic-table-row-icon-action-hover-info" size="small"> + { setOpenFileView(true); - }} size={"small"} color={"secondary"} style={{}}> - - + }} className="mythic-table-row-icon-action mythic-table-row-icon-action-hover-success" size="small"> + { setOpenFileManageView(true); - }} size={"small"} variant={"outlined"} color={"secondary"} style={{}}> + }} className="mythic-table-row-icon-action mythic-table-row-icon-action-hover-info" size="small"> - + @@ -324,8 +320,8 @@ export function EventGroupTable({selectedEventGroup, me, showInstances, showGrap { setOpenTriggerKeyword(true); - }} size={"small"} variant={"outlined"} color={"secondary"} style={{}}> - + }} className="mythic-table-row-icon-action mythic-table-row-icon-action-hover-success" size="small"> + } @@ -334,20 +330,20 @@ export function EventGroupTable({selectedEventGroup, me, showInstances, showGrap { setOpenConsumingContainerDialog(true); - }} size={"small"} variant={"outlined"} color={"secondary"}> - 0 ? "error": "info"} style={{marginRight: "-3px"}} /> + }} className={`mythic-table-row-icon-action ${consumingContainersErrors > 0 ? "mythic-table-row-icon-action-danger" : "mythic-table-row-icon-action-hover-info"}`} size="small"> + } +
- -
+ } {showGraph && } {showInstances && { if(selectedInstanceID > 0){ return @@ -467,13 +462,7 @@ function RenderSteps({selectedEventGroup, selectedInstanceID}){ } return ( -
+
{getRenderer()}
) diff --git a/MythicReactUI/src/components/pages/Eventing/EventStepRender.js b/MythicReactUI/src/components/pages/Eventing/EventStepRender.js index 94e9893d..f806ef1e 100644 --- a/MythicReactUI/src/components/pages/Eventing/EventStepRender.js +++ b/MythicReactUI/src/components/pages/Eventing/EventStepRender.js @@ -3,9 +3,7 @@ import {gql, useQuery, useSubscription, useMutation, useLazyQuery} from '@apollo import Typography from '@mui/material/Typography'; import DialogActions from '@mui/material/DialogActions'; import DialogContent from '@mui/material/DialogContent'; -import DialogTitle from '@mui/material/DialogTitle'; import Button from '@mui/material/Button'; -import Paper from '@mui/material/Paper'; import {Link} from '@mui/material'; import createLayout, {getSourcePosition, getTargetPosition} from "../Callbacks/C2PathDialog"; import {ReactFlow, @@ -27,7 +25,6 @@ import HighlightOffIcon from '@mui/icons-material/HighlightOff'; import {adjustDurationOutput, adjustOutput} from "./EventGroupInstancesTable"; import Moment from 'react-moment'; import {MythicDialog} from "../../MythicComponents/MythicDialog"; -import MythicTableCell from "../../MythicComponents/MythicTableCell"; import Table from '@mui/material/Table'; import TableBody from '@mui/material/TableBody'; import TableCell from '@mui/material/TableCell'; @@ -59,6 +56,8 @@ import {MythicConfirmDialog} from "../../MythicComponents/MythicConfirmDialog"; import {payloadsDelete} from "../Payloads/Payloads"; import DeleteIcon from '@mui/icons-material/Delete'; import {getStringSize} from "../Callbacks/ResponseDisplayTable"; +import {MythicPageHeader} from "../../MythicComponents/MythicPageHeader"; +import {MythicEmptyState, MythicErrorState, MythicLoadingState} from "../../MythicComponents/MythicStateDisplay"; const getEventSteps = gql` @@ -361,19 +360,212 @@ const GetTimeDuration = ({data, customStyle}) => { ) } +const getStatusLabel = (status) => { + switch(status || ""){ + case "success": + return "Success"; + case "running": + return "Running"; + case "error": + return "Error"; + case "cancelled": + return "Cancelled"; + case "skipped": + return "Skipped"; + case "queued": + return "Queued"; + default: + return "Waiting"; + } +} +const getStatusClass = (status) => { + switch(status || ""){ + case "success": + return "success"; + case "running": + return "running"; + case "error": + return "error"; + case "cancelled": + return "cancelled"; + case "skipped": + return "skipped"; + default: + return "waiting"; + } +} +const hasEventingStatus = (status) => status !== undefined && status !== null && status !== ""; +const getEventingStatusClass = (status) => hasEventingStatus(status) ? getStatusClass(status) : "configured"; +const EventingStatusChip = ({data}) => { + const hasStatus = hasEventingStatus(data?.status); + return ( + + {hasStatus ? : } + {hasStatus ? getStatusLabel(data?.status) : "Configured"} + + ) +} +const EventingDetailChip = ({label, value}) => ( + value === undefined || value === null || value === "" ? null : + + {label} + {value} + +) +const stringifyEventingValue = (value) => { + if(value === undefined || value === null || value === ""){ + return ""; + } + if(typeof value === "string"){ + return value; + } + try{ + return JSON.stringify(value, null, 2); + }catch(error){ + return String(value); + } +} +const EventingCodeBlock = ({value, emptyText="No data"}) => { + const displayValue = stringifyEventingValue(value); + return ( +
+            {displayValue === "" ? emptyText : displayValue}
+        
+ ) +} +const EventingDetailSection = ({title, subtitle, count, actions, children, className = "", collapsible = false, defaultExpanded = false}) => { + const [expanded, setExpanded] = React.useState(defaultExpanded); + const isExpanded = collapsible ? expanded : true; + const hasCount = count !== undefined && count !== null; + const hasPositiveCount = Number(count) > 0; + const toggleExpanded = () => { + if(collapsible){ + setExpanded((expanded) => !expanded); + } + } + return ( +
+
+ {collapsible ? ( + + ) : ( +
+
{title}
+ {subtitle && +
{subtitle}
+ } +
+ )} +
+ {hasCount && + {count} + } + {actions} +
+
+ {isExpanded && +
+ {children} +
+ } +
+ ) +} +const EventingSectionEmpty = ({title, description}) => ( +
+ +
+) +const EventingMetadataPair = ({label, original, instance, originalLabel="Configured", instanceLabel="Runtime"}) => ( +
+
{label}
+
+
+
{originalLabel}
+ +
+
+
{instanceLabel}
+ +
+
+
+) +const EventingHeaderTitle = ({statusData, title}) => ( + + {statusData && } + {title} + +) +const EventingHeaderDuration = ({data}) => ( +
+ Duration + +
+) +const EventingDialogTitle = ({actions, meta, statusData, subtitle, title}) => ( +
+ } + sx={{m: 0}} + /> +
+) +const EventingDialogState = ({description, onClose, title, type = "loading"}) => { + const StateComponent = type === "error" ? MythicErrorState : MythicLoadingState; + return ( + <> + + + + + + + + + ) +} function EventNode({data}) { + const sourcePosition = getSourcePosition(data["elk.direction"]); + const targetPosition = getTargetPosition(data["elk.direction"]); return ( -
-
- - {data.name} + <> + +
+
+ + {data.name} +
+
+ {data.action && + {data.action} + } + {data.status && + + } +
- {data.status && - - } - -
+ + ) } @@ -482,12 +674,10 @@ function EventStepRender({selectedEventGroup, useSuppliedData}) { position: {x: 0, y: 0}, type: "eventNode", maxNameLength: maxNameLength[node.order], - parentId: `groupEventNode-${node.order}`, - group: `groupEventNode-${node.order}`, - extent: "parent", data: { label: node.name, ...node, + graph_id: useSuppliedData ? `${node.id}-${node.order}-${node.name}` : `${node.id}`, id: useSuppliedData ? `${node.id}-${node.order}-${node.name}` : `${node.id}`, } } @@ -535,13 +725,13 @@ function EventStepRender({selectedEventGroup, useSuppliedData}) { } let tmpEdge = { id: `e${destination.id}-${source.id}`, - target: `groupEventNode-${destination.order}`, - source: `groupEventNode-${source.order}`, + target: destination.graph_id || destination.id, + source: source.graph_id || source.id, label: "", animated: true, data: { - source: {...destination, parentId: `groupEventNode-${destination.order}`}, - target: {...source, parentId: `groupEventNode-${source.order}`}, + source: {...destination}, + target: {...source}, } }; tmpEdge.markerEnd = { @@ -573,7 +763,7 @@ function EventStepRender({selectedEventGroup, useSuppliedData}) { } } setGraphData({ - groups: parentIds, + groups: [], nodes: tempNodes, edges: tempEdges }) @@ -603,7 +793,7 @@ function EventStepRender({selectedEventGroup, useSuppliedData}) { })(); }, [graphData]); return ( -
+
{selectedEventGroup.id > 0 && - - EventGroup: {selectedEventGroup.id} - +
Event group {selectedEventGroup.id}
} - +
{openContextMenu &&
{contextMenu.map( (m) => ( - @@ -768,18 +956,15 @@ function EventStepInstanceRender({selectedEventGroupInstance}) { }, {}); const tempNodes = steps.map(node => { return { - id: `${node.eventstep.id}`, + id: `${node.id}`, position: {x: 0, y: 0}, type: "eventNode", maxNameLength: maxNameLength[node.order], - parentId: `groupEventNode-${node.order}`, - group: `groupEventNode-${node.order}`, - dragHandle: '.eventnode-drag-handle', - extent: "parent", data: { label: node.eventstep.name, ...node, ...node.eventstep, + graph_id: `${node.id}`, id: node.id //preserve the ID being for the instance } } @@ -827,13 +1012,13 @@ function EventStepInstanceRender({selectedEventGroupInstance}) { } let tmpEdge = { id: `e${destination.id}-${source.id}`, - target: `groupEventNode-${destination.order}`, - source: `groupEventNode-${source.order}`, + target: destination.graph_id || destination.id, + source: source.graph_id || source.id, label: "", animated: true, data: { - source: {...destination, parentId: `groupEventNode-${destination.order}`}, - target: {...source, parentId: `groupEventNode-${source.order}`}, + source: {...destination}, + target: {...source}, } }; tmpEdge.markerEnd = { @@ -865,7 +1050,7 @@ function EventStepInstanceRender({selectedEventGroupInstance}) { } } setGraphData({ - groups: parentIds, + groups: [], nodes: tempNodes, edges: tempEdges }) @@ -895,7 +1080,7 @@ function EventStepInstanceRender({selectedEventGroupInstance}) { })(); }, [graphData]); return ( -
+
- - Instance: {selectedEventGroupInstance} - - +
Instance {selectedEventGroupInstance}
+
{openContextMenu &&
{contextMenu.map( (m) => ( - @@ -954,25 +1137,17 @@ function EventStepInstanceRender({selectedEventGroupInstance}) { } export function EventStepRenderDialog({selectedEventGroup, onClose, useSuppliedData}) { - const theme = useTheme(); return ( <> - -
- - {selectedEventGroup.name} - -
- - - {selectedEventGroup.description} - -
- + + - - @@ -980,22 +1155,18 @@ export function EventStepRenderDialog({selectedEventGroup, onClose, useSuppliedD ) } export function EventStepInstanceRenderDialog({selectedEventGroup, selectedEventGroupInstance, onClose}) { - const theme = useTheme(); return ( <> - - Workflow: {selectedEventGroup.name} -
- - {selectedEventGroup.description} - - -
- + } + /> + - - @@ -1041,264 +1212,140 @@ function EventStepInstanceDetailDialog({selectedEventStepInstance, onClose}) { onError: (data) => { } }) - if (loading || !data.eventstepinstance_by_pk){ - return ( - <> - Loading... - - - - - - ) + if (loading){ + return } - if (!data){ - return ( - <> - Failed to find step data - - - - - - ) + if (!data?.eventstepinstance_by_pk){ + return } + const stepInstance = data.eventstepinstance_by_pk; + const stepDefinition = stepInstance.eventstep; + const hasStdout = stepInstance.stdout !== undefined && stepInstance.stdout !== null && stepInstance.stdout !== ""; + const hasStderr = stepInstance.stderr !== undefined && stepInstance.stderr !== null && stepInstance.stderr !== ""; return ( <> - -
-
-
- - {data.eventstepinstance_by_pk.eventstep.name} -
- - {data.eventstepinstance_by_pk.eventstep.description} - - - Depends On: {data.eventstepinstance_by_pk.eventstep.depends_on.join(", ")} - -
- -
- Duration: -
-
- -
- - - Action: {data.eventstepinstance_by_pk.eventstep.action} - - - {setExpandStepTable(!expandStepTable)}} expanded={expandStepTable} - > - } aria-controls={`panel1c-stdout`}> - Original and Instance Metadata - - - - - - - - Original Metadata - - Instance Metadata - - - - - Original Environment - - {data.eventstepinstance_by_pk.eventstep.environment !== null && - JSON.stringify(data.eventstepinstance_by_pk.eventstep.environment, null, 2)} - Environment - - {data.eventstepinstance_by_pk.environment !== null && - JSON.stringify(data.eventstepinstance_by_pk.environment, null, 2)} - - - Original Step Inputs - - {data.eventstepinstance_by_pk.eventstep.inputs !== null && - JSON.stringify(data.eventstepinstance_by_pk.eventstep.inputs, null, 2)} - Step Inputs - - {data.eventstepinstance_by_pk.inputs !== null && - JSON.stringify(data.eventstepinstance_by_pk.inputs, null, 2)} - - - - Original Step Outputs - - {data.eventstepinstance_by_pk.eventstep.outputs !== null && - JSON.stringify(data.eventstepinstance_by_pk.eventstep.outputs, null, 2)} - - Step Outputs - - {data.eventstepinstance_by_pk.outputs !== null && - JSON.stringify(data.eventstepinstance_by_pk.outputs, null, 2)} - - - - Original Action Data - - {data.eventstepinstance_by_pk.eventstep.action_data !== null && - JSON.stringify(data.eventstepinstance_by_pk.eventstep.action_data, null, 2)} - - Action Data - - {data.eventstepinstance_by_pk.action_data !== null && - JSON.stringify(data.eventstepinstance_by_pk.action_data, null, 2)} - - - -
-
-
-
-
- { data.eventstepinstance_by_pk.stdout !== "" && - - { - setExpandStdout(!expandStdout) - }} expanded={expandStdout} - > - } - aria-controls={`panel1c-stdout`} - >StdOut - - - - - - - } - {data.eventstepinstance_by_pk.stderr !== "" && - - {setExpandStderr(!expandStderr)}} expanded={expandStderr} - > - } - aria-controls={`panel1c-stdout`} - >StdErr - - - - - - - } - {data.eventstepinstance_by_pk.callbacks.length > 0 && - <> - - - Callbacks Generated - - - - - - {data.eventstepinstance_by_pk.callbacks.map(trackedData => ( - - - {trackedData.display_id} - - - ))} - -
-
- - } - - - - {data.eventstepinstance_by_pk.apitokens.length > 0 && + } + meta={ <> - - - API Tokens generated - - - - - - - - Active - Created By - Token - Type - Name - - - - - {data.eventstepinstance_by_pk.apitokens.map(trackedData => ( - {}} - onDeleteAPIToken={() =>{}} - > - - ))} - -
-
+ + 0 ? stepDefinition.depends_on.join(", ") : "None"} /> } + statusData={stepInstance} + subtitle={stepDefinition.description} + title={stepDefinition.name} + /> + + + {setExpandStepTable(!expandStepTable)}} expanded={expandStepTable} + > + } aria-controls={`panel1c-metadata`}> + Original and runtime metadata + + +
+ + + + +
+
+
+
+ + {!hasStdout && !hasStderr ? ( + + ) : ( + <> + {hasStdout && ( + { + setExpandStdout(!expandStdout) + }} expanded={expandStdout} + > + } + aria-controls={`panel1c-stdout`} + > + View stdout + + + + + + )} + {hasStderr && ( + {setExpandStderr(!expandStderr)}} expanded={expandStderr} + > + } + aria-controls={`panel1c-stderr`} + > + View stderr + + + + + + )} + + )} + + + + + +
- - @@ -1306,7 +1353,6 @@ function EventStepInstanceDetailDialog({selectedEventStepInstance, onClose}) { ) } function EventGroupInstanceDetailDialog({selectedEventGroupInstance, onClose}) { - const theme = useTheme(); const [data, setData] = React.useState({}); const {loading} = useQuery(get_eventgroupinstance, { fetchPolicy: "no-cache", @@ -1340,136 +1386,50 @@ function EventGroupInstanceDetailDialog({selectedEventGroupInstance, onClose}) { } }); if (loading){ - return ( - <> - Loading... - - - - - - ) + return } - if (!data){ - return ( - <> - Failed to find step data - - - - - - ) + if (!data?.eventgroupinstance_by_pk){ + return } - + const groupInstance = data.eventgroupinstance_by_pk; return ( <> - -
-
-
- - {data.eventgroupinstance_by_pk?.eventgroup?.name} -
- - {data.eventgroupinstance_by_pk?.eventgroup?.description} - -
- -
- Duration: -
-
- -
- - - Aggregated Results of Instance {selectedEventGroupInstance}: {data.eventgroupinstance_by_pk?.eventgroup?.name} - - - {data?.callback?.length > 0 && + } + meta={ <> - - - Callbacks Generated - - - - - - {data.callback.map(trackedData => ( - - - Callback ID: {trackedData.display_id} - - - {trackedData.user} @ {trackedData.host} ( {trackedData.pid} ) - - - ))} - -
-
+ + } + statusData={groupInstance} + subtitle={groupInstance.eventgroup?.description} + title={groupInstance.eventgroup?.name} + /> + + +
+ +
+
+ - {data?.apitokens?.length > 0 && - <> - - - API Tokens generated - - - - - - - - Active - Created By - Token - Type - Name - - - - - {data.apitokens.map(trackedData => ( - {}} - onDeleteAPIToken={() =>{}} - > - - ))} - -
-
- - } - +
- - - ) } function EventStepDetailDialog({selectedEventStep, onClose}) { - const theme = useTheme(); const {loading, data} = useQuery(getEventStep, { fetchPolicy: "no-cache", variables: {eventstep_id: selectedEventStep}, @@ -1479,116 +1439,143 @@ function EventStepDetailDialog({selectedEventStep, onClose}) { } }) if (loading){ - return ( - <> - Loading... - - - - - - ) + return } - if (!data){ - return ( - <> - Failed to find step data - - - - - - ) + if (!data?.eventstep_by_pk){ + return } + const stepDefinition = data.eventstep_by_pk; return ( <> - -
-
-
- - {data.eventstep_by_pk.name} + + + 0 ? stepDefinition.depends_on.join(", ") : "None"} /> + + } + statusData={stepDefinition} + subtitle={stepDefinition.description} + title={stepDefinition.name} + /> + + +
+
+
Environment
+ +
+
+
Inputs
+ +
+
+
Outputs
+ +
+
+
Action data
+
- - {data.eventstep_by_pk.description} - - - Depends On: {data.eventstep_by_pk.depends_on.join(", ")} -
-
- - - - - Action: {data.eventstep_by_pk.action} - - - - - - - Original Metadata - - Instance Metadata - - - - - Original Environment - - {data.eventstep_by_pk.environment !== null && - JSON.stringify(data.eventstep_by_pk.environment, null, 2)} - Environment - - - - Original Step Inputs - - {data.eventstep_by_pk.inputs !== null && - JSON.stringify(data.eventstep_by_pk.inputs, null, 2)} - Step Inputs - - - - Original Step Outputs - - {data.eventstep_by_pk.outputs !== null && - JSON.stringify(data.eventstep_by_pk.outputs, null, 2)} - - Step Outputs - - - - Original Action Data - - {data.eventstep_by_pk.action_data !== null && - JSON.stringify(data.eventstep_by_pk.action_data, null, 2)} - - Action Data - - - -
-
+
- - - + ) +} +function EventDetailsCallbacksTable({callbacks, includeContext = false}){ + const callbackCount = callbacks?.length || 0; + return ( + + {callbackCount === 0 ? ( + + ) : ( + + + + + Callback + {includeContext && + Context + } + + + + {callbacks.map(trackedData => ( + + + {trackedData.display_id} + + {includeContext && + + + {trackedData.user || "unknown"} @ {trackedData.host || "unknown"} + {trackedData.pid ? ` (${trackedData.pid})` : ""} + {trackedData.process_name ? ` - ${trackedData.process_name}` : ""} + + + } + + ))} + +
+
+ )} +
+ ) +} +function EventDetailsAPITokensTable({tokens}){ + const tokenCount = tokens?.length || 0; + return ( + + {tokenCount === 0 ? ( + + ) : ( + + + + + + Active + Created By + Token + Type + Name + + + + + {tokens.map(trackedData => ( + {}} + onDeleteAPIToken={() =>{}} + > + + ))} + +
+
+ )} +
) } function EventDetailsPayloadsTable({payloads, deletePayload}){ - const theme = useTheme(); + const payloadCount = payloads?.length || 0; const [openDetailedView, setOpenDetailedView] = React.useState(false); const [openDelete, setOpenDeleteDialog] = React.useState(false); const selectedPayloadRef = React.useRef({}); @@ -1631,110 +1618,137 @@ function EventDetailsPayloadsTable({payloads, deletePayload}){ setOpenDetailedView(true); } return ( - payloads?.length > 0 && <> - - - Payloads Generated - - - - - - - {openDelete && - {setOpenDeleteDialog(false);}} onSubmit={onAcceptDelete} open={openDelete}/> - } - {openDetailedView && - {setOpenDetailedView(false);}} - innerDialog={{setOpenDetailedView(false);}} />} - /> - } - {payloads.map(trackedData => ( - - - {!trackedData.deleted && - - - {onDeletePayload(trackedData)}} color="error" variant="contained"> - - - } - - - - - - - - {b64DecodeUnicode(trackedData.filemetum.filename_text)} - - - - - onViewDetailedData(trackedData)}> - - - - - - ))} - -
-
+ {openDelete && + {setOpenDeleteDialog(false);}} onSubmit={onAcceptDelete} open={openDelete}/> + } + {openDetailedView && + {setOpenDetailedView(false);}} + innerDialog={{setOpenDetailedView(false);}} />} + /> + } + 0 && + + } + > + {payloadCount === 0 ? ( + + ) : ( + + + + + + Type + Filename + Status + + + + + {payloads.map(trackedData => ( + + + {!trackedData.deleted && + + {onDeletePayload(trackedData)}}> + + } + + + + + + + {b64DecodeUnicode(trackedData.filemetum.filename_text)} + + + + + onViewDetailedData(trackedData)}> + + + + + ))} + +
+
+ )} +
) } function EventDetailsTaskTable({tasks}){ - const theme = useTheme(); + const taskCount = tasks?.length || 0; return ( - tasks?.length > 0 && - <> - - - Tasks Issued - - - - - - {tasks.map(trackedData => ( - - - - - - Callback: {trackedData.callback.display_id} - - - Task: {trackedData.display_id} - - - {trackedData.command_name} {trackedData.display_params} - - - {trackedData.operator.username} - - - ))} - -
-
- + + {taskCount === 0 ? ( + + ) : ( + + + + + Type + Callback + Task + Command + Operator + + + + {tasks.map(trackedData => ( + + + {trackedData.callback?.payload?.payloadtype?.name && + + } + + + {trackedData.callback.display_id} + + + {trackedData.display_id} + + + {trackedData.command_name} {trackedData.display_params} + + + {trackedData.operator.username} + + + ))} + +
+
+ )} +
) } function EventDetailsFilesTable({files}){ - const theme = useTheme(); + const fileCount = files?.length || 0; const [downloadBulk] = useMutation(downloadBulkQuery, { onCompleted: (data) => { snackActions.dismiss(); @@ -1762,57 +1776,65 @@ function EventDetailsFilesTable({files}){ downloadBulk({variables:{files: fileIds}}) } return ( - files?.length > 0 && - <> - - - Files Tracked - - - - - - - - File Name - Comment - Size - - - - {files.map(trackedData => ( - - - {trackedData.deleted ? ( - - {b64DecodeUnicode(trackedData.full_remote_path_text) === "" ? - b64DecodeUnicode(trackedData.filename_text) : - b64DecodeUnicode(trackedData.full_remote_path_text)} - - ) : ( - - {b64DecodeUnicode(trackedData.full_remote_path_text) === "" ? - b64DecodeUnicode(trackedData.filename_text) : - b64DecodeUnicode(trackedData.full_remote_path_text)} - - ) - } - - - {trackedData.comment} - - - {getStringSize({cellData: {"plaintext": trackedData.size}})} - - - ))} - -
-
- + } + > + {fileCount === 0 ? ( + + ) : ( + + + + + File name + Comment + Size + + + + {files.map(trackedData => ( + + + {trackedData.deleted ? ( + + {b64DecodeUnicode(trackedData.full_remote_path_text) === "" ? + b64DecodeUnicode(trackedData.filename_text) : + b64DecodeUnicode(trackedData.full_remote_path_text)} + + ) : ( + + {b64DecodeUnicode(trackedData.full_remote_path_text) === "" ? + b64DecodeUnicode(trackedData.filename_text) : + b64DecodeUnicode(trackedData.full_remote_path_text)} + + ) + } + + + {trackedData.comment} + + + {getStringSize({cellData: {"plaintext": trackedData.size}})} + + + ))} + +
+
+ )} + ) } diff --git a/MythicReactUI/src/components/pages/Eventing/EventTriggerContextSelect.js b/MythicReactUI/src/components/pages/Eventing/EventTriggerContextSelect.js index 239eac08..0a0ef05d 100644 --- a/MythicReactUI/src/components/pages/Eventing/EventTriggerContextSelect.js +++ b/MythicReactUI/src/components/pages/Eventing/EventTriggerContextSelect.js @@ -21,6 +21,7 @@ import {snackActions} from "../../utilities/Snackbar"; import {MeContext} from "../../App"; import {EventGroupTable} from "./EventGroupTable"; import {MythicDraggableDialogTitle} from "../../MythicComponents/MythicDraggableDialogTitle"; +import AddCircleIcon from '@mui/icons-material/AddCircle'; const triggerManualMutation = gql(` mutation triggerManualMutation($eventgroup_id: Int!, $env_data: jsonb){ @@ -236,8 +237,8 @@ export function EventTriggerContextSelectDialog({onClose, triggerContext}) { {dictionaryData.map( (e, index) => ( - removeRow(index)} > - + removeRow(index)} > + - @@ -172,10 +173,10 @@ export function EventTriggerKeywordDialog({onClose, selectedEventGroup}) { - - diff --git a/MythicReactUI/src/components/pages/Eventing/Eventing.js b/MythicReactUI/src/components/pages/Eventing/Eventing.js index 819ade72..405a86a6 100644 --- a/MythicReactUI/src/components/pages/Eventing/Eventing.js +++ b/MythicReactUI/src/components/pages/Eventing/Eventing.js @@ -1,28 +1,26 @@ -import React, {useCallback} from 'react'; +import React from 'react'; import { gql, useSubscription, useQuery } from '@apollo/client'; import {snackActions} from '../../utilities/Snackbar'; -import {useTheme} from '@mui/material/styles'; -import Button from '@mui/material/Button'; import List from '@mui/material/List'; -import ListSubheader from '@mui/material/ListSubheader'; import Divider from '@mui/material/Divider'; import ListItem from '@mui/material/ListItem'; -import ListItemText from '@mui/material/ListItemText'; +import TextField from '@mui/material/TextField'; +import InputAdornment from '@mui/material/InputAdornment'; import {EventGroupTable} from "./EventGroupTable"; import {UploadEventFile} from "../../MythicComponents/MythicFileUpload"; import VisibilityOffIcon from '@mui/icons-material/VisibilityOff'; import VisibilityIcon from '@mui/icons-material/Visibility'; -import {MythicStyledTooltip} from "../../MythicComponents/MythicStyledTooltip"; -import { IconButton } from '@mui/material'; -import NotificationsOffTwoToneIcon from '@mui/icons-material/NotificationsOffTwoTone'; import CloudUploadIcon from '@mui/icons-material/CloudUpload'; +import SearchIcon from '@mui/icons-material/Search'; import {MythicDialog} from "../../MythicComponents/MythicDialog"; import {TestEventGroupFileDialog} from "./CreateEventWorkflowDialog"; import AddCircleIcon from '@mui/icons-material/AddCircle'; import Split from 'react-split'; -import Paper from '@mui/material/Paper'; import {CreateEventingStepper} from "./CreateEventingStepper"; import CategoryIcon from '@mui/icons-material/Category'; +import {MythicPageBody} from "../../MythicComponents/MythicPageBody"; +import {MythicPageHeader, MythicPageHeaderChip} from "../../MythicComponents/MythicPageHeader"; +import {MythicToolbarButton, MythicToolbarToggle} from "../../MythicComponents/MythicTableToolbar"; const get_eventgroups = gql` query GetEventGroups { @@ -164,12 +162,31 @@ steps: outputs: SCRIPT_TASK_ID: id `; +const sidebarFilterOptionsBase = [ + {key: "all", label: "All"}, + {key: "runnable", label: "Runnable"}, + {key: "needs_approval", label: "Needs approval"}, + {key: "disabled", label: "Disabled"}, +]; +const getEventGroupStatus = (eventGroup) => { + if(eventGroup.deleted){ + return {key: "deleted", label: "Deleted", rank: 3}; + } + if(!eventGroup.active){ + return {key: "disabled", label: "Disabled", rank: 2}; + } + if(!eventGroup.approved_to_run){ + return {key: "needs_approval", label: "Needs approval", rank: 1}; + } + return {key: "runnable", label: "Runnable", rank: 0}; +}; export function Eventing({me}){ - const theme = useTheme(); const [openTestModal, setOpenTestModal] = React.useState(false); const [openCreateEventingStepper, setOpenCreateEventingStepper] = React.useState(false); const [eventgroups, setEventgroups] = React.useState([]); const [showDeleted, setShowDeleted] = React.useState(false); + const [sidebarSearch, setSidebarSearch] = React.useState(""); + const [sidebarFilter, setSidebarFilter] = React.useState("all"); const [selectedEventGroup, setSelectedEventGroup] = React.useState({id: 0}); const foundQueryEvent = React.useRef(false); useQuery(get_eventgroups, { @@ -255,42 +272,98 @@ export function Eventing({me}){ } }, [eventgroups]); + const visibleEventGroups = React.useMemo(() => { + return eventgroups.filter(e => showDeleted || !e.deleted); + }, [eventgroups, showDeleted]); + const activeEventGroups = React.useMemo(() => { + return eventgroups.filter(e => !e.deleted && e.active); + }, [eventgroups]); + React.useEffect(() => { + if(!showDeleted && sidebarFilter === "deleted"){ + setSidebarFilter("all"); + } + }, [showDeleted, sidebarFilter]); + const sidebarFilterCounts = React.useMemo(() => { + return visibleEventGroups.reduce((prev, cur) => { + const status = getEventGroupStatus(cur); + return {...prev, [status.key]: (prev[status.key] || 0) + 1}; + }, {runnable: 0, needs_approval: 0, disabled: 0, deleted: 0}); + }, [visibleEventGroups]); + const sidebarFilterOptions = React.useMemo(() => { + if(showDeleted){ + return [...sidebarFilterOptionsBase, {key: "deleted", label: "Deleted"}]; + } + return sidebarFilterOptionsBase; + }, [showDeleted]); + const filteredEventGroups = React.useMemo(() => { + const search = sidebarSearch.trim().toLowerCase(); + return visibleEventGroups.filter((eventGroup) => { + const status = getEventGroupStatus(eventGroup); + if(sidebarFilter !== "all" && sidebarFilter !== status.key){ + return false; + } + if(search === ""){ + return true; + } + const searchText = [ + eventGroup.name, + eventGroup.description, + eventGroup.trigger, + eventGroup.run_as, + eventGroup.operator?.username, + ...(Array.isArray(eventGroup.keywords) ? eventGroup.keywords : []), + ].filter(Boolean).join(" ").toLowerCase(); + return searchText.includes(search); + }).sort((a, b) => { + const statusA = getEventGroupStatus(a); + const statusB = getEventGroupStatus(b); + if(statusA.rank !== statusB.rank){ + return statusA.rank - statusB.rank; + } + return (a.id > b.id) ? -1 : ((b.id > a.id) ? 1 : 0); + }); + }, [sidebarFilter, sidebarSearch, visibleEventGroups]); + const selectedEventLabel = selectedEventGroup.id === 0 ? "All runs" : selectedEventGroup.name; return ( -
-
- -
+ + + + + + {showDeleted && } + + } + actions={ + <> + }> + Upload + + + setOpenTestModal(true)} startIcon={}> + Text + + setOpenCreateEventingStepper(true)} startIcon={}> + Wizard + + setShowDeleted(!showDeleted)} + label="Deleted" + activeIcon={} + inactiveIcon={} + /> + + } + /> +
+ +
- - - - - - - - - - - - {openTestModal && { @@ -309,54 +382,86 @@ export function Eventing({me}){ innerDialog={} /> } +
+
+
+
Registered event groups
+
Browse workflows by run state
+
+ {filteredEventGroups.length}/{visibleEventGroups.length} +
+ setSidebarSearch(event.target.value)} + InputProps={{ + startAdornment: ( + + + + ) + }} + /> +
+ {sidebarFilterOptions.map((filterOption) => { + const filterCount = filterOption.key === "all" ? visibleEventGroups.length : sidebarFilterCounts[filterOption.key]; + return ( + + ); + })} +
+
setSelectedEventGroup({id: 0})} - className={selectedEventGroup.id === 0 ? "selectedCallback": ""} - style={selectedEventGroup.id === 0 ? - {paddingTop: 0, paddingBottom: 0, borderLeft: `5px solid ${theme.palette.info.main}`, - borderRight: `5px solid ${theme.palette.info.main}`} : - {paddingTop: 0, paddingBottom: 0}}> - + className={`mythic-eventing-list-item mythic-eventing-list-item-all ${selectedEventGroup.id === 0 ? "mythic-eventing-list-item-selected" : ""}`.trim()}> +
+
+
+ All workflow runs + {visibleEventGroups.length} +
+
Review instances across all event groups
+
-
- - Registered Event Groups - {showDeleted ? ( - - setShowDeleted(!showDeleted)}> - - - ) : ( - - setShowDeleted(!showDeleted)} > - - )} - - }> - {eventgroups.map( (e, i) => ( - (showDeleted || !e.deleted) && - setSelectedEventGroup(e)} - className={selectedEventGroup.id === e.id ? "selectedCallback": ""} - style={selectedEventGroup.id === e.id ? - {paddingTop: 0, paddingBottom: 0, - borderLeft: `5px solid ${theme.palette.info.main}`, borderRight: `5px solid ${theme.palette.info.main}`, - } : - {paddingTop: 0, paddingBottom: 0}}> - {!e.active && - - } - - - ))} +
+ + {filteredEventGroups.length === 0 ? ( +
No workflows match this view
+ ) : filteredEventGroups.map( (eventGroup) => { + const status = getEventGroupStatus(eventGroup); + return ( + setSelectedEventGroup(eventGroup)} + className={`mythic-eventing-list-item ${selectedEventGroup.id === eventGroup.id ? "mythic-eventing-list-item-selected" : ""} mythic-eventing-list-item-${status.key}`.trim()}> +
+
+
+ {eventGroup.name} + {status.label} +
+
+ {eventGroup.trigger || "No trigger"} + {eventGroup.run_as || "unknown"} +
+
+ + ); + })}
-
+
@@ -364,6 +469,6 @@ export function Eventing({me}){
-
+ ) -} \ No newline at end of file +} diff --git a/MythicReactUI/src/components/pages/ExpandedCallback/ExpandedCallbackSideDetails.js b/MythicReactUI/src/components/pages/ExpandedCallback/ExpandedCallbackSideDetails.js index 693108ef..60ac6b5a 100644 --- a/MythicReactUI/src/components/pages/ExpandedCallback/ExpandedCallbackSideDetails.js +++ b/MythicReactUI/src/components/pages/ExpandedCallback/ExpandedCallbackSideDetails.js @@ -5,10 +5,8 @@ import TableCell from '@mui/material/TableCell'; import TableContainer from '@mui/material/TableContainer'; import TableHead from '@mui/material/TableHead'; import TableRow from '@mui/material/TableRow'; -import Paper from '@mui/material/Paper'; import Typography from '@mui/material/Typography'; import { toLocalTime } from '../../utilities/Time'; -import {useTheme} from '@mui/material/styles'; import LockIcon from '@mui/icons-material/Lock'; import LockOpenIcon from '@mui/icons-material/LockOpen'; import { MythicDialog } from '../../MythicComponents/MythicDialog'; @@ -23,17 +21,12 @@ import { import {updateSleepInfoCallbackMutation} from "../Callbacks/CallbackMutations"; import {snackActions} from "../../utilities/Snackbar"; import {TagsDisplay, ViewEditTags} from "../../MythicComponents/MythicTag"; +import {MythicSectionHeader} from "../../MythicComponents/MythicPageHeader"; export function ExpandedCallbackSideDetails(props){ - const theme = useTheme(); return (
- - - Callback {props.callback.display_id} - - +
) diff --git a/MythicReactUI/src/components/pages/Home/CallbacksCard.js b/MythicReactUI/src/components/pages/Home/CallbacksCard.js index 5ff7b0c5..6d0f6e33 100644 --- a/MythicReactUI/src/components/pages/Home/CallbacksCard.js +++ b/MythicReactUI/src/components/pages/Home/CallbacksCard.js @@ -5,11 +5,10 @@ import Typography from '@mui/material/Typography'; import {useNavigate} from 'react-router-dom'; import {getStringSize} from "../Callbacks/ResponseDisplayTable"; import {getSkewedNow} from "../../utilities/Time"; -import Paper from '@mui/material/Paper'; import MenuItem from '@mui/material/MenuItem'; import Button from '@mui/material/Button'; import TextField from '@mui/material/TextField'; -import {CallbackDataCard, GaugeCard, LineTimeMultiChartCard, PieChartCard, TableDataCard} from './DashboardComponents'; +import {CallbackDataCard, DashboardEmptyCard, GaugeCard, LineTimeMultiChartCard, PieChartCard, TableDataCard} from './DashboardComponents'; import {MythicAgentSVGIcon} from "../../MythicComponents/MythicAgentSVGIcon"; import {MythicStyledTooltip} from "../../MythicComponents/MythicStyledTooltip"; import {b64DecodeUnicode} from "../Callbacks/ResponseDisplay"; @@ -58,7 +57,9 @@ import MythicStyledTableCell from "../../MythicComponents/MythicTableCell"; import {faCopy} from '@fortawesome/free-solid-svg-icons'; import MythicTextField from "../../MythicComponents/MythicTextField"; import {ImageWithAuth} from "../../utilities/ImageWithAuth"; -import {MythicPageHeader} from "../../MythicComponents/MythicPageHeader"; +import {MythicPageHeader, MythicPageHeaderChip} from "../../MythicComponents/MythicPageHeader"; +import {MythicStatusChip} from "../../MythicComponents/MythicStatusChip"; +import {MythicLoadingState} from "../../MythicComponents/MythicStateDisplay"; const LeadDashboardQuery = gql` ${taskingDataFragment} @@ -268,7 +269,7 @@ const ActiveCallbacksDashboardElement = ({me, data, editing, removeElement}) => return ( +
{"Active Callbacks"} } mainElement={ <> - navigate("/new/callbacks")}> + navigate("/new/callbacks")}> {active.active} - + / {active.total} } secondaryElement={ - + {active.recent} } @@ -492,7 +489,10 @@ const ProxyUsageDashboardElement = ({me, data, editing, removeElement}) => { } editing={editing} - removeElement={removeElement}/> + removeElement={removeElement} + empty={callbackPorts.length === 0} + emptyTitle="No proxy activity" + emptyDescription="Port forward activity will appear here once callbacks send or receive proxy traffic."/> ) } const Top10UserContextsDashboardElement = ({me, data, editing, removeElement}) => { @@ -581,6 +581,9 @@ const Top10UserContextsDashboardElement = ({me, data, editing, removeElement}) = } editing={editing} removeElement={removeElement} + empty={taskedUser.length === 0} + emptyTitle="No user context activity" + emptyDescription="Tasked user contexts will appear here once tasks run against callbacks." /> ) } @@ -664,7 +667,10 @@ const Top10HostContextsDashboardElement = ({me, data, editing, removeElement}) = } editing={editing} - removeElement={removeElement}/> + removeElement={removeElement} + empty={taskedHosts.length === 0} + emptyTitle="No host context activity" + emptyDescription="Tasked host contexts will appear here once tasks run against callbacks."/> ) } const Top10RecentPayloadsDashboardElement = ({me, data, editing, removeElement}) => { @@ -699,33 +705,23 @@ const Top10RecentPayloadsDashboardElement = ({me, data, editing, removeElement}) }, [data]); if( data.payload && payloads.length === 0){ return ( -
-

- {"No Payloads Created"} -

-
-
- -
-
-
+ navigate("/new/createpayload")} + startIcon={} + variant="outlined" + > + Create Your First Payload + + } + > + Build a payload to start populating this view. + ) } return ( @@ -754,8 +750,8 @@ const Top10RecentPayloadsDashboardElement = ({me, data, editing, removeElement}) removeElement={removeElement} customizeElement={ - navigate("/new/createpayload")}> - + navigate("/new/createpayload")}> + } @@ -810,45 +806,37 @@ const Top10RecentWorkflowsDashboardElement = ({me, data, editing, removeElement} }, [data]); if( data.eventgroupinstance && workflows.length === 0){ return ( -
-

- {"No Workflows Created"} -

-
-
- -
- {openNewWorkflowModal && - + Create a workflow to see recent eventing executions here. + + {openNewWorkflowModal && + { + setOpenNewWorkflowModal(false); + }} + innerDialog={ { setOpenNewWorkflowModal(false); - }} - innerDialog={ { - setOpenNewWorkflowModal(false); - }}/>} - /> - } -
-
+ }}/>} + /> + } + ) } return ( @@ -877,8 +865,8 @@ const Top10RecentWorkflowsDashboardElement = ({me, data, editing, removeElement} removeElement={removeElement} customizeElement={ - setOpenNewWorkflowModal(true)}> - + setOpenNewWorkflowModal(true)}> + } @@ -1016,74 +1004,49 @@ const MyOperationsDashboardElement = ({me, data, reloadDashboard, editing, remov return { operation: p, id: p.id, - label: <> - {p.name} - , - value: <> - {p.id === me.user.current_operation_id ? ("Current Operation") : ( - - - - )} - + label: p.name, } }) || []; setOperations(newPayloadData); }, [data]); if( data.operation && operations.length === 0){ return ( -
-

- {"You Belong to No Operations"} -

-
-
- {data?.operator[0]?.admin && - - } - {!data?.operator[0]?.admin && - - {"Ask an admin to create an operation for you or a lead to add you to an existing operation"} - - } -
- {openNewOperation && - {setOpenNewOperationDialog(false);}} - innerDialog={ - {setOpenNewOperationDialog(false);}} - value={""} - onSubmit={onSubmitNewOperation} - /> - } - /> + <> + setOpenNewOperationDialog(true)} + startIcon={} + variant="outlined" + > + Create Operation + + ) : null} + > + {data?.operator[0]?.admin ? + "Create an operation to begin organizing callbacks, payloads, and tasking." : + "Ask an admin to create an operation for you or a lead to add you to an existing operation." } -
-
+ + {openNewOperation && + {setOpenNewOperationDialog(false);}} + innerDialog={ + {setOpenNewOperationDialog(false);}} + value={""} + onSubmit={onSubmitNewOperation} + /> + } + /> + } + ) } return ( @@ -1101,13 +1064,20 @@ const MyOperationsDashboardElement = ({me, data, reloadDashboard, editing, remov } tableBody={ - {operations.map( (d, index) => ( - {handleHostContextClick(d)}} style={{cursor: "pointer"}}> - {d.label} - - + {operations.map( (d) => ( + {handleHostContextClick(d)}} style={{cursor: "pointer"}}> + +
+ {d.label} + {d.operation.complete && + + } +
+
+ + {openUpdateNotifications && {setOpenUpdateNotifications(false);}} @@ -1116,9 +1086,9 @@ const MyOperationsDashboardElement = ({me, data, reloadDashboard, editing, remov } - + startIcon={} variant="outlined">Edit {openUpdateOperators && {setOpenUpdateOperators(false);}} @@ -1126,18 +1096,28 @@ const MyOperationsDashboardElement = ({me, data, reloadDashboard, editing, remov /> } - {d.value} -
- ) - )} + + {d.id === me.user.current_operation_id ? ( + + ) : ( + + )} + +
+ ) + )}
} editing={editing} removeElement={removeElement} customizeElement={ - setOpenNewOperationDialog(true)}> - + setOpenNewOperationDialog(true)}> + } @@ -1482,6 +1462,9 @@ const Top10RecentFileDownloadsDashboardElement = ({me, data, editing, removeElem } editing={editing} removeElement={removeElement} + empty={files.length === 0} + emptyTitle="No downloaded files" + emptyDescription="Recent downloads will appear here once file transfers complete." /> {openPreviewMediaDialog.open && {files.length > 0 && - - - - - - onPreviewMedia(e, activeStep)} - /> - - - + + } @@ -1559,6 +1544,9 @@ const Top10RecentScreenshotsDashboardElement = ({me, data, editing, removeElemen removeElement={ removeElement } + empty={files.length === 0} + emptyTitle="No screenshots" + emptyDescription="Recent screenshots will appear here once screenshot files are created." /> { openScreenshot.open && removeElement={ removeElement } + empty={tasks.length === 0} + emptyTitle="No recent tasking" + emptyDescription="Recent tasks will appear here once operators issue tasking." /> @@ -1651,7 +1642,7 @@ const Top10RecentCredentialsDashboardElement = ({me, data, editing, removeElemen - onCopyToClipboard(c.credential_text)} size="small"> + onCopyToClipboard(c.credential_text)} size="small"> @@ -1674,6 +1665,9 @@ const Top10RecentCredentialsDashboardElement = ({me, data, editing, removeElemen removeElement={ removeElement } + empty={credentials.length === 0} + emptyTitle="No recent credentials" + emptyDescription="Captured credentials will appear here once credentials are created." /> @@ -1707,20 +1701,20 @@ const OperatorDashboard = ({me, setLoading, loading}) => { } return ( <> -
+
-
+
-
+
-
+
@@ -1747,16 +1741,16 @@ const LeadDashboard = ({me, setLoading, loading}) => { }, []); return ( <> -
+
-
+
-
+
@@ -1922,25 +1916,25 @@ const CustomDashboard = ({me, setLoading, loading, editing}) => { } }, [me, analysisData, removeDashboardElement]); return ( -
+
{dashboards.map((d, i) => (
+ className="mythic-dashboard-row"> {editing && -
+
- setOpenAddElement({open: true, row: i})} > - + setOpenAddElement({open: true, row: i})} size="small"> + - addDashboardRow(i)}> - + addDashboardRow(i)} size="small"> + - removeDashboardRow(i)}> - + removeDashboardRow(i)} size="small"> +
@@ -1985,20 +1979,42 @@ export function CallbacksCard({me}) { } return ( <> - - Welcome {me.user.username} - {me.user.current_operation_id === 0 ? null : ( - <> - {" to"} {me.user.current_operation}'s Dashboard - - )} - - }> -
+ + Welcome {me.user.username} + + } + subtitle={me.user.current_operation_id === 0 ? "No operation selected" : `${me.user.current_operation}'s Dashboard`} + meta={ + <> + + {editing && } + + } + actions={ + <> - setLoading(true)}> + setLoading(true)}> {dashboard === "custom" && - setEditing(!editing)}> + setEditing(!editing)}> } -
-
-
+ + } + /> +
{loading && -
-
- {"Analyzing Operation..."} +
+
+
} diff --git a/MythicReactUI/src/components/pages/Home/DashboardComponents.js b/MythicReactUI/src/components/pages/Home/DashboardComponents.js index e01a34c8..dd729168 100644 --- a/MythicReactUI/src/components/pages/Home/DashboardComponents.js +++ b/MythicReactUI/src/components/pages/Home/DashboardComponents.js @@ -1,14 +1,10 @@ import React from 'react'; import {useTheme} from '@mui/material/styles'; -import Typography from '@mui/material/Typography'; import { LineChart } from '@mui/x-charts/LineChart'; import Slider from '@mui/material/Slider'; import { PieChart, pieArcLabelClasses } from '@mui/x-charts/PieChart'; import { axisClasses } from '@mui/x-charts/ChartsAxis'; -import {Gauge, GaugeContainer, - GaugeValueArc, - GaugeReferenceArc, - useGaugeState} from '@mui/x-charts/Gauge'; +import {Gauge} from '@mui/x-charts/Gauge'; import Table from '@mui/material/Table'; import TableContainer from '@mui/material/TableContainer'; import { BarChart } from '@mui/x-charts/BarChart'; @@ -18,20 +14,110 @@ import IconButton from '@mui/material/IconButton'; import VisibilityOffIcon from '@mui/icons-material/VisibilityOff'; import VisibilityIcon from '@mui/icons-material/Visibility'; import Paper from "@mui/material/Paper"; +import {MythicEmptyState} from "../../MythicComponents/MythicStateDisplay"; -const normalColors = [ +const fallbackDashboardColors = [ '#09bdff', - '#4cd5ff', - '#1F94AD', - '#2a7b9b', - '#007FFF', - '#0754a2', - '#4b47a2', - '#635bce', - '#878be7', + '#39b86f', + '#d69d2d', + '#d65c6b', + '#7b6fd6', + '#24a3a3', + '#9c6ade', + '#d47f38', '#4e7ad7', + '#90a955', ]; +const getDashboardColors = (theme) => [ + theme.palette.info.main, + theme.palette.success.main, + theme.palette.warning.main, + theme.palette.error.main, + theme.palette.secondary.main, + theme.palette.primary.main, + ...fallbackDashboardColors, +]; + +const DashboardCard = ({ + actions, + bodyClassName = "", + children, + className = "", + editing, + removeElement, + title, + width = "100%", +}) => { + return ( + + {(title || actions || editing) && +
+
+ {title} +
+ {(editing || actions) && +
+ {actions} + {editing && + + + + + + } +
+ } +
+ } +
+ {children} +
+
+ ); +}; + +export const DashboardEmptyCard = ({action, children, editing, removeElement, title, width = "100%"}) => ( + +
+
+ {children} +
+ {action && +
+ {action} +
+ } +
+
+); + +const DashboardNoDataState = ({ + title = "No data yet", + description = "This dashboard element will populate when matching operation activity exists.", +}) => ( + +); export const PieChartCard = ({ data, width = "100%", additionalStyles, innerElement, @@ -40,7 +126,7 @@ export const PieChartCard = ({ right: 10, top: 10, bottom: 10, - }, colors = normalColors, + }, colors, onClick, title = "", editing, removeElement, customizeElement }) => { const [showLegend, setShowLegend] = React.useState(true); @@ -48,113 +134,79 @@ export const PieChartCard = ({ setShowLegend(!showLegend); } const theme = useTheme(); + const chartData = Array.isArray(data) ? data : []; + const hasChartData = chartData.length > 0; return ( - -

- {editing && - - - - - - - - } - {title} - + {customizeElement} - - {showLegend ? : } + + {showLegend ? : } - -

- `${item.label}`, - //arcLabelMinAngle: 35, - //arcLabelRadius: "60%", - data: data, - highlightScope: {fade: 'global', highlighted: 'item'}, - faded: {innerRadius: 0, additionalRadius: -10, color: 'gray'}, - paddingAngle: 1, - cornerRadius: 4, - innerRadius: 0, - ...additionalStyles - }, - ]} - height={200} - margin={margin} - sx={{ - [`& .${pieArcLabelClasses.root}`]: { - fill: 'white', - fontWeight: 'bold', - }, - }} - colors={colors} - onItemClick={onClick} - hideLegend={!showLegend} - slotProps={{ - legend: { - direction: "vertical", // "horizontal" - sx: { - gap: "4px", // itemGap (distance between legend items) - // CSS class - ['.MuiChartsLegend-series']: { - gap: '8px', // markGap (distance between legend dot and text) - }, - [`.MuiChartsLegend-mark`]: { - height: 12, // size of the legend dot - width: 12, - }, + + } + editing={editing} + removeElement={removeElement} + title={title} + width={width} + > + {hasChartData ? ( + `${item.label}`, + //arcLabelMinAngle: 35, + //arcLabelRadius: "60%", + data: chartData, + highlightScope: {fade: 'global', highlighted: 'item'}, + faded: {innerRadius: 0, additionalRadius: -10, color: 'gray'}, + paddingAngle: 1, + cornerRadius: 4, + innerRadius: 0, + ...additionalStyles + }, + ]} + height={200} + margin={margin} + sx={{ + [`& .${pieArcLabelClasses.root}`]: { + fill: 'white', + fontWeight: 'bold', + }, + }} + colors={colors || getDashboardColors(theme)} + onItemClick={onClick} + hideLegend={!showLegend} + slotProps={{ + legend: { + direction: "vertical", // "horizontal" + sx: { + gap: "4px", // itemGap (distance between legend items) + // CSS class + ['.MuiChartsLegend-series']: { + gap: '8px', // markGap (distance between legend dot and text) + }, + [`.MuiChartsLegend-mark`]: { + height: 12, // size of the legend dot + width: 12, + }, + } } - } - }}> - {innerElement} - -
+ }}> + {innerElement} + + ) : ( + + )} + ); } -function GaugePointer() { - const { valueAngle, outerRadius, cx, cy } = useGaugeState(); - - if (valueAngle === null) { - // No value to display - return null; - } - - const target = { - x: cx + outerRadius * Math.sin(valueAngle), - y: cy - outerRadius * Math.cos(valueAngle), - }; - return ( - - - - - ); -} -export const GaugeCard = ({data, width = "100%", additionalStyles, innerElement, hidden, margin = { - left: 10, - right: 10, - top: 10, - bottom: 10, -}, colors = normalColors, onClick, title = "", editing, removeElement, customizeElement }) => { +export const GaugeCard = ({data, width = "100%", title = "", editing, removeElement, customizeElement }) => { const theme = useTheme(); const getFillColor = () => { if(data['total'] === 0){return theme.palette.text.disabled} @@ -168,29 +220,14 @@ export const GaugeCard = ({data, width = "100%", additionalStyles, innerElement, } } return ( - -

- {editing && - - - - - - - - } - {title} - - {customizeElement} - -

+ -
+ ); } export const CallbackDataCard = ({mainTitle, secondTitle, mainElement, secondaryElement, width="100%", editing, removeElement}) => { - const theme = useTheme(); return ( - -

- {editing && - - - - - - - - } - {mainTitle} -

-
+ +
- {mainElement} -

- {secondTitle} -

- {secondaryElement} +
+ {mainElement} +
+ {secondTitle} +
+ {secondaryElement} +
- +
) } -export const TableDataCard = ({title, width = "100%", tableHead, tableBody, editing, removeElement, customizeElement}) => { - const theme = useTheme(); +export const TableDataCard = ({ + title, + width = "100%", + tableHead, + tableBody, + editing, + removeElement, + customizeElement, + empty = false, + emptyTitle, + emptyDescription, +}) => { return ( - -

- {editing && - - - - - - - - } - {title} - - {customizeElement} - -

- - - {tableHead} - {tableBody} -
-
-
+ + {empty ? ( + + ) : ( + + + {tableHead} + {tableBody} +
+
+ )} +
) } export const LineTimeChartCard = ({data, additionalStyles}) => { - const theme = useTheme(); const [value, setValue] = React.useState([0, 0]); const [range, setRange] = React.useState([0, 0]); React.useEffect( () => { @@ -315,17 +338,7 @@ export const LineTimeChartCard = ({data, additionalStyles}) => { } }; return ( - - - Tasks Issued per Day - + { }} margin={{ top: 10 }} dataset={data} - height={300} + height={200} {...additionalStyles} > { valueLabelDisplay="auto" min={range[0]} max={range[1]} - sx={{ mt: 2, width: "80%", left: "10%" }} + className="mythic-dashboard-slider" + sx={{ width: "80%" }} /> - + ) } -export const LineTimeMultiChartCard = ({data, additionalStyles, colors=normalColors, view_utc_time, editing, removeElement, customizeElement}) => { +export const LineTimeMultiChartCard = ({data, additionalStyles, colors, view_utc_time, editing, removeElement, customizeElement}) => { const theme = useTheme(); const [value, setValue] = React.useState([0, 0]); const [range, setRange] = React.useState([0, 0]); + const hasChartData = (data?.x?.length || 0) > 0 && (data?.y?.length || 0) > 0; React.useEffect( () => { if(data.x.length > 0){ @@ -430,105 +445,91 @@ export const LineTimeMultiChartCard = ({data, additionalStyles, colors=normalCol } } return ( - - - {editing && - - - - - - - - } - Activity per Day {view_utc_time ? "( UTC )" : "( " + Intl?.DateTimeFormat()?.resolvedOptions()?.timeZone + " )"} - + {customizeElement} - - {showLegend ? : } + + {showLegend ? : } - - - - sliderDate(sliderVal, view_utc_time)} - min={range[0]} - max={range[1]} - sx={{ mt: 2, width: "80%", left: "10%" }} - /> - + + } + editing={editing} + removeElement={removeElement} + title={`Activity per Day ${view_utc_time ? "(UTC)" : `(${Intl?.DateTimeFormat()?.resolvedOptions()?.timeZone})`}`} + > + {hasChartData ? ( + <> + + sliderDate(sliderVal, view_utc_time)} + min={range[0]} + max={range[1]} + className="mythic-dashboard-slider" + sx={{ width: "80%" }} + /> + + ) : ( + + )} + ) } -export const StackedBarChartCard = ({data, labels, title, width="100%", hidden, colors=normalColors, margin={ +export const StackedBarChartCard = ({data, labels, title, width="100%", hidden, colors, margin={ right: 10, top: 40, bottom: 10, }}) => { const theme = useTheme(); return ( - -

- {title} -

+ -
+ ); } diff --git a/MythicReactUI/src/components/pages/MITRE_ATTACK/MitreGrid.js b/MythicReactUI/src/components/pages/MITRE_ATTACK/MitreGrid.js index d1984269..551ce7ef 100644 --- a/MythicReactUI/src/components/pages/MITRE_ATTACK/MitreGrid.js +++ b/MythicReactUI/src/components/pages/MITRE_ATTACK/MitreGrid.js @@ -1,9 +1,6 @@ import React from 'react'; import Paper from '@mui/material/Paper'; import Button from '@mui/material/Button'; -import Typography from '@mui/material/Typography'; -import {useTheme} from '@mui/material/styles'; -import ButtonGroup from '@mui/material/ButtonGroup'; import ArrowDropDownIcon from '@mui/icons-material/ArrowDropDown'; import Grow from '@mui/material/Grow'; import Popper from '@mui/material/Popper'; @@ -16,26 +13,36 @@ import {CircularProgress} from '@mui/material'; import { MythicDisplayTextDialog} from '../../MythicComponents/MythicDisplayTextDialog'; import { MythicDialog } from '../../MythicComponents/MythicDialog'; import { SelectPayloadTypeDialog } from './SelectPayloadTypeDialog'; -import {MythicPageHeader} from "../../MythicComponents/MythicPageHeader"; +import {MythicPageHeader, MythicPageHeaderChip} from "../../MythicComponents/MythicPageHeader"; export function MitreGrid({entries, onGetCommands, onGetTasks, onGetCommandsFiltered, onGetTasksFiltered, onFilterByTags, showCountGrouping}){ - const theme = useTheme(); const [backdropOpen, setBackdropOpen] = React.useState(false); + const techniqueCount = Object.values(entries || {}).reduce((total, tactic) => total + (tactic?.rows?.length || 0), 0); return ( <> - - - + + + {showCountGrouping && } + + } + actions={ + + } + />
{backdropOpen && @@ -239,14 +246,18 @@ function PoperDropdown({onGetCommands, onGetTasks, onGetCommandsFiltered, onGetT } return ( - - - + {({ TransitionProps, placement }) => ( )) ) -} \ No newline at end of file +} diff --git a/MythicReactUI/src/components/pages/Operations/CommandBlockListTable.js b/MythicReactUI/src/components/pages/Operations/CommandBlockListTable.js index d42d709b..1e29ee36 100644 --- a/MythicReactUI/src/components/pages/Operations/CommandBlockListTable.js +++ b/MythicReactUI/src/components/pages/Operations/CommandBlockListTable.js @@ -6,19 +6,18 @@ import TableCell from '@mui/material/TableCell'; import TableContainer from '@mui/material/TableContainer'; import TableHead from '@mui/material/TableHead'; import TableRow from '@mui/material/TableRow'; -import Paper from '@mui/material/Paper'; -import Typography from '@mui/material/Typography'; import AddCircleIcon from '@mui/icons-material/AddCircle'; import { MythicDialog } from '../../MythicComponents/MythicDialog'; -import {useTheme} from '@mui/material/styles'; import {EditBlockListDialog} from './EditBlockListDialog'; import {snackActions} from '../../utilities/Snackbar'; import {useMutation, gql} from '@apollo/client'; import DeleteIcon from '@mui/icons-material/Delete'; import IconButton from '@mui/material/IconButton'; import {MythicConfirmDialog} from '../../MythicComponents/MythicConfirmDialog'; -import TuneIcon from '@mui/icons-material/Tune'; -import {MythicPageHeader} from "../../MythicComponents/MythicPageHeader"; +import EditIcon from '@mui/icons-material/Edit'; +import {MythicPageHeader, MythicPageHeaderChip} from "../../MythicComponents/MythicPageHeader"; +import {MythicToolbarButton} from "../../MythicComponents/MythicTableToolbar"; +import {MythicTableEmptyState} from "../../MythicComponents/MythicStateDisplay"; const newBlockListEntry = gql` mutation newBlockListsEntries($entries: [disabledcommandsprofile_insert_input!]!) { @@ -58,7 +57,6 @@ mutation deleteBlockListEntries($name: String!, $entries: [Int!]!){ `; export function CommandBlockListTable(props){ - const theme = useTheme(); const [openNew, setOpenNewDialog] = React.useState(false); const [blockLists, setBlockLists] = React.useState([]); const [newBlockListEntries] = useMutation(newBlockListEntry, { @@ -174,22 +172,26 @@ export function CommandBlockListTable(props){ if(props?.me?.user?.current_operation_id === 0){ return null; } + const blockListCountLabel = blockLists.length === 1 ? "1 list" : `${blockLists.length} lists`; return ( <> - - - {openNew && - {setOpenNewDialog(false);}} - innerDialog={ setOpenNewDialog(false)} - />} - /> + } + actions={ + {setOpenNewDialog(true);}} startIcon={}> + Block List + } - + /> + {openNew && + {setOpenNewDialog(false);}} + innerDialog={ setOpenNewDialog(false)} + />} + /> + } @@ -201,6 +203,14 @@ export function CommandBlockListTable(props){ + {blockLists.length === 0 && + + } { blockLists.map( b => ( @@ -214,7 +224,6 @@ export function CommandBlockListTable(props){ } function CommandBlockListTableRow(props){ - const theme = useTheme(); const [openDelete, setOpenDeleteDialog] = React.useState(false); const [blockedCommandDisplay, setBlockedCommandDisplay] = React.useState([]); const [openUpdate, setOpenUpdateDialog] = React.useState(false); @@ -242,11 +251,11 @@ function CommandBlockListTableRow(props){ return ( - {setOpenDeleteDialog(true);}} color={"error"} variant="contained"> + {setOpenDeleteDialog(true);}}> {setOpenDeleteDialog(false);}} onSubmit={onAcceptDelete} open={openDelete}/> - + {openUpdate && {setOpenUpdateDialog(false);}} @@ -266,4 +275,3 @@ function CommandBlockListTableRow(props){ ) } - diff --git a/MythicReactUI/src/components/pages/Operations/EditBlockListDialog.js b/MythicReactUI/src/components/pages/Operations/EditBlockListDialog.js index 4cf5ccaf..9e629f5d 100644 --- a/MythicReactUI/src/components/pages/Operations/EditBlockListDialog.js +++ b/MythicReactUI/src/components/pages/Operations/EditBlockListDialog.js @@ -1,6 +1,4 @@ import React, {useEffect} from 'react'; -import Button from '@mui/material/Button'; -import DialogActions from '@mui/material/DialogActions'; import DialogContent from '@mui/material/DialogContent'; import DialogTitle from '@mui/material/DialogTitle'; import Grid from '@mui/material/Grid'; @@ -9,20 +7,20 @@ import ListItem from '@mui/material/ListItem'; import ListItemIcon from '@mui/material/ListItemIcon'; import ListItemText from '@mui/material/ListItemText'; import Checkbox from '@mui/material/Checkbox'; -import Paper from '@mui/material/Paper'; -import Card from '@mui/material/Card'; -import CardHeader from '@mui/material/CardHeader'; -import CardContent from '@mui/material/CardContent'; import {useQuery, gql} from '@apollo/client'; -import {useTheme} from '@mui/material/styles'; -import Typography from '@mui/material/Typography'; import MythicTextField from '../../MythicComponents/MythicTextField'; import { snackActions } from '../../utilities/Snackbar'; -import {classes, StyledButton, StyledDivider} from '../../MythicComponents/MythicTransferList'; +import {classes, StyledButton} from '../../MythicComponents/MythicTransferList'; +import { + MythicDialogBody, + MythicDialogButton, + MythicDialogFooter, + MythicDialogSection, + MythicFormField +} from '../../MythicComponents/MythicDialogLayout'; function PayloadTypeBlockListPreMemo(props){ - const theme = useTheme(); const [checked, setChecked] = React.useState([]); const [left, setLeft] = React.useState([]); const [right, setRight] = React.useState(props.right); @@ -101,14 +99,10 @@ function PayloadTypeBlockListPreMemo(props){ props.onChange({selected: right, name: props.name}); }, [right]) const customList = (title, items) => ( - - - - - - +
+
{title}
+
+ {items.map((valueObj) => { const value = props.itemKey === undefined ? valueObj : valueObj[props.itemKey]; const labelId = `transfer-list-item-${value}-label`; @@ -127,23 +121,16 @@ function PayloadTypeBlockListPreMemo(props){ ); })} - - - +
+
); return ( - - - - - {props.name} - - - + + {customList(leftTitle, left)} - +
≪ - +
{customList(rightTitle, right)}
+
); } const PayloadTypeBlockList = React.memo(PayloadTypeBlockListPreMemo); @@ -267,20 +255,25 @@ export function EditBlockListDialog({dialogTitle, onSubmit, blockListName: propB <> {dialogTitle} - - {payloadtypes.map(p => ( - - ))} + + + + + + + {payloadtypes.map(p => ( + + ))} + - - - - + + ); } - diff --git a/MythicReactUI/src/components/pages/Operations/OperationTable.js b/MythicReactUI/src/components/pages/Operations/OperationTable.js index 048540c4..666f624b 100644 --- a/MythicReactUI/src/components/pages/Operations/OperationTable.js +++ b/MythicReactUI/src/components/pages/Operations/OperationTable.js @@ -1,28 +1,24 @@ import React from 'react'; -import {Button} from '@mui/material'; import Table from '@mui/material/Table'; import TableBody from '@mui/material/TableBody'; import TableCell from '@mui/material/TableCell'; import TableContainer from '@mui/material/TableContainer'; import TableHead from '@mui/material/TableHead'; import TableRow from '@mui/material/TableRow'; -import Paper from '@mui/material/Paper'; import { OperationTableRow } from './OperationTableRow'; -import Typography from '@mui/material/Typography'; import AddCircleIcon from '@mui/icons-material/AddCircle'; import { MythicDialog } from '../../MythicComponents/MythicDialog'; -import {useTheme} from '@mui/material/styles'; import {SettingsOperatorDialog} from '../Settings/SettingsOperatorDialog'; import {snackActions} from '../../utilities/Snackbar'; import {useMutation, gql} from '@apollo/client'; import {MythicModifyStringDialog} from '../../MythicComponents/MythicDialog'; import VisibilityOffIcon from '@mui/icons-material/VisibilityOff'; import VisibilityIcon from '@mui/icons-material/Visibility'; -import { MythicStyledTooltip } from '../../MythicComponents/MythicStyledTooltip'; -import { IconButton } from '@mui/material'; import PersonAddIcon from '@mui/icons-material/PersonAdd'; import {meState} from "../../../cache"; -import {MythicPageHeader} from "../../MythicComponents/MythicPageHeader"; +import {MythicPageHeader, MythicPageHeaderChip} from "../../MythicComponents/MythicPageHeader"; +import {MythicToolbarButton, MythicToolbarToggle} from "../../MythicComponents/MythicTableToolbar"; +import {MythicTableEmptyState} from "../../MythicComponents/MythicStateDisplay"; const newOperatorMutation = gql` mutation NewOperator($username: String!, $password: String!) { @@ -59,7 +55,6 @@ mutation newOperationMutation($name: String){ `; export function OperationTable(props){ - const theme = useTheme(); const [openNewOperator, setOpenNewOperatorDialog] = React.useState(false); const [openNewOperation, setOpenNewOperationDialog] = React.useState(false); const [showDeleted, setShowDeleted] = React.useState(false); @@ -143,72 +138,60 @@ export function OperationTable(props){ const onSubmitNewOperation = (operation_name) => { newOperation({variables: {name: operation_name}}) } + const visibleOperations = props.operations.filter((op) => showDeleted || !op.deleted); + const visibleOperationsLabel = visibleOperations.length === 1 ? "1 shown" : `${visibleOperations.length} shown`; + const totalOperationsLabel = props.operations.length === 1 ? "1 total" : `${props.operations.length} total`; return ( <> - - - - {setOpenNewOperatorDialog(true);}} - style={{}} - variant="contained"> - - - - {showDeleted ? ( - - setShowDeleted(!showDeleted)}> - - - - ) : ( - - setShowDeleted(!showDeleted)} > - - - - )} - {openNewOperator && - {setOpenNewOperatorDialog(false);}} - innerDialog={{setOpenNewOperatorDialog(false);}} {...props}/>} - /> + + + + {showDeleted && } + } - {openNewOperation && - {setOpenNewOperationDialog(false);}} - innerDialog={ - {setOpenNewOperationDialog(false);}} - value={""} - onSubmit={onSubmitNewOperation} - /> - } - /> + actions={ + <> + {setOpenNewOperationDialog(true);}} startIcon={}> + Operation + + {setOpenNewOperatorDialog(true);}} startIcon={}> + Operator + + setShowDeleted(!showDeleted)} + label="Deleted" + activeIcon={} + inactiveIcon={} + /> + } - - + /> + {openNewOperator && + {setOpenNewOperatorDialog(false);}} + innerDialog={{setOpenNewOperatorDialog(false);}} {...props}/>} + /> + } + {openNewOperation && + {setOpenNewOperationDialog(false);}} + innerDialog={ + {setOpenNewOperationDialog(false);}} + value={""} + onSubmit={onSubmitNewOperation} + /> + } + /> + } - {props.operations.length === 0 && -
-
- {"No Operations available!"}
- {"Ask a Mythic admin or operation lead to add you to an operation."} -
-
- }
@@ -222,22 +205,27 @@ export function OperationTable(props){ - {props.operations.map((op) => ( - showDeleted || !op.deleted ? ( + {visibleOperations.length === 0 && + + } + {visibleOperations.map((op) => ( - ) : null - ))} + onUpdateOperation={onUpdateOperation} + onUpdateCurrentOperation={props.onUpdateCurrentOperation} + updateDeleted={props.updateDeleted} + {...op} operator={props.operator} + /> + ))}
) } - diff --git a/MythicReactUI/src/components/pages/Operations/OperationTableRow.js b/MythicReactUI/src/components/pages/Operations/OperationTableRow.js index 17c42c4c..9b8fb7c6 100644 --- a/MythicReactUI/src/components/pages/Operations/OperationTableRow.js +++ b/MythicReactUI/src/components/pages/Operations/OperationTableRow.js @@ -17,6 +17,7 @@ import DeleteIcon from '@mui/icons-material/Delete'; import RestoreFromTrashOutlinedIcon from '@mui/icons-material/RestoreFromTrashOutlined'; import {restartWebsockets} from '../../../index'; import {useNavigate} from 'react-router-dom'; +import {MythicStatusChip} from '../../MythicComponents/MythicStatusChip'; export const updateCurrentOperationMutation = gql` mutation updateCurrentOpertionMutation($operator_id: Int!, $operation_id: Int!) { @@ -121,11 +122,25 @@ export function OperationTableRow(props){ {props.deleted ? ( - {setOpenDeleteDialog(true);}} color="success" variant="contained" - disabled={me?.user?.current_operation_id !== props.id}> + {setOpenDeleteDialog(true);}} + disabled={me?.user?.current_operation_id !== props.id} + > + + ) : ( - {setOpenDeleteDialog(true);}} color="error" variant="contained" - disabled={me?.user?.current_operation_id !== props.id}> + {setOpenDeleteDialog(true);}} + disabled={me?.user?.current_operation_id !== props.id} + > + + )} {openDelete && {setOpenDeleteDialog(false);}} onSubmit={onAcceptDelete} @@ -134,9 +149,9 @@ export function OperationTableRow(props){ acceptColor={props.deleted ? "success": "error"} /> } - + variant="outlined">Edit {openUpdateNotifications && {setOpenUpdateNotifications(false);}} @@ -144,9 +159,9 @@ export function OperationTableRow(props){ /> } - + startIcon={} variant="outlined">Edit {openUpdateOperators && {setOpenUpdateOperators(false);}} @@ -154,18 +169,30 @@ export function OperationTableRow(props){ /> } - {props.name} {props.complete ? " (Completed) " : ""} + +
+ {props.name} + {props.complete && + + } + {props.deleted && + + } +
+
{props.admin.username} - + variant="outlined">Analysis - {props.id === me.user.current_operation_id ? ("Current Operation") : ( + {props.id === me.user.current_operation_id ? ( + + ) : ( - + )} @@ -173,4 +200,3 @@ export function OperationTableRow(props){
) } - diff --git a/MythicReactUI/src/components/pages/Operations/OperationTableRowNotificationsDialog.js b/MythicReactUI/src/components/pages/Operations/OperationTableRowNotificationsDialog.js index 60ec4136..f402e2f4 100644 --- a/MythicReactUI/src/components/pages/Operations/OperationTableRowNotificationsDialog.js +++ b/MythicReactUI/src/components/pages/Operations/OperationTableRowNotificationsDialog.js @@ -2,7 +2,6 @@ import React from 'react'; import Button from '@mui/material/Button'; import DialogActions from '@mui/material/DialogActions'; import DialogContent from '@mui/material/DialogContent'; -import DialogContentText from '@mui/material/DialogContentText'; import DialogTitle from '@mui/material/DialogTitle'; import MythicTextField from '../../MythicComponents/MythicTextField'; import {useQuery, gql} from '@apollo/client'; @@ -10,7 +9,13 @@ import {snackActions} from '../../utilities/Snackbar'; import Switch from '@mui/material/Switch'; import Typography from '@mui/material/Typography'; import Box from '@mui/material/Box'; +import FormControlLabel from '@mui/material/FormControlLabel'; import {getReadableTextColor, MythicColorSwatchInput} from '../../MythicComponents/MythicColorInput'; +import { + MythicDialogBody, + MythicDialogGrid, + MythicDialogSection +} from '../../MythicComponents/MythicDialogLayout'; const GET_OperationData = gql` query GetOperations($operation_id: Int!) { @@ -89,73 +94,65 @@ export function OperationTableRowNotificationsDialog(props) { Modify {name} - - Use this dialog to update some information about an operation. - - - Complete Operation? - - - - + + + + } + /> + + + + + + + + + + } > - - - Banner Color - Operation banner background - - - - - - {bannerText || "Operation Banner"} - - - + + + + {bannerText || "Operation Banner"} + + + + - + + ); } diff --git a/MythicReactUI/src/components/pages/PayloadTypesC2Profiles/C2ProfileConfigDialog.js b/MythicReactUI/src/components/pages/PayloadTypesC2Profiles/C2ProfileConfigDialog.js index fb25d877..b812a945 100644 --- a/MythicReactUI/src/components/pages/PayloadTypesC2Profiles/C2ProfileConfigDialog.js +++ b/MythicReactUI/src/components/pages/PayloadTypesC2Profiles/C2ProfileConfigDialog.js @@ -1,14 +1,14 @@ -import React, {useState} from 'react'; -import Button from '@mui/material/Button'; -import DialogActions from '@mui/material/DialogActions'; +import React from 'react'; +import DialogContent from '@mui/material/DialogContent'; import DialogTitle from '@mui/material/DialogTitle'; import {useQuery, gql} from '@apollo/client'; -import LinearProgress from '@mui/material/LinearProgress'; import 'ace-builds/src-noconflict/mode-json'; import 'ace-builds/src-noconflict/theme-monokai'; import 'ace-builds/src-noconflict/theme-xcode'; import {ResponseDisplayPlaintext} from "../Callbacks/ResponseDisplayPlaintext"; import {textExtensionTypesToSyntax} from "../Callbacks/ResponseDisplayMedia"; +import {MythicDialogButton, MythicDialogFooter} from "../../MythicComponents/MythicDialogLayout"; +import {MythicErrorState, MythicLoadingState} from "../../MythicComponents/MythicStateDisplay"; const getProfileConfigQuery = gql` query getProfileConfigOutput($container_name: String!, $filename: String!) { @@ -36,7 +36,6 @@ const getInitialMode = (filename) => { } export function C2ProfileConfigDialog(props) { const config = React.useRef(""); - const [fetchedData, setFetchedData] = React.useState(false); const { loading, error } = useQuery(getProfileConfigQuery, { variables: {container_name: props.container_name, filename: props.filename}, onCompleted: data => { @@ -46,18 +45,31 @@ export function C2ProfileConfigDialog(props) { //console.log(data); config.current = atob(data.containerDownloadFile.data); } - setFetchedData(true); }, fetchPolicy: "network-only" }); const initialMode = React.useRef(getInitialMode(props.filename)); if (loading) { - return ; + return ( + <> + {props.container_name}'s {props.filename} + + + + + ); } if (error) { console.error(error); - return
Error! {error.message}
; + return ( + <> + {props.container_name}'s {props.filename} + + + + + ); } const onConfigSubmit = () => { props.onConfigSubmit(btoa(config.current)); @@ -70,6 +82,7 @@ export function C2ProfileConfigDialog(props) { return ( <> {props.container_name}'s {props.filename} +
- - - - + + ); } - diff --git a/MythicReactUI/src/components/pages/PayloadTypesC2Profiles/C2ProfileListFilesDialog.js b/MythicReactUI/src/components/pages/PayloadTypesC2Profiles/C2ProfileListFilesDialog.js index f2a1da34..e817f701 100644 --- a/MythicReactUI/src/components/pages/PayloadTypesC2Profiles/C2ProfileListFilesDialog.js +++ b/MythicReactUI/src/components/pages/PayloadTypesC2Profiles/C2ProfileListFilesDialog.js @@ -1,9 +1,8 @@ import React from 'react'; import Button from '@mui/material/Button'; -import DialogActions from '@mui/material/DialogActions'; +import DialogContent from '@mui/material/DialogContent'; import DialogTitle from '@mui/material/DialogTitle'; import {useQuery, gql, useMutation, useLazyQuery} from '@apollo/client'; -import LinearProgress from '@mui/material/LinearProgress'; import { snackActions } from '../../utilities/Snackbar'; import Table from '@mui/material/Table'; import TableBody from '@mui/material/TableBody'; @@ -20,6 +19,9 @@ import DownloadIcon from '@mui/icons-material/Download'; import DeleteIcon from '@mui/icons-material/Delete'; import {MythicConfirmDialog} from '../../MythicComponents/MythicConfirmDialog'; import {downloadFileFromMemory} from '../../utilities/Clipboard'; +import {MythicDialogBody, MythicDialogButton, MythicDialogFooter, MythicDialogSection} from "../../MythicComponents/MythicDialogLayout"; +import {MythicErrorState, MythicLoadingState} from "../../MythicComponents/MythicStateDisplay"; +import {MythicStyledTooltip} from "../../MythicComponents/MythicStyledTooltip"; export const containerListFilesQuery = gql` query listC2ProfileFilesQuery($container_name: String!){ @@ -96,21 +98,47 @@ export function C2ProfileListFilesDialog(props) { } if (loading) { - return ; + return ( + <> + {props.container_name}'s Current Files + + + + + ); } if (error) { console.error(error); - return
Error! {error.message}
; + return ( + <> + {props.container_name}'s Current Files + + + + + ); } return ( - {props.container_name}'s Current Files - + +
+ {props.container_name}'s Current Files + +
+ + + @@ -128,12 +156,14 @@ export function C2ProfileListFilesDialog(props) {
- - - - + +
); } @@ -200,13 +230,25 @@ const C2ProfileListFilesDialogTableRow = ({container_name, id, filename}) => { - {setOpenDeleteDialog(true);}} > + + {setOpenDeleteDialog(true);}} > + + + - + + + + + - {setOpenProfileConfigDialog(true);}}> + + {setOpenProfileConfigDialog(true);}}> + + + {filename} diff --git a/MythicReactUI/src/components/pages/PayloadTypesC2Profiles/C2ProfileOutputDialog.js b/MythicReactUI/src/components/pages/PayloadTypesC2Profiles/C2ProfileOutputDialog.js index 8ee70e3d..8e1e28a4 100644 --- a/MythicReactUI/src/components/pages/PayloadTypesC2Profiles/C2ProfileOutputDialog.js +++ b/MythicReactUI/src/components/pages/PayloadTypesC2Profiles/C2ProfileOutputDialog.js @@ -1,11 +1,10 @@ import React from 'react'; -import Button from '@mui/material/Button'; -import DialogActions from '@mui/material/DialogActions'; -import DialogContentText from '@mui/material/DialogContentText'; +import DialogContent from '@mui/material/DialogContent'; import DialogTitle from '@mui/material/DialogTitle'; import {useQuery, gql} from '@apollo/client'; import {snackActions} from "../../utilities/Snackbar"; import {ResponseDisplayPlaintext} from "../Callbacks/ResponseDisplayPlaintext"; +import {MythicDialogButton, MythicDialogFooter, MythicDialogSection} from "../../MythicComponents/MythicDialogLayout"; const getProfileOutputQuery = gql` query getProfileOutput($id: Int!) { @@ -45,10 +44,9 @@ export function C2ProfileOutputDialog(props) { return ( {props.container_name}'s Current Stdout/Stderr - - This is the current Stdout/Stderr for the profile. This goes away once you close this dialog. - -
+ + +
- - - + + ); } - diff --git a/MythicReactUI/src/components/pages/PayloadTypesC2Profiles/C2ProfileSavedInstancesDialog.js b/MythicReactUI/src/components/pages/PayloadTypesC2Profiles/C2ProfileSavedInstancesDialog.js index 321d0029..07352215 100644 --- a/MythicReactUI/src/components/pages/PayloadTypesC2Profiles/C2ProfileSavedInstancesDialog.js +++ b/MythicReactUI/src/components/pages/PayloadTypesC2Profiles/C2ProfileSavedInstancesDialog.js @@ -1,24 +1,23 @@ import React, {useState} from 'react'; import {Button, Typography} from '@mui/material'; -import DialogActions from '@mui/material/DialogActions'; import DialogContent from '@mui/material/DialogContent'; import DialogTitle from '@mui/material/DialogTitle'; import MythicTextField from '../../MythicComponents/MythicTextField'; import {useQuery, gql, useLazyQuery, useMutation} from '@apollo/client'; -import LinearProgress from '@mui/material/LinearProgress'; import {CreatePayloadC2ProfileParametersTable} from '../CreatePayload/CreatePayloadC2ProfileParametersTable'; import FormControl from '@mui/material/FormControl'; import Select from '@mui/material/Select'; import Grid from '@mui/material/Grid'; import InputLabel from '@mui/material/InputLabel'; import MenuItem from '@mui/material/MenuItem'; -import {useTheme} from '@mui/material/styles'; import { snackActions } from '../../utilities/Snackbar'; import {getDefaultValueForType, getDefaultChoices} from '../CreatePayload/Step2SelectPayloadType'; import {UploadTaskFile} from "../../MythicComponents/MythicFileUpload"; import IosShareIcon from '@mui/icons-material/IosShare'; import SystemUpdateAltIcon from '@mui/icons-material/SystemUpdateAlt'; import DeleteIcon from '@mui/icons-material/Delete'; +import {MythicDialogButton, MythicDialogFooter} from "../../MythicComponents/MythicDialogLayout"; +import {MythicLoadingState} from "../../MythicComponents/MythicStateDisplay"; const getProfileConfigQuery = gql` query getProfileParameters($id: Int!) { @@ -91,7 +90,6 @@ importC2Instance( `; export function C2ProfileSavedInstancesDialog(props) { - const theme = useTheme(); const [instanceName, setInstanceName] = useState(""); const [selectedInstance, setSelectedInstance] = useState(""); const [createdInstances, setCreatedInstances] = useState([]); @@ -233,7 +231,14 @@ export function C2ProfileSavedInstancesDialog(props) { } }) if (loading) { - return ; + return ( + <> + Save an Instance of {props.name}'s Parameters + + + + + ); } const onConfigSubmit = async () => { if(instanceName.length === 0){ @@ -396,14 +401,14 @@ export function C2ProfileSavedInstancesDialog(props) { {selectedInstance.length > 0 ? ( <> - - + + ) : null} - @@ -414,22 +419,22 @@ export function C2ProfileSavedInstancesDialog(props) { inline={createdInstances.length === 0}/> {createdInstances.length === 0 && <> - }
- - - - + + ); } diff --git a/MythicReactUI/src/components/pages/PayloadTypesC2Profiles/C2ProfileStartStopOutputDialog.js b/MythicReactUI/src/components/pages/PayloadTypesC2Profiles/C2ProfileStartStopOutputDialog.js index 43e0240d..5293f09c 100644 --- a/MythicReactUI/src/components/pages/PayloadTypesC2Profiles/C2ProfileStartStopOutputDialog.js +++ b/MythicReactUI/src/components/pages/PayloadTypesC2Profiles/C2ProfileStartStopOutputDialog.js @@ -1,19 +1,17 @@ import React from 'react'; -import Button from '@mui/material/Button'; -import DialogActions from '@mui/material/DialogActions'; -import DialogContentText from '@mui/material/DialogContentText'; +import DialogContent from '@mui/material/DialogContent'; import DialogTitle from '@mui/material/DialogTitle'; import {ResponseDisplayPlaintext} from "../Callbacks/ResponseDisplayPlaintext"; +import {MythicDialogButton, MythicDialogFooter, MythicDialogSection} from "../../MythicComponents/MythicDialogLayout"; export function C2ProfileStartStopOutputDialog(props) { return ( <> {props.container_name}'s Current Stdout/Stderr - - This is the current Stdout/Stderr for the profile. This goes away once you close this dialog. - -
+ + +
- - - + + ); } - diff --git a/MythicReactUI/src/components/pages/PayloadTypesC2Profiles/C2ProfilesCard.js b/MythicReactUI/src/components/pages/PayloadTypesC2Profiles/C2ProfilesCard.js index af5cc676..0a6babd7 100644 --- a/MythicReactUI/src/components/pages/PayloadTypesC2Profiles/C2ProfilesCard.js +++ b/MythicReactUI/src/components/pages/PayloadTypesC2Profiles/C2ProfilesCard.js @@ -17,7 +17,6 @@ import {C2ProfileOutputDialog} from './C2ProfileOutputDialog'; import {C2ProfileConfigDialog} from './C2ProfileConfigDialog'; import {C2ProfileStartStopOutputDialog} from './C2ProfileStartStopOutputDialog'; import {snackActions} from '../../utilities/Snackbar'; -import {useTheme} from '@mui/material/styles'; import {C2ProfileSavedInstancesDialog} from './C2ProfileSavedInstancesDialog'; import {FontAwesomeIcon} from '@fortawesome/react-fontawesome'; import { faLink } from '@fortawesome/free-solid-svg-icons'; @@ -27,13 +26,15 @@ import RestoreFromTrashOutlinedIcon from '@mui/icons-material/RestoreFromTrashOu import {C2ProfileListFilesDialog} from './C2ProfileListFilesDialog'; import MenuBookIcon from '@mui/icons-material/MenuBook'; import IconButton from '@mui/material/IconButton'; -import BuildIcon from '@mui/icons-material/Build'; +import TuneIcon from '@mui/icons-material/Tune'; import SaveIcon from '@mui/icons-material/Save'; import AttachFileIcon from '@mui/icons-material/AttachFile'; import TableRow from '@mui/material/TableRow'; import MythicTableCell from "../../MythicComponents/MythicTableCell"; import {MythicStyledTooltip} from "../../MythicComponents/MythicStyledTooltip"; import {MythicAgentSVGIcon} from "../../MythicComponents/MythicAgentSVGIcon"; +import {MythicStatusChip} from "../../MythicComponents/MythicStatusChip"; +import CheckCircleOutlineIcon from '@mui/icons-material/CheckCircleOutline'; const toggleDeleteStatus = gql` @@ -63,7 +64,6 @@ mutation setProfileConfiguration($container_name: String!, $file_path: String!, `; export function C2ProfilesRow({service, showDeleted}) { - const theme = useTheme(); const [openBuildingDialog, setOpenBuildingDialog] = React.useState(false); const [openListFilesDialog, setOpenListFilesDialog] = React.useState(false); const [dropdownOpen, setDropdownOpen] = React.useState(false); @@ -148,17 +148,19 @@ export function C2ProfilesRow({service, showDeleted}) { if(service.deleted && !showDeleted){ return null; } + const containerStatus = !service.container_running ? "error" : service.is_p2p ? "success" : "neutral"; + const containerIcon = service.container_running && !service.is_p2p ? : undefined; return ( <> {service.deleted ? ( - {setOpenDeleteDialog(true);}} color="success" size="small"> - + {setOpenDeleteDialog(true);}} size="small"> + ) : ( - {setOpenDeleteDialog(true);}} color="error" size="small"> - + {setOpenDeleteDialog(true);}} size="small"> + )} @@ -199,27 +201,22 @@ export function C2ProfilesRow({service, showDeleted}) { Container Status: - - {service.container_running ? "Online" : "Offline"} - - {!service.is_p2p && service.running && + + {!service.is_p2p && C2 Server Status: - - {"Accepting Connections"} - - - } - {!service.is_p2p && !service.running && - - - C2 Server Status: - - - {"Not Accepting Connections"} - + } @@ -227,8 +224,9 @@ export function C2ProfilesRow({service, showDeleted}) { {service.container_running ? ( service.running ? ( - + ) : ( service.is_p2p ? null : ( - + ) ) ) : null} -
+
- + size="small"> + {setOpenBuildingDialog(true);}} - size="medium"> - + size="small"> + {setOpenProfileSavedInstancesDialog(true);}} - size="medium"> - + size="small"> + {setOpenListFilesDialog(true);}} - size="medium"> - + size="small"> + +
{({ TransitionProps, placement }) => ( @@ -385,4 +391,4 @@ export function C2ProfilesRow({service, showDeleted}) { ); -} \ No newline at end of file +} diff --git a/MythicReactUI/src/components/pages/PayloadTypesC2Profiles/ConsumingServicesGetIDPMetadataDialog.js b/MythicReactUI/src/components/pages/PayloadTypesC2Profiles/ConsumingServicesGetIDPMetadataDialog.js index 5841f23a..8831d787 100644 --- a/MythicReactUI/src/components/pages/PayloadTypesC2Profiles/ConsumingServicesGetIDPMetadataDialog.js +++ b/MythicReactUI/src/components/pages/PayloadTypesC2Profiles/ConsumingServicesGetIDPMetadataDialog.js @@ -1,10 +1,6 @@ import React, {useState} from 'react'; -import Button from '@mui/material/Button'; -import DialogActions from '@mui/material/DialogActions'; import DialogContent from '@mui/material/DialogContent'; import DialogTitle from '@mui/material/DialogTitle'; -import {useQuery, gql} from '@apollo/client'; -import LinearProgress from '@mui/material/LinearProgress'; import { snackActions } from '../../utilities/Snackbar'; import AceEditor from 'react-ace'; import 'ace-builds/src-noconflict/mode-json'; @@ -12,6 +8,7 @@ import 'ace-builds/src-noconflict/theme-monokai'; import 'ace-builds/src-noconflict/theme-xcode'; import "ace-builds/src-noconflict/ext-searchbox"; import {useTheme} from '@mui/material/styles'; +import {MythicDialogBody, MythicDialogButton, MythicDialogFooter, MythicDialogSection} from "../../MythicComponents/MythicDialogLayout"; export function ConsumingServicesGetIDPMetadataDialog(props) { @@ -51,6 +48,8 @@ export function ConsumingServicesGetIDPMetadataDialog(props) { {props.container}'s {props.idp} Metadata + + + + - - - + + ); } - diff --git a/MythicReactUI/src/components/pages/PayloadTypesC2Profiles/ConsumingServicesTable.js b/MythicReactUI/src/components/pages/PayloadTypesC2Profiles/ConsumingServicesTable.js index c90ec60c..c21c9473 100644 --- a/MythicReactUI/src/components/pages/PayloadTypesC2Profiles/ConsumingServicesTable.js +++ b/MythicReactUI/src/components/pages/PayloadTypesC2Profiles/ConsumingServicesTable.js @@ -2,12 +2,9 @@ import React from 'react'; import Table from '@mui/material/Table'; import TableBody from '@mui/material/TableBody'; import TableCell from '@mui/material/TableCell'; -import TableContainer from '@mui/material/TableContainer'; import TableHead from '@mui/material/TableHead'; import TableRow from '@mui/material/TableRow'; -import Paper from '@mui/material/Paper'; import Typography from '@mui/material/Typography'; -import {useTheme} from '@mui/material/styles'; import PublicIcon from '@mui/icons-material/Public'; import {IconButton} from '@mui/material'; import {gql, useMutation} from '@apollo/client'; @@ -23,8 +20,7 @@ import {MythicDialog} from "../../MythicComponents/MythicDialog"; import AttachFileIcon from '@mui/icons-material/AttachFile'; import {ConsumingServicesGetIDPMetadataDialog} from "./ConsumingServicesGetIDPMetadataDialog"; import {C2ProfileListFilesDialog} from "./C2ProfileListFilesDialog"; -import VisibilityOffIcon from '@mui/icons-material/VisibilityOff'; -import VisibilityIcon from '@mui/icons-material/Visibility'; +import {MythicStatusChip} from "../../MythicComponents/MythicStatusChip"; const testWebhookMutation = gql` mutation testWebhookWorks($service_type: String!){ @@ -53,7 +49,6 @@ const webhook_events = ["new_alert","new_callback","new_custom","new_feedback", const logging_events = ["new_artifact","new_callback", "new_credential","new_file", "new_keylog", "new_payload", "new_response", "new_task"]; export const ConsumingServicesTableRow = ({service, showDeleted}) => { - const theme = useTheme(); const openListFilesInformation = React.useRef(""); const [testWebhook] = useMutation(testWebhookMutation, { onCompleted: data => { @@ -159,6 +154,35 @@ export const ConsumingServicesTableRow = ({service, showDeleted}) => { default: } }, [service]); + const renderDeleteButton = (w) => ( + + adjustingDelete(w)} + size="small" + > + {w.deleted ? : } + + + ); + const renderContainerStatus = (w) => ( + + ); + const renderFileButton = (w) => ( + + {onOpenListFilesDialog(w.name);}} + size="small" + > + + + + ); const getTableRow = (w) => { switch(service.type){ case "webhook": @@ -166,19 +190,7 @@ export const ConsumingServicesTableRow = ({service, showDeleted}) => { (showDeleted || !w.deleted) && - {w.deleted ? ( - { - adjustingDelete(w); - }} color="success" size="medium"> - - - ) : ( - { - adjustingDelete(w); - }} color="error" size="medium"> - - - )} + {renderDeleteButton(w)} @@ -190,43 +202,27 @@ export const ConsumingServicesTableRow = ({service, showDeleted}) => { Description: {w.description} - - {w.container_running && - - {"Online"} - - } - {!w.container_running && - - {"Offline"} - - } + {renderContainerStatus(w)} - - {onOpenListFilesDialog(w.name);}} - size="medium"> - - - + {renderFileButton(w)} - {webhook_events.map(s => ( - - { - issueTestWebhook(s) - }} - size="medium"> - - - - ))} +
+ {webhook_events.map(s => ( + + { + issueTestWebhook(s) + }} + size="small"> + + + + ))} +
) @@ -235,19 +231,7 @@ export const ConsumingServicesTableRow = ({service, showDeleted}) => { (showDeleted || !w.deleted) && - {w.deleted ? ( - { - adjustingDelete(w); - }} color="success" size="medium"> - - - ) : ( - { - adjustingDelete(w); - }} color="error" size="medium"> - - - )} + {renderDeleteButton(w)} @@ -259,42 +243,27 @@ export const ConsumingServicesTableRow = ({service, showDeleted}) => { Description: {w.description} - {w.container_running && - - {"Online"} - - } - {!w.container_running && - - {"Offline"} - - } + {renderContainerStatus(w)} - - {onOpenListFilesDialog(w.name);}} - size="medium"> - - - + {renderFileButton(w)} - {logging_events.map(s => ( - - { - issueTestLog(s) - }} - size="medium"> - - - - ))} +
+ {logging_events.map(s => ( + + { + issueTestLog(s) + }} + size="small"> + + + + ))} +
) @@ -303,19 +272,7 @@ export const ConsumingServicesTableRow = ({service, showDeleted}) => { (showDeleted || !w.deleted) && - {w.deleted ? ( - { - adjustingDelete(w); - }} color="success" size="medium"> - - - ) : ( - { - adjustingDelete(w); - }} color="error" size="medium"> - - - )} + {renderDeleteButton(w)} @@ -327,28 +284,10 @@ export const ConsumingServicesTableRow = ({service, showDeleted}) => { Description: {w.description} - {w.container_running && - - {"Online"} - - } - {!w.container_running && - - {"Offline"} - - } + {renderContainerStatus(w)} - - {onOpenListFilesDialog(w.name);}} - size="medium"> - - - + {renderFileButton(w)} @@ -376,19 +315,7 @@ export const ConsumingServicesTableRow = ({service, showDeleted}) => { (showDeleted || !w.deleted) && - {w.deleted ? ( - { - adjustingDelete(w); - }} color="success" size="medium"> - - - ) : ( - { - adjustingDelete(w); - }} color="error" size="medium"> - - - )} + {renderDeleteButton(w)} @@ -400,36 +327,22 @@ export const ConsumingServicesTableRow = ({service, showDeleted}) => { Description: {w.description} - {w.container_running && - - {"Online"} - - } - {!w.container_running && - - {"Offline"} - - } + {renderContainerStatus(w)} - - {onOpenListFilesDialog(w.name);}} - size="medium"> - - - + {renderFileButton(w)} {w.subscriptions.map(s => ( - getIDPMetadata(w.name, s.name)} - disabled={!w.container_running}> - + getIDPMetadata(w.name, s.name)} + disabled={!w.container_running} + size="small" + > + {s.name} @@ -470,4 +383,3 @@ export const ConsumingServicesTableRow = ({service, showDeleted}) => { ) } - diff --git a/MythicReactUI/src/components/pages/PayloadTypesC2Profiles/CustomBrowserCard.js b/MythicReactUI/src/components/pages/PayloadTypesC2Profiles/CustomBrowserCard.js index a60b17a7..fb8bf212 100644 --- a/MythicReactUI/src/components/pages/PayloadTypesC2Profiles/CustomBrowserCard.js +++ b/MythicReactUI/src/components/pages/PayloadTypesC2Profiles/CustomBrowserCard.js @@ -1,7 +1,6 @@ import React from 'react'; import AttachFileIcon from '@mui/icons-material/AttachFile'; import Typography from '@mui/material/Typography'; -import {useTheme} from '@mui/material/styles'; import IconButton from '@mui/material/IconButton'; import {useMutation, gql} from '@apollo/client'; import {snackActions} from '../../utilities/Snackbar'; @@ -19,6 +18,7 @@ import Table from '@mui/material/Table'; import TableBody from '@mui/material/TableBody'; import TableContainer from '@mui/material/TableContainer'; import TableHead from '@mui/material/TableHead'; +import {MythicStatusChip} from "../../MythicComponents/MythicStatusChip"; const toggleDeleteStatus = gql` mutation toggleCustomBrowserDeleteStatus($custombrowser_id: Int!, $deleted: Boolean!){ @@ -29,7 +29,6 @@ mutation toggleCustomBrowserDeleteStatus($custombrowser_id: Int!, $deleted: Bool `; export function CustomBrowserRow({service, showDeleted}) { - const theme = useTheme(); const [openListFilesDialog, setOpenListFilesDialog] = React.useState(false); const [openDelete, setOpenDeleteDialog] = React.useState(false); const [updateDeleted] = useMutation(toggleDeleteStatus, { @@ -55,9 +54,9 @@ export function CustomBrowserRow({service, showDeleted}) { {service.deleted ? ( - {setOpenDeleteDialog(true);}} color="success" variant="contained"> + {setOpenDeleteDialog(true);}}> ) : ( - {setOpenDeleteDialog(true);}} color="error" variant="contained"> + {setOpenDeleteDialog(true);}}> )} {openDelete && {setOpenDeleteDialog(false);}} onSubmit={onAcceptDelete} @@ -71,9 +70,9 @@ export function CustomBrowserRow({service, showDeleted}) { {service.name} - - {service.container_running ? "Online" : "Offline"} - +
+ +
{service.type} @@ -96,10 +95,11 @@ export function CustomBrowserRow({service, showDeleted}) { {setOpenListFilesDialog(true);}} - size="medium"> - + size="small"> + diff --git a/MythicReactUI/src/components/pages/PayloadTypesC2Profiles/InstalledServiceParameterDetails.js b/MythicReactUI/src/components/pages/PayloadTypesC2Profiles/InstalledServiceParameterDetails.js new file mode 100644 index 00000000..edd803fe --- /dev/null +++ b/MythicReactUI/src/components/pages/PayloadTypesC2Profiles/InstalledServiceParameterDetails.js @@ -0,0 +1,85 @@ +import React from 'react'; +import {MythicStatusChip} from "../../MythicComponents/MythicStatusChip"; + +export const formatParameterValue = (value, emptyValue = "Not set") => { + if (value === undefined || value === null || value === "") { + return emptyValue; + } + if (typeof value === "boolean") { + return value ? "Yes" : "No"; + } + if (Array.isArray(value)) { + return value.length > 0 ? value.join(", ") : emptyValue; + } + if (typeof value === "object") { + return Object.keys(value).length > 0 ? JSON.stringify(value, null, 2) : emptyValue; + } + return `${value}`; +}; + +export function ParameterMetadataItem({label, value, code = false, emptyValue = "Not set"}) { + return ( +
+ {label} + + {formatParameterValue(value, emptyValue)} + +
+ ); +} + +export function ParameterCodeBlock({children}) { + return ( + + {formatParameterValue(children)} + + ); +} + +export function BuildParameterList({parameters}) { + if (parameters.length === 0) { + return ( +
+
No build parameters
+
This service does not define build-time parameters.
+
+ ); + } + return ( +
+ {parameters.map((param) => ( +
+
+
+
{param.name}
+
+ {param.description || "No description provided."} +
+
+
+ + {param.required && + + } + {param.randomize && + + } +
+
+
+ + + + + {(param.choices || "").length > 0 && + + } + {param.randomize && + + } +
+
+ ))} +
+ ); +} diff --git a/MythicReactUI/src/components/pages/PayloadTypesC2Profiles/PayloadTypeBuildDialog.js b/MythicReactUI/src/components/pages/PayloadTypesC2Profiles/PayloadTypeBuildDialog.js index e6873c95..ded3dd94 100644 --- a/MythicReactUI/src/components/pages/PayloadTypesC2Profiles/PayloadTypeBuildDialog.js +++ b/MythicReactUI/src/components/pages/PayloadTypesC2Profiles/PayloadTypeBuildDialog.js @@ -1,19 +1,12 @@ import React, {useState} from 'react'; -import Button from '@mui/material/Button'; -import DialogActions from '@mui/material/DialogActions'; import DialogContent from '@mui/material/DialogContent'; -import DialogContentText from '@mui/material/DialogContentText'; import DialogTitle from '@mui/material/DialogTitle'; import {useQuery, gql} from '@apollo/client'; -import TableCell from '@mui/material/TableCell'; -import TableRow from '@mui/material/TableRow'; -import Table from '@mui/material/Table'; -import TableBody from '@mui/material/TableBody'; -import TableContainer from '@mui/material/TableContainer'; -import TableHead from '@mui/material/TableHead'; -import LinearProgress from '@mui/material/LinearProgress'; import { getDefaultChoices } from '../CreatePayload/Step2SelectPayloadType'; import { getDefaultValueForType } from '../CreatePayload/Step2SelectPayloadType'; +import {MythicDialogBody, MythicDialogButton, MythicDialogFooter, MythicDialogSection} from "../../MythicComponents/MythicDialogLayout"; +import {MythicErrorState, MythicLoadingState} from "../../MythicComponents/MythicStateDisplay"; +import {BuildParameterList} from "./InstalledServiceParameterDetails"; const GET_Payload_Details = gql` query GetPayloadDetails($payload_name: String!) { @@ -82,68 +75,48 @@ export function PayloadTypeBuildDialog(props) { return {...param, choices: choices, default_value: default_value} }); setBuildParams(buildParams); - console.log(buildParams); } }); if (loading) { - return ; + return ( + <> + {props.payload_name}'s Build Parameters + + + + + ); } if (error) { console.error(error); - return
Error! {error.message}
; + return ( + <> + {props.payload_name}'s Build Parameters + + + + + ); } return ( {props.payload_name}'s Build Parameters - - These are the build parameters associated with this payload - - -
- - - Parameter - Value - - - - { - buildParams.map( (param) => ( - - {param.description} - - Scripting/Building Name:
{param.name}

- Parameter Type:
{param.parameter_type}

- Default Value:
{param.default_value}

- {param.choices.length > 0 ? ( - - Parameter Options:
{param.choices}

-
- ) : (null)} - Required?
{param.required ? "Yes": "No"}

- Verifier Regex:
{param.verifier_regex}

- Randomized:
{param.randomize ? "Yes": "No"}

- {param.randomize ? ( - - Format String:
{param.format_string}

-
- ) : (null)} -
-
- )) - - } -
-
- + + + + +
- - - + + ); } diff --git a/MythicReactUI/src/components/pages/PayloadTypesC2Profiles/PayloadTypeCard.js b/MythicReactUI/src/components/pages/PayloadTypesC2Profiles/PayloadTypeCard.js index ac400a42..416d1348 100644 --- a/MythicReactUI/src/components/pages/PayloadTypesC2Profiles/PayloadTypeCard.js +++ b/MythicReactUI/src/components/pages/PayloadTypesC2Profiles/PayloadTypeCard.js @@ -1,6 +1,5 @@ import React, { useEffect } from 'react'; import Typography from '@mui/material/Typography'; -import {useTheme} from '@mui/material/styles'; import { MythicDialog } from '../../MythicComponents/MythicDialog'; import {PayloadTypeBuildDialog} from './PayloadTypeBuildDialog'; import {MythicConfirmDialog} from '../../MythicComponents/MythicConfirmDialog'; @@ -8,17 +7,18 @@ import DeleteIcon from '@mui/icons-material/Delete'; import RestoreFromTrashOutlinedIcon from '@mui/icons-material/RestoreFromTrashOutlined'; import MenuBookIcon from '@mui/icons-material/MenuBook'; import IconButton from '@mui/material/IconButton'; -import BuildIcon from '@mui/icons-material/Build'; +import TuneIcon from '@mui/icons-material/Tune'; import TableRow from '@mui/material/TableRow'; import MythicTableCell from "../../MythicComponents/MythicTableCell"; import {gql, useMutation} from '@apollo/client'; import { snackActions } from '../../utilities/Snackbar'; import {MythicStyledTooltip} from "../../MythicComponents/MythicStyledTooltip"; -import SendTwoToneIcon from '@mui/icons-material/SendTwoTone'; +import TerminalIcon from '@mui/icons-material/Terminal'; import {PayloadTypeCommandDialog} from "./PayloadTypeCommandsDialog"; import {MythicAgentSVGIcon} from "../../MythicComponents/MythicAgentSVGIcon"; import {C2ProfileListFilesDialog} from "./C2ProfileListFilesDialog"; import AttachFileIcon from '@mui/icons-material/AttachFile'; +import {MythicStatusChip} from "../../MythicComponents/MythicStatusChip"; const toggleDeleteStatus = gql` mutation togglePayloadTypeDeleteStatus($payloadtype_id: Int!, $deleted: Boolean!){ @@ -29,7 +29,6 @@ mutation togglePayloadTypeDeleteStatus($payloadtype_id: Int!, $deleted: Boolean! `; export function PayloadTypeRow({service, showDeleted}){ - const theme = useTheme(); const [wrappedPayloads, setWrappedPayloads] = React.useState(""); const [openBuildingDialog, setOpenBuildingDialog] = React.useState(false); const [supportedOS, setSupportedOS] = React.useState(""); @@ -72,12 +71,12 @@ export function PayloadTypeRow({service, showDeleted}){ {service.deleted ? ( - {setOpenDeleteDialog(true);}} color="success" > - + {setOpenDeleteDialog(true);}} > + ) : ( - {setOpenDeleteDialog(true);}} color="error" > - + {setOpenDeleteDialog(true);}} > + )} @@ -112,41 +111,45 @@ export function PayloadTypeRow({service, showDeleted}){ - - {service.container_running ? "Online" : "Offline"} - + +
- + size="small"> + {setOpenBuildingDialog(true);}} - size="medium"> - + size="small"> + {setOpenCommandsDialog(true)}} - size={"medium"}> - + size="small"> + {setOpenListFilesDialog(true);}} - size="medium"> - + size="small"> + +
{openDelete && @@ -176,4 +179,4 @@ export function PayloadTypeRow({service, showDeleted}){ ); -} \ No newline at end of file +} diff --git a/MythicReactUI/src/components/pages/PayloadTypesC2Profiles/PayloadTypeCommandsDialog.js b/MythicReactUI/src/components/pages/PayloadTypesC2Profiles/PayloadTypeCommandsDialog.js index 4b131421..7a26a200 100644 --- a/MythicReactUI/src/components/pages/PayloadTypesC2Profiles/PayloadTypeCommandsDialog.js +++ b/MythicReactUI/src/components/pages/PayloadTypesC2Profiles/PayloadTypeCommandsDialog.js @@ -1,8 +1,5 @@ import React, {useState} from 'react'; -import Button from '@mui/material/Button'; -import DialogActions from '@mui/material/DialogActions'; import DialogContent from '@mui/material/DialogContent'; -import DialogContentText from '@mui/material/DialogContentText'; import PlayCircleFilledTwoToneIcon from '@mui/icons-material/PlayCircleFilledTwoTone'; import DialogTitle from '@mui/material/DialogTitle'; import {useQuery, gql} from '@apollo/client'; @@ -12,15 +9,21 @@ import Table from '@mui/material/Table'; import TableBody from '@mui/material/TableBody'; import TableContainer from '@mui/material/TableContainer'; import TableHead from '@mui/material/TableHead'; -import LinearProgress from '@mui/material/LinearProgress'; import MenuBookIcon from '@mui/icons-material/MenuBook'; import IconButton from '@mui/material/IconButton'; import Typography from '@mui/material/Typography'; import { useTheme } from '@mui/material/styles'; import {MythicDialog} from "../../MythicComponents/MythicDialog"; -import Paper from '@mui/material/Paper'; -import MythicStyledTableCell from "../../MythicComponents/MythicTableCell"; import {getDynamicQueryParamsString} from "../Callbacks/TaskParametersDialogRow"; +import {MythicDialogBody, MythicDialogButton, MythicDialogFooter, MythicDialogSection} from "../../MythicComponents/MythicDialogLayout"; +import {MythicErrorState, MythicLoadingState} from "../../MythicComponents/MythicStateDisplay"; +import {MythicStatusChip} from "../../MythicComponents/MythicStatusChip"; +import {MythicStyledTooltip} from "../../MythicComponents/MythicStyledTooltip"; +import { + formatParameterValue, + ParameterCodeBlock, + ParameterMetadataItem +} from "./InstalledServiceParameterDetails"; const GET_Payload_Details = gql` query GetPayloadDetails($payload_name: String!) { @@ -76,34 +79,63 @@ export function PayloadTypeCommandDialog({service, payload_name, onClose}) { const notDeleted = data.command.filter(c => !c.deleted); setCommands([...notDeleted, ...deleted]); } - }); + }); if (loading) { - return ; + return ( + <> + {payload_name}'s Commands + + + + + ); } if (error) { - console.error(error); - return
Error! {error.message}
; + return ( + <> + {payload_name}'s Commands + + + + + ); } const onClickOpenScriptDialog = (e, command) => { e.preventDefault(); e.stopPropagation(); setOpenScriptDialog({open: true, command_id: command.id, command_name: command.cmd}); } + const activeCommandCount = commands.filter((command) => !command.deleted).length; + const deletedCommandCount = commands.length - activeCommandCount; + const adminCommandCount = commands.filter((command) => !command.deleted && command.needs_admin).length; return ( {payload_name}'s Commands - + + + +
+ + {adminCommandCount > 0 && + + } + {deletedCommandCount > 0 && + + } +
- Command + Command Version - Docs - Script Description + Actions @@ -111,34 +143,46 @@ export function PayloadTypeCommandDialog({service, payload_name, onClose}) { commands.map((param) => ( - + {param.cmd} - {param.needs_admin && - {"Needs Admin"} - } +
+ {param.needs_admin && + + } + {param.deleted && + + } +
{param.version} - - - - - - - onClickOpenScriptDialog(e, param)} > - - - {param.description} + +
+ + + + + + + onClickOpenScriptDialog(e, param)} size="small"> + + + +
+
)) }
+
+
{openScriptDialog.open && {setOpenScriptDialog({open: false, command_id: 0});}} @@ -149,11 +193,11 @@ export function PayloadTypeCommandDialog({service, payload_name, onClose}) { } />}
- - - + +
); } @@ -189,204 +233,194 @@ const exampleCredentialJson = `{ `; export function ScriptingCommandDialog({command_id, command_name, onClose}){ const [params, setParams] = useState([]); - const theme = useTheme(); - useQuery(GET_CommandParameters, { + const {loading, error} = useQuery(GET_CommandParameters, { variables: {command_id: command_id}, onCompleted: data => { const grouped = data.commandparameters.reduce( (prev, cur) => { - if(prev[cur.parameter_group_name] === undefined){ - prev[cur.parameter_group_name] = [cur]; + const groupName = cur.parameter_group_name || "Default"; + if(prev[groupName] === undefined){ + prev[groupName] = [cur]; return prev; } - prev[cur.parameter_group_name].push(cur); - prev[cur.parameter_group_name].sort((a, b) => {return a.ui_position < b.ui_position ? -1 : 1}); + prev[groupName].push(cur); return prev; }, {}); - let groupedArray = []; - for(const key in grouped){ - groupedArray.push(grouped[key]); - } + const groupedArray = Object.keys(grouped).sort((a, b) => a.localeCompare(b)).map((key) => { + return grouped[key].sort((a, b) => a.ui_position < b.ui_position ? -1 : 1); + }); setParams(groupedArray); - console.log(groupedArray); } }); + if (loading) { + return ( + <> + {command_name}'s Parameters + + + + + ); + } + if (error) { + return ( + <> + {command_name}'s Parameters + + + + + ); + } return ( {command_name}'s Parameters - - - Each "Parameter Group" is one way to issue this task. These "Parameter Groups" allow a single task to offer a wider range of tasking options with similar code paths without creating new commands for each option and by cleaning up the total parameter options to make it less confusing. - - - Note: Parameters with "Required" must have a value supplied by you, but everything else has a "Default Value" that will be supplied if you choose not to submit one. - - - - {params.map( (paramGroup) => ( -
- - - Parameter Group: {paramGroup[0].parameter_group_name} - - - - - - - Name - Value - - - - {paramGroup.map( (param) => ( - - - - {param.name} - - - {param.description} - - {param.required && Required} - - - - Value Type: {param.type} - - {param.verifier_regex !== "" && - - Note: Value must match the following regex: {param.verifier_regex} - - } - - Default Value: {param.default_value} - - {param.choices.length > 0 && - - Choose from the following: {param.choices.join(", ")} - - } - {param.choices_are_all_commands && - - Note: Provide any command name - - } - {param.choices_are_loaded_commands && - - Note: Provide any currently loaded command name - - } - {param.dynamic_query_function !== "" && - - Note: This command in the UI will dynamically give you a list of options to choose from either through the modal or via tab complete. - The following mutation can be used to dynamically generate the same choices. In this case, "other_parameters", are just the values of the other parameters you've set for this command in case they offer meaningful context for this dynamic function. They are provided as a standard dictionary (not a string) -
- - {getDynamicQueryParamsString} - -
- -
- } - {param.type === "File" && - - Note: This parameter expects a file's AgentFileID (UUID) value. When issuing this task through the UI, you will be prompted to select a file from your file system to upload. - When scripting, you should upload a new file first and provide the returned AgentFileID here.
-
- - {exampleUploadFile(command_name, param.name)} - -
-
- } - {param.type === "TypedArray" && - - Note: TypedArrays are arrays of strings that also carry along a "type" identifier with it. This is expressed in scripting and the code as an array of arrays. - In the UI, tab complete or the modal will show you how to fill this out. - When scripting, you can provide this nested array explicitly: -
- - [ ["{param.choices[0]}", "test"], ["{param.choices[0]}", "values"] ] - -
-
- } - {param.type === "AgentConnect" && - - Note: This parameter type is more complex and is meant to provide a handy way in the UI to allow an operator to select a payload or callback to connect to. In the UI, this dynamically resolves hosts and their payloads/callbacks to list out their P2P C2 profile options so that you can link up without remember things like randomized named pipe values. - You can do the same thing programmatically, but you'll have to fetch the pieces of information yourself and set the value as the resulting dictionary: -
- - {exampleAgentConnect} - -
-
- } - {param.type === "LinkInfo" && - - Note: This parameter type is more complex and is meant to provide a handy way in the UI to allow an operator to select an existing P2P link for either unlinking or relinking. The UI dynamically resolves existing "callbackgraphedge" values and their info. - You can do the same thing programmatically, but you'll have to fetch the pieces of information yourself and set the value as the resulting dictionary. You'll notice that the data is the exact same as if the type was "AgentConnect", it's just displayed in a different format in the UI. -
- - {exampleAgentConnect} - -
-
- } - {param.type === "PayloadList" && - <> - - Note: This parameter simply supplies a list of payloads for the user to select. From here, the actual value that's passed along is just the UUID of the payload the user selected. - - {param.supported_agents.length > 0 && - - Note: This command has an explicit list of agents it supports; this is typically done in conjunction with the "PayloadList" type to help reduce the options to only valid ones. -
The supported agents are: {param.supported_agents.join(", ")} -
- } - {Object.keys(param.supported_agent_build_parameters).length > 0 && - - Note: This command has an explicit list of build parameter values that must be set for them to be considered valid. -
The supported agents and build parameter values are: {JSON.stringify(param.supported_agent_build_parameters, null, 2)} -
- } - - } - {param.type === "CredentialJson" && - <> - - Note: This parameter type allows the user to explicitly select a credential that exists in Mythic's credential store and pass along all of that data to the command. - The value supplied here is the actual JSON credential data like follows: -
- {exampleCredentialJson} -
-
- {param.limit_credentials_by_type.length > 0 && - - Note: This command limits the credential to those with the following types: {param.limit_credentials_by_type.join(", ")} - - } - - - } -
-
- ))} -
-
-
- -
- ))} + + + {params.length === 0 ? ( + + ) : ( + params.map( (paramGroup) => { + const parameterGroupName = paramGroup[0].parameter_group_name || "Default"; + return ( + +
+ {paramGroup.map((param) => ( + + ))} +
+
+ ); + }) + )} +
- - - + +
) } + +const ScriptingParameterCard = ({commandName, param}) => { + const choices = param.choices || []; + return ( +
+
+
+
{param.name}
+
+ {param.description || "No description provided."} +
+
+
+ + {param.required && + + } +
+
+
+ + + {param.display_name && + + } + {param.cli_name && + + } + {param.verifier_regex && + + } + {choices.length > 0 && + + } +
+ +
+ ); +}; + +const ScriptingParameterNotes = ({commandName, param}) => { + const choices = param.choices || []; + const typedArrayChoice = choices.length > 0 ? choices[0] : "type"; + const supportedAgents = param.supported_agents || []; + const supportedBuildParameters = param.supported_agent_build_parameters || {}; + const credentialTypes = param.limit_credentials_by_type || []; + return ( +
+ {param.choices_are_all_commands && +
Command Choice: Provide any command name.
+ } + {param.choices_are_loaded_commands && +
Loaded Command Choice: Provide any command name currently loaded into the callback.
+ } + {param.dynamic_query_function && +
+ Dynamic Choices: Use this mutation to generate the same choices outside the UI. + {getDynamicQueryParamsString} +
+ } + {param.type === "File" && +
+ File Value: Upload the file first, then provide the returned AgentFileID. + {exampleUploadFile(commandName, param.name)} +
+ } + {param.type === "TypedArray" && +
+ Typed Array: Provide nested arrays with a type identifier and value. + {`[ ["${typedArrayChoice}", "test"], ["${typedArrayChoice}", "values"] ]`} +
+ } + {param.type === "AgentConnect" && +
+ Agent Connect: Provide the connection target and C2 profile parameters as a dictionary. + {exampleAgentConnect} +
+ } + {param.type === "LinkInfo" && +
+ Link Info: Provide the existing P2P link information as a dictionary. + {exampleAgentConnect} +
+ } + {param.type === "PayloadList" && + <> +
+ Payload Value: The submitted value is the selected payload UUID. +
+ {supportedAgents.length > 0 && +
+ Supported Agents: {supportedAgents.join(", ")} +
+ } + {Object.keys(supportedBuildParameters).length > 0 && +
+ Required Build Parameter Values: + {JSON.stringify(supportedBuildParameters, null, 2)} +
+ } + + } + {param.type === "CredentialJson" && + <> +
+ Credential JSON: Provide the full credential JSON value. + {exampleCredentialJson} +
+ {credentialTypes.length > 0 && +
+ Allowed Credential Types: {formatParameterValue(credentialTypes)} +
+ } + + } +
+ ); +}; diff --git a/MythicReactUI/src/components/pages/PayloadTypesC2Profiles/PayloadTypesC2Profiles.js b/MythicReactUI/src/components/pages/PayloadTypesC2Profiles/PayloadTypesC2Profiles.js index 7eeca7aa..3513ec5f 100644 --- a/MythicReactUI/src/components/pages/PayloadTypesC2Profiles/PayloadTypesC2Profiles.js +++ b/MythicReactUI/src/components/pages/PayloadTypesC2Profiles/PayloadTypesC2Profiles.js @@ -13,16 +13,12 @@ import AppBar from '@mui/material/AppBar'; import Tabs from '@mui/material/Tabs'; import {MythicSearchTabLabel, MythicTabPanel} from "../../MythicComponents/MythicTabPanel"; import {ConsumingServicesTableRow} from './ConsumingServicesTable'; -import {MythicStyledTooltip} from "../../MythicComponents/MythicStyledTooltip"; -import Paper from '@mui/material/Paper'; -import Typography from '@mui/material/Typography'; import VisibilityOffIcon from '@mui/icons-material/VisibilityOff'; import VisibilityIcon from '@mui/icons-material/Visibility'; -import { IconButton } from '@mui/material'; -import {useTheme} from '@mui/material/styles'; import {CustomBrowserRow} from "./CustomBrowserCard"; import {MythicPageBody} from "../../MythicComponents/MythicPageBody"; -import {MythicPageHeader} from "../../MythicComponents/MythicPageHeader"; +import {MythicPageHeader, MythicPageHeaderChip} from "../../MythicComponents/MythicPageHeader"; +import {MythicToolbarToggle} from "../../MythicComponents/MythicTableToolbar"; const SUB_Payload_Types = gql` subscription getPayloadTypesSubscription { @@ -148,7 +144,6 @@ const filterDeleted = (c, showDeleted) => { } export function PayloadTypesC2Profiles({me}){ - const theme = useTheme(); const [value, setValue] = React.useState(0); const [allData, setAllData] = React.useState([]); const payloadTypes = useCustomSubscription({ @@ -180,57 +175,105 @@ export function PayloadTypesC2Profiles({me}){ const handleChange = (event, newValue) => { setValue(newValue); }; + const visibleData = allData.filter(c => filterDeleted(c, showDeleted)); + const getContainersForTab = (tabName) => { + switch(tabName){ + case "Payload Types": + return visibleData.filter(c => c.__typename === "payloadtype" && (c.agent_type === "agent" || c.agent_type === "wrapper")); + case "C2 Profiles": + return visibleData.filter(c => c.__typename === "c2profile"); + case "Translators": + return visibleData.filter(c => c.__typename === "translationcontainer"); + case "Command Augmentation": + return visibleData.filter(c => c.__typename === "payloadtype" && (c.agent_type === "command_augment")); + case "3rd Party": + return visibleData.filter(c => c.__typename === "payloadtype" && (c.agent_type === "service")); + case "Webhooks": + return visibleData.filter(c => c.__typename === "consuming_container" && c.type === "webhook"); + case "Loggers": + return visibleData.filter(c => c.__typename === "consuming_container" && c.type === "logging"); + case "Eventing": + return visibleData.filter(c => c.__typename === "consuming_container" && c.type === "eventing"); + case "Auth": + return visibleData.filter(c => c.__typename === "consuming_container" && c.type === "auth"); + case "Browsers": + return visibleData.filter(c => c.__typename === "custombrowser"); + default: + return []; + } + } + const currentTab = tabTypes[value] || tabTypes[0]; + const currentContainers = getContainersForTab(currentTab); const getTabComponent = () => { switch(value){ case 0: return filterDeleted(c, showDeleted)).filter(c => c.__typename === "payloadtype" && (c.agent_type === "agent" || c.agent_type === "wrapper"))} /> + containers={getContainersForTab("Payload Types")} /> case 1: return filterDeleted(c, showDeleted)).filter(c => c.__typename === "c2profile")} /> + containers={getContainersForTab("C2 Profiles")} /> case 2: return filterDeleted(c, showDeleted)).filter(c => c.__typename === "translationcontainer")} /> + containers={getContainersForTab("Translators")} /> case 3: return filterDeleted(c, showDeleted)).filter(c => c.__typename === "payloadtype" && (c.agent_type === "command_augment"))} /> + containers={getContainersForTab("Command Augmentation")} /> case 4: return filterDeleted(c, showDeleted)).filter(c => c.__typename === "payloadtype" && (c.agent_type === "service"))} /> + containers={getContainersForTab("3rd Party")} /> case 5: return filterDeleted(c, showDeleted)).filter(c => c.__typename === "consuming_container" && c.type === "webhook")} /> + containers={getContainersForTab("Webhooks")} /> case 6: return filterDeleted(c, showDeleted)).filter(c => c.__typename === "consuming_container" && c.type === "logging")} /> + containers={getContainersForTab("Loggers")} /> case 7: return filterDeleted(c, showDeleted)).filter(c => c.__typename === "consuming_container" && c.type === "eventing")} /> + containers={getContainersForTab("Eventing")} /> case 8: return filterDeleted(c, showDeleted)).filter(c => c.__typename === "consuming_container" && c.type === "auth")} /> + containers={getContainersForTab("Auth")} /> case 9: return filterDeleted(c, showDeleted)).filter(c => c.__typename === "custombrowser")} /> + containers={getContainersForTab("Browsers")} /> default: return null; } } return ( - - {showDeleted ? ( - - setShowDeleted(!showDeleted)}> - - - ) : ( - - setShowDeleted(!showDeleted)} > - - )} - - + + + + + {showDeleted && } + + } + actions={ + setShowDeleted(!showDeleted)} + label="Deleted" + activeIcon={} + inactiveIcon={} + /> + } + /> + ({ + backgroundColor: theme.surfaces?.muted || theme.palette.background.paper, + border: `1px solid ${theme.table?.borderSoft || theme.borderColor}`, + borderRadius: `${theme.shape.borderRadius}px`, + overflow: "hidden", + })} + > filterDeleted(c, showDeleted)).filter(c => c.__typename === "payloadtype" && (c.agent_type === "agent" || c.agent_type === "wrapper"))}/>; + containers={getContainersForTab("Payload Types")}/>; case "C2 Profiles": return filterDeleted(c, showDeleted)).filter(c => c.__typename === "c2profile")}/>; + containers={getContainersForTab("C2 Profiles")}/>; case "Translators": return filterDeleted(c, showDeleted)).filter(c => c.__typename === "translationcontainer")}/>; + containers={getContainersForTab("Translators")}/>; case "Command Augmentation": return filterDeleted(c, showDeleted)).filter(c => c.__typename === "payloadtype" && (c.agent_type === "command_augment"))}/>; + containers={getContainersForTab("Command Augmentation")}/>; case "3rd Party": return filterDeleted(c, showDeleted)).filter(c => c.__typename === "payloadtype" && (c.agent_type === "service"))} />; + containers={getContainersForTab("3rd Party")} />; case "Webhooks": return filterDeleted(c, showDeleted)).filter(c => c.__typename === "consuming_container" && c.type === "webhook")}/>; + containers={getContainersForTab("Webhooks")}/>; case "Loggers": return filterDeleted(c, showDeleted)).filter(c => c.__typename === "consuming_container" && c.type === "logging")}/>; + containers={getContainersForTab("Loggers")}/>; case "Eventing": return filterDeleted(c, showDeleted)).filter(c => c.__typename === "consuming_container" && c.type === "eventing")}/>; + containers={getContainersForTab("Eventing")}/>; case "Auth": return filterDeleted(c, showDeleted)).filter(c => c.__typename === "consuming_container" && c.type === "auth")}/>; + containers={getContainersForTab("Auth")}/>; case "Browsers": return filterDeleted(c, showDeleted)).filter(c => c.__typename === "custombrowser")}/>; + containers={getContainersForTab("Browsers")}/>; default: return null; diff --git a/MythicReactUI/src/components/pages/PayloadTypesC2Profiles/TranslationContainerCard.js b/MythicReactUI/src/components/pages/PayloadTypesC2Profiles/TranslationContainerCard.js index 198cb557..7e9cdab6 100644 --- a/MythicReactUI/src/components/pages/PayloadTypesC2Profiles/TranslationContainerCard.js +++ b/MythicReactUI/src/components/pages/PayloadTypesC2Profiles/TranslationContainerCard.js @@ -3,7 +3,6 @@ import AttachFileIcon from '@mui/icons-material/AttachFile'; import Typography from '@mui/material/Typography'; import {FontAwesomeIcon} from '@fortawesome/react-fontawesome'; import { faLanguage } from '@fortawesome/free-solid-svg-icons'; -import {useTheme} from '@mui/material/styles'; import IconButton from '@mui/material/IconButton'; import {useMutation, gql} from '@apollo/client'; import {snackActions} from '../../utilities/Snackbar'; @@ -16,6 +15,7 @@ import MythicTableCell from "../../MythicComponents/MythicTableCell"; import {MythicStyledTooltip} from "../../MythicComponents/MythicStyledTooltip"; import {MythicDialog} from "../../MythicComponents/MythicDialog"; import {C2ProfileListFilesDialog} from "./C2ProfileListFilesDialog"; +import {MythicStatusChip} from "../../MythicComponents/MythicStatusChip"; const toggleDeleteStatus = gql` mutation toggleC2ProfileDeleteStatus($translationcontainer_id: Int!, $deleted: Boolean!){ @@ -26,7 +26,6 @@ mutation toggleC2ProfileDeleteStatus($translationcontainer_id: Int!, $deleted: B `; export function TranslationContainerRow({service, showDeleted}) { - const theme = useTheme(); const [openListFilesDialog, setOpenListFilesDialog] = React.useState(false); const [openDelete, setOpenDeleteDialog] = React.useState(false); const [updateDeleted] = useMutation(toggleDeleteStatus, { @@ -52,9 +51,9 @@ export function TranslationContainerRow({service, showDeleted}) { {service.deleted ? ( - {setOpenDeleteDialog(true);}} color="success" variant="contained"> + {setOpenDeleteDialog(true);}}> ) : ( - {setOpenDeleteDialog(true);}} color="error" variant="contained"> + {setOpenDeleteDialog(true);}}> )} {openDelete && {setOpenDeleteDialog(false);}} onSubmit={onAcceptDelete} @@ -84,27 +83,29 @@ export function TranslationContainerRow({service, showDeleted}) { - - {service.container_running ? "Online" : "Offline"} - + +
- + size="small"> + {setOpenListFilesDialog(true);}} - size="medium"> - + size="small"> + +
{openListFilesDialog && { - setOriginalLeft(data.command); setOriginalRight(data.payloadcommand); const leftData = data.command.reduce( (prev, cur) => { if( data.payloadcommand.filter(c => c.command.cmd === cur.cmd).length === 0){ @@ -116,10 +116,9 @@ export function AddRemoveCommandsDialog(props) { setRight([]); }; const customList = (title, items) => ( - <> - - - +
+
{title}
+
{items.map((valueObj) => { const value = valueObj.cmd; @@ -140,16 +139,16 @@ export function AddRemoveCommandsDialog(props) { })} - - +
+
); const setFinalTags = () => { // things to add are in the `right` now but weren't for `originalRight` const commandsToAdd = right.filter( (command) => { - return originalRight.filter(orig => orig.command.cmd === command.cmd).length == 0; + return originalRight.filter(orig => orig.command.cmd === command.cmd).length === 0; }); const commandsToRemove = originalRight.filter( (command) => { - return right.filter(newCommand => newCommand.cmd === command.command.cmd).length == 0; + return right.filter(newCommand => newCommand.cmd === command.command.cmd).length === 0; }) props.onSubmit({commandsToAdd, commandsToRemove}); props.onClose(); @@ -158,69 +157,72 @@ export function AddRemoveCommandsDialog(props) { <> Add or Remove Commands for Payload {props.filename} - This will add or remove commands associated with this payload from Mythic's perspective. - This does NOT add or remove commands within the payload itself. -
-
- {customList(leftTitle, left)} -
-
- - >> - - - > - - - < - - - << - - -
-
- {customList(rightTitle, right)} -
-
+ + +
+
+ {customList(leftTitle, left)} +
+
+ + >> + + + > + + + < + + + << + +
+
+ {customList(rightTitle, right)} +
+
+
+
- - - - + + ); } - diff --git a/MythicReactUI/src/components/pages/Payloads/DetailedPayloadTable.js b/MythicReactUI/src/components/pages/Payloads/DetailedPayloadTable.js index 0b04cea9..6ca463e1 100644 --- a/MythicReactUI/src/components/pages/Payloads/DetailedPayloadTable.js +++ b/MythicReactUI/src/components/pages/Payloads/DetailedPayloadTable.js @@ -8,7 +8,6 @@ import TableHead from '@mui/material/TableHead'; import Typography from '@mui/material/Typography'; import {useQuery, gql, useMutation} from '@apollo/client'; import LinearProgress from '@mui/material/LinearProgress'; -import Paper from '@mui/material/Paper'; import {useTheme} from '@mui/material/styles'; import DialogActions from '@mui/material/DialogActions'; import DialogContent from '@mui/material/DialogContent'; @@ -37,6 +36,8 @@ import {MythicAgentSVGIcon} from "../../MythicComponents/MythicAgentSVGIcon"; import {ViewPayloadConfigJSON} from "./EditPayloadConfigDialog"; import {meState} from "../../../cache"; import { useReactiveVar } from '@apollo/client'; +import {MythicErrorState, MythicLoadingState} from "../../MythicComponents/MythicStateDisplay"; +import {MythicSectionHeader} from "../../MythicComponents/MythicPageHeader"; const GET_Payload_Details = gql` @@ -208,19 +209,22 @@ export function DetailedPayloadComparisonTable(props){ return ( - Compare Payload Configurations - +
+ Compare Payload Configurations + +
; + return ; } if (error) { console.error(error); - return
Error! {error.message}
; + return ; } return ( - - - Payload Information - - + @@ -592,7 +592,13 @@ function DetailedPayloadInnerTable(props){ {window.location.origin + "/direct/download/" + data.payload[0].filemetum.agent_file_id} - {setOpenHostDialog(true);}} /> + {setOpenHostDialog(true);}} + > + + {openHostDialog &&
- - - Build Parameters - - + @@ -700,11 +702,7 @@ function DetailedPayloadInnerTable(props){
- - - Build Steps - - + @@ -732,11 +730,7 @@ function DetailedPayloadInnerTable(props){ { c2Profiles.map( (c2) => ( - - - {c2.c2_profile} - - +
@@ -777,12 +771,12 @@ function DetailedPayloadInnerTable(props){ ))} - - - Loaded Commands - - - + {setOpenAddRemoveCommandsDialog(true)}}>Add/Remove Commands + } + /> {commands.length > 0 &&
@@ -802,10 +796,16 @@ function DetailedPayloadInnerTable(props){ {cmd.mythic}{cmd.payload} - - - + + + + + )) @@ -861,11 +861,7 @@ function DetailedPayloadInnerTable(props){ } {data.payload[0].wrapped_payload_id !== null && - - - Wrapped Payload Information - - + } diff --git a/MythicReactUI/src/components/pages/Payloads/HostFileDialog.js b/MythicReactUI/src/components/pages/Payloads/HostFileDialog.js index 74ddad67..8fb85deb 100644 --- a/MythicReactUI/src/components/pages/Payloads/HostFileDialog.js +++ b/MythicReactUI/src/components/pages/Payloads/HostFileDialog.js @@ -1,21 +1,25 @@ import React, {useState} from 'react'; -import Button from '@mui/material/Button'; -import DialogActions from '@mui/material/DialogActions'; import DialogContent from '@mui/material/DialogContent'; import DialogTitle from '@mui/material/DialogTitle'; import {useQuery, gql, useMutation} from '@apollo/client'; import { snackActions } from '../../utilities/Snackbar'; -import MythicTableCell from "../../MythicComponents/MythicTableCell"; -import TableRow from '@mui/material/TableRow'; -import Table from '@mui/material/Table'; -import TableBody from '@mui/material/TableBody'; -import TableContainer from '@mui/material/TableContainer'; import MythicTextField from "../../MythicComponents/MythicTextField"; import MenuItem from '@mui/material/MenuItem'; import FormControl from '@mui/material/FormControl'; import Select from '@mui/material/Select'; -import Input from '@mui/material/Input'; +import InputLabel from '@mui/material/InputLabel'; import Switch from '@mui/material/Switch'; +import Box from '@mui/material/Box'; +import Typography from '@mui/material/Typography'; +import { + MythicDialogBody, + MythicDialogButton, + MythicDialogFooter, + MythicDialogGrid, + MythicDialogSection, + MythicFormField, + MythicFormSwitchRow +} from "../../MythicComponents/MythicDialogLayout"; const hostFileMutation = gql` mutation hostFileMutation($c2_id: Int!, $file_uuid: String!, $host_url: String!, $alert_on_download: Boolean, $remove: Boolean) { @@ -67,7 +71,8 @@ export function HostFileDialog(props) { setMessage(value); } const handleChange = (event) => { - setSelectedC2Profile(event.target.value); + const selected = availableC2Profiles.find((profile) => profile.id === event.target.value); + setSelectedC2Profile(selected || {id: 0}); }; const onChangeAlert = (event) => { setAlertOnDownload(event.target.checked); @@ -102,59 +107,66 @@ export function HostFileDialog(props) { Host File via C2 Profile - -
- - - File - {props.file_name} - - - C2 Profile - - - - - - - - Hosting URL Path (with /) - - - - - - - Send alert when file is downloaded? - - - - - -
-
+ + + + + {props.file_name} + + + + + + + + C2 Profile + + + + + + + + + + } + /> + +
- - - - - + +
); } diff --git a/MythicReactUI/src/components/pages/Payloads/ImportPayloadConfigDialog.js b/MythicReactUI/src/components/pages/Payloads/ImportPayloadConfigDialog.js index a72c49a9..ff8fbd8e 100644 --- a/MythicReactUI/src/components/pages/Payloads/ImportPayloadConfigDialog.js +++ b/MythicReactUI/src/components/pages/Payloads/ImportPayloadConfigDialog.js @@ -1,11 +1,15 @@ import React from 'react'; -import Button from '@mui/material/Button'; -import DialogActions from '@mui/material/DialogActions'; import DialogContent from '@mui/material/DialogContent'; import DialogTitle from '@mui/material/DialogTitle'; import {gql, useMutation} from '@apollo/client'; import { snackActions } from '../../utilities/Snackbar'; import {DragAndDropFileUpload} from "../Callbacks/TaskParametersDialogRow"; +import { + MythicDialogBody, + MythicDialogButton, + MythicDialogFooter, + MythicDialogSection +} from '../../MythicComponents/MythicDialogLayout'; const create_payload = gql` mutation createPayloadMutation($payload: String!) { @@ -44,17 +48,20 @@ export function ImportPayloadConfigDialog(props) { Import Payload Config to Generate New Payload + + + + - - - - + + ); } - diff --git a/MythicReactUI/src/components/pages/Payloads/PayloadBuildMessageDialog.js b/MythicReactUI/src/components/pages/Payloads/PayloadBuildMessageDialog.js index 99dfa7d2..6496a930 100644 --- a/MythicReactUI/src/components/pages/Payloads/PayloadBuildMessageDialog.js +++ b/MythicReactUI/src/components/pages/Payloads/PayloadBuildMessageDialog.js @@ -3,8 +3,8 @@ import Button from '@mui/material/Button'; import DialogActions from '@mui/material/DialogActions'; import DialogTitle from '@mui/material/DialogTitle'; import {useQuery, gql} from '@apollo/client'; -import LinearProgress from '@mui/material/LinearProgress'; import {ResponseDisplayPlaintext} from "../Callbacks/ResponseDisplayPlaintext"; +import {MythicErrorState, MythicLoadingState} from "../../MythicComponents/MythicStateDisplay"; const getDescriptionQuery = gql` query getDescriptionQuery ($payload_id: Int!) { @@ -36,11 +36,11 @@ export function PayloadBuildMessageDialog(props) { setViewError(props.viewError); }, [props.viewError]); if (loading) { - return ; + return ; } if (error) { console.error(error); - return
Error!
; + return ; } return ( @@ -63,4 +63,3 @@ export function PayloadBuildMessageDialog(props) { ); } - diff --git a/MythicReactUI/src/components/pages/Payloads/PayloadConfigCheckDialog.js b/MythicReactUI/src/components/pages/Payloads/PayloadConfigCheckDialog.js index fcacd4ba..818e69e9 100644 --- a/MythicReactUI/src/components/pages/Payloads/PayloadConfigCheckDialog.js +++ b/MythicReactUI/src/components/pages/Payloads/PayloadConfigCheckDialog.js @@ -3,9 +3,9 @@ import Button from '@mui/material/Button'; import DialogActions from '@mui/material/DialogActions'; import DialogTitle from '@mui/material/DialogTitle'; import {useQuery, gql} from '@apollo/client'; -import LinearProgress from '@mui/material/LinearProgress'; import { snackActions } from '../../utilities/Snackbar'; import {ResponseDisplayPlaintext} from "../Callbacks/ResponseDisplayPlaintext"; +import {MythicErrorState, MythicLoadingState} from "../../MythicComponents/MythicStateDisplay"; const checkPayloadConfigurationQuery = gql` query checkPayloadConfigurationQuery($uuid: String!) { @@ -33,11 +33,11 @@ export function PayloadConfigCheckDialog(props) { fetchPolicy: "network-only" }); if (loading) { - return ; + return ; } if (error) { console.error(error); - return
Error!
; + return ; } return ( diff --git a/MythicReactUI/src/components/pages/Payloads/PayloadFilenameDialog.js b/MythicReactUI/src/components/pages/Payloads/PayloadFilenameDialog.js index 0350f528..0196550a 100644 --- a/MythicReactUI/src/components/pages/Payloads/PayloadFilenameDialog.js +++ b/MythicReactUI/src/components/pages/Payloads/PayloadFilenameDialog.js @@ -1,9 +1,9 @@ import React, {useState} from 'react'; import {useQuery, gql, useMutation} from '@apollo/client'; -import LinearProgress from '@mui/material/LinearProgress'; import { snackActions } from '../../utilities/Snackbar'; import {b64DecodeUnicode} from '../Callbacks/ResponseDisplay'; import {MythicModifyStringDialog} from "../../MythicComponents/MythicDialog"; +import {MythicErrorState, MythicLoadingState} from "../../MythicComponents/MythicStateDisplay"; const updateDescriptionMutation = gql` mutation updateDescription ($file_id: Int!, $filename: bytea!) { @@ -43,11 +43,11 @@ export function PayloadFilenameDialog(props) { } }); if (loading) { - return ; + return ; } if (error) { console.error(error); - return
Error!
; + return ; } const onCommitSubmit = (newDescription) => { updateDescription({variables: {file_id: fileId, filename: newDescription}}); @@ -65,4 +65,3 @@ export function PayloadFilenameDialog(props) { ); } - diff --git a/MythicReactUI/src/components/pages/Payloads/PayloadGetIOCDialog.js b/MythicReactUI/src/components/pages/Payloads/PayloadGetIOCDialog.js index cec13e02..f574301a 100644 --- a/MythicReactUI/src/components/pages/Payloads/PayloadGetIOCDialog.js +++ b/MythicReactUI/src/components/pages/Payloads/PayloadGetIOCDialog.js @@ -3,9 +3,9 @@ import Button from '@mui/material/Button'; import DialogActions from '@mui/material/DialogActions'; import DialogTitle from '@mui/material/DialogTitle'; import {useQuery, gql} from '@apollo/client'; -import LinearProgress from '@mui/material/LinearProgress'; import { snackActions } from '../../utilities/Snackbar'; import {ResponseDisplayPlaintext} from "../Callbacks/ResponseDisplayPlaintext"; +import {MythicErrorState, MythicLoadingState} from "../../MythicComponents/MythicStateDisplay"; const generateIOCMutation = gql` query generateIOCQuery($uuid: String!) { @@ -33,11 +33,11 @@ export function PayloadGetIOCDialog(props) { fetchPolicy: "network-only" }); if (loading) { - return ; + return ; } if (error) { console.error(error); - return
Error!
; + return ; } return ( @@ -60,4 +60,3 @@ export function PayloadGetIOCDialog(props) { ); } - diff --git a/MythicReactUI/src/components/pages/Payloads/PayloadGetSampleMessageDialog.js b/MythicReactUI/src/components/pages/Payloads/PayloadGetSampleMessageDialog.js index 7466d054..33976a6f 100644 --- a/MythicReactUI/src/components/pages/Payloads/PayloadGetSampleMessageDialog.js +++ b/MythicReactUI/src/components/pages/Payloads/PayloadGetSampleMessageDialog.js @@ -3,9 +3,9 @@ import Button from '@mui/material/Button'; import DialogActions from '@mui/material/DialogActions'; import DialogTitle from '@mui/material/DialogTitle'; import {useQuery, gql} from '@apollo/client'; -import LinearProgress from '@mui/material/LinearProgress'; import { snackActions } from '../../utilities/Snackbar'; import {ResponseDisplayPlaintext} from "../Callbacks/ResponseDisplayPlaintext"; +import {MythicErrorState, MythicLoadingState} from "../../MythicComponents/MythicStateDisplay"; const generateSampleMessageMutation = gql` query generateSampleMessageQuery($uuid: String!) { @@ -33,11 +33,11 @@ export function PayloadGetSampleMessageDialog(props) { fetchPolicy: "network-only" }); if (loading) { - return ; + return ; } if (error) { console.error(error); - return
Error!
; + return ; } return ( @@ -60,4 +60,3 @@ export function PayloadGetSampleMessageDialog(props) { ); } - diff --git a/MythicReactUI/src/components/pages/Payloads/PayloadRedirectRulesDialog.js b/MythicReactUI/src/components/pages/Payloads/PayloadRedirectRulesDialog.js index 79093a3d..af30eca0 100644 --- a/MythicReactUI/src/components/pages/Payloads/PayloadRedirectRulesDialog.js +++ b/MythicReactUI/src/components/pages/Payloads/PayloadRedirectRulesDialog.js @@ -3,9 +3,9 @@ import Button from '@mui/material/Button'; import DialogActions from '@mui/material/DialogActions'; import DialogTitle from '@mui/material/DialogTitle'; import {useQuery, gql} from '@apollo/client'; -import LinearProgress from '@mui/material/LinearProgress'; import { snackActions } from '../../utilities/Snackbar'; import {ResponseDisplayPlaintext} from "../Callbacks/ResponseDisplayPlaintext"; +import {MythicErrorState, MythicLoadingState} from "../../MythicComponents/MythicStateDisplay"; const generateRedirectRulesMutation = gql` query generateRedirectRulesMutation($uuid: String!) { @@ -33,11 +33,11 @@ export function PayloadRedirectRulesDialog(props) { fetchPolicy: "network-only" }); if (loading) { - return ; + return ; } if (error) { console.error(error); - return
Error!
; + return ; } return ( diff --git a/MythicReactUI/src/components/pages/Payloads/PayloadsTable.js b/MythicReactUI/src/components/pages/Payloads/PayloadsTable.js index 5be5a088..96ffd1b3 100644 --- a/MythicReactUI/src/components/pages/Payloads/PayloadsTable.js +++ b/MythicReactUI/src/components/pages/Payloads/PayloadsTable.js @@ -5,23 +5,20 @@ import TableCell from '@mui/material/TableCell'; import TableContainer from '@mui/material/TableContainer'; import TableHead from '@mui/material/TableHead'; import TableRow from '@mui/material/TableRow'; -import Paper from '@mui/material/Paper'; -import Button from '@mui/material/Button'; -import Typography from '@mui/material/Typography'; import { PayloadsTableRow } from './PayloadsTableRow'; import {ImportPayloadConfigDialog} from './ImportPayloadConfigDialog'; import { MythicDialog } from '../../MythicComponents/MythicDialog'; -import ButtonGroup from '@mui/material/ButtonGroup'; -import ArrowDropDownIcon from '@mui/icons-material/ArrowDropDown'; -import Grow from '@mui/material/Grow'; -import Popper from '@mui/material/Popper'; -import MenuItem from '@mui/material/MenuItem'; -import MenuList from '@mui/material/MenuList'; -import ClickAwayListener from '@mui/material/ClickAwayListener'; import {useNavigate} from 'react-router-dom'; -import Pagination from '@mui/material/Pagination'; -import { Backdrop, CircularProgress } from '@mui/material'; -import {MythicPageHeader} from "../../MythicComponents/MythicPageHeader"; +import { Backdrop } from '@mui/material'; +import {MythicPageHeader, MythicPageHeaderChip} from "../../MythicComponents/MythicPageHeader"; +import {MythicTablePagination} from "../../MythicComponents/MythicTablePagination"; +import {MythicToolbarButton, MythicToolbarToggle} from "../../MythicComponents/MythicTableToolbar"; +import {MythicLoadingState, MythicTableEmptyState} from "../../MythicComponents/MythicStateDisplay"; +import AddCircleIcon from '@mui/icons-material/AddCircle'; +import FileUploadIcon from '@mui/icons-material/FileUpload'; +import Inventory2Icon from '@mui/icons-material/Inventory2'; +import VisibilityOffIcon from '@mui/icons-material/VisibilityOff'; +import VisibilityIcon from '@mui/icons-material/Visibility'; export function PayloadsTable({payload, onDeletePayload, onUpdateCallbackAlert, onRestorePayload, me, @@ -30,8 +27,6 @@ export function PayloadsTable({payload, onDeletePayload, onUpdateCallbackAlert, const [showDeleted, setShowDeleted] = React.useState(false); const [showAutogenerated, setShowAutogenerated] = React.useState(false); const [openPayloadImport, setOpenPayloadImport] = React.useState(false); - const dropdownAnchorRef = React.useRef(null); - const [dropdownOpen, setDropdownOpen] = React.useState(false); const navigate = useNavigate(); const toggleShowDeleted = () => { setShowDeleted(!showDeleted); @@ -41,101 +36,83 @@ export function PayloadsTable({payload, onDeletePayload, onUpdateCallbackAlert, setShowAutogenerated(!showAutogenerated); onChangeShowAutogenerated(!showAutogenerated); } - const dropDownOptions = [ - { - name: "Generate New Payload", - click: () => { - navigate("/new/createpayload"); - } - }, - { - name: "Generate New Wrapper Payload", - click: () => { - navigate("/new/createwrapper"); - } - }, - { - name: "Import Payload Config", - click: () => { - setOpenPayloadImport(true) - } - }, - { - name: showDeleted ? "Hide Deleted Payloads" : "Show Deleted Payloads", - click: toggleShowDeleted - }, - { - name: showAutogenerated ? "Hide Autogenerated Payloads" : "Show Autogenerated Payloads", - click: toggleShowAutogenerated - } - ] - const handleMenuItemClick = (event, index) => { - dropDownOptions[index].click(); - setDropdownOpen(false); - }; + const payloadCountLabel = payload.length === 1 ? "1 shown" : `${payload.length} shown`; + const totalCountLabel = pageData.totalCount === 1 ? "1 total" : `${pageData.totalCount} total`; return ( <> - - - - - - - {({ TransitionProps, placement }) => ( - - - setDropdownOpen(false)}> - - {dropDownOptions.map((option, index) => ( - handleMenuItemClick(event, index)} - > - {option.name} - - ))} - - - - - )} - - {openPayloadImport && - {setOpenPayloadImport(false);}} - innerDialog={{setOpenPayloadImport(false);}} />} - /> + + + + {showDeleted && } + {showAutogenerated && } + } - - + actions={ + <> + } onClick={() => navigate("/new/createpayload")}> + Payload + + } onClick={() => navigate("/new/createwrapper")}> + Wrapper + + } onClick={() => setOpenPayloadImport(true)}> + Import Config + + } + inactiveIcon={} + /> + } + inactiveIcon={} + /> + + } + /> + {openPayloadImport && + {setOpenPayloadImport(false);}} + innerDialog={{setOpenPayloadImport(false);}} />} + /> + } +
{openBackdrop && - + } + - - - File - Progress - Description + + + File + Progress + Description C2 Status Tags + {payload.length === 0 && !openBackdrop && + + } {payload.map( (op) => (
-
- - Total Results: {pageData.totalCount}
+ ) diff --git a/MythicReactUI/src/components/pages/Payloads/PayloadsTableRow.js b/MythicReactUI/src/components/pages/Payloads/PayloadsTableRow.js index 17d03568..dd9f7850 100644 --- a/MythicReactUI/src/components/pages/Payloads/PayloadsTableRow.js +++ b/MythicReactUI/src/components/pages/Payloads/PayloadsTableRow.js @@ -47,6 +47,7 @@ import NotificationsOffOutlinedIcon from '@mui/icons-material/NotificationsOffOu import {EditPayloadConfigDialog} from "./EditPayloadConfigDialog"; import DifferenceIcon from '@mui/icons-material/Difference'; import {Dropdown, DropdownMenuItem, DropdownNestedMenuItem} from "../../MythicComponents/MythicNestedMenus"; +import {MythicStatusChip} from "../../MythicComponents/MythicStatusChip"; const rebuildPayloadMutation = gql` mutation triggerRebuildMutation($uuid: String!) { @@ -312,8 +313,9 @@ export function PayloadsTableRow(props){ { name: props.deleted ? "Restore Payload" : "Delete the Payload from Disk", type: "item", + danger: !props.deleted, icon: props.deleted ? : - , + , click: ({event}) => { event.preventDefault(); event.stopPropagation(); @@ -346,8 +348,12 @@ export function PayloadsTableRow(props){ - {openUpdate && @@ -361,6 +367,7 @@ export function PayloadsTableRow(props){ handleMenuItemClick(event, option.click)} > {option.icon}{option.name} @@ -374,6 +381,7 @@ export function PayloadsTableRow(props){ handleMenuItemClick(event, menuOption.click)} > {menuOption.icon}{menuOption.name} @@ -477,18 +485,30 @@ export function PayloadsTableRow(props){ } - {props.auto_generated && props.task && - - - - - - } - {b64DecodeUnicode(props.filemetum.filename_text)} +
+ {props.auto_generated && props.task && + + + + + + } + {b64DecodeUnicode(props.filemetum.filename_text)} + {props.deleted && + + } +
+
+
{props.description} diff --git a/MythicReactUI/src/components/pages/Payloads/PayloadsTableRowBuildProgress.js b/MythicReactUI/src/components/pages/Payloads/PayloadsTableRowBuildProgress.js index 70dc8397..0951215e 100644 --- a/MythicReactUI/src/components/pages/Payloads/PayloadsTableRowBuildProgress.js +++ b/MythicReactUI/src/components/pages/Payloads/PayloadsTableRowBuildProgress.js @@ -6,6 +6,7 @@ import HideSourceIcon from '@mui/icons-material/HideSource'; import HighlightOffIcon from '@mui/icons-material/HighlightOff'; import {MythicStyledTooltip} from '../../MythicComponents/MythicStyledTooltip'; import Button from '@mui/material/Button'; +import IconButton from '@mui/material/IconButton'; import DialogActions from '@mui/material/DialogActions'; import DialogTitle from '@mui/material/DialogTitle'; import { MythicDialog } from '../../MythicComponents/MythicDialog'; @@ -16,6 +17,26 @@ import TableBody from '@mui/material/TableBody'; import TableContainer from '@mui/material/TableContainer'; import TableHead from '@mui/material/TableHead'; import DialogContent from '@mui/material/DialogContent'; +import {MythicStatusChip} from '../../MythicComponents/MythicStatusChip'; + +const buildStepToneClasses = { + info: "mythic-table-row-icon-action-info", + success: "mythic-table-row-icon-action-success", + error: "mythic-table-row-icon-action-danger", +}; + +function BuildStepIconButton({children, muted = false, onClick, tone = "info"}) { + return ( + + {children} + + ); +} export function PayloadsTableRowBuildProgress(props){ const [buildProgressData, setBuildProgressData] = React.useState({ @@ -89,29 +110,29 @@ export function PayloadsTableRowBuildProgress(props){ } const getButton = (step) => { if(step.step_skip){ - return buildStepClick(step)}/> + return buildStepClick(step)}> } if(step.end_time === null){ // this will either be the current step or a future step if(step.start_time === null){ // this we have no info on it, so it's just waiting - return buildStepClick(step)} /> + return buildStepClick(step)}> } else { if(props.build_phase === "building"){ - return buildStepClick(step)}/> + return buildStepClick(step)}> } else { - return buildStepClick(step)}/> + return buildStepClick(step)}> } } } else if(step.step_success) { - return buildStepClick(step)}/> + return buildStepClick(step)}> } else { - return buildStepClick(step)}/> + return buildStepClick(step)}> } } return ( <> - {buildProgressData.total_steps > 0 && @@ -168,6 +189,19 @@ export function PayloadBuildStepStatusDialog(props) { return "Error"; } } + const getStatusVariant = () => { + if(props.step.step_skip){ + return "skipped"; + } else if (props.step.current_step === props.step.step_number) { + return "building"; + } else if(props.step.end_time === null) { + return "info"; + } else if(props.step.step_success){ + return "success"; + } else { + return "error"; + } + } return ( Step {props.step.step_number + 1} - {props.step.step_name} @@ -195,7 +229,8 @@ export function PayloadBuildStepStatusDialog(props) {
Status - {getStatusMessage()} + + @@ -291,24 +326,24 @@ export function PayloadsTableRowBuildProcessPerStep(props){ } const getButton = (step) => { if(step.step_skip){ - return buildStepClick(step)}/> + return buildStepClick(step)}> } if(step.end_time === null){ // this will either be the current step or a future step if(step.start_time === null){ // this we have no info on it, so it's just waiting - return buildStepClick(step)} /> + return buildStepClick(step)}> } else { if(props.build_status === "building"){ - return buildStepClick(step)}/> + return buildStepClick(step)}> } else { - return buildStepClick(step)}/> + return buildStepClick(step)}> } } } else if(step.step_success) { - return buildStepClick(step)}/> + return buildStepClick(step)}> } else { - return buildStepClick(step)}/> + return buildStepClick(step)}> } } return ( diff --git a/MythicReactUI/src/components/pages/Payloads/PayloadsTableRowBuildStatus.js b/MythicReactUI/src/components/pages/Payloads/PayloadsTableRowBuildStatus.js index 3cb35dc7..2d847078 100644 --- a/MythicReactUI/src/components/pages/Payloads/PayloadsTableRowBuildStatus.js +++ b/MythicReactUI/src/components/pages/Payloads/PayloadsTableRowBuildStatus.js @@ -1,11 +1,10 @@ -import IconButton from '@mui/material/IconButton'; -import CircularProgress from '@mui/material/CircularProgress'; import React from 'react'; import GetAppIcon from '@mui/icons-material/GetApp'; import ErrorIcon from '@mui/icons-material/Error'; import {MythicStyledTooltip} from '../../MythicComponents/MythicStyledTooltip'; import { MythicDialog } from '../../MythicComponents/MythicDialog'; import {PayloadBuildMessageDialog} from './PayloadBuildMessageDialog'; +import {MythicStatusChip} from '../../MythicComponents/MythicStatusChip'; export function PayloadsTableRowBuildStatus(props){ const [openBuildMessage, setOpenBuildMessageDialog] = React.useState(false); @@ -19,20 +18,31 @@ export function PayloadsTableRowBuildStatus(props){ {props.build_phase === "success" ? ( - - - + } + /> ) : (props.build_phase === "building" ? ( - + ) : (<> - + } + onClick={onErrorClick} + /> {openBuildMessage && ); } - diff --git a/MythicReactUI/src/components/pages/Payloads/PayloadsTableRowC2Status.js b/MythicReactUI/src/components/pages/Payloads/PayloadsTableRowC2Status.js index 09d21808..bce14ce7 100644 --- a/MythicReactUI/src/components/pages/Payloads/PayloadsTableRowC2Status.js +++ b/MythicReactUI/src/components/pages/Payloads/PayloadsTableRowC2Status.js @@ -1,44 +1,67 @@ import React from 'react'; -import Typography from '@mui/material/Typography'; import CancelIcon from '@mui/icons-material/Cancel'; import CheckCircleOutlineIcon from '@mui/icons-material/CheckCircleOutline'; import {MythicStyledTooltip} from '../../MythicComponents/MythicStyledTooltip'; import PermScanWifiIcon from '@mui/icons-material/PermScanWifi'; +import {MythicStatusChip} from '../../MythicComponents/MythicStatusChip'; + +const getC2Status = (c2) => { + if(c2.c2profile.is_p2p){ + return c2.c2profile.container_running ? { + label: c2.c2profile.name, + status: "success", + icon: , + tooltip: "C2 Container online", + } : { + label: c2.c2profile.name, + status: "error", + icon: , + tooltip: "C2 Container offline", + }; + } + if(c2.c2profile.running){ + return { + label: c2.c2profile.name, + status: "success", + icon: , + tooltip: "C2 Internal Server Running", + }; + } + if(c2.c2profile.container_running){ + return { + label: c2.c2profile.name, + status: "warning", + icon: , + tooltip: "C2 Internal Server Not Running, but Container Online", + }; + } + return { + label: c2.c2profile.name, + status: "error", + icon: , + tooltip: "C2 Container offline", + }; +}; export function PayloadsTableRowC2Status(props){ return ( - +
{ - props.payloadc2profiles.map( (c2, i) => ( - - {c2.c2profile.is_p2p ? - ( c2.c2profile.container_running ? - - - : - - - ) - : - ( c2.c2profile.running ? - - - : - (c2.c2profile.container_running ? ( - - - - ) : ( - - - - )) - ) - } - {c2.c2profile.name} - - )) + props.payloadc2profiles.map( (c2, i) => { + const c2Status = getC2Status(c2); + return ( + + + + ); + }) } - +
) } diff --git a/MythicReactUI/src/components/pages/Reporting/Reporting.js b/MythicReactUI/src/components/pages/Reporting/Reporting.js index 1171f5bc..bfe9fbd6 100644 --- a/MythicReactUI/src/components/pages/Reporting/Reporting.js +++ b/MythicReactUI/src/components/pages/Reporting/Reporting.js @@ -1,7 +1,4 @@ import React from 'react'; -import Paper from '@mui/material/Paper'; -import {useTheme} from '@mui/material/styles'; -import Typography from '@mui/material/Typography'; import {ReportingTable} from './ReportingTable'; import {MythicPageBody} from "../../MythicComponents/MythicPageBody"; import {MythicPageHeader} from "../../MythicComponents/MythicPageHeader"; @@ -10,8 +7,10 @@ import {MythicPageHeader} from "../../MythicComponents/MythicPageHeader"; export function Reporting() { return ( - - + ); diff --git a/MythicReactUI/src/components/pages/Search/ArtifactTable.js b/MythicReactUI/src/components/pages/Search/ArtifactTable.js index 3a3dd1a2..e3a4095e 100644 --- a/MythicReactUI/src/components/pages/Search/ArtifactTable.js +++ b/MythicReactUI/src/components/pages/Search/ArtifactTable.js @@ -114,22 +114,22 @@ function ArtifactTableRow(props){ {props.needs_cleanup && !props.resolved && - - + + } {props.needs_cleanup && props.resolved && - - + + } {!props.needs_cleanup && - - + + } @@ -174,4 +174,3 @@ function ArtifactTableRow(props){
) } - diff --git a/MythicReactUI/src/components/pages/Search/ArtifactTableNewArtifactDialog.js b/MythicReactUI/src/components/pages/Search/ArtifactTableNewArtifactDialog.js index 7ee1cf02..e84404ca 100644 --- a/MythicReactUI/src/components/pages/Search/ArtifactTableNewArtifactDialog.js +++ b/MythicReactUI/src/components/pages/Search/ArtifactTableNewArtifactDialog.js @@ -1,5 +1,4 @@ import React from 'react'; -import { styled } from '@mui/material/styles'; import Button from '@mui/material/Button'; import Switch from '@mui/material/Switch'; import DialogActions from '@mui/material/DialogActions'; @@ -7,48 +6,18 @@ import DialogContent from '@mui/material/DialogContent'; import DialogTitle from '@mui/material/DialogTitle'; import MythicTextField from '../../MythicComponents/MythicTextField'; import Select from '@mui/material/Select'; -import Input from '@mui/material/Input'; import InputLabel from '@mui/material/InputLabel'; import MenuItem from '@mui/material/MenuItem'; import FormControl from '@mui/material/FormControl'; import ListItemText from '@mui/material/ListItemText'; import FormControlLabel from '@mui/material/FormControlLabel'; import { gql, useQuery} from '@apollo/client'; - -const PREFIX = 'ArtifactTableNewArtifactDialog'; - - -const classes = { - formControl: `${PREFIX}-formControl`, - chips: `${PREFIX}-chips`, - chip: `${PREFIX}-chip`, - noLabel: `${PREFIX}-noLabel` -}; - -const Root = styled('div')(( - { - theme - } -) => ({ - [`& .${classes.formControl}`]: { - margin: theme.spacing(1), - width: "50%", - marginRight: "5px" - }, - - [`& .${classes.chips}`]: { - display: 'flex', - flexWrap: 'wrap', - }, - - [`& .${classes.chip}`]: { - margin: 2, - }, - - [`& .${classes.noLabel}`]: { - marginTop: theme.spacing(2), - } -})); +import { + MythicDialogBody, + MythicDialogChoiceDivider, + MythicDialogChoiceRow, + MythicDialogSection +} from '../../MythicComponents/MythicDialogLayout'; const artifactHostSearch = gql` query artifactHostAndTypeQuery { @@ -126,81 +95,93 @@ export function ArtifactTableNewArtifactDialog(props) { setCustomBaseArtifact(value); } return ( - + Register New Artifact -
- - Existing Artifact Type - - - {"OR "} - -
-
- - Existing Host - - - {"OR "} - -
- - - } - labelPlacement={"start"} - /> -
- {needsCleanup && - } + + + + + Existing Artifact Type + + + + + + + + + + Existing Host + + + + + + + + + + + + } + labelPlacement={"start"} /> - } - + {needsCleanup && + } + /> + } + +
-
+
); } - diff --git a/MythicReactUI/src/components/pages/Search/CallbackSearchTable.js b/MythicReactUI/src/components/pages/Search/CallbackSearchTable.js index d0fe1fdf..e9e89d8c 100644 --- a/MythicReactUI/src/components/pages/Search/CallbackSearchTable.js +++ b/MythicReactUI/src/components/pages/Search/CallbackSearchTable.js @@ -10,7 +10,6 @@ import TableRow from '@mui/material/TableRow'; import {MythicConfirmDialog} from '../../MythicComponents/MythicConfirmDialog'; import { useMutation } from '@apollo/client'; import {snackActions} from '../../utilities/Snackbar'; -import {useTheme} from '@mui/material/styles'; import VisibilityOffIcon from '@mui/icons-material/VisibilityOff'; import VisibilityIcon from '@mui/icons-material/Visibility'; import {toggleHideCallbackMutations} from '../Callbacks/CallbackMutations'; @@ -20,6 +19,7 @@ import InfoIconOutline from '@mui/icons-material/InfoOutlined'; import MythicStyledTableCell from '../../MythicComponents/MythicTableCell'; import {MythicAgentSVGIcon} from "../../MythicComponents/MythicAgentSVGIcon"; import {CallbacksTableLastCheckinCell} from "../Callbacks/CallbacksTableRow"; +import {MythicStatusChip} from "../../MythicComponents/MythicStatusChip"; @@ -46,6 +46,7 @@ export function CallbackSearchTable(props){ View + Status User Domain Host @@ -74,7 +75,6 @@ export function CallbackSearchTable(props){ } function CallbackSearchTableRow(props){ - const theme = useTheme(); const [openDeleteDialog, setOpenDeleteDialog] = React.useState(false); const [openMetaDialog, setOpenMetaDialog] = React.useState(false); const [updateDeleted] = useMutation(toggleHideCallbackMutations, { @@ -97,13 +97,27 @@ function CallbackSearchTableRow(props){ {!props.active ? ( - {setOpenDeleteDialog(true);}} style={{color: theme.palette.error.main}} variant="contained"> + {setOpenDeleteDialog(true);}}> ) : ( - {setOpenDeleteDialog(true);}} style={{color: theme.palette.success.main}} variant="contained"> + {setOpenDeleteDialog(true);}}> )} + +
+ + {props.dead && + + } + {props.locked && + + } +
+
{props.user} @@ -134,7 +148,11 @@ function CallbackSearchTableRow(props){ - setOpenMetaDialog(true)} style={{color: theme.palette.info.main, cursor: "pointer"}}/> + + setOpenMetaDialog(true)}> + + + {openMetaDialog && {setOpenMetaDialog(false);}} @@ -146,4 +164,3 @@ function CallbackSearchTableRow(props){
) } - diff --git a/MythicReactUI/src/components/pages/Search/CredentialTable.js b/MythicReactUI/src/components/pages/Search/CredentialTable.js index 47816ba5..be21bbca 100644 --- a/MythicReactUI/src/components/pages/Search/CredentialTable.js +++ b/MythicReactUI/src/components/pages/Search/CredentialTable.js @@ -12,7 +12,6 @@ import {MythicSelectFromRawListDialog} from '../../MythicComponents/MythicSelect import {MythicConfirmDialog} from '../../MythicComponents/MythicConfirmDialog'; import { gql, useMutation } from '@apollo/client'; import {snackActions} from '../../utilities/Snackbar'; -import {useTheme} from '@mui/material/styles'; import DeleteIcon from '@mui/icons-material/Delete'; import RestoreFromTrashIcon from '@mui/icons-material/RestoreFromTrash'; import { MythicStyledTooltip } from '../../MythicComponents/MythicStyledTooltip'; @@ -198,7 +197,6 @@ export function CredentialTable(props){ function CredentialTableRow(props){ const me = props.me; - const theme = useTheme(); const [openDeleteDialog, setOpenDeleteDialog] = React.useState(false); const [editCommentDialogOpen, setEditCommentDialogOpen] = React.useState(false); const [editAccountDialogOpen, setEditAccountDialogOpen] = React.useState(false); @@ -353,15 +351,15 @@ function CredentialTableRow(props){ {props.deleted ? ( - {setOpenDeleteDialog(true);}} style={{color: theme.palette.success.main}} variant="contained"> + {setOpenDeleteDialog(true);}}> ) : ( - {setOpenDeleteDialog(true);}} style={{color: theme.palette.error.main}} variant="contained"> + {setOpenDeleteDialog(true);}}> )} - @@ -412,7 +410,7 @@ function CredentialTableRow(props){
- onCopyToClipboard(props.credential_text)} size="small"> + onCopyToClipboard(props.credential_text)} size="small"> @@ -433,4 +431,3 @@ function CredentialTableRow(props){ ) } - diff --git a/MythicReactUI/src/components/pages/Search/CredentialTableNewCredentialDialog.js b/MythicReactUI/src/components/pages/Search/CredentialTableNewCredentialDialog.js index 8f5d58ae..898c1d82 100644 --- a/MythicReactUI/src/components/pages/Search/CredentialTableNewCredentialDialog.js +++ b/MythicReactUI/src/components/pages/Search/CredentialTableNewCredentialDialog.js @@ -1,48 +1,19 @@ import React from 'react'; -import { styled } from '@mui/material/styles'; import Button from '@mui/material/Button'; import DialogActions from '@mui/material/DialogActions'; import DialogContent from '@mui/material/DialogContent'; import DialogTitle from '@mui/material/DialogTitle'; import MythicTextField from '../../MythicComponents/MythicTextField'; import Select from '@mui/material/Select'; -import Input from '@mui/material/Input'; import InputLabel from '@mui/material/InputLabel'; import MenuItem from '@mui/material/MenuItem'; import FormControl from '@mui/material/FormControl'; import ListItemText from '@mui/material/ListItemText'; -const PREFIX = 'CredentialTableNewCredentialDialog'; - -const classes = { - formControl: `${PREFIX}-formControl`, - chips: `${PREFIX}-chips`, - chip: `${PREFIX}-chip`, - noLabel: `${PREFIX}-noLabel` -}; - -const Root = styled('div')(( - { - theme - } -) => ({ - [`& .${classes.formControl}`]: { - margin: theme.spacing(1), - width: "100%", - }, - - [`& .${classes.chips}`]: { - display: 'flex', - flexWrap: 'wrap', - }, - - [`& .${classes.chip}`]: { - margin: 2, - }, - - [`& .${classes.noLabel}`]: { - marginTop: theme.spacing(2), - } -})); +import { + MythicDialogBody, + MythicDialogGrid, + MythicDialogSection +} from '../../MythicComponents/MythicDialogLayout'; export function CredentialTableNewCredentialDialog(props) { const [credentialType, setCredentialType] = React.useState("plaintext"); @@ -81,32 +52,37 @@ export function CredentialTableNewCredentialDialog(props) { setCredentialType(event.target.value); } return ( - + Register New Credential - - - Which Type of Credential - - - - - - - - + + + + + Credential Type + + + + + + + + + + + - + ); } - diff --git a/MythicReactUI/src/components/pages/Search/CustomBrowserTable.js b/MythicReactUI/src/components/pages/Search/CustomBrowserTable.js index a5b1d18a..70ae6e6b 100644 --- a/MythicReactUI/src/components/pages/Search/CustomBrowserTable.js +++ b/MythicReactUI/src/components/pages/Search/CustomBrowserTable.js @@ -1,5 +1,5 @@ import React, { useEffect } from 'react'; -import {Button, IconButton, Typography, Link} from '@mui/material'; +import {IconButton, Typography, Link} from '@mui/material'; import Table from '@mui/material/Table'; import TableBody from '@mui/material/TableBody'; import TableCell from '@mui/material/TableCell'; @@ -13,6 +13,7 @@ import {snackActions} from '../../utilities/Snackbar'; import EditIcon from '@mui/icons-material/Edit'; import MythicStyledTableCell from '../../MythicComponents/MythicTableCell'; import {TagsDisplay, ViewEditTags} from '../../MythicComponents/MythicTag'; +import { MythicStyledTooltip } from '../../MythicComponents/MythicStyledTooltip'; const updateFileComment = gql` mutation updateCommentMutation($mythictree_id: Int!, $comment: String!){ @@ -94,7 +95,15 @@ function CustomBrowserTableRow(props){ /> } - + + setViewPermissionsDialogOpen(true)} + > + + + Host: {props.host} {props.callback?.mythictree_groups.length > 0 ? ( @@ -122,7 +131,13 @@ function CustomBrowserTableRow(props){ ))} - setEditCommentDialogOpen(true)} size="small" style={{display: "inline-block"}}> + setEditCommentDialogOpen(true)} + size="small" + > + + {props.comment} @@ -134,4 +149,3 @@ function CustomBrowserTableRow(props){ ) } - diff --git a/MythicReactUI/src/components/pages/Search/FileBrowserTable.js b/MythicReactUI/src/components/pages/Search/FileBrowserTable.js index 93057d41..f2ad650b 100644 --- a/MythicReactUI/src/components/pages/Search/FileBrowserTable.js +++ b/MythicReactUI/src/components/pages/Search/FileBrowserTable.js @@ -1,5 +1,5 @@ import React, { useEffect } from 'react'; -import {Button, IconButton, Typography, Link} from '@mui/material'; +import {IconButton, Typography, Link} from '@mui/material'; import Table from '@mui/material/Table'; import TableBody from '@mui/material/TableBody'; import TableCell from '@mui/material/TableCell'; @@ -133,7 +133,13 @@ function FileBrowserTableRow(props){ - setEditCommentDialogOpen(true)} size="small" style={{display: "inline-block"}}> + setEditCommentDialogOpen(true)} + size="small" + > + + {props.comment} @@ -141,12 +147,26 @@ function FileBrowserTableRow(props){ - + + setViewPermissionsDialogOpen(true)} + > + + + {props.filemeta.length > 0 ? ( - + setFileHistoryDialogOpen(true)} + > + + ): (null)} @@ -154,4 +174,3 @@ function FileBrowserTableRow(props){ ) } - diff --git a/MythicReactUI/src/components/pages/Search/FileMetaTable.js b/MythicReactUI/src/components/pages/Search/FileMetaTable.js index fbcdeead..65a39c0e 100644 --- a/MythicReactUI/src/components/pages/Search/FileMetaTable.js +++ b/MythicReactUI/src/components/pages/Search/FileMetaTable.js @@ -238,18 +238,22 @@ export function FileMetaDownloadTable(props){ }, [selected]); return ( - - - @@ -261,7 +265,7 @@ export function FileMetaDownloadTable(props){ sx={{pl: "3px"}} inputProps={{ 'aria-label': 'controlled', }} /> - Actions + Actions File Comment Size @@ -364,12 +368,24 @@ function FileMetaDownloadTableRow(props){ {props.deleted || props.size === 0 ? null : ( -
- {setOpenDelete(true);}}/> +
+ + {setOpenDelete(true);}} + > + + + - + + + {openPreviewMediaDialog && - {props.comment} onOpenCloseComment(e, true)} size="small" style={{display: "inline-block"}}> + {props.comment} onOpenCloseComment(e, true)} size="small"> {editCommentDialogOpen && {onOpenCloseComment(e, false);}} @@ -414,7 +430,7 @@ function FileMetaDownloadTableRow(props){ - + {openDetails ? : } @@ -466,7 +482,13 @@ function FileMetaDownloadTableRow(props){ - {setOpenHostDialog(true);}} /> + {setOpenHostDialog(true);}} + > + + {openHostDialog && - - - @@ -649,7 +675,7 @@ export function FileMetaUploadTable(props){ sx={{pl: "3px"}} inputProps={{'aria-label': 'controlled'}}/> - Actions + Actions Source Destination Comment @@ -750,12 +776,24 @@ function FileMetaUploadTableRow(props){ {props.deleted ? null : ( -
- {setOpenDelete(true);}}/> +
+ + {setOpenDelete(true);}} + > + + + - + + + {openPreviewMediaDialog && - {props.comment} onOpenCloseComment(e, true)} size="small" style={{display: "inline-block"}}> + {props.comment} onOpenCloseComment(e, true)} size="small"> {onOpenCloseComment(e, false)}} innerDialog={{onOpenCloseComment(e, false)}} />} @@ -808,7 +846,7 @@ function FileMetaUploadTableRow(props){ - + {openDetails ? : } @@ -859,7 +897,13 @@ function FileMetaUploadTableRow(props){ - {setOpenHostDialog(true);}} /> + {setOpenHostDialog(true);}} + > + + {openHostDialog && {setOpenDelete(false);}} onSubmit={onAcceptDelete} open={openDelete}/>} {props.deleted ? null : ( - {setOpenDelete(true);}} style={{color: theme.palette.error.main}} variant="contained"> + + {setOpenDelete(true);}} + > + + + )} @@ -1069,7 +1120,7 @@ function FileMetaScreenshotTableRow(props){ {toLocalTime(props.timestamp, me.user.view_utc_time)} {props.host} - {props.comment} setEditCommentDialogOpen(true)} size="small" style={{display: "inline-block"}}> + {props.comment} setEditCommentDialogOpen(true)} size="small"> {setEditCommentDialogOpen(false);}} innerDialog={{setEditCommentDialogOpen(false);}} />} @@ -1083,7 +1134,7 @@ function FileMetaScreenshotTableRow(props){ - setOpenDetails(!openDetails)}> + setOpenDetails(!openDetails)}> {openDetails ? : } @@ -1269,18 +1320,22 @@ export function FileMetaEventingWorkflowsTable(props){ }, [selected]); return ( - - - @@ -1377,12 +1432,24 @@ function FileMetaEventingWorkflowsTableRow(props){ {props.deleted ? null : ( -
- {setOpenDelete(true);}}/> +
+ + {setOpenDelete(true);}} + > + + + - + + + {openPreviewMediaDialog && - + {openDetails ? : } @@ -1444,7 +1511,13 @@ function FileMetaEventingWorkflowsTableRow(props){ - {setOpenHostDialog(true);}} /> + {setOpenHostDialog(true);}} + > + + {openHostDialog && { @@ -188,8 +185,12 @@ function KeylogTableRow(props){ - props.onGroupKeylogData(props.window, props.user, props.task.callback.host)}> - + props.onGroupKeylogData(props.window, props.user, props.task.callback.host)} + size="small" + > + @@ -197,8 +198,12 @@ function KeylogTableRow(props){ - onCopyToClipboard(keylogData)} size="small"> - + onCopyToClipboard(keylogData)} + size="small" + > + @@ -206,8 +211,13 @@ function KeylogTableRow(props){ {keylogData.length > 500 ? ( <> {"..."}
- {setOpenDisplayKeylogData(true);}}/> + {setOpenDisplayKeylogData(true);}} + size="small" + > + + ) : null} {openDisplayKeylogData && diff --git a/MythicReactUI/src/components/pages/Search/ProcessTable.js b/MythicReactUI/src/components/pages/Search/ProcessTable.js index 8c57e6a0..73b4ae74 100644 --- a/MythicReactUI/src/components/pages/Search/ProcessTable.js +++ b/MythicReactUI/src/components/pages/Search/ProcessTable.js @@ -1,5 +1,5 @@ import React, { useEffect } from 'react'; -import {Button, IconButton, Typography, Link} from '@mui/material'; +import {IconButton, Typography, Link} from '@mui/material'; import Table from '@mui/material/Table'; import TableBody from '@mui/material/TableBody'; import TableCell from '@mui/material/TableCell'; @@ -13,6 +13,7 @@ import {snackActions} from '../../utilities/Snackbar'; import EditIcon from '@mui/icons-material/Edit'; import MythicStyledTableCell from '../../MythicComponents/MythicTableCell'; import {TagsDisplay, ViewEditTags} from '../../MythicComponents/MythicTag'; +import { MythicStyledTooltip } from '../../MythicComponents/MythicStyledTooltip'; const updateFileComment = gql` mutation updateCommentMutation($mythictree_id: Int!, $comment: String!){ @@ -94,7 +95,15 @@ function ProcessTableRow(props){ /> } - + + setViewPermissionsDialogOpen(true)} + > + + + {props.full_path_text} @@ -127,7 +136,13 @@ function ProcessTableRow(props){ } - setEditCommentDialogOpen(true)} size="small" style={{display: "inline-block"}}> + setEditCommentDialogOpen(true)} + size="small" + > + + {props.comment} @@ -139,4 +154,3 @@ function ProcessTableRow(props){ ) } - diff --git a/MythicReactUI/src/components/pages/Search/ProxySearchTable.js b/MythicReactUI/src/components/pages/Search/ProxySearchTable.js index 02eea633..b616529e 100644 --- a/MythicReactUI/src/components/pages/Search/ProxySearchTable.js +++ b/MythicReactUI/src/components/pages/Search/ProxySearchTable.js @@ -10,9 +10,7 @@ import Moment from 'react-moment'; import {MythicConfirmDialog} from '../../MythicComponents/MythicConfirmDialog'; import { gql, useMutation } from '@apollo/client'; import {snackActions} from '../../utilities/Snackbar'; -import {useTheme} from '@mui/material/styles'; import DeleteIcon from '@mui/icons-material/Delete'; -import Tooltip from '@mui/material/Tooltip'; import RestoreFromTrashIcon from '@mui/icons-material/RestoreFromTrash'; import {getStringSize} from '../Callbacks/ResponseDisplayTable'; import {MythicStyledTooltip} from "../../MythicComponents/MythicStyledTooltip"; @@ -58,7 +56,7 @@ export function ProxySearchTable(props){ - + User@Host Task Info Bound Port @@ -90,7 +88,6 @@ export function ProxySearchTable(props){ } function ProxySearchTableRow(props){ - const theme = useTheme(); const [openDeleteDialog, setOpenDeleteDialog] = React.useState(false); const confirmDialogText = "This does not issue any start/stop command to the agent. This only opens/closes ports that Mythic controls. For rpfwd, this will not open/close that port on the remote host - you need to issue a task to your agent to do that."; const [updateDeleted] = useMutation(toggleProxy, { @@ -146,13 +143,25 @@ function ProxySearchTableRow(props){ /> } {props.deleted ? ( - - {setOpenDeleteDialog(true);}} style={{color: theme.palette.success.main}} variant="contained"> - + + {setOpenDeleteDialog(true);}} + > + + + ) : - ( - {setOpenDeleteDialog(true);}} style={{color: theme.palette.error.main}} variant="contained"> - + ( + {setOpenDeleteDialog(true);}} + > + + + )} @@ -211,9 +220,12 @@ function ProxySearchTableRow(props){ {props.remote_port !== 0 && - - + + } diff --git a/MythicReactUI/src/components/pages/Search/Search.js b/MythicReactUI/src/components/pages/Search/Search.js index 21fc8f4e..22471e89 100644 --- a/MythicReactUI/src/components/pages/Search/Search.js +++ b/MythicReactUI/src/components/pages/Search/Search.js @@ -1,5 +1,4 @@ import React from 'react'; -import { styled } from '@mui/material/styles'; import AppBar from '@mui/material/AppBar'; import Tabs from '@mui/material/Tabs'; import {useNavigate} from 'react-router-dom'; @@ -15,31 +14,33 @@ import {SearchTabProcessesLabel, SearchTabProcessPanel} from "./SearchTabProcess import {SearchTabTagsLabel, SearchTabTagsPanel} from "./SearchTabTags"; import {SearchTabPayloadsLabel, SearchTabPayloadsPanel} from "./SearchTabPayloads"; import {SearchTabCustomBrowserLabel, SearchTabCustomBrowserPanel} from "./SearchTabCustomBrowsers"; - -const PREFIX = 'Search'; - -const classes = { - root: `${PREFIX}-root` -}; - -const Root = styled('div')(( - { - theme - } -) => ({ - [`&.${classes.root}`]: { - width: "100%", - } -})); +import {MythicPageBody} from "../../MythicComponents/MythicPageBody"; +import {MythicPageHeader, MythicPageHeaderChip} from "../../MythicComponents/MythicPageHeader"; export function Search(props){ const navigate = useNavigate(); const tabTypes = ["callbacks", "tasks", "payloads", "files", "credentials", "keylogs", "artifacts", "tokens", "socks", "processes", "tags", "browsers"]; + const tabLabels = { + artifacts: "Artifacts", + browsers: "Browser scripts", + callbacks: "Callbacks", + credentials: "Credentials", + files: "Files", + keylogs: "Keylogs", + payloads: "Payloads", + processes: "Processes", + socks: "SOCKS", + tags: "Tags", + tasks: "Tasks", + tokens: "Tokens", + }; var params = new URLSearchParams(window.location.search); var valueString = params.get("tab") ? params.get("tab") : tabTypes[0]; var valueIndex = tabTypes.findIndex(t => t === valueString); var value = valueIndex === -1 ? 0 : valueIndex; + const currentTab = valueIndex === -1 ? tabTypes[0] : valueString; + const currentTabLabel = tabLabels[currentTab] || currentTab; const handleChange = (event, newValue) => { params.set("tab", tabTypes[newValue]); @@ -52,7 +53,7 @@ export function Search(props){ navigate(newRelativePathQuery); } const getTabComponent = () => { - switch(valueString){ + switch(currentTab){ case "tasks": return case "payloads": @@ -80,10 +81,30 @@ export function Search(props){ default: return null; } - } + } return ( - - + + + + + + } + /> + ({ + backgroundColor: theme.surfaces?.muted || theme.palette.background.paper, + border: `1px solid ${theme.table?.borderSoft || theme.borderColor}`, + borderRadius: `${theme.shape.borderRadius}px`, + overflow: "hidden", + })} + > + ); } diff --git a/MythicReactUI/src/components/pages/Search/SearchTabArtifacts.js b/MythicReactUI/src/components/pages/Search/SearchTabArtifacts.js index 97224910..a45c2859 100644 --- a/MythicReactUI/src/components/pages/Search/SearchTabArtifacts.js +++ b/MythicReactUI/src/components/pages/Search/SearchTabArtifacts.js @@ -1,22 +1,16 @@ import {MythicTabPanel, MythicSearchTabLabel} from '../../MythicComponents/MythicTabPanel'; import React from 'react'; -import MythicTextField from '../../MythicComponents/MythicTextField'; import FingerprintIcon from '@mui/icons-material/Fingerprint'; -import Grid from '@mui/material/Grid'; -import SearchIcon from '@mui/icons-material/Search'; -import Tooltip from '@mui/material/Tooltip'; -import {useTheme} from '@mui/material/styles'; -import IconButton from '@mui/material/IconButton'; import { gql, useMutation} from '@apollo/client'; import { snackActions } from '../../utilities/Snackbar'; -import Pagination from '@mui/material/Pagination'; -import { Typography, Button } from '@mui/material'; import {ArtifactTable} from './ArtifactTable'; import MenuItem from '@mui/material/MenuItem'; -import Select from '@mui/material/Select'; import {useMythicLazyQuery} from "../../utilities/useMythicLazyQuery"; import {MythicDialog} from "../../MythicComponents/MythicDialog"; import {ArtifactTableNewArtifactDialog} from "./ArtifactTableNewArtifactDialog"; +import {MythicTablePagination} from "../../MythicComponents/MythicTablePagination"; +import {MythicSearchField, MythicTableToolbar, MythicTableToolbarGroup, MythicToolbarButton, MythicToolbarSelect} from "../../MythicComponents/MythicTableToolbar"; +import {MythicSearchEmptyState} from "../../MythicComponents/MythicStateDisplay"; const artifactFragment = gql` fragment artifactData on taskartifact{ @@ -155,7 +149,6 @@ export function SearchTabArtifactsLabel(props){ } const SearchTabArtifactsSearchPanel = (props) => { - const theme = useTheme(); const [search, setSearch] = React.useState(""); const [searchField, setSearchField] = React.useState("Artifact"); const searchFieldOptions = ["Artifact", "Command", "Host", "Type", "Task", "Callback", "Operator"]; @@ -240,22 +233,12 @@ const SearchTabArtifactsSearchPanel = (props) => { } }, [props.value, props.index]) return ( - - - - - - - , - style: {padding: 0} - }}/> - - - - - - + + + {createArtifactDialogOpen && {setCreateArtifactDialogOpen(false);}} @@ -285,13 +267,11 @@ const SearchTabArtifactsSearchPanel = (props) => { /> } - - - + + + ); } export const SearchTabArtifactsPanel = (props) =>{ @@ -523,16 +503,16 @@ export const SearchTabArtifactsPanel = (props) =>{
{artifactData.length > 0 ? ( ) : ( -
No Search Results
+ )}
-
- - Total Results: {totalCount} -
+ ) -} \ No newline at end of file +} diff --git a/MythicReactUI/src/components/pages/Search/SearchTabCallbacks.js b/MythicReactUI/src/components/pages/Search/SearchTabCallbacks.js index 19db796d..b2d64f5f 100644 --- a/MythicReactUI/src/components/pages/Search/SearchTabCallbacks.js +++ b/MythicReactUI/src/components/pages/Search/SearchTabCallbacks.js @@ -1,20 +1,14 @@ import {MythicTabPanel, MythicSearchTabLabel} from '../../MythicComponents/MythicTabPanel'; import React from 'react'; -import MythicTextField from '../../MythicComponents/MythicTextField'; import PhoneCallbackIcon from '@mui/icons-material/PhoneCallback'; -import Grid from '@mui/material/Grid'; -import SearchIcon from '@mui/icons-material/Search'; -import Tooltip from '@mui/material/Tooltip'; -import {useTheme} from '@mui/material/styles'; -import IconButton from '@mui/material/IconButton'; import { gql, useLazyQuery} from '@apollo/client'; import { snackActions } from '../../utilities/Snackbar'; -import Pagination from '@mui/material/Pagination'; -import { Typography } from '@mui/material'; import {CallbackSearchTable} from './CallbackSearchTable'; import MenuItem from '@mui/material/MenuItem'; -import Select from '@mui/material/Select'; import {useMythicLazyQuery} from "../../utilities/useMythicLazyQuery"; +import {MythicTablePagination} from "../../MythicComponents/MythicTablePagination"; +import {MythicSearchField, MythicTableToolbar, MythicTableToolbarGroup, MythicToolbarSelect} from "../../MythicComponents/MythicTableToolbar"; +import {MythicSearchEmptyState} from "../../MythicComponents/MythicStateDisplay"; const callbackFragment = gql` fragment callbackSearchData on callback{ @@ -29,6 +23,7 @@ fragment callbackSearchData on callback{ pid active dead + locked last_checkin payload { payloadtype { @@ -164,7 +159,6 @@ export function SearchTabCallbacksLabel(props){ } const SearchTabCallbacksSearchPanel = (props) => { - const theme = useTheme(); const [search, setSearch] = React.useState(""); const [searchField, setSearchField] = React.useState("Host"); const searchFieldOptions = ["User", "Domain", "Host", "Description", "IP", "PID", "Group", "PayloadType", "CallbackDisplayID"]; @@ -236,22 +230,12 @@ const SearchTabCallbacksSearchPanel = (props) => { } }, [props.value, props.index]) return ( - - - - - - - , - style: {padding: 0} - }}/> - - - - - + + + ); } export const SearchTabCallbacksPanel = (props) =>{ @@ -522,14 +506,14 @@ export const SearchTabCallbacksPanel = (props) =>{
{callbackData.length > 0 ? ( ) : ( -
No Search Results
+ )}
-
- - Total Results: {totalCount} -
+ ) -} \ No newline at end of file +} diff --git a/MythicReactUI/src/components/pages/Search/SearchTabCredentials.js b/MythicReactUI/src/components/pages/Search/SearchTabCredentials.js index 546e1be4..9642bda0 100644 --- a/MythicReactUI/src/components/pages/Search/SearchTabCredentials.js +++ b/MythicReactUI/src/components/pages/Search/SearchTabCredentials.js @@ -1,23 +1,18 @@ import {MythicTabPanel, MythicSearchTabLabel} from '../../MythicComponents/MythicTabPanel'; import React from 'react'; -import MythicTextField from '../../MythicComponents/MythicTextField'; import VpnKeyIcon from '@mui/icons-material/VpnKey'; -import Grid from '@mui/material/Grid'; -import SearchIcon from '@mui/icons-material/Search'; -import Tooltip from '@mui/material/Tooltip'; -import IconButton from '@mui/material/IconButton'; import { gql, useMutation} from '@apollo/client'; import { snackActions } from '../../utilities/Snackbar'; -import Pagination from '@mui/material/Pagination'; -import { Button, Typography } from '@mui/material'; import {CredentialTable} from './CredentialTable'; import { MythicDialog } from '../../MythicComponents/MythicDialog'; import {CredentialTableNewCredentialDialog} from './CredentialTableNewCredentialDialog'; import MenuItem from '@mui/material/MenuItem'; -import Select from '@mui/material/Select'; import VisibilityOffIcon from '@mui/icons-material/VisibilityOff'; import VisibilityIcon from '@mui/icons-material/Visibility'; import {useMythicLazyQuery} from "../../utilities/useMythicLazyQuery"; +import {MythicTablePagination} from "../../MythicComponents/MythicTablePagination"; +import {MythicSearchField, MythicTableToolbar, MythicTableToolbarGroup, MythicToolbarButton, MythicToolbarSelect, MythicToolbarToggle} from "../../MythicComponents/MythicTableToolbar"; +import {MythicSearchEmptyState} from "../../MythicComponents/MythicStateDisplay"; const credentialFragment = gql` fragment credentialSearchData on credential{ @@ -221,22 +216,12 @@ const SearchTabCredentialsSearchPanel = (props) => { } }, [props.value, props.index]) return ( - - - - - - - , - style: {padding: 0} - }}/> - - - - - + + + {createCredentialDialogOpen && {setCreateCredentialDialogOpen(false);}} @@ -255,28 +240,18 @@ const SearchTabCredentialsSearchPanel = (props) => { /> } - - - - + + } + inactiveIcon={} + /> + + ); } @@ -470,14 +445,14 @@ export const SearchTabCredentialsPanel = (props) =>{
{credentialaData.length > 0 ? ( ) : ( -
No Search Results
+ )}
-
- - Total Results: {totalCount} -
+ ) -} \ No newline at end of file +} diff --git a/MythicReactUI/src/components/pages/Search/SearchTabCustomBrowsers.js b/MythicReactUI/src/components/pages/Search/SearchTabCustomBrowsers.js index dd359798..01c7375f 100644 --- a/MythicReactUI/src/components/pages/Search/SearchTabCustomBrowsers.js +++ b/MythicReactUI/src/components/pages/Search/SearchTabCustomBrowsers.js @@ -1,20 +1,14 @@ import {MythicTabPanel, MythicSearchTabLabel} from '../../MythicComponents/MythicTabPanel'; import React from 'react'; -import MythicTextField from '../../MythicComponents/MythicTextField'; import FolderIcon from '@mui/icons-material/Folder'; -import Grid from '@mui/material/Grid'; -import SearchIcon from '@mui/icons-material/Search'; -import Tooltip from '@mui/material/Tooltip'; -import {useTheme} from '@mui/material/styles'; -import IconButton from '@mui/material/IconButton'; import { gql, useQuery} from '@apollo/client'; import { snackActions } from '../../utilities/Snackbar'; -import Pagination from '@mui/material/Pagination'; -import { Typography } from '@mui/material'; import MenuItem from '@mui/material/MenuItem'; -import Select from '@mui/material/Select'; import {useMythicLazyQuery} from "../../utilities/useMythicLazyQuery"; import {CustomBrowserTable} from "./CustomBrowserTable"; +import {MythicTablePagination} from "../../MythicComponents/MythicTablePagination"; +import {MythicSearchField, MythicTableToolbar, MythicTableToolbarGroup, MythicToolbarSelect} from "../../MythicComponents/MythicTableToolbar"; +import {MythicSearchEmptyState} from "../../MythicComponents/MythicStateDisplay"; const mythictreeFragment = gql` @@ -115,7 +109,6 @@ export function SearchTabCustomBrowserLabel(props){ ) } const SearchTabCustomBrowserSearchPanel = (props) => { - const theme = useTheme(); const [browserOptions, setBrowserOptions] = React.useState([]); const [selectedBrowser, setSelectedBrowser] = React.useState(""); const [search, setSearch] = React.useState(""); @@ -202,10 +195,9 @@ const SearchTabCustomBrowserSearchPanel = (props) => { } }, [props.value, props.index]); return ( - - - - - - - - - - - - - , - style: {padding: 0} - }}/> - - - - - + + + ); } export const SearchTabCustomBrowserPanel = (props) =>{ @@ -404,14 +386,14 @@ export const SearchTabCustomBrowserPanel = (props) =>{
{processData.length > 0 ? ( ) : ( -
No Search Results
+ )}
-
- - Total Results: {totalCount} -
+ ) -} \ No newline at end of file +} diff --git a/MythicReactUI/src/components/pages/Search/SearchTabFiles.js b/MythicReactUI/src/components/pages/Search/SearchTabFiles.js index e9b5d2cf..8cf29bd2 100644 --- a/MythicReactUI/src/components/pages/Search/SearchTabFiles.js +++ b/MythicReactUI/src/components/pages/Search/SearchTabFiles.js @@ -1,16 +1,8 @@ import {MythicSearchTabLabel, MythicTabPanel} from '../../MythicComponents/MythicTabPanel'; import React from 'react'; -import MythicTextField from '../../MythicComponents/MythicTextField'; import AttachmentIcon from '@mui/icons-material/Attachment'; -import Grid from '@mui/material/Grid'; -import SearchIcon from '@mui/icons-material/Search'; -import Tooltip from '@mui/material/Tooltip'; -import {useTheme} from '@mui/material/styles'; -import IconButton from '@mui/material/IconButton'; import {gql, useLazyQuery} from '@apollo/client'; import {snackActions} from '../../utilities/Snackbar'; -import Pagination from '@mui/material/Pagination'; -import {Button, Typography} from '@mui/material'; import { FileMetaDownloadTable, FileMetaEventingWorkflowsTable, @@ -19,12 +11,14 @@ import { } from './FileMetaTable'; import {FileBrowserTable} from './FileBrowserTable'; import MenuItem from '@mui/material/MenuItem'; -import Select from '@mui/material/Select'; import {UploadTaskFile} from '../../MythicComponents/MythicFileUpload'; import VisibilityOffIcon from '@mui/icons-material/VisibilityOff'; import VisibilityIcon from '@mui/icons-material/Visibility'; import BackupIcon from '@mui/icons-material/Backup'; import {useMythicLazyQuery} from "../../utilities/useMythicLazyQuery"; +import {MythicTablePagination} from "../../MythicComponents/MythicTablePagination"; +import {MythicSearchField, MythicTableToolbar, MythicTableToolbarGroup, MythicToolbarButton, MythicToolbarSelect, MythicToolbarToggle} from "../../MythicComponents/MythicTableToolbar"; +import {MythicSearchEmptyState} from "../../MythicComponents/MythicStateDisplay"; const fileMetaFragment = gql` fragment filemetaData on filemeta{ @@ -438,7 +432,6 @@ export function SearchTabFilesLabel(props) { } const SearchTabFilesSearchPanel = (props) => { - const theme = useTheme(); const [searchHost, setSearchHost] = React.useState(""); const [search, setSearch] = React.useState(""); const [searchField, setSearchField] = React.useState("Filename"); @@ -559,29 +552,16 @@ const SearchTabFilesSearchPanel = (props) => { } }, [props.value, props.index]); return ( - - - - - - - - - - , - style: {padding: 0} - }}/> - - - - - - - - - - - - + + } + inactiveIcon={} + /> + + ); } @@ -1130,6 +1101,11 @@ export const SearchTabFilesPanel = (props) => { break; } } + const currentResultCount = searchLocation === "Uploads" ? fileMetaUploadData.length : + searchLocation === "Downloads" ? fileMetaDownloadData.length : + searchLocation === "Screenshots" ? fileMetaScreenshotData.length : + searchLocation === "FileBrowser" ? fileBrowserData.length : + fileMetaUploadData.length; return ( { onChangeDeletedField={onChangeDeletedField} changeSearchParam={props.changeSearchParam}/>
- {searchLocation === "Uploads" && } - {searchLocation === "Downloads" && } - {searchLocation === "Screenshots" && } - {searchLocation === "FileBrowser" && } - {searchLocation === "Eventing Workflows" && } -
-
- - Total Results: {totalCount} + {currentResultCount > 0 ? ( + <> + {searchLocation === "Uploads" && } + {searchLocation === "Downloads" && } + {searchLocation === "Screenshots" && } + {searchLocation === "FileBrowser" && } + {searchLocation === "Eventing Workflows" && } + + ) : ( + + )}
+
) -} \ No newline at end of file +} diff --git a/MythicReactUI/src/components/pages/Search/SearchTabKeylog.js b/MythicReactUI/src/components/pages/Search/SearchTabKeylog.js index a9b0e5f0..d718924e 100644 --- a/MythicReactUI/src/components/pages/Search/SearchTabKeylog.js +++ b/MythicReactUI/src/components/pages/Search/SearchTabKeylog.js @@ -1,20 +1,14 @@ import {MythicTabPanel, MythicSearchTabLabel} from '../../MythicComponents/MythicTabPanel'; import React from 'react'; -import MythicTextField from '../../MythicComponents/MythicTextField'; import KeyboardIcon from '@mui/icons-material/Keyboard'; -import Grid from '@mui/material/Grid'; -import SearchIcon from '@mui/icons-material/Search'; -import Tooltip from '@mui/material/Tooltip'; -import {useTheme} from '@mui/material/styles'; -import IconButton from '@mui/material/IconButton'; import { gql, useLazyQuery} from '@apollo/client'; import { snackActions } from '../../utilities/Snackbar'; -import Pagination from '@mui/material/Pagination'; -import { Typography } from '@mui/material'; import {KeylogsTable} from './KeylogsTable'; import MenuItem from '@mui/material/MenuItem'; -import Select from '@mui/material/Select'; import {useMythicLazyQuery} from "../../utilities/useMythicLazyQuery"; +import {MythicTablePagination} from "../../MythicComponents/MythicTablePagination"; +import {MythicSearchField, MythicTableToolbar, MythicTableToolbarGroup, MythicToolbarSelect} from "../../MythicComponents/MythicTableToolbar"; +import {MythicSearchEmptyState} from "../../MythicComponents/MythicStateDisplay"; const keylogFragment = gql` @@ -123,7 +117,6 @@ export function SearchTabKeylogsLabel(props){ } const SearchTabKeylogsSearchPanel = (props) => { - const theme = useTheme(); const [search, setSearch] = React.useState(""); const [searchField, setSearchField] = React.useState("Keystroke"); const searchFieldOptions = ["Keystroke", "User", "Program", "Host"]; @@ -179,22 +172,12 @@ const SearchTabKeylogsSearchPanel = (props) => { } }, [props.value, props.index]) return ( - - - - - - - , - style: {padding: 0} - }}/> - - - - - + + + ); } export const SearchTabKeylogsPanel = (props) =>{ @@ -377,14 +360,14 @@ export const SearchTabKeylogsPanel = (props) =>{
{keylogData.length > 0 ? ( ) : ( -
No Search Results
+ )}
-
- - Total Results: {totalCount} -
+ ) -} \ No newline at end of file +} diff --git a/MythicReactUI/src/components/pages/Search/SearchTabPayloads.js b/MythicReactUI/src/components/pages/Search/SearchTabPayloads.js index 080069a2..2d1080c1 100644 --- a/MythicReactUI/src/components/pages/Search/SearchTabPayloads.js +++ b/MythicReactUI/src/components/pages/Search/SearchTabPayloads.js @@ -1,25 +1,19 @@ import {MythicSearchTabLabel, MythicTabPanel} from '../../MythicComponents/MythicTabPanel'; import React from 'react'; -import MythicTextField from '../../MythicComponents/MythicTextField'; -import Grid from '@mui/material/Grid'; -import SearchIcon from '@mui/icons-material/Search'; -import Tooltip from '@mui/material/Tooltip'; -import {useTheme} from '@mui/material/styles'; -import IconButton from '@mui/material/IconButton'; import {gql, useLazyQuery} from '@apollo/client'; import {snackActions} from '../../utilities/Snackbar'; -import Pagination from '@mui/material/Pagination'; -import {Button, Typography} from '@mui/material'; import MenuItem from '@mui/material/MenuItem'; -import Select from '@mui/material/Select'; import VisibilityOffIcon from '@mui/icons-material/VisibilityOff'; import VisibilityIcon from '@mui/icons-material/Visibility'; import {payloadFragment} from "../Payloads/Payloads"; import {SearchPayloadsTable} from "./PayloadsTable"; -import { Backdrop, CircularProgress } from '@mui/material'; +import { Backdrop } from '@mui/material'; import {faBiohazard} from '@fortawesome/free-solid-svg-icons'; import {FontAwesomeIcon} from '@fortawesome/react-fontawesome'; import {useMythicLazyQuery} from "../../utilities/useMythicLazyQuery"; +import {MythicTablePagination} from "../../MythicComponents/MythicTablePagination"; +import {MythicSearchField, MythicTableToolbar, MythicTableToolbarGroup, MythicToolbarSelect, MythicToolbarToggle} from "../../MythicComponents/MythicTableToolbar"; +import {MythicLoadingState, MythicSearchEmptyState} from "../../MythicComponents/MythicStateDisplay"; const fetchLimit = 20; const filenameSearch = gql` @@ -108,7 +102,6 @@ export function SearchTabPayloadsLabel(props) { } const SearchTabPayloadsSearchPanel = (props) => { - const theme = useTheme(); const [search, setSearch] = React.useState(""); const searchFieldOptions = ["Filename", "Description", "UUID", "C2 Parameter Value", "Build Parameter"]; const [searchField, setSearchField] = React.useState(searchFieldOptions[0]); @@ -242,24 +235,12 @@ const SearchTabPayloadsSearchPanel = (props) => { fetchOptions(); }, []); return ( - - - - - - - , - style: {padding: 0} - }}/> - - - - - - + - - - - - - - - - + + + + } + inactiveIcon={} + /> + } + inactiveIcon={} + /> + + ); } @@ -666,6 +629,7 @@ export const SearchTabPayloadsPanel = (props) => { } } + const hasPayloadResults = payloads.length > 0; return ( {
{openBackdrop && {setOpenBackdrop(false);}} style={{zIndex: 2000, position: "absolute"}}> - + } - -
-
- - Total Results: {totalCount} + {hasPayloadResults ? ( + + ) : !openBackdrop && ( + + )}
+
) -} \ No newline at end of file +} diff --git a/MythicReactUI/src/components/pages/Search/SearchTabProcesses.js b/MythicReactUI/src/components/pages/Search/SearchTabProcesses.js index 563cc087..bb9d6bbd 100644 --- a/MythicReactUI/src/components/pages/Search/SearchTabProcesses.js +++ b/MythicReactUI/src/components/pages/Search/SearchTabProcesses.js @@ -1,20 +1,14 @@ import {MythicTabPanel, MythicSearchTabLabel} from '../../MythicComponents/MythicTabPanel'; import React from 'react'; -import MythicTextField from '../../MythicComponents/MythicTextField'; import AccountTreeIcon from '@mui/icons-material/AccountTree'; -import Grid from '@mui/material/Grid'; -import SearchIcon from '@mui/icons-material/Search'; -import Tooltip from '@mui/material/Tooltip'; -import {useTheme} from '@mui/material/styles'; -import IconButton from '@mui/material/IconButton'; import { gql, useLazyQuery} from '@apollo/client'; import { snackActions } from '../../utilities/Snackbar'; -import Pagination from '@mui/material/Pagination'; -import { Typography } from '@mui/material'; import MenuItem from '@mui/material/MenuItem'; -import Select from '@mui/material/Select'; import {ProcessTable} from "./ProcessTable"; import {useMythicLazyQuery} from "../../utilities/useMythicLazyQuery"; +import {MythicTablePagination} from "../../MythicComponents/MythicTablePagination"; +import {MythicSearchField, MythicTableToolbar, MythicTableToolbarGroup, MythicToolbarSelect} from "../../MythicComponents/MythicTableToolbar"; +import {MythicSearchEmptyState} from "../../MythicComponents/MythicStateDisplay"; const mythictreeFragment = gql` @@ -96,7 +90,6 @@ export function SearchTabProcessesLabel(props){ } const SearchTabProcessesSearchPanel = (props) => { - const theme = useTheme(); const [search, setSearch] = React.useState(""); const [searchHost, setSearchHost] = React.useState(""); const [searchField, setSearchField] = React.useState("Name"); @@ -160,26 +153,16 @@ const SearchTabProcessesSearchPanel = (props) => { } }, [props.value, props.index]); return ( - - - - - - - - - - , - style: {padding: 0} - }}/> - - - - - + + + ); } export const SearchTabProcessPanel = (props) =>{ @@ -312,14 +295,14 @@ export const SearchTabProcessPanel = (props) =>{
{processData.length > 0 ? ( ) : ( -
No Search Results
+ )}
-
- - Total Results: {totalCount} -
+ ) -} \ No newline at end of file +} diff --git a/MythicReactUI/src/components/pages/Search/SearchTabProxies.js b/MythicReactUI/src/components/pages/Search/SearchTabProxies.js index 09804809..f03b3452 100644 --- a/MythicReactUI/src/components/pages/Search/SearchTabProxies.js +++ b/MythicReactUI/src/components/pages/Search/SearchTabProxies.js @@ -1,5 +1,5 @@ import {MythicTabPanel, MythicSearchTabLabel} from '../../MythicComponents/MythicTabPanel'; -import React, { useEffect } from 'react'; +import React from 'react'; import { gql, useSubscription} from '@apollo/client'; import { snackActions } from '../../utilities/Snackbar'; import {ProxySearchTable} from './ProxySearchTable'; @@ -7,8 +7,8 @@ import {FontAwesomeIcon} from '@fortawesome/react-fontawesome'; import {faSocks} from '@fortawesome/free-solid-svg-icons'; import VisibilityOffIcon from '@mui/icons-material/VisibilityOff'; import VisibilityIcon from '@mui/icons-material/Visibility'; -import {MythicStyledTooltip} from "../../MythicComponents/MythicStyledTooltip"; -import {IconButton} from '@mui/material'; +import {MythicTableToolbar, MythicTableToolbarGroup, MythicToolbarToggle} from "../../MythicComponents/MythicTableToolbar"; +import {MythicSearchEmptyState} from "../../MythicComponents/MythicStateDisplay"; const callbackPortsSub = gql` subscription portsSub{ @@ -82,24 +82,27 @@ export const SearchTabSocksPanel = (props) =>{ return ( -
- {showDeleted ? ( - - setShowDeleted(!showDeleted)}> - - - ) : ( - - setShowDeleted(!showDeleted)} > - - )} -
+ + + setShowDeleted(!showDeleted)} + label="Stopped" + activeIcon={} + inactiveIcon={} + /> + +
{callbackData.length > 0 ? ( ) : ( -
No Search Results
+ )}
) -} \ No newline at end of file +} diff --git a/MythicReactUI/src/components/pages/Search/SearchTabTags.js b/MythicReactUI/src/components/pages/Search/SearchTabTags.js index bb531f06..ca74f2ae 100644 --- a/MythicReactUI/src/components/pages/Search/SearchTabTags.js +++ b/MythicReactUI/src/components/pages/Search/SearchTabTags.js @@ -1,20 +1,14 @@ import {MythicTabPanel, MythicSearchTabLabel} from '../../MythicComponents/MythicTabPanel'; import React from 'react'; -import MythicTextField from '../../MythicComponents/MythicTextField'; import LocalOfferIcon from '@mui/icons-material/LocalOffer'; -import Grid from '@mui/material/Grid'; -import SearchIcon from '@mui/icons-material/Search'; -import Tooltip from '@mui/material/Tooltip'; -import {useTheme} from '@mui/material/styles'; -import IconButton from '@mui/material/IconButton'; import { gql, useLazyQuery} from '@apollo/client'; import { snackActions } from '../../utilities/Snackbar'; -import Pagination from '@mui/material/Pagination'; -import { Typography } from '@mui/material'; import MenuItem from '@mui/material/MenuItem'; -import Select from '@mui/material/Select'; import {TagTable} from "./TagTable"; import {useMythicLazyQuery} from "../../utilities/useMythicLazyQuery"; +import {MythicTablePagination} from "../../MythicComponents/MythicTablePagination"; +import {MythicSearchField, MythicTableToolbar, MythicTableToolbarGroup, MythicToolbarSelect} from "../../MythicComponents/MythicTableToolbar"; +import {MythicSearchEmptyState} from "../../MythicComponents/MythicStateDisplay"; const tagFragment = gql` @@ -150,7 +144,6 @@ export function SearchTabTagsLabel(props){ } const SearchTabTagsSearchPanel = (props) => { - const theme = useTheme(); const [search, setSearch] = React.useState(""); const [searchField, setSearchField] = React.useState("TagType"); const searchFieldOptions = ["TagType", "Source", "Data"]; @@ -202,22 +195,12 @@ const SearchTabTagsSearchPanel = (props) => { } }, [props.value, props.index]); return ( - - - - - - - , - style: {padding: 0} - }}/> - - - - - + + + ); } export const SearchTabTagsPanel = (props) =>{ @@ -337,14 +320,14 @@ export const SearchTabTagsPanel = (props) =>{
{tagData.length > 0 ? ( ) : ( -
No Search Results
+ )}
-
- - Total Results: {totalCount} -
+ ) -} \ No newline at end of file +} diff --git a/MythicReactUI/src/components/pages/Search/SearchTabTasks.js b/MythicReactUI/src/components/pages/Search/SearchTabTasks.js index 0fbeb8b6..50476af6 100644 --- a/MythicReactUI/src/components/pages/Search/SearchTabTasks.js +++ b/MythicReactUI/src/components/pages/Search/SearchTabTasks.js @@ -1,23 +1,18 @@ import {MythicTabPanel, MythicSearchTabLabel} from '../../MythicComponents/MythicTabPanel'; import React from 'react'; -import MythicTextField from '../../MythicComponents/MythicTextField'; import {TaskDisplay} from '../Callbacks/TaskDisplay'; import AssignmentIcon from '@mui/icons-material/Assignment'; -import Grid from '@mui/material/Grid'; -import SearchIcon from '@mui/icons-material/Search'; -import Tooltip from '@mui/material/Tooltip'; -import {useTheme} from '@mui/material/styles'; -import IconButton from '@mui/material/IconButton'; import { gql, useQuery } from '@apollo/client'; import {taskingDataFragment} from '../Callbacks/CallbackMutations' import { snackActions } from '../../utilities/Snackbar'; -import Pagination from '@mui/material/Pagination'; import { Typography } from '@mui/material'; import MenuItem from '@mui/material/MenuItem'; -import Select from '@mui/material/Select'; import {TaskDisplayInteractiveSearch} from "./SearchTabInteractiveTasks"; import {useMythicLazyQuery} from "../../utilities/useMythicLazyQuery"; import SmartToyTwoToneIcon from '@mui/icons-material/SmartToyTwoTone'; +import {MythicTablePagination} from "../../MythicComponents/MythicTablePagination"; +import {MythicSearchField, MythicTableToolbar, MythicTableToolbarGroup, MythicToolbarSelect} from "../../MythicComponents/MythicTableToolbar"; +import {MythicSearchEmptyState} from "../../MythicComponents/MythicStateDisplay"; const fetchLimit = 50; const responseSearch = gql` @@ -147,7 +142,6 @@ export function SearchTabTasksLabel(props){ const AllOperator = {id: 0, username: "All Operators", account_type: "", active: true, deleted: false} const SearchTabTasksSearchPanel = (props) => { - const theme = useTheme(); const [search, setSearch] = React.useState(""); const [host, setHost] = React.useState(""); const [searchField, setSearchField] = React.useState("Command and Parameters"); @@ -262,30 +256,17 @@ const SearchTabTasksSearchPanel = (props) => { } }, [props.value, props.index]); return ( - - - - - - - - - - , - style: {padding: 0} - }}/> - - - - - - - - - - - + + + ); } export const SearchTabTasksPanel = (props) =>{ @@ -789,14 +769,16 @@ export const SearchTabTasksPanel = (props) =>{ )) - ) : (
No Search Results
) + ) : ( + + ) } -
- - Total Results: {totalCount} -
+ ) -} \ No newline at end of file +} diff --git a/MythicReactUI/src/components/pages/Search/SearchTabTokens.js b/MythicReactUI/src/components/pages/Search/SearchTabTokens.js index b20d5ee9..21934401 100644 --- a/MythicReactUI/src/components/pages/Search/SearchTabTokens.js +++ b/MythicReactUI/src/components/pages/Search/SearchTabTokens.js @@ -1,20 +1,14 @@ import {MythicTabPanel, MythicSearchTabLabel} from '../../MythicComponents/MythicTabPanel'; import React from 'react'; -import MythicTextField from '../../MythicComponents/MythicTextField'; import ConfirmationNumberIcon from '@mui/icons-material/ConfirmationNumber'; -import Grid from '@mui/material/Grid'; -import SearchIcon from '@mui/icons-material/Search'; -import Tooltip from '@mui/material/Tooltip'; -import {useTheme} from '@mui/material/styles'; -import IconButton from '@mui/material/IconButton'; import { gql, useLazyQuery} from '@apollo/client'; import { snackActions } from '../../utilities/Snackbar'; -import Pagination from '@mui/material/Pagination'; -import { Typography } from '@mui/material'; import {TokenTable} from './TokenTable'; import MenuItem from '@mui/material/MenuItem'; -import Select from '@mui/material/Select'; import {useMythicLazyQuery} from "../../utilities/useMythicLazyQuery"; +import {MythicTablePagination} from "../../MythicComponents/MythicTablePagination"; +import {MythicSearchField, MythicTableToolbar, MythicTableToolbarGroup, MythicToolbarSelect} from "../../MythicComponents/MythicTableToolbar"; +import {MythicSearchEmptyState} from "../../MythicComponents/MythicStateDisplay"; const tokenFragment = gql` fragment tokenData on token{ @@ -85,7 +79,6 @@ export function SearchTabTokensLabel(props){ } const SearchTabTokensSearchPanel = (props) => { - const theme = useTheme(); const [search, setSearch] = React.useState(""); const [searchField, setSearchField] = React.useState("Host"); const searchFieldOptions = ["User/Group", "SID", "Host"]; @@ -138,22 +131,12 @@ const SearchTabTokensSearchPanel = (props) => { } }, [props.value, props.index]) return ( - - - - - - - , - style: {padding: 0} - }}/> - - - - - + + + ); } export const SearchTabTokensPanel = (props) =>{ @@ -274,14 +257,14 @@ export const SearchTabTokensPanel = (props) =>{
{tokenData.length > 0 ? ( ) : ( -
No Search Results
+ )}
-
- - Total Results: {totalCount} -
+ ) -} \ No newline at end of file +} diff --git a/MythicReactUI/src/components/pages/Search/TagTable.js b/MythicReactUI/src/components/pages/Search/TagTable.js index acb217c8..68841fd6 100644 --- a/MythicReactUI/src/components/pages/Search/TagTable.js +++ b/MythicReactUI/src/components/pages/Search/TagTable.js @@ -1,5 +1,5 @@ import React, { useEffect } from 'react'; -import {Link, Button, IconButton, Typography} from '@mui/material'; +import {Link, IconButton, Typography} from '@mui/material'; import Table from '@mui/material/Table'; import TableBody from '@mui/material/TableBody'; import TableCell from '@mui/material/TableCell'; @@ -12,7 +12,6 @@ import {MythicDialog, MythicViewJSONAsTableDialog} from "../../MythicComponents/ import {MythicConfirmDialog} from '../../MythicComponents/MythicConfirmDialog'; import DeleteIcon from '@mui/icons-material/Delete'; import PlaylistAddCheckIcon from '@mui/icons-material/PlaylistAddCheck'; -import {useTheme} from '@mui/material/styles'; import {deleteTagMutation} from '../../MythicComponents/MythicTag'; import {useMutation} from '@apollo/client'; import {snackActions} from "../../utilities/Snackbar"; @@ -58,7 +57,6 @@ export function TagTable(props){ ) } function TagTableRow(props){ - const theme = useTheme(); const [openDeleteDialog, setOpenDeleteDialog] = React.useState(false); const [deleteTag] = useMutation(deleteTagMutation, { onCompleted: (data) => { @@ -76,10 +74,15 @@ function TagTableRow(props){ - {setOpenDeleteDialog(true);}} - style={{color: theme.palette.error.main}} - variant="contained"> - + + {setOpenDeleteDialog(true);}} + > + + +
@@ -216,9 +219,15 @@ function TagTableRowElement(props){ Metadata - + + setViewPermissionsDialogOpen(true)} + > + + + @@ -275,7 +284,13 @@ function TagTableRowElement(props){ File Hosting - {setOpenHostDialog(true);}} /> + {setOpenHostDialog(true);}} + > + + @@ -317,8 +332,12 @@ function TagTableRowElement(props){ UUID {props.payload.uuid} - setOpenDetailedView(true)}> - + setOpenDetailedView(true)} + size="small" + > + {openDetailedView && {props.deleted ? ( - {setOpenDeleteDialog(true);}} style={{color: theme.palette.error.main}} variant="contained"> + {setOpenDeleteDialog(true);}}> ) : ( - {setOpenDeleteDialog(true);}} style={{color: theme.palette.success.main}} variant="contained"> + {setOpenDeleteDialog(true);}}> )}
- setEditUserDialog(true)} size="small"> + setEditUserDialog(true)} size="small"> {props.user} {editUserDialog && - {setViewTokenDialog(true);}}> + {setViewTokenDialog(true);}}> {props.token_id} {viewTokenDialog && } - setEditDescriptionDialog(true)} size="small"> + setEditDescriptionDialog(true)} size="small"> {props.description} {editDescriptionDialog && ) } - diff --git a/MythicReactUI/src/components/pages/Settings/InviteLinksDialog.js b/MythicReactUI/src/components/pages/Settings/InviteLinksDialog.js index d51c3228..e825a6cf 100644 --- a/MythicReactUI/src/components/pages/Settings/InviteLinksDialog.js +++ b/MythicReactUI/src/components/pages/Settings/InviteLinksDialog.js @@ -1,6 +1,6 @@ import React from 'react'; import Button from '@mui/material/Button'; -import DialogActions from '@mui/material/DialogActions'; +import DialogContent from '@mui/material/DialogContent'; import DialogTitle from '@mui/material/DialogTitle'; import TableCell from '@mui/material/TableCell'; import TableRow from '@mui/material/TableRow'; @@ -12,20 +12,26 @@ import Typography from '@mui/material/Typography'; import {useMutation, useQuery, useLazyQuery, gql} from '@apollo/client'; import {snackActions} from "../../utilities/Snackbar"; import IconButton from '@mui/material/IconButton'; -import DeleteIcon from '@mui/icons-material/Delete'; -import RestoreFromTrashOutlinedIcon from '@mui/icons-material/RestoreFromTrashOutlined'; import {MythicStyledTooltip} from "../../MythicComponents/MythicStyledTooltip"; -import MythicStyledTableCell from "../../MythicComponents/MythicTableCell"; import MythicTextField from "../../MythicComponents/MythicTextField"; import {copyStringToClipboard} from "../../utilities/Clipboard"; import {MythicDialog} from "../../MythicComponents/MythicDialog"; -import DialogContentText from '@mui/material/DialogContentText'; import MenuItem from '@mui/material/MenuItem'; import Select from '@mui/material/Select'; -import Input from '@mui/material/Input'; import ContentCopyIcon from '@mui/icons-material/ContentCopy'; import {useTheme} from '@mui/material/styles'; import EditIcon from '@mui/icons-material/Edit'; +import FormControl from '@mui/material/FormControl'; +import InputLabel from '@mui/material/InputLabel'; +import { + MythicDialogBody, + MythicDialogButton, + MythicDialogFooter, + MythicDialogSection, + MythicFormField, + MythicFormGrid, + MythicFormNote +} from "../../MythicComponents/MythicDialogLayout"; const GetInviteLinks = gql` query getOutstandingInviteLinks { @@ -118,13 +124,15 @@ export function InviteLinksDialog(props) { return ( - Manage Outstanding Invite Links - - - +
+ Manage Outstanding Invite Links + + + +
{openInviteLinksDialog && } /> } - -
- - - Code - Name - Creator - Assigned To - Usage - Link - - - - {inviteLinks.map( l => ( - - - - {l.code} - + + + + +
+ + + Code + Name + Creator + Assigned To + Usage + Link + + + + {inviteLinks.map( l => ( + + + + {l.code} + - - {l.name} - {l.operator} - - {l.operation_id > 0 && - - {operationDataRef.current[l.operation_id]} as {l.operation_role} - - } - - - = l.total ? theme.palette.error.main : theme.palette.success.main}> - {l.used} - - - {" /"} {l.total} - - updateInviteLink(l)}> - - - - - {l.valid && - copyStringToClipboard(l.link)}> - - - } + + {l.name} + {l.operator} + + {l.operation_id > 0 && + + {operationDataRef.current[l.operation_id]} as {l.operation_role} + + } + + + = l.total ? theme.palette.error.main : theme.palette.success.main}> + {l.used} + + + {" /"} {l.total} + + updateInviteLink(l)}> + + + + + {l.valid && + copyStringToClipboard(l.link)}> + + + } - - - ))} - -
- - - - + + ); } @@ -290,80 +307,99 @@ export function CreateInviteLinksDialog(props) { return ( {props.create ? "Create" : "Update"} Invite Link - - {"Note:"} Invite links are deleted if Mythic restarts - - - - - - Invite Code Name - - - - - - Custom Invite Code - - - - - - Assign to Operation - - - - - {linkData.operation_id > 0 && - - Desired Role in Operation - + marginTop="0px" + marginBottom="0px" + /> + + + + + + + + + + + + + + Operation - - - } - - - Total Uses - - - - - -
-
- - - - + +
); } diff --git a/MythicReactUI/src/components/pages/Settings/SettingsGlobalDialog.js b/MythicReactUI/src/components/pages/Settings/SettingsGlobalDialog.js index ad207490..cf3eb5d8 100644 --- a/MythicReactUI/src/components/pages/Settings/SettingsGlobalDialog.js +++ b/MythicReactUI/src/components/pages/Settings/SettingsGlobalDialog.js @@ -1,19 +1,21 @@ import React from 'react'; -import Button from '@mui/material/Button'; -import DialogActions from '@mui/material/DialogActions'; +import Box from '@mui/material/Box'; +import DialogContent from '@mui/material/DialogContent'; import DialogTitle from '@mui/material/DialogTitle'; -import DialogContentText from '@mui/material/DialogContentText'; -import Typography from '@mui/material/Typography'; -import TableRow from '@mui/material/TableRow'; import Switch from '@mui/material/Switch'; -import Table from '@mui/material/Table'; -import TableBody from '@mui/material/TableBody'; -import TableContainer from '@mui/material/TableContainer'; import {useMutation, useQuery, gql} from '@apollo/client'; import {snackActions} from "../../utilities/Snackbar"; import MythicTextField from "../../MythicComponents/MythicTextField"; -import MythicStyledTableCell from "../../MythicComponents/MythicTableCell"; import {ResponseDisplayPlaintext} from "../Callbacks/ResponseDisplayPlaintext"; +import { + MythicDialogBody, + MythicDialogButton, + MythicDialogFooter, + MythicDialogSection, + MythicFormField, + MythicFormGrid, + MythicFormSwitchRow +} from "../../MythicComponents/MythicDialogLayout"; export const GET_GLOBAL_SETTINGS = gql` query getGlobalSettings { @@ -34,7 +36,7 @@ export function SettingsGlobalDialog(props) { const [debugAgentMessage, setDebugAgentMessage] = React.useState(false); const [allowInviteLinks, setAllowInviteLinks] = React.useState(false); const [allowWebhooksOnNewCallbacks, setAllowWebhooksOnNewCallbacks] = React.useState(true); - const [serverName, setServerName] = React.useState(false); + const [serverName, setServerName] = React.useState(""); const userPreferencesRef = React.useRef("{}"); useQuery(GET_GLOBAL_SETTINGS, {fetchPolicy: "no-cache", onCompleted: (data) => { @@ -94,66 +96,83 @@ export function SettingsGlobalDialog(props) { return ( Configure Global Settings - - - - Adjust the local server name sent as part of webhooks. This can also be configured initially by the GLOBAL_SERVER_NAME config variable before Mythic starts for the first time. - - - - - - Emit detailed agent message parsing information to the event logs. This is very noisy and can slow down the server. Also set by the MYTHIC_DEBUG_AGENT_MESSAGE config variable before Mythic starts for the first time. - - - - - - Allow Admin users to create one-time-use links that allow user creation. This can also be set by the MYTHIC_SERVER_ALLOW_INVITE_LINKS config variable before Mythic starts for the first time. - - - - - - Allow Mythic to send webhook notifications on new callbacks. This can also be set by the MYTHIC_SERVER_ALLOW_WEBHOOKS_ON_NEW_CALLBACKS config variable before Mythic starts for the first time. - - - - - - Configure user preferences for new users. This does NOT override existing user preferences, but only applies to new users created after this is updated. - - - - - -
- - - - + +
); } - diff --git a/MythicReactUI/src/components/pages/Settings/SettingsOperatorAPITokenRow.js b/MythicReactUI/src/components/pages/Settings/SettingsOperatorAPITokenRow.js index c4345c78..75278a30 100644 --- a/MythicReactUI/src/components/pages/Settings/SettingsOperatorAPITokenRow.js +++ b/MythicReactUI/src/components/pages/Settings/SettingsOperatorAPITokenRow.js @@ -11,9 +11,8 @@ export function APITokenRow(props){ {props.deleted ? null : ( - {props.onDeleteAPIToken(props.id)}} - color="error" variant="contained"> - + {props.onDeleteAPIToken(props.id)}}> + )} diff --git a/MythicReactUI/src/components/pages/Settings/SettingsOperatorDialog.js b/MythicReactUI/src/components/pages/Settings/SettingsOperatorDialog.js index 905bab0b..f44c7a39 100644 --- a/MythicReactUI/src/components/pages/Settings/SettingsOperatorDialog.js +++ b/MythicReactUI/src/components/pages/Settings/SettingsOperatorDialog.js @@ -1,9 +1,7 @@ import React from 'react'; import {Box, Checkbox, Chip, Divider, FormControlLabel, InputAdornment, TextField, Typography} from '@mui/material'; import Button from '@mui/material/Button'; -import DialogActions from '@mui/material/DialogActions'; import DialogContent from '@mui/material/DialogContent'; -import DialogContentText from '@mui/material/DialogContentText'; import DialogTitle from '@mui/material/DialogTitle'; import MythicTextField from '../../MythicComponents/MythicTextField'; import {MythicStyledTooltip} from "../../MythicComponents/MythicStyledTooltip"; @@ -11,6 +9,15 @@ import {MythicDialog} from "../../MythicComponents/MythicDialog"; import {CreateInviteLinksDialog} from "./InviteLinksDialog"; import SearchIcon from '@mui/icons-material/Search'; import {gql, useQuery} from "@apollo/client"; +import { + MythicDialogBody, + MythicDialogButton, + MythicDialogFooter, + MythicDialogSection, + MythicFormField, + MythicFormGrid, + MythicFormNote +} from "../../MythicComponents/MythicDialogLayout"; const apiTokenScopeDefinitionsQuery = gql` query apiTokenScopeDefinitionsQuery { @@ -58,13 +65,15 @@ export function SettingsOperatorDialog(props) { return ( - {props.title} - - - + + {props.title} + + + + {openInviteLinkDialog && - - Note: If you're an admin, you don't need to know a user's old password to set a new one. - -
- - {!props.userIsAdmin && - - } - - - - - + + + If you're an admin, you don't need to know a user's old password to set a new one. + + + + + + + {!props.userIsAdmin && + + + + } + + + + + + + + +
- - - - + +
); } @@ -145,27 +172,37 @@ export function SettingsBotDialog(props) { {props.title} - - Note: Bot accounts cannot log in. They can be assigned to operations and perform actions as part of workflows (once approved). - - + + + Bot accounts cannot log in. They can be assigned to operations and perform actions as part of workflows once approved. + + + + + + + + + - - - - + + ); } @@ -260,30 +297,39 @@ export function SettingsAPITokenDialog(props) { {props.title} - - - - Scopes - - - - - - - Tokens with no scopes are created with no API access. Write scopes include read access for the same resource. - + + + + + + + + + + + + + + } + > } + + - - - - + + ); } diff --git a/MythicReactUI/src/components/pages/Settings/SettingsOperatorExperimentalUIConfigDialog.js b/MythicReactUI/src/components/pages/Settings/SettingsOperatorExperimentalUIConfigDialog.js index e2a05403..1ab2d7fd 100644 --- a/MythicReactUI/src/components/pages/Settings/SettingsOperatorExperimentalUIConfigDialog.js +++ b/MythicReactUI/src/components/pages/Settings/SettingsOperatorExperimentalUIConfigDialog.js @@ -4,23 +4,22 @@ import DialogActions from '@mui/material/DialogActions'; import DialogTitle from '@mui/material/DialogTitle'; import DialogContent from '@mui/material/DialogContent'; import MythicTextField from '../../MythicComponents/MythicTextField'; -import TableCell from '@mui/material/TableCell'; -import TableRow from '@mui/material/TableRow'; -import Table from '@mui/material/Table'; -import TableBody from '@mui/material/TableBody'; -import TableContainer from '@mui/material/TableContainer'; import { GetMythicSetting, useSetMythicSetting } from "../../MythicComponents/MythicSavedUserSetting"; import {snackActions} from "../../utilities/Snackbar"; +import { + MythicDialogBody, + MythicDialogSection +} from "../../MythicComponents/MythicDialogLayout"; export function SettingsOperatorExperimentalUIConfigDialog(props) { const initialResponseStreamLimit = GetMythicSetting({setting_name: "experiment-responseStreamLimit", default_value: 50}) const [newResponseStreamLimit, setNewResponseStreamLimit] = React.useState(initialResponseStreamLimit); - const [updateSetting, _] = useSetMythicSetting(); + const [updateSetting] = useSetMythicSetting(); const onNewResponseStreamLimitChange = (name, value, error) => { setNewResponseStreamLimit(parseInt(value)); } @@ -43,30 +42,21 @@ export function SettingsOperatorExperimentalUIConfigDialog(props) { Configure Experimental UI Settings - These experimental configurations that are opt-in currently, but might become default or part of Mythic in the future. -
- This list of experimental features will change over time, so be sure to always check it out! -
- If you use any of these experimental features, please drop a comment on Mythic's GitHub or reach out on Twitter/Slack/Discord to let me know how it works for you. - + + + + +
- - - - - {"Determine how many responses to fetch per task before paginating (0 uses the safe default)"} - - - - - -
-
+ ) : ( + + setShowDeleted(!showDeleted)}> + + )} + +
+
{openNewAPIToken && ( params.row.deleted ? null : ( - {params.row.onDeleteAPIToken(params.row.id)}} - color="error" variant="contained"> - + {params.row.onDeleteAPIToken(params.row.id)}}> + ) ), @@ -514,26 +525,27 @@ const APITokenValueDialog = ({tokenValue, onClose}) => { <> Copy API Token - - This token value is only shown once. Copy it now before closing this dialog. - - + + + This token value is only shown once. Copy it now before closing this dialog. + + + - - - - + + ); } diff --git a/MythicReactUI/src/components/pages/Tags/NewTagtypesDialog.js b/MythicReactUI/src/components/pages/Tags/NewTagtypesDialog.js index 53a55c30..ad19d287 100644 --- a/MythicReactUI/src/components/pages/Tags/NewTagtypesDialog.js +++ b/MythicReactUI/src/components/pages/Tags/NewTagtypesDialog.js @@ -1,20 +1,20 @@ import React from 'react'; import Button from '@mui/material/Button'; -import DialogActions from '@mui/material/DialogActions'; import DialogContent from '@mui/material/DialogContent'; import DialogTitle from '@mui/material/DialogTitle'; import {gql, useMutation} from '@apollo/client'; import { snackActions } from '../../utilities/Snackbar'; -import Table from '@mui/material/Table'; -import TableBody from '@mui/material/TableBody'; -import TableCell from '@mui/material/TableCell'; -import TableContainer from '@mui/material/TableContainer'; -import TableHead from '@mui/material/TableHead'; -import TableRow from '@mui/material/TableRow'; -import Paper from '@mui/material/Paper'; import MythicTextField from '../../MythicComponents/MythicTextField'; -import {HexColorInput, HexColorPicker} from 'react-colorful'; -import {Typography, Box} from '@mui/material'; +import {Typography, Box, Chip} from '@mui/material'; +import { + MythicDialogBody, + MythicDialogButton, + MythicDialogFooter, + MythicDialogSection, + MythicFormField, + MythicFormGrid +} from "../../MythicComponents/MythicDialogLayout"; +import {isValidHexColor, MythicColorSwatchInput} from "../../MythicComponents/MythicColorInput"; const newTagtypeMutation = gql` mutation newTagType($name: String!, $description: String!, $color: String!) { @@ -32,17 +32,68 @@ mutation updateTagType($id: Int!, $name: String!, $description: String!, $color: } `; +const TagColorPreview = ({mode, color, label}) => { + const darkMode = mode === "dark"; + const hasValidColor = isValidHexColor(color); + const textColor = darkMode ? "#ffffff" : "#111827"; + const surfaceColor = darkMode ? "#1f2937" : "#f8fafc"; + const surfaceBorderColor = darkMode ? "rgba(255,255,255,0.16)" : "rgba(17,24,39,0.12)"; + const chipBorderColor = hasValidColor ? (darkMode ? "rgba(255,255,255,0.2)" : "rgba(17,24,39,0.16)") : surfaceBorderColor; + return ( + + + {darkMode ? "Dark mode" : "Light mode"} + + + + ); +} + export function NewTagtypesDialog(props) { const [name, setName] = React.useState(""); const [description, setDescription] = React.useState(""); - const [color, setColor] = React.useState(""); + const [color, setColor] = React.useState("#6f7ddf"); React.useEffect( () => { if(props.currentTag !== undefined){ setName(props.currentTag.name); setDescription(props.currentTag.description); - setColor(props.currentTag.color); + setColor(props.currentTag.color || "#6f7ddf"); } - }, []); + }, [props.currentTag]); const [createNewTagtype] = useMutation(newTagtypeMutation, { update: (cache, {data}) => { if(data.insert_tagtype_one.id !== undefined){ @@ -70,6 +121,8 @@ export function NewTagtypesDialog(props) { snackActions.warning("Must supply a name"); } else if(description === ""){ snackActions.warning("Must supply a description"); + } else if(color !== "" && !isValidHexColor(color)){ + snackActions.warning("Must supply a valid hex color"); } else { if(props.currentTag !== undefined){ updateTagtype({variables: {name, description, color, id: props.currentTag.id}}); @@ -85,56 +138,68 @@ export function NewTagtypesDialog(props) { const onChangeDescription = (name, value, error) => { setDescription(value); } + const hasValidColor = isValidHexColor(color); + const title = props.currentTag === undefined ? "Create Tag Type" : "Edit Tag Type"; return ( - Create a new type of tag - - - - - - Field - Value - - - - - Name - - - - - - Description - - - - - - Color - - - - - - {"Sample Text"} - - - - - -
-
- - + + } + > + + + + + + + + + Close - - - + +
); } - diff --git a/MythicReactUI/src/components/pages/Tags/TagtypesTable.js b/MythicReactUI/src/components/pages/Tags/TagtypesTable.js index c459dfd0..6834f77f 100644 --- a/MythicReactUI/src/components/pages/Tags/TagtypesTable.js +++ b/MythicReactUI/src/components/pages/Tags/TagtypesTable.js @@ -5,21 +5,19 @@ import TableCell from '@mui/material/TableCell'; import TableContainer from '@mui/material/TableContainer'; import TableHead from '@mui/material/TableHead'; import TableRow from '@mui/material/TableRow'; -import Paper from '@mui/material/Paper'; -import Button from '@mui/material/Button'; -import Typography from '@mui/material/Typography'; import { TagtypesTableRow } from './TagtypesTableRow'; -import {useTheme} from '@mui/material/styles'; import { MythicDialog } from '../../MythicComponents/MythicDialog'; import {NewTagtypesDialog} from './NewTagtypesDialog'; -import {gql, useMutation, useLazyQuery} from '@apollo/client'; +import {gql, useMutation} from '@apollo/client'; import { snackActions } from '../../utilities/Snackbar'; import FileUploadIcon from '@mui/icons-material/FileUpload'; import FileDownloadIcon from '@mui/icons-material/FileDownload'; import AddCircleIcon from '@mui/icons-material/AddCircle'; import {downloadFileFromMemory} from '../../utilities/Clipboard'; import {useMythicLazyQuery} from "../../utilities/useMythicLazyQuery"; -import {MythicPageHeader} from "../../MythicComponents/MythicPageHeader"; +import {MythicPageHeader, MythicPageHeaderChip} from "../../MythicComponents/MythicPageHeader"; +import {MythicToolbarButton} from "../../MythicComponents/MythicTableToolbar"; +import {MythicTableEmptyState} from "../../MythicComponents/MythicStateDisplay"; const importTagtypesMutation = gql` mutation importMultipleTagtypes($tagtypes: String!) { @@ -41,7 +39,6 @@ query getAllTagTypes { `; export function TagtypesTable({tagtypes, onDeleteTagtype, onNewTag, onUpdateTagtype, me}){ - const theme = useTheme(); const fileValue = React.useRef(""); const [openNewDialog, setOpenNewDialog] = React.useState(false); const [importTagTypes] = useMutation(importTagtypesMutation, { @@ -83,27 +80,41 @@ export function TagtypesTable({tagtypes, onDeleteTagtype, onNewTag, onUpdateTagt exportTagtypes({variables: {operation_id: me?.user?.current_operation_id || 0}}) .then(({data}) => exportTagTypesSuccess(data)).catch(({data}) => exportTagTypesError(data)); } + const tagTypeCountLabel = tagtypes.length === 1 ? "1 type" : `${tagtypes.length} types`; + const tagInstanceCount = tagtypes.reduce((total, tagtype) => total + (tagtype.tags_aggregate?.aggregate?.count || 0), 0); + const tagInstanceCountLabel = tagInstanceCount === 1 ? "1 tag" : `${tagInstanceCount} tags`; return ( <> - - - - - {openNewDialog && - {setOpenNewDialog(false);}} - innerDialog={{setOpenNewDialog(false);}} onSubmit={onNewTag} />} - /> + + + + } - + actions={ + <> + }> + Import + + + }> + Export + + setOpenNewDialog(true)} startIcon={}> + Tag Type + + + } + /> + {openNewDialog && + {setOpenNewDialog(false);}} + innerDialog={{setOpenNewDialog(false);}} onSubmit={onNewTag} />} + /> + }
@@ -117,7 +128,14 @@ export function TagtypesTable({tagtypes, onDeleteTagtype, onNewTag, onUpdateTagt - + {tagtypes.length === 0 && + + } {tagtypes.map( (op) => ( ) } - diff --git a/MythicReactUI/src/components/pages/Tags/TagtypesTableRow.js b/MythicReactUI/src/components/pages/Tags/TagtypesTableRow.js index b422f140..c686357e 100644 --- a/MythicReactUI/src/components/pages/Tags/TagtypesTableRow.js +++ b/MythicReactUI/src/components/pages/Tags/TagtypesTableRow.js @@ -8,7 +8,7 @@ import { MythicStyledTooltip } from '../../MythicComponents/MythicStyledTooltip' import MythicStyledTableCell from '../../MythicComponents/MythicTableCell'; import {NewTagtypesDialog} from './NewTagtypesDialog'; import Chip from '@mui/material/Chip'; -import SettingsIcon from '@mui/icons-material/Settings'; +import EditIcon from '@mui/icons-material/Edit'; export function TagtypesTableRow(props){ @@ -27,7 +27,7 @@ export function TagtypesTableRow(props){ - {setOpenDeleteDialog(true);}} color="error" variant="contained"> + {setOpenDeleteDialog(true);}}> {openDelete && @@ -36,7 +36,9 @@ export function TagtypesTableRow(props){ - {setOpenUpdateDialog(true);}} variant="contained"> + {setOpenUpdateDialog(true);}}> + + {openUpdate && {setOpenUpdateDialog(false);}} @@ -56,4 +58,3 @@ export function TagtypesTableRow(props){ ) } - diff --git a/MythicReactUI/src/themes/GlobalStyles.js b/MythicReactUI/src/themes/GlobalStyles.js index 56954990..10548a3e 100644 --- a/MythicReactUI/src/themes/GlobalStyles.js +++ b/MythicReactUI/src/themes/GlobalStyles.js @@ -1,4 +1,5 @@ import { createGlobalStyle} from "styled-components" +import {alpha} from "@mui/material/styles"; // hex transparencies https://gist.github.com/lopspower/03fb1cc0ac9f32ef38f4 export const GlobalStyles = createGlobalStyle` body { @@ -68,7 +69,16 @@ html, body, #root { border-top: 0; border-bottom: 1px solid ${(props) => props.theme.table?.border || props.theme.borderColor}; } -tr:nth-child(even) { +.MuiTableContainer-root.mythicElement { + border: 1px solid ${(props) => props.theme.table?.border || props.theme.borderColor}; + border-radius: ${(props) => props.theme.shape.borderRadius}px; + background-color: ${(props) => props.theme.palette.background.paper}; + min-height: 0; +} +.MuiTableContainer-root.mythicElement .MuiTableContainer-root.mythicElement { + border-color: ${(props) => props.theme.table?.borderSoft || props.theme.borderColor}; +} +.MuiTableBody-root > .MuiTableRow-root:nth-of-type(even):not(.Mui-selected):not(.selectedCallback):not(.selectedCallbackHierarchy) { background-color: ${(props) => props.theme.table?.rowStripe || props.theme.tableHover + "66"}; } .alternateRow { @@ -360,7 +370,7 @@ tspan { padding: 0 0 0 5px; min-height: 2rem; } -.MuiPaper-root > .MuiTableContainer-root { +.MuiPaper-root > .MuiTableContainer-root.mythicElement { display: flex; flex-grow: 1; } @@ -408,9 +418,3067 @@ tspan { .MuiTableCell-root { padding: 6px 10px; border-bottom: 1px solid ${(props) => props.theme.table?.borderSoft || props.theme.borderColor}; + font-variant-numeric: tabular-nums; line-height: 1.35; vertical-align: middle; } +.mythic-table-footer { + background: transparent; + display: flex; + justify-content: center; + align-items: center; + flex-wrap: wrap; + gap: 0.5rem; + padding: 0.5rem 0 0.75rem; +} +.mythic-table-footer .MuiPagination-root { + padding: 0; +} +.mythic-table-total { + color: ${(props) => props.theme.palette.text.secondary}; + font-size: 0.82rem; + font-weight: 650; + padding-left: 0; + white-space: nowrap; +} +.mythic-table-empty { + color: ${(props) => props.theme.palette.text.secondary}; + display: flex; + align-items: center; + justify-content: center; + height: 100%; + min-height: 8rem; + margin: 0.5rem; + padding: 1rem; + text-align: center; + width: 100%; + background-color: ${(props) => props.theme.palette.mode === "dark" ? "rgba(255,255,255,0.03)" : "rgba(0,0,0,0.018)"}; + border: 1px solid ${(props) => props.theme.table?.borderSoft || props.theme.borderColor}; + border-radius: ${(props) => props.theme.shape.borderRadius}px; + font-size: 0.86rem; + font-weight: 750; +} +.mythic-table-toolbar { + align-items: center; + background-color: ${(props) => props.theme.surfaces?.muted || props.theme.palette.background.paper}; + border: 1px solid ${(props) => props.theme.table?.borderSoft || props.theme.borderColor}; + border-radius: ${(props) => props.theme.shape.borderRadius}px; + display: flex; + flex: 0 0 auto; + flex-wrap: wrap; + gap: 0.5rem; + height: auto; + max-height: none; + min-height: fit-content; + overflow: visible; + padding: 0.5rem; + width: 100%; +} +.mythic-table-toolbar-group { + align-items: center; + display: flex; + flex: 0 1 auto; + flex-wrap: wrap; + gap: 0.4rem; + max-width: 100%; + min-width: min(100%, 9rem); +} +.mythic-table-toolbar-group-grow { + flex: 1 1 20rem; + min-width: min(100%, 16rem); +} +.mythic-table-toolbar-group .MuiFormControl-root, +.mythic-table-toolbar-group .MythicTextField-root { + margin: 0; + min-width: 0; +} +.mythic-toolbar-select { + min-height: 32px; + min-width: min(100%, 10rem); + width: 100%; +} +.mythic-toolbar-button { + white-space: nowrap; +} +.mythic-toolbar-button-hover-success { + background-color: ${(props) => props.theme.palette.mode === "dark" ? "rgba(255,255,255,0.06)" : "rgba(0,0,0,0.035)"} !important; + border: 1px solid ${(props) => props.theme.table?.borderSoft || props.theme.borderColor} !important; + border-radius: ${(props) => props.theme.shape.borderRadius}px !important; + box-shadow: none !important; + color: ${(props) => props.theme.palette.text.primary} !important; + font-weight: 750; + min-height: 32px; + text-transform: none; +} +.mythic-toolbar-button-hover-success:hover { + background-color: ${(props) => props.theme.palette.success.main + "2b"} !important; + border-color: ${(props) => props.theme.palette.success.main + "88"} !important; + color: ${(props) => props.theme.palette.success.main} !important; +} +.mythic-toolbar-toggle { + white-space: nowrap; +} +.mythic-toolbar-icon-button { + margin-right: 2px; +} +.mythic-dialog-body { + display: flex; + flex-direction: column; + gap: 0.75rem; + min-width: 0; + width: 100%; +} +.mythic-dialog-body-compact { + gap: 0.55rem; +} +.mythic-dialog-title-row { + align-items: center; + display: flex; + flex-wrap: wrap; + gap: 0.65rem; + justify-content: space-between; + min-width: 0; +} +.mythic-dialog-title-action { + background-color: ${(props) => props.theme.palette.mode === "dark" ? "rgba(255,255,255,0.06)" : "rgba(0,0,0,0.035)"} !important; + border: 1px solid ${(props) => props.theme.table?.borderSoft || props.theme.borderColor} !important; + border-radius: ${(props) => props.theme.shape.borderRadius}px; + box-shadow: none !important; + color: ${(props) => props.theme.palette.text.primary} !important; + font-size: 0.76rem; + font-weight: 750; + min-height: 30px; + text-transform: none; +} +.mythic-dialog-title-select { + background-color: ${(props) => props.theme.palette.mode === "dark" ? "rgba(255,255,255,0.06)" : "rgba(0,0,0,0.035)"}; + border-radius: ${(props) => props.theme.shape.borderRadius}px; + min-width: 11rem; +} +.mythic-detail-section-header { + align-items: center; + background-color: ${(props) => props.theme.pageHeader.main}; + background-image: ${(props) => { + const headerTextColor = props.theme.pageHeaderText?.main || props.theme.palette.text.primary; + return `linear-gradient(90deg, ${alpha(props.theme.palette.primary.main, props.theme.palette.mode === "dark" ? 0.28 : 0.18)} 0%, ${alpha(props.theme.palette.primary.main, props.theme.palette.mode === "dark" ? 0.12 : 0.08)} 48%, ${alpha(headerTextColor, props.theme.palette.mode === "dark" ? 0.055 : 0.04)} 100%)`; + }}; + border: 1px solid ${(props) => alpha(props.theme.palette.primary.main, props.theme.palette.mode === "dark" ? 0.55 : 0.38)}; + border-radius: ${(props) => props.theme.shape.borderRadius}px; + box-shadow: inset 0 1px 0 ${(props) => alpha(props.theme.pageHeaderText?.main || props.theme.palette.text.primary, 0.22)}, 0 2px 6px ${(props) => alpha(props.theme.palette.common.black, props.theme.palette.mode === "dark" ? 0.28 : 0.12)}; + color: ${(props) => props.theme.pageHeaderText?.main || props.theme.palette.text.primary}; + display: flex; + flex-wrap: wrap; + gap: 1rem; + justify-content: space-between; + margin: 0.75rem 0 0.5rem; + min-height: 58px; + min-width: 0; + overflow: hidden; + padding: 0.7rem 0.85rem 0.7rem 1rem; + position: relative; + width: 100%; +} +.mythic-detail-section-header::before { + background-color: ${(props) => props.theme.palette.primary.main}; + bottom: 0; + box-shadow: 0 0 0 1px ${(props) => alpha(props.theme.pageHeaderText?.main || props.theme.palette.text.primary, 0.2)}; + content: ""; + left: 0; + position: absolute; + top: 0; + width: 6px; +} +.mythic-section-header { + background-image: ${(props) => { + const headerTextColor = props.theme.pageHeaderText?.main || props.theme.palette.text.primary; + return `linear-gradient(90deg, ${alpha(props.theme.palette.primary.main, props.theme.palette.mode === "dark" ? 0.28 : 0.18)} 0%, ${alpha(props.theme.palette.primary.main, props.theme.palette.mode === "dark" ? 0.12 : 0.08)} 48%, ${alpha(headerTextColor, props.theme.palette.mode === "dark" ? 0.055 : 0.04)} 100%)`; + }} !important; + border-color: ${(props) => alpha(props.theme.palette.primary.main, props.theme.palette.mode === "dark" ? 0.55 : 0.38)} !important; + box-shadow: inset 0 1px 0 ${(props) => alpha(props.theme.pageHeaderText?.main || props.theme.palette.text.primary, 0.22)}, 0 2px 6px ${(props) => alpha(props.theme.palette.common.black, props.theme.palette.mode === "dark" ? 0.28 : 0.12)} !important; + overflow: hidden !important; + padding-left: 1rem !important; + position: relative !important; +} +.mythic-section-header::before { + background-color: ${(props) => props.theme.palette.primary.main}; + bottom: 0; + box-shadow: 0 0 0 1px ${(props) => alpha(props.theme.pageHeaderText?.main || props.theme.palette.text.primary, 0.2)}; + content: ""; + left: 0; + position: absolute; + top: 0; + width: 6px; +} +.mythic-detail-section-title { + color: ${(props) => props.theme.pageHeaderText?.main || props.theme.palette.text.primary}; + font-size: 1.25rem; + font-weight: 750; + letter-spacing: 0; + line-height: 1.18; +} +.mythic-detail-section-actions { + align-items: center; + display: flex; + flex: 0 0 auto; + flex-wrap: wrap; + gap: 0.45rem; +} +.mythic-detail-section-header .MuiButton-root, +.mythic-detail-section-header .MuiIconButton-root { + background-color: ${(props) => (props.theme.pageHeaderText?.main || props.theme.palette.text.primary) + "1a"} !important; + border: 1px solid ${(props) => (props.theme.pageHeaderText?.main || props.theme.palette.text.primary) + "3d"} !important; + border-radius: ${(props) => props.theme.shape.borderRadius}px !important; + box-shadow: none !important; + color: ${(props) => props.theme.pageHeaderText?.main || props.theme.palette.text.primary} !important; + font-size: 0.76rem; + font-weight: 750; + min-height: 32px; + text-transform: none; +} +.mythic-detail-section-header .MuiButton-root:hover, +.mythic-detail-section-header .MuiIconButton-root:hover { + background-color: ${(props) => (props.theme.pageHeaderText?.main || props.theme.palette.text.primary) + "29"} !important; + border-color: ${(props) => (props.theme.pageHeaderText?.main || props.theme.palette.text.primary) + "6b"} !important; +} +.mythic-dialog-section { + background-color: ${(props) => props.theme.surfaces?.muted || props.theme.palette.background.paper}; + border: 1px solid ${(props) => props.theme.table?.borderSoft || props.theme.borderColor}; + border-radius: ${(props) => props.theme.shape.borderRadius}px; + min-width: 0; + padding: 0.75rem; +} +.mythic-dialog-section-header { + align-items: flex-start; + display: flex; + gap: 0.75rem; + justify-content: space-between; + margin-bottom: 0.65rem; + min-width: 0; +} +.mythic-dialog-section-title { + color: ${(props) => props.theme.palette.text.primary}; + font-size: 0.86rem; + font-weight: 700; + line-height: 1.25; +} +.mythic-dialog-section-description { + color: ${(props) => props.theme.palette.text.secondary}; + font-size: 0.78rem; + line-height: 1.35; + margin-top: 0.12rem; +} +.mythic-dialog-section-actions { + align-items: center; + display: flex; + flex: 0 0 auto; + gap: 0.4rem; +} +.mythic-dialog-section-actions .MuiButton-root { + background-color: ${(props) => props.theme.palette.mode === "dark" ? "rgba(255,255,255,0.06)" : "rgba(0,0,0,0.035)"} !important; + border: 1px solid ${(props) => props.theme.table?.borderSoft || props.theme.borderColor} !important; + border-radius: ${(props) => props.theme.shape.borderRadius}px; + color: ${(props) => props.theme.palette.text.primary} !important; + font-size: 0.74rem; + font-weight: 750; + min-height: 28px; + text-transform: none; +} +.mythic-dialog-section-actions .MuiButton-root:hover { + background-color: ${(props) => props.theme.palette.mode === "dark" ? "rgba(255,255,255,0.1)" : "rgba(0,0,0,0.06)"} !important; +} +.mythic-reorder-list { + display: flex; + flex: 1 1 auto; + flex-direction: column; + gap: 0.45rem; + max-height: 100%; + min-height: 0; + overflow: auto; + padding: 0.1rem; +} +.mythic-reorder-row { + align-items: center; + background-color: ${(props) => props.theme.surfaces?.raised || props.theme.palette.background.paper}; + border: 1px solid ${(props) => props.theme.table?.borderSoft || props.theme.borderColor}; + border-radius: ${(props) => props.theme.shape.borderRadius}px; + box-shadow: none; + color: ${(props) => props.theme.palette.text.primary}; + display: flex; + flex: 0 0 auto; + gap: 0.5rem; + min-height: 44px; + min-width: 0; + padding: 0.45rem 0.55rem; + transition: background-color 120ms ease, border-color 120ms ease, box-shadow 120ms ease; + width: 100%; +} +.mythic-reorder-row:hover { + background-color: ${(props) => props.theme.surfaces?.hover || props.theme.palette.action.hover}; + border-color: ${(props) => props.theme.table?.border || props.theme.borderColor}; +} +.mythic-reorder-row-dragging { + background-color: ${(props) => props.theme.palette.primary.main + "1f"}; + border-color: ${(props) => props.theme.palette.primary.main + "88"}; + box-shadow: ${(props) => props.theme.palette.mode === "dark" ? "0 12px 28px rgba(0,0,0,0.32)" : "0 12px 28px rgba(15,23,42,0.14)"}; +} +.mythic-reorder-row-disabled { + background-color: ${(props) => props.theme.palette.mode === "dark" ? "rgba(255,255,255,0.025)" : "rgba(0,0,0,0.018)"}; +} +.mythic-reorder-row-disabled .mythic-reorder-row-title { + color: ${(props) => props.theme.palette.text.secondary}; +} +.mythic-reorder-drag-handle { + align-items: center; + background-color: ${(props) => props.theme.palette.mode === "dark" ? "rgba(255,255,255,0.05)" : "rgba(0,0,0,0.03)"}; + border: 1px solid ${(props) => props.theme.table?.borderSoft || props.theme.borderColor}; + border-radius: ${(props) => props.theme.shape.borderRadius}px; + color: ${(props) => props.theme.palette.text.secondary}; + cursor: grab; + display: inline-flex; + flex: 0 0 30px; + height: 30px; + justify-content: center; + width: 30px; +} +.mythic-reorder-drag-handle:active { + cursor: grabbing; +} +.mythic-reorder-row-main { + align-items: center; + display: flex; + flex: 1 1 auto; + gap: 0.5rem; + min-width: 0; +} +.mythic-reorder-row-title { + font-size: 0.82rem; + font-weight: 750; + line-height: 1.25; + min-width: 0; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} +.mythic-reorder-row-actions { + align-items: center; + display: flex; + flex: 0 0 auto; + gap: 0.35rem; +} +.mythic-reorder-select { + flex: 1 1 auto; + min-width: 0; +} +.mythic-reorder-select .MuiSelect-select { + min-height: 30px; + padding-bottom: 0.25rem; + padding-top: 0.25rem; +} +.mythic-parameter-list { + display: grid; + gap: 0.65rem; + min-width: 0; +} +.mythic-parameter-card { + background-color: ${(props) => props.theme.surfaces?.raised || props.theme.palette.background.paper}; + border: 1px solid ${(props) => props.theme.table?.borderSoft || props.theme.borderColor}; + border-radius: ${(props) => props.theme.shape.borderRadius}px; + min-width: 0; + padding: 0.75rem; +} +.mythic-parameter-card-header { + align-items: flex-start; + display: flex; + gap: 0.75rem; + justify-content: space-between; + min-width: 0; +} +.mythic-parameter-title { + color: ${(props) => props.theme.palette.text.primary}; + font-size: 0.9rem; + font-weight: 800; + line-height: 1.25; + min-width: 0; + overflow-wrap: anywhere; +} +.mythic-parameter-description { + color: ${(props) => props.theme.palette.text.secondary}; + font-size: 0.8rem; + line-height: 1.4; + margin-top: 0.25rem; + overflow-wrap: anywhere; +} +.mythic-metadata-grid { + display: grid; + gap: 0.5rem; + grid-template-columns: repeat(auto-fit, minmax(min(100%, 13rem), 1fr)); + margin-top: 0.65rem; + min-width: 0; +} +.mythic-metadata-item { + background-color: ${(props) => props.theme.palette.mode === "dark" ? "rgba(255,255,255,0.035)" : "rgba(0,0,0,0.025)"}; + border: 1px solid ${(props) => props.theme.table?.borderSoft || props.theme.borderColor}; + border-radius: ${(props) => props.theme.shape.borderRadius}px; + min-width: 0; + padding: 0.5rem 0.55rem; +} +.mythic-metadata-label { + color: ${(props) => props.theme.palette.text.secondary}; + display: block; + font-size: 0.68rem; + font-weight: 800; + line-height: 1.2; + text-transform: uppercase; +} +.mythic-metadata-value, +.mythic-metadata-code { + color: ${(props) => props.theme.palette.text.primary}; + display: block; + font-size: 0.78rem; + line-height: 1.35; + margin-top: 0.18rem; + min-width: 0; + overflow-wrap: anywhere; + white-space: pre-wrap; +} +.mythic-metadata-code { + font-family: ${(props) => props.theme.typography.fontFamilyMono || "monospace"}; +} +.mythic-parameter-notes { + display: grid; + gap: 0.5rem; + margin-top: 0.65rem; +} +.mythic-parameter-note { + background-color: ${(props) => props.theme.palette.info.main + "12"}; + border: 1px solid ${(props) => props.theme.palette.info.main + "40"}; + border-radius: ${(props) => props.theme.shape.borderRadius}px; + color: ${(props) => props.theme.palette.text.secondary}; + font-size: 0.78rem; + line-height: 1.4; + padding: 0.55rem 0.65rem; +} +.mythic-parameter-note strong { + color: ${(props) => props.theme.palette.text.primary}; +} +.mythic-code-block { + background-color: ${(props) => props.theme.palette.mode === "dark" ? "rgba(0,0,0,0.36)" : "rgba(15,23,42,0.06)"}; + border: 1px solid ${(props) => props.theme.table?.borderSoft || props.theme.borderColor}; + border-radius: ${(props) => props.theme.shape.borderRadius}px; + color: ${(props) => props.theme.palette.text.primary}; + display: block; + font-family: ${(props) => props.theme.typography.fontFamilyMono || "monospace"}; + font-size: 0.76rem; + line-height: 1.45; + margin: 0.45rem 0 0; + overflow: auto; + padding: 0.55rem 0.65rem; + white-space: pre; +} +.MuiDialogActions-root, +.mythic-dialog-actions { + align-items: center; + background-color: ${(props) => props.theme.surfaces?.muted || props.theme.palette.background.paper}; + border-top: 1px solid ${(props) => props.theme.table?.borderSoft || props.theme.borderColor}; + display: flex; + flex-wrap: wrap; + gap: 0.5rem; + justify-content: flex-end; + margin: 0; + padding: 0.65rem 0.9rem; +} +.MuiDialogActions-root > :not(style) ~ :not(style) { + margin-left: 0; +} +.MuiDialogActions-root .MuiButton-root, +.mythic-dialog-actions .MuiButton-root { + align-items: center; + background-color: ${(props) => props.theme.palette.mode === "dark" ? "rgba(255,255,255,0.06)" : "rgba(0,0,0,0.035)"} !important; + border: 1px solid ${(props) => props.theme.table?.borderSoft || props.theme.borderColor} !important; + border-radius: ${(props) => props.theme.shape.borderRadius}px; + box-shadow: none !important; + color: ${(props) => props.theme.palette.text.primary} !important; + font-size: 0.78rem; + font-weight: 750; + justify-content: center; + letter-spacing: 0; + line-height: 1.2; + min-height: 34px; + min-width: min(100%, 7rem); + padding: 0.35rem 0.85rem; + text-transform: none; + white-space: nowrap; +} +.MuiDialogActions-root .MuiButton-root:hover, +.mythic-dialog-actions .MuiButton-root:hover { + background-color: ${(props) => props.theme.palette.mode === "dark" ? "rgba(255,255,255,0.1)" : "rgba(0,0,0,0.06)"} !important; + border-color: ${(props) => props.theme.table?.border || props.theme.borderColor} !important; +} +.MuiDialogActions-root .MuiButton-root.MuiButton-colorSuccess, +.MuiDialogActions-root .MuiButton-root.MuiButton-containedSuccess, +.MuiDialogActions-root .MuiButton-root.MuiButton-outlinedSuccess, +.MuiDialogActions-root .mythic-dialog-button-primary, +.mythic-dialog-actions .MuiButton-root.MuiButton-colorSuccess, +.mythic-dialog-actions .MuiButton-root.MuiButton-containedSuccess, +.mythic-dialog-actions .MuiButton-root.MuiButton-outlinedSuccess, +.mythic-dialog-actions .mythic-dialog-button-primary { + background-color: ${(props) => props.theme.palette.primary.main} !important; + border-color: ${(props) => props.theme.palette.primary.main} !important; + color: ${(props) => props.theme.palette.primary.contrastText} !important; +} +.MuiDialogActions-root .MuiButton-root.MuiButton-colorSuccess:hover, +.MuiDialogActions-root .MuiButton-root.MuiButton-containedSuccess:hover, +.MuiDialogActions-root .MuiButton-root.MuiButton-outlinedSuccess:hover, +.MuiDialogActions-root .mythic-dialog-button-primary:hover, +.mythic-dialog-actions .MuiButton-root.MuiButton-colorSuccess:hover, +.mythic-dialog-actions .MuiButton-root.MuiButton-containedSuccess:hover, +.mythic-dialog-actions .MuiButton-root.MuiButton-outlinedSuccess:hover, +.mythic-dialog-actions .mythic-dialog-button-primary:hover { + background-color: ${(props) => props.theme.palette.primary.dark || props.theme.palette.primary.main} !important; + border-color: ${(props) => props.theme.palette.primary.dark || props.theme.palette.primary.main} !important; +} +.MuiDialogActions-root .MuiButton-root.MuiButton-colorWarning, +.MuiDialogActions-root .MuiButton-root.MuiButton-containedWarning, +.MuiDialogActions-root .MuiButton-root.MuiButton-outlinedWarning, +.MuiDialogActions-root .mythic-dialog-button-warning, +.mythic-dialog-actions .MuiButton-root.MuiButton-colorWarning, +.mythic-dialog-actions .MuiButton-root.MuiButton-containedWarning, +.mythic-dialog-actions .MuiButton-root.MuiButton-outlinedWarning, +.mythic-dialog-actions .mythic-dialog-button-warning { + background-color: ${(props) => props.theme.palette.warning.main + "22"} !important; + border-color: ${(props) => props.theme.palette.warning.main + "88"} !important; + color: ${(props) => props.theme.palette.warning.main} !important; +} +.MuiDialogActions-root .MuiButton-root.MuiButton-colorError, +.MuiDialogActions-root .MuiButton-root.MuiButton-containedError, +.MuiDialogActions-root .MuiButton-root.MuiButton-outlinedError, +.MuiDialogActions-root .mythic-dialog-button-destructive, +.mythic-dialog-actions .MuiButton-root.MuiButton-colorError, +.mythic-dialog-actions .MuiButton-root.MuiButton-containedError, +.mythic-dialog-actions .MuiButton-root.MuiButton-outlinedError, +.mythic-dialog-actions .mythic-dialog-button-destructive { + background-color: ${(props) => props.theme.palette.error.main + "22"} !important; + border-color: ${(props) => props.theme.palette.error.main + "99"} !important; + color: ${(props) => props.theme.palette.error.main} !important; +} +.MuiDialogActions-root .MuiButton-root.MuiButton-colorInfo, +.MuiDialogActions-root .MuiButton-root.MuiButton-containedInfo, +.MuiDialogActions-root .MuiButton-root.MuiButton-outlinedInfo, +.MuiDialogActions-root .mythic-dialog-button-info, +.mythic-dialog-actions .MuiButton-root.MuiButton-colorInfo, +.mythic-dialog-actions .MuiButton-root.MuiButton-containedInfo, +.mythic-dialog-actions .MuiButton-root.MuiButton-outlinedInfo, +.mythic-dialog-actions .mythic-dialog-button-info { + background-color: ${(props) => props.theme.palette.info.main + "1c"} !important; + border-color: ${(props) => props.theme.palette.info.main + "66"} !important; + color: ${(props) => props.theme.palette.info.main} !important; +} +.MuiDialogActions-root .MuiButton-root.Mui-disabled, +.mythic-dialog-actions .MuiButton-root.Mui-disabled { + background-color: ${(props) => props.theme.palette.mode === "dark" ? "rgba(255,255,255,0.03)" : "rgba(0,0,0,0.02)"} !important; + border-color: ${(props) => props.theme.table?.borderSoft || props.theme.borderColor} !important; + color: ${(props) => props.theme.palette.text.disabled} !important; +} +.mythic-dialog-grid { + align-items: start; + display: grid; + gap: 0.65rem; + grid-template-columns: repeat(auto-fit, minmax(min(100%, var(--mythic-dialog-grid-min, 16rem)), 1fr)); + min-width: 0; + width: 100%; +} +.mythic-dialog-choice-row { + align-items: center; + display: grid; + gap: 0.55rem; + grid-template-columns: minmax(0, 1fr) auto minmax(0, 1fr); + width: 100%; +} +.mythic-dialog-choice-divider { + color: ${(props) => props.theme.palette.text.secondary}; + font-size: 0.72rem; + font-weight: 700; + text-align: center; +} +.mythic-form { + display: flex; + flex-direction: column; + gap: 0.75rem; + min-width: 0; + width: 100%; +} +.mythic-form-grid { + align-items: start; + display: grid; + gap: 0.75rem; + grid-template-columns: repeat(auto-fit, minmax(min(100%, var(--mythic-form-grid-min, 16rem)), 1fr)); + min-width: 0; + width: 100%; +} +.mythic-form-field { + display: flex; + flex-direction: column; + gap: 0.35rem; + min-width: 0; +} +.mythic-form-field-copy { + min-width: 0; +} +.mythic-form-field-label { + color: ${(props) => props.theme.palette.text.primary}; + display: block; + font-size: 0.78rem; + font-weight: 750; + letter-spacing: 0; + line-height: 1.25; +} +.mythic-form-field-required { + color: ${(props) => props.theme.palette.error.main}; +} +.mythic-form-field-description { + color: ${(props) => props.theme.palette.text.secondary}; + font-size: 0.74rem; + line-height: 1.35; + margin-top: 0.1rem; +} +.mythic-form-field-control { + min-width: 0; + width: 100%; +} +.mythic-form-field-control .MuiFormControl-root, +.mythic-form-field-control .MuiTextField-root, +.mythic-form .MuiFormControl-root, +.mythic-form .MuiTextField-root { + margin: 0 !important; +} +.mythic-form-field-control .MuiInputBase-root, +.mythic-form .MuiInputBase-root { + min-height: 38px; +} +.mythic-form-note { + background-color: ${(props) => props.theme.palette.mode === "dark" ? "rgba(255,255,255,0.045)" : "rgba(0,0,0,0.025)"}; + border: 1px solid ${(props) => props.theme.table?.borderSoft || props.theme.borderColor}; + border-radius: ${(props) => props.theme.shape.borderRadius}px; + color: ${(props) => props.theme.palette.text.secondary}; + font-size: 0.8rem; + line-height: 1.4; + padding: 0.65rem 0.75rem; +} +.mythic-form-switch-row { + align-items: center; + background-color: ${(props) => props.theme.palette.background.paper}; + border: 1px solid ${(props) => props.theme.table?.borderSoft || props.theme.borderColor}; + border-radius: ${(props) => props.theme.shape.borderRadius}px; + display: flex; + gap: 0.75rem; + justify-content: space-between; + min-width: 0; + padding: 0.65rem 0.75rem; + width: 100%; +} +.mythic-form-switch-control { + flex: 0 0 auto; +} +.mythic-dashboard-content { + display: flex; + flex-direction: column; + gap: 0.75rem; + height: 100%; + min-height: 0; + overflow-y: auto; + padding: 0.75rem 0 0.9rem; + position: relative; + width: 100%; +} +.mythic-dashboard-custom { + display: flex; + flex-direction: column; + gap: 0.75rem; + height: 100%; + min-height: 0; + overflow-y: auto; + width: 100%; +} +.mythic-dashboard-row { + align-items: stretch; + display: flex; + flex: 0 0 auto; + flex-wrap: wrap; + gap: 0.75rem; + min-width: 0; + padding: 0 0.75rem; + width: 100%; +} +.mythic-dashboard-card { + background-color: ${(props) => props.theme.surfaces?.raised || props.theme.palette.background.paper}; + border: 1px solid ${(props) => props.theme.table?.borderSoft || props.theme.borderColor}; + border-radius: ${(props) => props.theme.shape.borderRadius}px; + box-shadow: ${(props) => props.theme.palette.mode === "dark" ? "inset 0 1px 0 rgba(255,255,255,0.04)" : "0 1px 2px rgba(15,23,42,0.06)"} !important; + display: flex; + flex: 1 1 18rem; + flex-direction: column; + height: var(--mythic-dashboard-card-height, 18rem); + max-height: var(--mythic-dashboard-card-height, 18rem); + min-height: var(--mythic-dashboard-card-height, 18rem); + min-width: min(100%, 18rem); + overflow: hidden; +} +.mythic-dashboard-card-header { + align-items: center; + background-color: ${(props) => props.theme.surfaces?.muted || props.theme.palette.background.default}; + border-bottom: 1px solid ${(props) => props.theme.table?.borderSoft || props.theme.borderColor}; + display: flex; + flex: 0 0 auto; + gap: 0.65rem; + justify-content: space-between; + min-height: 42px; + min-width: 0; + padding: 0.55rem 0.65rem; +} +.mythic-dashboard-card-title { + color: ${(props) => props.theme.palette.text.primary}; + font-size: 0.86rem; + font-weight: 800; + letter-spacing: 0; + line-height: 1.2; + min-width: 0; +} +.mythic-dashboard-card-actions { + align-items: center; + display: flex; + flex: 0 0 auto; + flex-wrap: wrap; + gap: 0.35rem; + justify-content: flex-end; +} +.mythic-dashboard-card-body { + display: flex; + flex: 1 1 auto; + flex-direction: column; + min-height: 0; + min-width: 0; + overflow: hidden; + padding: 0.35rem 0.5rem 0.55rem; +} +.mythic-dashboard-card-body-centered { + align-items: center; + justify-content: center; +} +.mythic-dashboard-card-body-empty { + padding: 0.75rem; +} +.mythic-dashboard-icon-button { + background-color: ${(props) => props.theme.palette.mode === "dark" ? "rgba(255,255,255,0.06)" : "rgba(0,0,0,0.035)"} !important; + border: 1px solid ${(props) => props.theme.table?.borderSoft || props.theme.borderColor} !important; + border-radius: ${(props) => props.theme.shape.borderRadius}px !important; + box-shadow: none !important; + color: ${(props) => props.theme.palette.text.primary} !important; + height: 30px; + padding: 0; + width: 30px; +} +.mythic-dashboard-icon-button:hover { + background-color: ${(props) => props.theme.palette.mode === "dark" ? "rgba(255,255,255,0.1)" : "rgba(0,0,0,0.06)"} !important; + border-color: ${(props) => props.theme.table?.border || props.theme.borderColor} !important; +} +.mythic-dashboard-icon-button-danger { + color: ${(props) => props.theme.palette.error.main} !important; +} +.mythic-dashboard-icon-button-hover-danger:hover { + color: ${(props) => props.theme.palette.error.main} !important; +} +.mythic-dashboard-icon-button.Mui-disabled { + background-color: ${(props) => props.theme.palette.mode === "dark" ? "rgba(255,255,255,0.03)" : "rgba(0,0,0,0.02)"} !important; + border-color: ${(props) => props.theme.table?.borderSoft || props.theme.borderColor} !important; + color: ${(props) => props.theme.palette.text.disabled} !important; +} +.mythic-dashboard-primary-button, +.mythic-dashboard-table-action { + background-color: ${(props) => props.theme.palette.mode === "dark" ? "rgba(255,255,255,0.06)" : "rgba(0,0,0,0.035)"} !important; + border: 1px solid ${(props) => props.theme.table?.borderSoft || props.theme.borderColor} !important; + border-radius: ${(props) => props.theme.shape.borderRadius}px !important; + box-shadow: none !important; + color: ${(props) => props.theme.palette.text.primary} !important; + font-size: 0.74rem; + font-weight: 750; + min-height: 30px; + text-transform: none; +} +.mythic-dashboard-primary-button { + background-color: ${(props) => props.theme.palette.primary.main} !important; + border-color: ${(props) => props.theme.palette.primary.main} !important; + color: ${(props) => props.theme.palette.primary.contrastText} !important; +} +.mythic-dashboard-primary-button:hover { + background-color: ${(props) => props.theme.palette.primary.dark || props.theme.palette.primary.main} !important; + border-color: ${(props) => props.theme.palette.primary.dark || props.theme.palette.primary.main} !important; +} +.mythic-dashboard-table-action:hover { + background-color: ${(props) => props.theme.palette.mode === "dark" ? "rgba(255,255,255,0.1)" : "rgba(0,0,0,0.06)"} !important; +} +.mythic-dashboard-table-action-info { + background-color: ${(props) => props.theme.palette.info.main + "1c"} !important; + border-color: ${(props) => props.theme.palette.info.main + "66"} !important; + color: ${(props) => props.theme.palette.info.main} !important; +} +.mythic-dashboard-table-action-info:hover { + background-color: ${(props) => props.theme.palette.info.main + "2b"} !important; + border-color: ${(props) => props.theme.palette.info.main + "88"} !important; +} +.mythic-dashboard-table-action.Mui-disabled { + background-color: ${(props) => props.theme.palette.mode === "dark" ? "rgba(255,255,255,0.03)" : "rgba(0,0,0,0.02)"} !important; + border-color: ${(props) => props.theme.table?.borderSoft || props.theme.borderColor} !important; + color: ${(props) => props.theme.palette.text.disabled} !important; +} +.mythic-dashboard-table-body { + padding: 0; +} +.mythic-dashboard-table-container { + flex: 1 1 auto; + height: 100%; + max-height: 100%; + min-height: 0; + overflow: auto; + width: 100%; +} +.mythic-dashboard-table { + max-width: 100%; + overflow: auto; +} +.mythic-dashboard-screenshot-row { + align-items: center; + display: flex !important; + height: 100%; + justify-content: space-between; + min-width: 0; + width: 100%; +} +.mythic-dashboard-screenshot-nav-cell { + align-items: center; + border-bottom: 0 !important; + display: flex !important; + flex: 0 0 2.75rem; + justify-content: center; + min-width: 2.75rem; + padding: 0.25rem !important; +} +.mythic-dashboard-screenshot-preview-cell { + align-items: center; + border-bottom: 0 !important; + display: flex !important; + flex: 1 1 auto; + justify-content: center; + min-width: 0; + overflow: hidden; + padding: 0.25rem !important; +} +.mythic-dashboard-screenshot-image { + cursor: pointer; + display: block; + height: 12.5rem; + max-height: 100%; + max-width: 100%; + min-width: 0; + object-fit: contain; + width: 100%; +} +.mythic-dashboard-empty-state { + align-items: center; + background-color: ${(props) => props.theme.palette.mode === "dark" ? "rgba(255,255,255,0.035)" : "rgba(0,0,0,0.02)"}; + border: 1px dashed ${(props) => props.theme.table?.border || props.theme.borderColor}; + border-radius: ${(props) => props.theme.shape.borderRadius}px; + color: ${(props) => props.theme.palette.text.secondary}; + display: flex; + flex: 1 1 auto; + flex-direction: column; + font-size: 0.82rem; + gap: 0.75rem; + justify-content: center; + line-height: 1.4; + min-height: 0; + padding: 1rem; + text-align: center; +} +.mythic-dashboard-empty-copy { + max-width: 26rem; +} +.mythic-dashboard-empty-action { + align-items: center; + display: flex; + justify-content: center; +} +.mythic-dashboard-metric-body { + cursor: pointer; + padding: 0.45rem 0.65rem 0.65rem; +} +.mythic-dashboard-metric-title-row { + align-items: center; + display: flex; + gap: 0.65rem; + justify-content: space-between; + min-width: 0; +} +.mythic-dashboard-metric-title-row .MuiFormControl-root { + margin: 0 !important; +} +.mythic-dashboard-metric-content, +.mythic-dashboard-metric-link { + display: flex; + flex: 1 1 auto; + flex-direction: column; + justify-content: center; + min-height: 0; +} +.mythic-dashboard-metric-value { + color: ${(props) => props.theme.palette.text.primary}; + display: inline-block; + font-weight: 850 !important; + letter-spacing: 0; + line-height: 0.95; + margin-left: 0.1rem; +} +.mythic-dashboard-metric-total { + color: ${(props) => props.theme.palette.text.secondary}; + display: inline-block; + font-weight: 700 !important; + margin-left: 0.35rem; +} +.mythic-dashboard-metric-label { + color: ${(props) => props.theme.palette.text.secondary}; + font-size: 0.78rem; + font-weight: 750; + letter-spacing: 0; + line-height: 1.25; + margin-top: 0.35rem; +} +.mythic-dashboard-metric-secondary { + color: ${(props) => props.theme.palette.text.primary}; + font-weight: 850 !important; + letter-spacing: 0; + line-height: 1; + margin-top: 0.1rem; +} +.mythic-dashboard-slider { + align-self: center; + margin-top: 0.65rem; +} +.mythic-dashboard-edit-rail { + align-items: center; + background-color: ${(props) => props.theme.surfaces?.muted || props.theme.palette.background.paper}; + border: 1px solid ${(props) => props.theme.table?.borderSoft || props.theme.borderColor}; + border-radius: ${(props) => props.theme.shape.borderRadius}px; + display: flex; + flex: 0 0 auto; + flex-direction: column; + gap: 0.4rem; + justify-content: center; + padding: 0.45rem; +} +.mythic-dashboard-loading-overlay { + align-items: center; + background-color: ${(props) => props.theme.palette.mode === "dark" ? "rgba(0,0,0,0.45)" : "rgba(255,255,255,0.55)"}; + display: flex; + inset: 0; + justify-content: center; + position: absolute; + z-index: 5; +} +.mythic-dashboard-loading-card { + background-color: ${(props) => props.theme.surfaces?.raised || props.theme.palette.background.paper}; + border: 1px solid ${(props) => props.theme.table?.borderSoft || props.theme.borderColor}; + border-radius: ${(props) => props.theme.shape.borderRadius}px; + box-shadow: ${(props) => props.theme.palette.mode === "dark" ? "0 12px 28px rgba(0,0,0,0.35)" : "0 12px 28px rgba(15,23,42,0.14)"}; + color: ${(props) => props.theme.palette.text.primary}; + font-size: 0.84rem; + font-weight: 750; + padding: 0.75rem 0.9rem; +} +.mythic-form-code-editor { + border: 1px solid ${(props) => props.theme.table?.borderSoft || props.theme.borderColor}; + border-radius: ${(props) => props.theme.shape.borderRadius}px; + min-height: 16rem; + min-width: 0; + overflow: hidden; +} +.mythic-dialog-body .MuiFormControl-root, +.mythic-dialog-body .MuiTextField-root { + margin: 0 !important; +} +.mythic-table-bulk-actions { + align-items: center; + border-bottom: 1px solid ${(props) => props.theme.table?.borderSoft || props.theme.borderColor}; + display: flex; + flex-wrap: wrap; + gap: 0.5rem; + padding: 0.5rem; + width: 100%; +} +.mythic-table-bulk-actions .mythic-table-row-action { + flex: 1 1 13rem; + justify-content: center; + margin: 0 !important; +} +.mythic-table-row-actions { + align-items: center; + display: flex; + flex-wrap: wrap; + gap: 0.35rem; +} +.mythic-table-row-actions-nowrap { + flex-wrap: nowrap; +} +.mythic-payload-progress-cell { + align-items: center; + display: flex; + flex-wrap: wrap; + gap: 0.35rem; + min-width: 0; +} +.mythic-table-row-action { + background-color: ${(props) => props.theme.palette.mode === "dark" ? "rgba(255,255,255,0.06)" : "rgba(0,0,0,0.035)"} !important; + border: 1px solid ${(props) => props.theme.table?.borderSoft || props.theme.borderColor} !important; + border-radius: ${(props) => props.theme.shape.borderRadius}px !important; + box-shadow: none !important; + color: ${(props) => props.theme.palette.text.primary} !important; + font-size: 0.74rem; + font-weight: 750; + min-height: 30px; + text-transform: none; +} +.mythic-table-row-action:hover { + background-color: ${(props) => props.theme.palette.mode === "dark" ? "rgba(255,255,255,0.1)" : "rgba(0,0,0,0.06)"} !important; + border-color: ${(props) => props.theme.table?.border || props.theme.borderColor} !important; +} +.mythic-table-row-action-info { + background-color: ${(props) => props.theme.palette.info.main + "1c"} !important; + border-color: ${(props) => props.theme.palette.info.main + "66"} !important; + color: ${(props) => props.theme.palette.info.main} !important; +} +.mythic-table-row-action-info:hover { + background-color: ${(props) => props.theme.palette.info.main + "2b"} !important; + border-color: ${(props) => props.theme.palette.info.main + "88"} !important; +} +.mythic-table-row-action-hover-info:hover { + background-color: ${(props) => props.theme.palette.info.main + "2b"} !important; + border-color: ${(props) => props.theme.palette.info.main + "88"} !important; + color: ${(props) => props.theme.palette.info.main} !important; +} +.mythic-table-row-action-hover-success:hover { + background-color: ${(props) => props.theme.palette.success.main + "2b"} !important; + border-color: ${(props) => props.theme.palette.success.main + "88"} !important; + color: ${(props) => props.theme.palette.success.main} !important; +} +.mythic-table-row-action-hover-warning:hover { + background-color: ${(props) => props.theme.palette.warning.main + "2b"} !important; + border-color: ${(props) => props.theme.palette.warning.main + "88"} !important; + color: ${(props) => props.theme.palette.warning.main} !important; +} +.mythic-table-row-action-hover-danger:hover { + background-color: ${(props) => props.theme.palette.error.main + "2b"} !important; + border-color: ${(props) => props.theme.palette.error.main + "88"} !important; + color: ${(props) => props.theme.palette.error.main} !important; +} +.mythic-table-row-action-success { + background-color: ${(props) => props.theme.palette.success.main + "1c"} !important; + border-color: ${(props) => props.theme.palette.success.main + "66"} !important; + color: ${(props) => props.theme.palette.success.main} !important; +} +.mythic-table-row-action-success:hover { + background-color: ${(props) => props.theme.palette.success.main + "2b"} !important; + border-color: ${(props) => props.theme.palette.success.main + "88"} !important; +} +.mythic-table-row-action-danger { + background-color: ${(props) => props.theme.palette.error.main + "1c"} !important; + border-color: ${(props) => props.theme.palette.error.main + "66"} !important; + color: ${(props) => props.theme.palette.error.main} !important; +} +.mythic-table-row-action-danger:hover { + background-color: ${(props) => props.theme.palette.error.main + "2b"} !important; + border-color: ${(props) => props.theme.palette.error.main + "88"} !important; +} +.mythic-table-row-action.Mui-disabled { + background-color: ${(props) => props.theme.palette.mode === "dark" ? "rgba(255,255,255,0.03)" : "rgba(0,0,0,0.02)"} !important; + border-color: ${(props) => props.theme.table?.borderSoft || props.theme.borderColor} !important; + color: ${(props) => props.theme.palette.text.disabled} !important; +} +.mythic-table-row-icon-action { + background-color: ${(props) => props.theme.palette.mode === "dark" ? "rgba(255,255,255,0.06)" : "rgba(0,0,0,0.035)"} !important; + border: 1px solid ${(props) => props.theme.table?.borderSoft || props.theme.borderColor} !important; + border-radius: ${(props) => props.theme.shape.borderRadius}px !important; + box-shadow: none !important; + color: ${(props) => props.theme.palette.text.primary} !important; + height: 30px; + padding: 0; + width: 30px; +} +.mythic-table-row-icon-action:hover { + background-color: ${(props) => props.theme.palette.mode === "dark" ? "rgba(255,255,255,0.1)" : "rgba(0,0,0,0.06)"} !important; + border-color: ${(props) => props.theme.table?.border || props.theme.borderColor} !important; +} +.mythic-table-row-icon-action-danger { + color: ${(props) => props.theme.palette.error.main} !important; +} +.mythic-table-row-icon-action-success { + color: ${(props) => props.theme.palette.success.main} !important; +} +.mythic-table-row-icon-action-info { + color: ${(props) => props.theme.palette.info.main} !important; +} +.mythic-table-row-icon-action-warning { + color: ${(props) => props.theme.palette.warning.main} !important; +} +.mythic-table-row-icon-action-hover-info:hover { + color: ${(props) => props.theme.palette.info.main} !important; +} +.mythic-table-row-icon-action-hover-success:hover { + color: ${(props) => props.theme.palette.success.main} !important; +} +.mythic-table-row-icon-action-hover-warning:hover { + color: ${(props) => props.theme.palette.warning.main} !important; +} +.mythic-table-row-icon-action-hover-danger:hover { + color: ${(props) => props.theme.palette.error.main} !important; +} +.mythic-eventing-workspace { + display: flex; + flex: 1 1 auto; + gap: 0.5rem; + min-height: 0; + min-width: 0; + overflow: hidden; +} +.mythic-eventing-split { + display: flex; + height: 100%; + min-height: 0; + min-width: 0; + overflow: hidden; + width: 100%; +} +.mythic-eventing-sidebar, +.mythic-eventing-content { + background-color: ${(props) => props.theme.surfaces?.raised || props.theme.palette.background.paper}; + border: 1px solid ${(props) => props.theme.table?.borderSoft || props.theme.borderColor}; + border-radius: ${(props) => props.theme.shape.borderRadius}px; + min-height: 0; + min-width: 0; + overflow: hidden; +} +.mythic-eventing-sidebar { + display: flex !important; + flex-direction: column; +} +.mythic-eventing-content { + display: flex !important; +} +.mythic-eventing-content > div { + min-height: 0; + min-width: 0; +} +.mythic-eventing-list { + background: transparent !important; + border: 0 !important; + padding: 0.35rem !important; +} +.mythic-eventing-sidebar-toolbar { + border-bottom: 1px solid ${(props) => props.theme.table?.borderSoft || props.theme.borderColor}; + display: flex; + flex-direction: column; + gap: 0.55rem; + padding: 0.65rem 0.65rem 0.55rem; +} +.mythic-eventing-sidebar-title-row { + align-items: flex-start; + display: flex; + gap: 0.65rem; + justify-content: space-between; + min-width: 0; +} +.mythic-eventing-sidebar-title { + color: ${(props) => props.theme.palette.text.primary}; + font-size: 0.82rem; + font-weight: 800; + line-height: 1.2; +} +.mythic-eventing-sidebar-subtitle { + color: ${(props) => props.theme.palette.text.secondary}; + font-size: 0.72rem; + font-weight: 600; + line-height: 1.2; + margin-top: 0.12rem; +} +.mythic-eventing-sidebar-count { + align-items: center; + background-color: ${(props) => props.theme.palette.primary.main + "16"}; + border: 1px solid ${(props) => props.theme.palette.primary.main + "38"}; + border-radius: ${(props) => props.theme.shape.borderRadius}px; + color: ${(props) => props.theme.palette.primary.main}; + display: inline-flex; + flex: 0 0 auto; + font-size: 0.72rem; + font-weight: 800; + line-height: 1; + min-height: 1.45rem; + padding: 0 0.45rem; +} +.mythic-eventing-sidebar-search .MuiOutlinedInput-root { + background-color: ${(props) => props.theme.palette.mode === "dark" ? "rgba(255,255,255,0.035)" : "rgba(0,0,0,0.018)"}; + border-radius: ${(props) => props.theme.shape.borderRadius}px; + font-size: 0.82rem; +} +.mythic-eventing-sidebar-search .MuiOutlinedInput-input { + padding-bottom: 0.45rem; + padding-top: 0.45rem; +} +.mythic-eventing-filter-row { + display: flex; + flex-wrap: nowrap; + gap: 0.35rem; + overflow-x: auto; + scrollbar-width: none; +} +.mythic-eventing-filter-row::-webkit-scrollbar { + display: none; +} +.mythic-eventing-filter-button { + align-items: center; + background-color: transparent; + border: 1px solid ${(props) => props.theme.table?.borderSoft || props.theme.borderColor}; + border-radius: ${(props) => props.theme.shape.borderRadius}px; + color: ${(props) => props.theme.palette.text.secondary}; + cursor: pointer; + display: inline-flex; + flex: 0 0 auto; + font-family: inherit; + font-size: 0.72rem; + font-weight: 750; + gap: 0.35rem; + min-height: 1.65rem; + padding: 0 0.45rem; + white-space: nowrap; +} +.mythic-eventing-filter-button:hover { + background-color: ${(props) => props.theme.tableHover}; + border-color: ${(props) => props.theme.palette.primary.main + "44"}; + color: ${(props) => props.theme.palette.text.primary}; +} +.mythic-eventing-filter-button-active { + background-color: ${(props) => props.theme.palette.primary.main + "16"}; + border-color: ${(props) => props.theme.palette.primary.main + "66"}; + color: ${(props) => props.theme.palette.primary.main}; +} +.mythic-eventing-filter-count { + color: inherit; + opacity: 0.78; +} +.mythic-eventing-list-scroll { + flex-grow: 1; + height: 100%; + min-height: 0; + overflow-y: auto; +} +.mythic-eventing-list-header { + background-color: ${(props) => props.theme.surfaces?.raised || props.theme.palette.background.paper} !important; + color: ${(props) => props.theme.palette.text.secondary} !important; + font-size: 0.74rem !important; + font-weight: 800 !important; + letter-spacing: 0 !important; + line-height: 1.2 !important; + padding: 0.45rem 0.55rem !important; + text-transform: none; +} +.mythic-eventing-list-item { + align-items: flex-start !important; + border: 1px solid transparent; + border-radius: ${(props) => props.theme.shape.borderRadius}px; + cursor: pointer; + display: flex !important; + gap: 0.5rem; + margin-bottom: 0.2rem; + min-height: 3.35rem; + min-width: 0; + padding: 0.45rem 0.55rem !important; +} +.mythic-eventing-list-item-all { + margin: 0.45rem 0.35rem 0.2rem !important; + width: auto !important; +} +.mythic-eventing-list-item:hover { + background-color: ${(props) => props.theme.tableHover} !important; + border-color: ${(props) => props.theme.table?.borderSoft || props.theme.borderColor}; +} +.mythic-eventing-list-item-selected { + background-color: ${(props) => props.theme.palette.primary.main + "18"} !important; + border-color: ${(props) => props.theme.palette.primary.main + "66"}; + box-shadow: inset 3px 0 0 ${(props) => props.theme.palette.primary.main}; +} +.mythic-eventing-status-dot { + border-radius: 999px; + box-shadow: 0 0 0 3px ${(props) => props.theme.palette.background.paper}; + flex: 0 0 0.52rem; + height: 0.52rem; + margin-top: 0.42rem; + width: 0.52rem; +} +.mythic-eventing-status-all { + background-color: ${(props) => props.theme.palette.info.main}; +} +.mythic-eventing-status-runnable { + background-color: ${(props) => props.theme.palette.success.main}; +} +.mythic-eventing-status-needs_approval { + background-color: transparent; + border: 2px solid ${(props) => props.theme.palette.warning.main}; + border-radius: 0.16rem; + transform: rotate(45deg); +} +.mythic-eventing-status-disabled { + background-color: ${(props) => props.theme.palette.action.disabled}; + position: relative; +} +.mythic-eventing-status-disabled::after { + background-color: ${(props) => props.theme.palette.background.paper}; + border-radius: 999px; + content: ""; + height: 2px; + left: 50%; + position: absolute; + top: 50%; + transform: translate(-50%, -50%) rotate(-35deg); + width: 0.72rem; +} +.mythic-eventing-status-deleted { + background-color: ${(props) => props.theme.palette.error.main}; +} +.mythic-eventing-list-item-content { + flex: 1 1 auto; + min-width: 0; +} +.mythic-eventing-list-item-main { + align-items: center; + display: flex; + gap: 0.45rem; + justify-content: space-between; + min-width: 0; +} +.mythic-eventing-list-item-name { + color: ${(props) => props.theme.palette.text.primary}; + flex: 1 1 auto; + font-size: 0.84rem; + font-weight: 800; + line-height: 1.25; + min-width: 0; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} +.mythic-eventing-list-item-name-deleted { + color: ${(props) => props.theme.palette.text.secondary}; + text-decoration: line-through; +} +.mythic-eventing-list-item-meta { + color: ${(props) => props.theme.palette.text.secondary}; + display: flex; + flex-wrap: wrap; + font-size: 0.72rem; + font-weight: 650; + gap: 0.25rem 0.45rem; + line-height: 1.2; + margin-top: 0.22rem; + min-width: 0; +} +.mythic-eventing-row-chip { + align-items: center; + border: 1px solid transparent; + border-radius: ${(props) => props.theme.shape.borderRadius}px; + display: inline-flex; + flex: 0 0 auto; + font-size: 0.66rem; + font-weight: 850; + line-height: 1; + min-height: 1.25rem; + padding: 0 0.35rem; +} +.mythic-eventing-row-chip-all { + background-color: ${(props) => props.theme.palette.info.main + "16"}; + border-color: ${(props) => props.theme.palette.info.main + "40"}; + color: ${(props) => props.theme.palette.info.main}; +} +.mythic-eventing-row-chip-runnable { + background-color: ${(props) => props.theme.palette.success.main + "18"}; + border-color: ${(props) => props.theme.palette.success.main + "42"}; + color: ${(props) => props.theme.palette.success.main}; +} +.mythic-eventing-row-chip-needs_approval { + background-color: ${(props) => props.theme.palette.warning.main + "22"}; + border-color: ${(props) => props.theme.palette.warning.main + "66"}; + color: ${(props) => props.theme.palette.warning.main}; +} +.mythic-eventing-row-chip-disabled { + background-color: ${(props) => props.theme.palette.action.disabledBackground}; + border-color: ${(props) => props.theme.palette.action.disabled}; + color: ${(props) => props.theme.palette.text.secondary}; +} +.mythic-eventing-header-chip-disabled { + background-color: ${(props) => props.theme.palette.action.disabledBackground} !important; + border-color: ${(props) => props.theme.palette.action.disabled} !important; + color: ${(props) => props.theme.palette.text.secondary} !important; +} +.mythic-eventing-header-chip-disabled .MuiChip-icon { + color: ${(props) => props.theme.palette.text.secondary} !important; +} +.mythic-eventing-runas-chip { + align-items: center; + background-color: ${(props) => props.theme.palette.mode === "dark" ? "rgba(255,255,255,0.06)" : "rgba(0,0,0,0.04)"}; + border: 1px solid ${(props) => props.theme.table?.borderSoft || props.theme.borderColor}; + border-radius: ${(props) => props.theme.shape.borderRadius}px; + color: ${(props) => props.theme.palette.text.secondary}; + display: inline-flex; + font-size: 0.68rem; + font-weight: 800; + line-height: 1; + min-height: 1.2rem; + padding: 0 0.35rem; +} +.mythic-eventing-row-chip-deleted { + background-color: ${(props) => props.theme.palette.error.main + "16"}; + border-color: ${(props) => props.theme.palette.error.main + "40"}; + color: ${(props) => props.theme.palette.error.main}; +} +.mythic-eventing-list-empty { + color: ${(props) => props.theme.palette.text.secondary}; + font-size: 0.78rem; + font-weight: 650; + padding: 0.8rem 0.55rem; + text-align: center; +} +.mythic-eventing-wizard-dialog-content { + height: min(95vh, 920px); + margin: 0 !important; + overflow: hidden; + padding: 0 !important; +} +.mythic-eventing-wizard { + background-color: ${(props) => props.theme.surfaces?.raised || props.theme.palette.background.paper}; + display: flex; + flex-direction: column; + height: 100%; + min-height: 0; + min-width: 0; +} +.mythic-eventing-wizard-header { + border-bottom: 1px solid ${(props) => props.theme.table?.borderSoft || props.theme.borderColor}; + padding: 0.75rem 1rem; +} +.mythic-eventing-wizard-title-row { + align-items: flex-start; + display: flex; + gap: 1rem; + justify-content: space-between; + min-width: 0; +} +.mythic-eventing-wizard-title { + color: ${(props) => props.theme.palette.text.primary}; + font-size: 1.02rem; + font-weight: 850; + line-height: 1.2; +} +.mythic-eventing-wizard-subtitle { + color: ${(props) => props.theme.palette.text.secondary}; + font-size: 0.78rem; + font-weight: 600; + line-height: 1.35; + margin-top: 0.15rem; +} +.mythic-eventing-wizard-progress-chip { + align-items: center; + background-color: ${(props) => props.theme.palette.primary.main + "16"}; + border: 1px solid ${(props) => props.theme.palette.primary.main + "38"}; + border-radius: ${(props) => props.theme.shape.borderRadius}px; + color: ${(props) => props.theme.palette.primary.main}; + display: inline-flex; + flex: 0 0 auto; + font-size: 0.74rem; + font-weight: 850; + line-height: 1; + min-height: 1.55rem; + padding: 0 0.55rem; +} +.mythic-eventing-wizard-content { + display: flex; + flex: 1 1 auto; + flex-direction: column; + min-height: 0; +} +.mythic-eventing-wizard-content-heading { + border-bottom: 1px solid ${(props) => props.theme.table?.borderSoft || props.theme.borderColor}; + padding: 0.75rem 1rem; +} +.mythic-eventing-wizard-content-title { + color: ${(props) => props.theme.palette.text.primary}; + font-size: 0.9rem; + font-weight: 850; + line-height: 1.2; +} +.mythic-eventing-wizard-content-subtitle { + color: ${(props) => props.theme.palette.text.secondary}; + font-size: 0.74rem; + font-weight: 600; + line-height: 1.35; + margin-top: 0.12rem; +} +.mythic-eventing-wizard-step { + display: flex; + flex: 1 1 auto; + flex-direction: column; + min-height: 0; +} +.mythic-eventing-wizard-step-scroll { + flex: 1 1 auto; + min-height: 0; + overflow: auto; + padding: 0.8rem 1rem; +} +.mythic-eventing-wizard-table-container { + background: transparent !important; + border: 0 !important; + box-shadow: none !important; + flex: 1 1 auto; + overflow: visible !important; + width: 100%; +} +.mythic-eventing-wizard-table { + border-collapse: separate !important; + border-spacing: 0 0.5rem !important; +} +.mythic-eventing-wizard-table .MuiTableHead-root { + display: none; +} +.mythic-eventing-wizard-table .MuiTableRow-root { + background-color: ${(props) => props.theme.palette.mode === "dark" ? "rgba(255,255,255,0.025)" : "rgba(0,0,0,0.018)"}; +} +.mythic-eventing-wizard-table .MuiTableCell-root { + border-bottom: 1px solid ${(props) => props.theme.table?.borderSoft || props.theme.borderColor} !important; + border-top: 1px solid ${(props) => props.theme.table?.borderSoft || props.theme.borderColor} !important; + padding: 0.65rem 0.75rem !important; + vertical-align: top; +} +.mythic-eventing-wizard-table .MuiTableCell-root:first-of-type { + border-left: 1px solid ${(props) => props.theme.table?.borderSoft || props.theme.borderColor} !important; + border-radius: ${(props) => props.theme.shape.borderRadius}px 0 0 ${(props) => props.theme.shape.borderRadius}px; + color: ${(props) => props.theme.palette.text.secondary}; + font-size: 0.74rem; + font-weight: 850; + text-transform: none; + width: 10rem; +} +.mythic-eventing-wizard-table .MuiTableCell-root:last-of-type { + border-radius: 0 ${(props) => props.theme.shape.borderRadius}px ${(props) => props.theme.shape.borderRadius}px 0; + border-right: 1px solid ${(props) => props.theme.table?.borderSoft || props.theme.borderColor} !important; +} +.mythic-eventing-step-table .MuiTableCell-root:first-of-type { + width: 9rem; +} +.mythic-eventing-wizard-table .MuiTypography-root { + color: ${(props) => props.theme.palette.text.secondary}; + font-size: 0.78rem; + line-height: 1.35; + margin-top: 0.35rem; +} +.mythic-eventing-metadata-layout { + display: grid; + gap: 0.75rem; + grid-template-columns: repeat(2, minmax(0, 1fr)); + min-width: 0; +} +.mythic-eventing-metadata-card { + background-color: ${(props) => props.theme.palette.mode === "dark" ? "rgba(255,255,255,0.025)" : "rgba(0,0,0,0.018)"}; + border: 1px solid ${(props) => props.theme.table?.borderSoft || props.theme.borderColor}; + border-radius: ${(props) => props.theme.shape.borderRadius}px; + min-width: 0; + overflow: hidden; +} +.mythic-eventing-metadata-card-wide { + grid-column: 1 / -1; +} +.mythic-eventing-metadata-card-header { + background-color: ${(props) => props.theme.palette.mode === "dark" ? "rgba(255,255,255,0.025)" : "rgba(0,0,0,0.012)"}; + border-bottom: 1px solid ${(props) => props.theme.table?.borderSoft || props.theme.borderColor}; + padding: 0.7rem 0.75rem 0.6rem; +} +.mythic-eventing-metadata-card-title { + color: ${(props) => props.theme.palette.text.primary}; + font-size: 0.88rem; + font-weight: 850; + line-height: 1.2; +} +.mythic-eventing-metadata-card-subtitle { + color: ${(props) => props.theme.palette.text.secondary}; + font-size: 0.74rem; + font-weight: 600; + line-height: 1.35; + margin-top: 0.16rem; +} +.mythic-eventing-metadata-field-grid { + display: grid; + gap: 0.65rem; + grid-template-columns: repeat(2, minmax(0, 1fr)); + padding: 0.75rem; +} +.mythic-eventing-metadata-field { + min-width: 0; + padding: 0.75rem; +} +.mythic-eventing-metadata-field + .mythic-eventing-metadata-field { + border-top: 1px solid ${(props) => props.theme.table?.borderSoft || props.theme.borderColor}; +} +.mythic-eventing-metadata-field-grid .mythic-eventing-metadata-field { + padding: 0; +} +.mythic-eventing-metadata-field-grid .mythic-eventing-metadata-field + .mythic-eventing-metadata-field { + border-top: 0; +} +.mythic-eventing-metadata-label { + color: ${(props) => props.theme.palette.text.secondary}; + font-size: 0.72rem; + font-weight: 850; + line-height: 1.2; + margin-bottom: 0.4rem; +} +.mythic-eventing-metadata-empty { + align-items: center; + background-color: ${(props) => props.theme.palette.action.disabledBackground}; + border: 1px solid ${(props) => props.theme.palette.action.disabled}; + border-radius: ${(props) => props.theme.shape.borderRadius}px; + color: ${(props) => props.theme.palette.text.secondary}; + display: inline-flex; + font-size: 0.74rem; + font-weight: 800; + min-height: 1.65rem; + padding: 0 0.55rem; +} +.mythic-eventing-metadata-trigger-table { + border-collapse: separate !important; + border-spacing: 0 0.4rem !important; + table-layout: fixed; + width: 100%; +} +.mythic-eventing-metadata-trigger-table > .MuiTableHead-root { + display: none; +} +.mythic-eventing-metadata-trigger-table > .MuiTableBody-root > .MuiTableRow-root > .MuiTableCell-root { + border-bottom: 1px solid ${(props) => props.theme.table?.borderSoft || props.theme.borderColor} !important; + border-top: 1px solid ${(props) => props.theme.table?.borderSoft || props.theme.borderColor} !important; + padding: 0.5rem 0.6rem !important; +} +.mythic-eventing-metadata-trigger-table > .MuiTableBody-root > .MuiTableRow-root > .MuiTableCell-root:first-of-type { + border-left: 1px solid ${(props) => props.theme.table?.borderSoft || props.theme.borderColor} !important; + border-radius: ${(props) => props.theme.shape.borderRadius}px 0 0 ${(props) => props.theme.shape.borderRadius}px; + color: ${(props) => props.theme.palette.text.secondary}; + font-size: 0.72rem; + font-weight: 850; + vertical-align: top; + width: 38%; +} +.mythic-eventing-metadata-trigger-table > .MuiTableBody-root > .MuiTableRow-root > .MuiTableCell-root:last-of-type { + border-radius: 0 ${(props) => props.theme.shape.borderRadius}px ${(props) => props.theme.shape.borderRadius}px 0; + border-right: 1px solid ${(props) => props.theme.table?.borderSoft || props.theme.borderColor} !important; + width: 62%; +} +.mythic-eventing-metadata-trigger-table .MuiTable-root:not(.mythic-eventing-metadata-trigger-table) .MuiTableCell-root { + vertical-align: middle; +} +.mythic-eventing-metadata-trigger-table .MuiTable-root:not(.mythic-eventing-metadata-trigger-table) .MuiTableCell-root:first-of-type { + padding-right: 0.4rem !important; + width: 2.5rem !important; +} +.mythic-eventing-metadata-trigger-table .MuiTable-root:not(.mythic-eventing-metadata-trigger-table) .MuiTableCell-root:first-of-type + .MuiTableCell-root { + padding-left: 0.4rem !important; +} +.mythic-eventing-choice-row { + align-items: flex-start; + display: grid; + gap: 0.5rem; + grid-template-columns: minmax(0, 1fr) auto minmax(0, 1fr); +} +.mythic-eventing-choice-separator { + align-items: center; + align-self: center; + background-color: ${(props) => props.theme.palette.mode === "dark" ? "rgba(255,255,255,0.06)" : "rgba(0,0,0,0.04)"}; + border: 1px solid ${(props) => props.theme.table?.borderSoft || props.theme.borderColor}; + border-radius: ${(props) => props.theme.shape.borderRadius}px; + color: ${(props) => props.theme.palette.text.secondary}; + display: inline-flex; + font-size: 0.68rem; + font-weight: 850; + height: 1.45rem; + justify-content: center; + min-width: 1.9rem; +} +.mythic-eventing-choice-custom { + min-width: 0; +} +.mythic-eventing-array-list { + align-items: flex-start; + display: flex; + flex-direction: column; + gap: 0.4rem; +} +.mythic-eventing-array-row { + align-items: center; + display: flex; + gap: 0.35rem; + min-width: 0; + width: 100%; +} +.mythic-eventing-array-row > .MuiIconButton-root { + flex: 0 0 auto; +} +.mythic-eventing-array-row > div { + flex: 1 1 auto; + min-width: 0; +} +.mythic-eventing-file-select { + display: flex; + flex-direction: column; + gap: 0.5rem; +} +.mythic-eventing-file-chip-list { + display: flex; + flex-wrap: wrap; + gap: 0.35rem; +} +.mythic-eventing-file-chip { + align-items: center; + background-color: ${(props) => props.theme.palette.info.main + "12"}; + border: 1px solid ${(props) => props.theme.palette.info.main + "35"}; + border-radius: ${(props) => props.theme.shape.borderRadius}px; + color: ${(props) => props.theme.palette.text.primary}; + display: inline-flex; + font-size: 0.72rem; + font-weight: 750; + line-height: 1.2; + min-height: 1.45rem; + padding: 0 0.45rem; +} +.mythic-eventing-metadata-editor { + min-height: 16rem; + padding: 0.75rem; +} +.mythic-eventing-metadata-editor > div { + min-height: 15rem; +} +@media (max-width: 1100px) { + .mythic-eventing-metadata-layout, + .mythic-eventing-metadata-field-grid, + .mythic-eventing-choice-row { + grid-template-columns: 1fr; + } + .mythic-eventing-choice-separator { + justify-self: start; + } +} +.mythic-eventing-wizard-toolbar { + align-items: center; + border-bottom: 1px solid ${(props) => props.theme.table?.borderSoft || props.theme.borderColor}; + display: flex; + gap: 0.85rem; + justify-content: space-between; + padding: 0.7rem 1rem; +} +.mythic-eventing-wizard-toolbar-title { + color: ${(props) => props.theme.palette.text.primary}; + font-size: 0.86rem; + font-weight: 850; + line-height: 1.2; +} +.mythic-eventing-wizard-toolbar-subtitle { + color: ${(props) => props.theme.palette.text.secondary}; + font-size: 0.72rem; + font-weight: 650; + margin-top: 0.1rem; +} +.mythic-eventing-wizard-toolbar-actions { + display: flex; + flex: 0 0 auto; + flex-wrap: wrap; + gap: 0.45rem; + justify-content: flex-end; +} +.mythic-eventing-wizard-step-browser { + display: flex; + flex: 1 1 auto; + min-height: 0; + min-width: 0; +} +.mythic-eventing-wizard-step-browser-empty { + display: block; +} +.mythic-eventing-step-nav { + background-color: ${(props) => props.theme.palette.mode === "dark" ? "rgba(255,255,255,0.025)" : "rgba(0,0,0,0.018)"}; + border-right: 1px solid ${(props) => props.theme.table?.borderSoft || props.theme.borderColor}; + display: flex; + flex: 0 0 14.5rem; + flex-direction: column; + min-height: 0; + min-width: 0; +} +.mythic-eventing-step-nav-header { + border-bottom: 1px solid ${(props) => props.theme.table?.borderSoft || props.theme.borderColor}; + padding: 0.7rem 0.75rem 0.55rem; +} +.mythic-eventing-step-nav-title { + color: ${(props) => props.theme.palette.text.primary}; + font-size: 0.78rem; + font-weight: 850; + line-height: 1.2; +} +.mythic-eventing-step-nav-subtitle { + color: ${(props) => props.theme.palette.text.secondary}; + font-size: 0.7rem; + font-weight: 650; + line-height: 1.2; + margin-top: 0.12rem; +} +.mythic-eventing-step-nav-list { + display: flex; + flex: 1 1 auto; + flex-direction: column; + gap: 0.35rem; + min-height: 0; + overflow-y: auto; + padding: 0.55rem; +} +.mythic-eventing-step-nav-item { + align-items: flex-start; + background-color: transparent; + border: 1px solid transparent; + border-radius: ${(props) => props.theme.shape.borderRadius}px; + color: ${(props) => props.theme.palette.text.secondary}; + cursor: pointer; + display: flex; + font-family: inherit; + gap: 0.5rem; + min-width: 0; + padding: 0.45rem 0.5rem; + text-align: left; + width: 100%; +} +.mythic-eventing-step-nav-item:hover { + background-color: ${(props) => props.theme.tableHover}; + border-color: ${(props) => props.theme.table?.borderSoft || props.theme.borderColor}; + color: ${(props) => props.theme.palette.text.primary}; +} +.mythic-eventing-step-nav-item-active { + background-color: ${(props) => props.theme.palette.primary.main + "16"}; + border-color: ${(props) => props.theme.palette.primary.main + "55"}; + color: ${(props) => props.theme.palette.text.primary}; +} +.mythic-eventing-step-nav-number { + align-items: center; + background-color: ${(props) => props.theme.palette.primary.main + "18"}; + border: 1px solid ${(props) => props.theme.palette.primary.main + "38"}; + border-radius: ${(props) => props.theme.shape.borderRadius}px; + color: ${(props) => props.theme.palette.primary.main}; + display: inline-flex; + flex: 0 0 1.45rem; + font-size: 0.72rem; + font-weight: 900; + height: 1.45rem; + justify-content: center; + line-height: 1; +} +.mythic-eventing-step-nav-copy { + display: flex; + flex: 1 1 auto; + flex-direction: column; + min-width: 0; +} +.mythic-eventing-step-nav-name { + color: inherit; + font-size: 0.76rem; + font-weight: 850; + line-height: 1.2; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} +.mythic-eventing-step-nav-action { + color: ${(props) => props.theme.palette.text.secondary}; + font-size: 0.66rem; + font-weight: 700; + line-height: 1.2; + margin-top: 0.16rem; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} +.mythic-eventing-wizard-step-scroll-browser { + flex: 1 1 auto; + scroll-padding-top: 0.75rem; +} +@media (max-width: 900px) { + .mythic-eventing-wizard-step-browser { + flex-direction: column; + } + .mythic-eventing-step-nav { + border-bottom: 1px solid ${(props) => props.theme.table?.borderSoft || props.theme.borderColor}; + border-right: 0; + flex: 0 0 auto; + max-height: 10rem; + } + .mythic-eventing-step-nav-list { + flex-direction: row; + overflow-x: auto; + overflow-y: hidden; + } + .mythic-eventing-step-nav-item { + flex: 0 0 12rem; + } +} +.mythic-eventing-step-shell { + margin-bottom: 0.75rem; + scroll-margin-top: 0.75rem; +} +.mythic-eventing-step-shell-header { + align-items: center; + display: flex; + gap: 0.75rem; + justify-content: space-between; + margin-bottom: 0.35rem; +} +.mythic-eventing-step-shell-title { + color: ${(props) => props.theme.palette.primary.main}; + font-size: 0.72rem; + font-weight: 900; + line-height: 1.2; + text-transform: none; +} +.mythic-eventing-step-shell-subtitle { + color: ${(props) => props.theme.palette.text.primary}; + font-size: 0.88rem; + font-weight: 850; + line-height: 1.2; + margin-top: 0.1rem; +} +.mythic-eventing-step-config-card, +.mythic-eventing-wizard-review-card, +.mythic-eventing-wizard-empty { + background-color: ${(props) => props.theme.palette.mode === "dark" ? "rgba(255,255,255,0.025)" : "rgba(0,0,0,0.018)"}; + border: 1px solid ${(props) => props.theme.table?.borderSoft || props.theme.borderColor}; + border-radius: ${(props) => props.theme.shape.borderRadius}px; + overflow: hidden; +} +.mythic-eventing-step-config-card-modern { + background-color: ${(props) => props.theme.palette.background.paper}; +} +.mythic-eventing-step-config-summary { + align-items: flex-start; + background-color: ${(props) => props.theme.palette.mode === "dark" ? "rgba(255,255,255,0.025)" : "rgba(0,0,0,0.014)"}; + border-bottom: 1px solid ${(props) => props.theme.table?.borderSoft || props.theme.borderColor}; + display: flex; + gap: 0.8rem; + justify-content: space-between; + min-width: 0; + padding: 0.75rem; +} +.mythic-eventing-step-config-summary-copy { + min-width: 0; +} +.mythic-eventing-step-config-summary-title { + color: ${(props) => props.theme.palette.text.primary}; + font-size: 0.92rem; + font-weight: 850; + line-height: 1.2; +} +.mythic-eventing-step-config-summary-subtitle { + color: ${(props) => props.theme.palette.text.secondary}; + font-size: 0.74rem; + font-weight: 600; + line-height: 1.35; + margin-top: 0.16rem; +} +.mythic-eventing-step-config-summary-actions { + align-items: flex-start; + display: flex; + flex: 0 0 auto; + gap: 0.55rem; +} +.mythic-eventing-step-action-chip { + align-items: center; + background-color: ${(props) => props.theme.palette.info.main + "14"}; + border: 1px solid ${(props) => props.theme.palette.info.main + "45"}; + border-radius: ${(props) => props.theme.shape.borderRadius}px; + color: ${(props) => props.theme.palette.text.primary}; + display: inline-flex; + font-size: 0.72rem; + font-weight: 800; + line-height: 1.2; + min-height: 1.65rem; + padding: 0 0.55rem; +} +.mythic-eventing-step-switch-row { + align-items: center; + background-color: ${(props) => props.theme.palette.mode === "dark" ? "rgba(255,255,255,0.03)" : "rgba(0,0,0,0.018)"}; + border: 1px solid ${(props) => props.theme.table?.borderSoft || props.theme.borderColor}; + border-radius: ${(props) => props.theme.shape.borderRadius}px; + cursor: pointer; + display: flex; + gap: 0.55rem; + min-width: 16rem; + padding: 0.3rem 0.35rem 0.3rem 0.6rem; +} +.mythic-eventing-step-switch-copy { + display: flex; + flex: 1 1 auto; + flex-direction: column; + min-width: 0; +} +.mythic-eventing-step-switch-title { + color: ${(props) => props.theme.palette.text.primary}; + font-size: 0.72rem; + font-weight: 850; + line-height: 1.2; +} +.mythic-eventing-step-switch-subtitle { + color: ${(props) => props.theme.palette.text.secondary}; + font-size: 0.66rem; + font-weight: 600; + line-height: 1.25; + margin-top: 0.08rem; +} +.mythic-eventing-step-config-content { + display: flex; + flex-direction: column; + gap: 0.75rem; + padding: 0.75rem; +} +.mythic-eventing-step-config-section { + background-color: ${(props) => props.theme.palette.mode === "dark" ? "rgba(255,255,255,0.018)" : "rgba(0,0,0,0.01)"}; + border: 1px solid ${(props) => props.theme.table?.borderSoft || props.theme.borderColor}; + border-radius: ${(props) => props.theme.shape.borderRadius}px; + min-width: 0; + overflow: hidden; +} +.mythic-eventing-step-config-section-header { + background-color: ${(props) => props.theme.palette.mode === "dark" ? "rgba(255,255,255,0.025)" : "rgba(0,0,0,0.012)"}; + border-bottom: 1px solid ${(props) => props.theme.table?.borderSoft || props.theme.borderColor}; + padding: 0.62rem 0.7rem 0.55rem; +} +.mythic-eventing-step-config-section-title { + color: ${(props) => props.theme.palette.text.primary}; + font-size: 0.82rem; + font-weight: 850; + line-height: 1.2; +} +.mythic-eventing-step-config-section-subtitle { + color: ${(props) => props.theme.palette.text.secondary}; + font-size: 0.72rem; + font-weight: 600; + line-height: 1.35; + margin-top: 0.14rem; +} +.mythic-eventing-step-config-section-body { + padding: 0.7rem; +} +.mythic-eventing-step-section-grid, +.mythic-eventing-step-field-grid { + display: grid; + gap: 0.75rem; + grid-template-columns: repeat(2, minmax(0, 1fr)); + min-width: 0; +} +.mythic-eventing-step-config-section-wide { + width: 100%; +} +.mythic-eventing-step-field { + min-width: 0; +} +.mythic-eventing-step-field-heading { + align-items: center; + display: flex; + flex-wrap: wrap; + gap: 0.35rem; + margin-bottom: 0.35rem; +} +.mythic-eventing-step-field-label { + color: ${(props) => props.theme.palette.text.secondary}; + font-size: 0.72rem; + font-weight: 850; + line-height: 1.2; +} +.mythic-eventing-step-field-required { + align-items: center; + background-color: ${(props) => props.theme.palette.warning.main + "16"}; + border: 1px solid ${(props) => props.theme.palette.warning.main + "45"}; + border-radius: ${(props) => props.theme.shape.borderRadius}px; + color: ${(props) => props.theme.palette.warning.main}; + display: inline-flex; + font-size: 0.64rem; + font-weight: 850; + line-height: 1; + min-height: 1.2rem; + padding: 0 0.35rem; +} +.mythic-eventing-step-field-description { + color: ${(props) => props.theme.palette.text.secondary}; + font-size: 0.7rem; + font-weight: 600; + line-height: 1.35; + margin-bottom: 0.45rem; +} +.mythic-eventing-step-field-control { + min-width: 0; +} +.mythic-eventing-step-dynamic-section { + align-items: flex-start; + display: flex; + flex-direction: column; + gap: 0.55rem; +} +.mythic-eventing-step-list { + display: flex; + flex-direction: column; + gap: 0.5rem; + min-width: 0; + width: 100%; +} +.mythic-eventing-step-list-item { + align-items: flex-start; + background-color: ${(props) => props.theme.palette.mode === "dark" ? "rgba(255,255,255,0.02)" : "rgba(0,0,0,0.012)"}; + border: 1px solid ${(props) => props.theme.table?.borderSoft || props.theme.borderColor}; + border-radius: ${(props) => props.theme.shape.borderRadius}px; + display: flex; + gap: 0.45rem; + min-width: 0; + padding: 0.5rem; + width: 100%; +} +.mythic-eventing-step-list-item > .MuiIconButton-root { + flex: 0 0 auto; + margin-top: 0.15rem; +} +.mythic-eventing-step-list-content { + flex: 1 1 auto; + min-width: 0; +} +.mythic-eventing-step-input-grid { + display: grid; + gap: 0.5rem; + grid-template-columns: minmax(9rem, 0.75fr) minmax(10rem, 0.8fr) minmax(14rem, 1.45fr); + min-width: 0; +} +.mythic-eventing-step-output-grid { + display: grid; + gap: 0.5rem; + grid-template-columns: minmax(9rem, 0.75fr) minmax(14rem, 1.5fr); + min-width: 0; +} +.mythic-eventing-step-helper-text { + color: ${(props) => props.theme.palette.text.secondary}; + font-size: 0.68rem; + font-weight: 600; + line-height: 1.35; + margin-top: 0.4rem; +} +.mythic-eventing-step-empty-inline { + align-items: center; + background-color: ${(props) => props.theme.palette.action.disabledBackground}; + border: 1px dashed ${(props) => props.theme.table?.borderSoft || props.theme.borderColor}; + border-radius: ${(props) => props.theme.shape.borderRadius}px; + color: ${(props) => props.theme.palette.text.secondary}; + display: flex; + font-size: 0.74rem; + font-weight: 700; + min-height: 2.3rem; + padding: 0 0.65rem; +} +.mythic-eventing-step-action-data { + min-width: 0; +} +.mythic-eventing-step-action-data > .MuiTypography-root { + background-color: ${(props) => props.theme.palette.info.main + "10"}; + border: 1px solid ${(props) => props.theme.palette.info.main + "2f"}; + border-radius: ${(props) => props.theme.shape.borderRadius}px; + color: ${(props) => props.theme.palette.text.secondary}; + font-size: 0.74rem; + font-weight: 600; + line-height: 1.4; + margin: 0 0 0.65rem; + padding: 0.55rem 0.65rem; +} +.mythic-eventing-step-action-data > .MuiTable-root { + border-collapse: separate !important; + border-spacing: 0 0.5rem !important; + table-layout: fixed; + width: 100%; +} +.mythic-eventing-step-action-data > .MuiTable-root > .MuiTableHead-root { + display: none; +} +.mythic-eventing-step-action-data > .MuiTable-root > .MuiTableBody-root > .MuiTableRow-root { + background-color: transparent; +} +.mythic-eventing-step-action-data > .MuiTable-root > .MuiTableBody-root > .MuiTableRow-root > .MuiTableCell-root { + background-color: ${(props) => props.theme.palette.mode === "dark" ? "rgba(255,255,255,0.018)" : "rgba(0,0,0,0.01)"}; + border-bottom: 1px solid ${(props) => props.theme.table?.borderSoft || props.theme.borderColor} !important; + border-top: 1px solid ${(props) => props.theme.table?.borderSoft || props.theme.borderColor} !important; + padding: 0.62rem 0.7rem !important; + vertical-align: top; +} +.mythic-eventing-step-action-data > .MuiTable-root > .MuiTableBody-root > .MuiTableRow-root > .MuiTableCell-root:first-of-type { + border-left: 1px solid ${(props) => props.theme.table?.borderSoft || props.theme.borderColor} !important; + border-radius: ${(props) => props.theme.shape.borderRadius}px 0 0 ${(props) => props.theme.shape.borderRadius}px; + width: 34%; +} +.mythic-eventing-step-action-data > .MuiTable-root > .MuiTableBody-root > .MuiTableRow-root > .MuiTableCell-root:last-of-type { + border-radius: 0 ${(props) => props.theme.shape.borderRadius}px ${(props) => props.theme.shape.borderRadius}px 0; + border-right: 1px solid ${(props) => props.theme.table?.borderSoft || props.theme.borderColor} !important; + width: 66%; +} +.mythic-eventing-step-action-data > .MuiTable-root > .MuiTableBody-root > .MuiTableRow-root > .MuiTableCell-root:first-of-type .MuiTypography-root:first-of-type { + color: ${(props) => props.theme.palette.text.primary}; + font-size: 0.76rem; + font-weight: 850 !important; + line-height: 1.25; +} +.mythic-eventing-step-action-data > .MuiTable-root > .MuiTableBody-root > .MuiTableRow-root > .MuiTableCell-root:first-of-type .MuiTypography-root:not(:first-of-type) { + color: ${(props) => props.theme.palette.text.secondary}; + font-size: 0.7rem; + font-weight: 600; + line-height: 1.35; + margin-top: 0.2rem; +} +.mythic-eventing-step-config-card .mythic-eventing-wizard-table { + border-spacing: 0 !important; +} +.mythic-eventing-step-config-card .mythic-eventing-wizard-table .MuiTableRow-root { + background-color: transparent; +} +.mythic-eventing-step-config-card .mythic-eventing-wizard-table .MuiTableCell-root { + border-left: 0 !important; + border-radius: 0 !important; + border-right: 0 !important; + border-top: 0 !important; +} +@media (max-width: 1200px) { + .mythic-eventing-step-input-grid { + grid-template-columns: 1fr; + } +} +@media (max-width: 900px) { + .mythic-eventing-step-config-summary, + .mythic-eventing-step-config-summary-actions { + flex-direction: column; + } + .mythic-eventing-step-config-summary-actions, + .mythic-eventing-step-switch-row { + width: 100%; + } + .mythic-eventing-step-section-grid, + .mythic-eventing-step-field-grid, + .mythic-eventing-step-output-grid { + grid-template-columns: 1fr; + } +} +.mythic-eventing-wizard-empty { + color: ${(props) => props.theme.palette.text.secondary}; + font-size: 0.82rem; + font-weight: 650; + padding: 1.4rem; + text-align: center; +} +.mythic-eventing-wizard-empty-title { + color: ${(props) => props.theme.palette.text.primary}; + font-size: 0.92rem; + font-weight: 850; + margin-bottom: 0.25rem; +} +.mythic-eventing-wizard-empty-subtitle { + color: ${(props) => props.theme.palette.text.secondary}; + font-size: 0.76rem; + font-weight: 650; +} +.mythic-eventing-wizard-review-card { + display: flex; + flex-direction: column; + height: 100%; + min-height: 0; +} +.mythic-eventing-wizard-review-toolbar { + align-items: center; + border-bottom: 1px solid ${(props) => props.theme.table?.borderSoft || props.theme.borderColor}; + display: flex; + flex-wrap: wrap; + gap: 0.5rem; + padding: 0.65rem; +} +.mythic-eventing-wizard-editor { + flex: 1 1 auto; + min-height: 0; + overflow: hidden; +} +.mythic-eventing-wizard-editor > div { + height: 100%; +} +.mythic-eventing-wizard-actions { + border-top: 1px solid ${(props) => props.theme.table?.borderSoft || props.theme.borderColor}; + gap: 0.45rem; + margin: 0 !important; + padding: 0.65rem 1rem !important; +} +.mythic-eventing-editor-dialog-title { + align-items: flex-start; + border-bottom: 1px solid ${(props) => props.theme.table?.borderSoft || props.theme.borderColor}; + display: flex; + gap: 1rem; + justify-content: space-between; + padding: 0.85rem 1rem !important; +} +.mythic-eventing-editor-title { + color: ${(props) => props.theme.palette.text.primary}; + font-size: 1rem; + font-weight: 850; + line-height: 1.2; +} +.mythic-eventing-editor-subtitle { + color: ${(props) => props.theme.palette.text.secondary}; + font-size: 0.76rem; + font-weight: 600; + line-height: 1.35; + margin-top: 0.15rem; +} +.mythic-eventing-editor-title-actions { + display: flex; + flex: 0 0 auto; + gap: 0.35rem; +} +.mythic-eventing-editor-dialog-content { + height: min(84vh, 800px); + margin: 0 !important; + overflow: hidden; + padding: 0 !important; +} +.mythic-eventing-editor-dialog-content > div { + height: 100%; +} +.mythic-eventing-list-item .MuiListItemText-primary { + font-size: 0.84rem; + font-weight: 700; + line-height: 1.25; +} +.mythic-eventing-detail { + display: flex; + flex-direction: column; + gap: 0.5rem; + height: 100%; + margin-left: 0 !important; + min-height: 0; + min-width: 0; + overflow: auto; + padding: 0.5rem; +} +.mythic-eventing-graph-panel { + background-color: ${(props) => props.theme.surfaces?.muted || props.theme.palette.background.default}; + border: 1px solid ${(props) => props.theme.table?.borderSoft || props.theme.borderColor}; + border-radius: ${(props) => props.theme.shape.borderRadius}px; + flex: 0 0 200px; + min-height: 200px; + overflow: hidden; +} +.mythic-eventing-flow-canvas { + background-color: ${(props) => props.theme.surfaces?.muted || props.theme.palette.background.default}; + height: 100%; + min-height: 0; + position: relative; + width: 100%; +} +.mythic-eventing-flow-canvas .react-flow__pane { + cursor: grab; +} +.mythic-eventing-flow-badge { + align-items: center; + background-color: ${(props) => props.theme.palette.background.paper}; + border: 1px solid ${(props) => props.theme.table?.borderSoft || props.theme.borderColor}; + border-radius: ${(props) => props.theme.shape.borderRadius}px; + box-shadow: 0 6px 18px ${(props) => alpha(props.theme.palette.common.black, props.theme.palette.mode === "dark" ? 0.22 : 0.08)}; + color: ${(props) => props.theme.palette.text.secondary}; + display: inline-flex; + font-size: 0.72rem; + font-weight: 800; + left: 0.75rem; + line-height: 1; + min-height: 1.75rem; + padding: 0 0.55rem; + position: absolute; + top: 0.75rem; + z-index: 5; +} +.mythic-eventing-flow-controls { + background-color: ${(props) => props.theme.palette.background.paper}; + border: 1px solid ${(props) => props.theme.table?.borderSoft || props.theme.borderColor}; + border-radius: ${(props) => props.theme.shape.borderRadius}px; + box-shadow: 0 6px 18px ${(props) => alpha(props.theme.palette.common.black, props.theme.palette.mode === "dark" ? 0.22 : 0.08)} !important; + overflow: hidden; +} +.mythic-eventing-flow-controls .react-flow__controls-button { + background-color: ${(props) => props.theme.palette.background.paper}; + border-bottom-color: ${(props) => props.theme.table?.borderSoft || props.theme.borderColor}; + color: ${(props) => props.theme.palette.text.secondary}; +} +.mythic-eventing-flow-controls .react-flow__controls-button:hover { + background-color: ${(props) => props.theme.tableHover}; + color: ${(props) => props.theme.palette.text.primary}; +} +.mythic-eventing-flow-canvas .react-flow__node-eventNode { + background: transparent !important; + border: 0 !important; + box-sizing: border-box; + box-shadow: none !important; + padding: 0 !important; +} +.mythic-eventing-flow-canvas .react-flow__node-eventNode.selected, +.mythic-eventing-flow-canvas .react-flow__node-eventNode:focus, +.mythic-eventing-flow-canvas .react-flow__node-eventNode:focus-visible { + box-shadow: none !important; +} +.mythic-eventing-flow-canvas .groupEventNode { + background: transparent !important; + border: 0 !important; + padding: 0 !important; +} +.mythic-eventing-flow-node { + background-color: ${(props) => props.theme.palette.background.paper}; + border: 1px solid ${(props) => props.theme.table?.borderSoft || props.theme.borderColor}; + border-radius: ${(props) => props.theme.shape.borderRadius}px; + box-shadow: 0 8px 22px ${(props) => alpha(props.theme.palette.common.black, props.theme.palette.mode === "dark" ? 0.24 : 0.09)}; + color: ${(props) => props.theme.palette.text.primary}; + display: flex; + flex-direction: column; + height: 100%; + justify-content: center; + min-width: 0; + overflow: hidden; + padding: 0.5rem 0.58rem; + width: 100%; +} +.mythic-eventing-flow-node-success { + border-color: ${(props) => props.theme.palette.success.main + "55"}; +} +.mythic-eventing-flow-node-running { + border-color: ${(props) => props.theme.palette.info.main + "55"}; +} +.mythic-eventing-flow-node-error { + border-color: ${(props) => props.theme.palette.error.main + "66"}; +} +.mythic-eventing-flow-node-cancelled { + border-color: ${(props) => props.theme.palette.warning.main + "60"}; +} +.mythic-eventing-flow-node-skipped { + border-color: ${(props) => alpha(props.theme.palette.text.secondary, 0.35)}; +} +.mythic-eventing-flow-node-waiting, +.mythic-eventing-flow-node-configured { + border-color: ${(props) => props.theme.table?.border || props.theme.borderColor}; +} +.mythic-eventing-flow-node-main { + display: flex; + flex-direction: column; + gap: 0.35rem; + min-width: 0; +} +.mythic-eventing-flow-node-title { + color: ${(props) => props.theme.palette.text.primary}; + font-size: 0.82rem !important; + font-weight: 850 !important; + line-height: 1.18 !important; + margin: 0 !important; + max-width: 15rem; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} +.mythic-eventing-flow-node-meta { + align-items: center; + color: ${(props) => props.theme.palette.text.secondary}; + display: flex; + flex-wrap: wrap; + gap: 0.4rem; + justify-content: space-between; + margin-top: 0.42rem; +} +.mythic-eventing-flow-node-meta .MuiTypography-root { + color: ${(props) => props.theme.palette.text.secondary} !important; + float: none !important; + font-size: 0.68rem !important; + line-height: 1.2; +} +.mythic-eventing-flow-node-action { + align-items: center; + background-color: ${(props) => props.theme.palette.mode === "dark" ? "rgba(255,255,255,0.04)" : "rgba(0,0,0,0.035)"}; + border: 1px solid ${(props) => props.theme.table?.borderSoft || props.theme.borderColor}; + border-radius: ${(props) => props.theme.shape.borderRadius}px; + color: ${(props) => props.theme.palette.text.secondary}; + display: inline-flex; + font-size: 0.66rem; + font-weight: 750; + line-height: 1; + min-height: 1.28rem; + padding: 0 0.38rem; +} +.mythic-eventing-status-chip { + align-items: center; + background-color: ${(props) => props.theme.palette.action.disabledBackground}; + border: 1px solid ${(props) => props.theme.table?.borderSoft || props.theme.borderColor}; + border-radius: ${(props) => props.theme.shape.borderRadius}px; + color: ${(props) => props.theme.palette.text.secondary}; + display: inline-flex; + font-size: 0.7rem; + font-weight: 850; + line-height: 1; + min-height: 1.45rem; + padding: 0 0.45rem; + width: fit-content; +} +.mythic-eventing-status-chip .MuiSvgIcon-root { + font-size: 0.95rem !important; + margin: 0 0.28rem 0 0 !important; +} +.mythic-eventing-status-chip-success { + background-color: ${(props) => props.theme.palette.success.main + "16"}; + border-color: ${(props) => props.theme.palette.success.main + "45"}; + color: ${(props) => props.theme.palette.success.main}; +} +.mythic-eventing-status-chip-running { + background-color: ${(props) => props.theme.palette.info.main + "16"}; + border-color: ${(props) => props.theme.palette.info.main + "45"}; + color: ${(props) => props.theme.palette.info.main}; +} +.mythic-eventing-status-chip-error { + background-color: ${(props) => props.theme.palette.error.main + "16"}; + border-color: ${(props) => props.theme.palette.error.main + "50"}; + color: ${(props) => props.theme.palette.error.main}; +} +.mythic-eventing-status-chip-cancelled { + background-color: ${(props) => props.theme.palette.warning.main + "18"}; + border-color: ${(props) => props.theme.palette.warning.main + "52"}; + color: ${(props) => props.theme.palette.warning.main}; +} +.mythic-eventing-status-chip-skipped, +.mythic-eventing-status-chip-configured { + background-color: ${(props) => props.theme.palette.mode === "dark" ? "rgba(255,255,255,0.045)" : "rgba(0,0,0,0.035)"}; + border-color: ${(props) => props.theme.table?.border || props.theme.borderColor}; + color: ${(props) => props.theme.palette.text.secondary}; +} +.mythic-eventing-status-chip-waiting { + background-color: ${(props) => props.theme.palette.action.disabledBackground}; + border-color: ${(props) => props.theme.palette.action.disabled}; + color: ${(props) => props.theme.palette.text.secondary}; +} +.mythic-eventing-detail-chip-row { + display: flex; + flex-wrap: wrap; + gap: 0.35rem; + margin-top: 0.55rem; +} +.mythic-eventing-detail-chip { + align-items: center; + background-color: ${(props) => props.theme.palette.mode === "dark" ? "rgba(255,255,255,0.08)" : "rgba(255,255,255,0.72)"}; + border: 1px solid ${(props) => alpha(props.theme.pageHeaderText?.main || props.theme.palette.text.primary, 0.22)}; + border-radius: ${(props) => props.theme.shape.borderRadius}px; + color: ${(props) => props.theme.pageHeaderText?.main || props.theme.palette.text.primary}; + display: inline-flex; + font-size: 0.72rem; + font-weight: 800; + gap: 0.35rem; + line-height: 1; + min-height: 1.55rem; + padding: 0 0.48rem; +} +.mythic-eventing-detail-chip-label { + color: inherit; + opacity: 0.72; +} +.mythic-eventing-detail-chip-value { + color: inherit; + max-width: 14rem; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} +.mythic-eventing-dialog-header { + padding: 0.75rem 0.75rem 0.5rem; +} +.mythic-eventing-header-title { + align-items: center; + color: inherit; + display: inline-flex; + flex-wrap: wrap; + gap: 0.5rem; + min-width: 0; +} +.mythic-eventing-header-title > span:last-child { + min-width: 0; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} +.mythic-eventing-header-duration { + align-items: flex-end; + color: inherit; + display: inline-flex; + flex-direction: column; + gap: 0.2rem; + line-height: 1.2; + text-align: right; +} +.mythic-eventing-header-duration-label { + color: inherit; + font-size: 0.68rem; + font-weight: 850; + opacity: 0.7; +} +.mythic-eventing-header-duration .MuiTypography-root { + color: inherit !important; + float: none !important; + font-size: 0.78rem !important; + font-weight: 800; +} +.mythic-eventing-render-dialog-content { + height: min(75vh, 720px); + margin: 0 !important; + min-height: 24rem; + overflow: hidden; + padding: 0 !important; +} +.mythic-eventing-render-dialog-content .mythic-eventing-flow-canvas { + height: 100%; +} +.mythic-eventing-detail-dialog-content { + background-color: ${(props) => props.theme.palette.background.default}; + display: flex; + flex-direction: column; + gap: 0.75rem; + height: min(75vh, 760px); + min-height: 24rem; + overflow: auto; + padding: 0.75rem !important; +} +.mythic-eventing-detail-dialog-content-state { + align-items: stretch; + justify-content: center; +} +.mythic-eventing-detail-dialog-content > .mythic-eventing-detail-section { + flex: 0 0 auto; +} +.mythic-eventing-detail-dialog-actions { + background-color: ${(props) => props.theme.palette.background.paper}; + border-top: 1px solid ${(props) => props.theme.table?.borderSoft || props.theme.borderColor}; + gap: 0.45rem; + margin: 0 !important; + padding: 0.65rem 1rem !important; +} +.mythic-eventing-detail-section { + background-color: ${(props) => props.theme.palette.background.paper}; + border: 1px solid ${(props) => props.theme.table?.borderSoft || props.theme.borderColor}; + border-radius: ${(props) => props.theme.shape.borderRadius}px; + min-width: 0; + overflow: hidden; +} +.mythic-eventing-detail-section-header { + align-items: center; + background-color: ${(props) => props.theme.palette.mode === "dark" ? "rgba(255,255,255,0.035)" : "rgba(0,0,0,0.018)"}; + border-bottom: 1px solid ${(props) => props.theme.table?.borderSoft || props.theme.borderColor}; + display: flex; + gap: 0.75rem; + justify-content: space-between; + min-height: 2.5rem; + padding: 0.62rem 0.75rem; +} +.mythic-eventing-detail-section-collapsed .mythic-eventing-detail-section-header { + border-bottom: 0; +} +.mythic-eventing-detail-section-title-stack { + display: flex; + flex-direction: column; + min-width: 0; +} +.mythic-eventing-detail-section-toggle { + align-items: center; + background: transparent; + border: 0; + color: ${(props) => props.theme.palette.text.primary}; + cursor: pointer; + display: flex; + flex: 1 1 auto; + font: inherit; + gap: 0.45rem; + margin: -0.25rem 0; + min-width: 0; + outline: none; + padding: 0.25rem 0; + text-align: left; +} +.mythic-eventing-detail-section-toggle:focus-visible .mythic-eventing-detail-section-title { + color: ${(props) => props.theme.palette.primary.main}; + text-decoration: underline; + text-underline-offset: 0.18rem; +} +.mythic-eventing-detail-section-toggle:hover .mythic-eventing-detail-section-title { + color: ${(props) => props.theme.palette.primary.main}; +} +.mythic-eventing-detail-section-toggle-icon { + color: ${(props) => props.theme.palette.text.secondary}; + flex: 0 0 auto; + transition: transform 140ms ease, color 140ms ease; +} +.mythic-eventing-detail-section-expanded .mythic-eventing-detail-section-toggle-icon { + color: ${(props) => props.theme.palette.primary.main}; + transform: rotate(180deg); +} +.mythic-eventing-detail-section-title { + color: ${(props) => props.theme.palette.text.primary}; + font-size: 0.86rem; + font-weight: 850; + line-height: 1.2; +} +.mythic-eventing-detail-section-subtitle { + color: ${(props) => props.theme.palette.text.secondary}; + font-size: 0.72rem; + font-weight: 600; + line-height: 1.32; + margin-top: 0.14rem; +} +.mythic-eventing-detail-section-actions { + align-items: center; + display: flex; + flex: 0 0 auto; + gap: 0.4rem; + justify-content: flex-end; +} +.mythic-eventing-detail-section-body { + min-width: 0; + padding: 0.72rem; +} +.mythic-eventing-section-empty { + background-color: ${(props) => props.theme.palette.mode === "dark" ? "rgba(255,255,255,0.018)" : "rgba(0,0,0,0.01)"}; + border: 1px dashed ${(props) => props.theme.table?.borderSoft || props.theme.borderColor}; + border-radius: ${(props) => props.theme.shape.borderRadius}px; + overflow: hidden; +} +.mythic-eventing-detail-count { + align-items: center; + border-radius: ${(props) => props.theme.shape.borderRadius}px; + display: inline-flex; + font-size: 0.72rem; + font-weight: 850; + justify-content: center; + line-height: 1; + min-height: 1.45rem; + min-width: 1.45rem; + padding: 0 0.4rem; +} +.mythic-eventing-detail-count-active { + background-color: ${(props) => props.theme.palette.primary.main + "16"}; + border: 1px solid ${(props) => props.theme.palette.primary.main + "45"}; + color: ${(props) => props.theme.palette.text.primary}; +} +.mythic-eventing-detail-count-empty { + background-color: ${(props) => props.theme.palette.mode === "dark" ? "rgba(255,255,255,0.025)" : "rgba(0,0,0,0.018)"}; + border: 1px solid ${(props) => props.theme.table?.borderSoft || props.theme.borderColor}; + color: ${(props) => props.theme.palette.text.secondary}; + font-weight: 750; + opacity: 0.62; +} +.mythic-eventing-detail-accordion { + background-color: ${(props) => props.theme.palette.background.paper} !important; + border: 1px solid ${(props) => props.theme.table?.borderSoft || props.theme.borderColor} !important; + border-radius: ${(props) => props.theme.shape.borderRadius}px !important; + box-shadow: none !important; + margin: 0 !important; + overflow: hidden; +} +.mythic-eventing-detail-accordion:before { + display: none; +} +.mythic-eventing-detail-accordion + .mythic-eventing-detail-accordion { + margin-top: 0.5rem !important; +} +.mythic-eventing-detail-accordion .MuiAccordionSummary-root { + background-color: ${(props) => props.theme.palette.mode === "dark" ? "rgba(255,255,255,0.025)" : "rgba(0,0,0,0.012)"}; + min-height: 2.4rem !important; + padding: 0 0.7rem !important; +} +.mythic-eventing-detail-accordion .MuiAccordionSummary-content { + color: ${(props) => props.theme.palette.text.primary}; + font-size: 0.78rem; + font-weight: 850; + margin: 0.48rem 0 !important; +} +.mythic-eventing-detail-accordion .MuiAccordionDetails-root { + border-top: 1px solid ${(props) => props.theme.table?.borderSoft || props.theme.borderColor}; + padding: 0.7rem !important; +} +.mythic-eventing-metadata-accordion .MuiAccordionDetails-root { + max-height: min(48vh, 30rem); + overflow: auto; +} +.mythic-eventing-metadata-comparison { + display: flex; + flex-direction: column; + gap: 0.65rem; +} +.mythic-eventing-metadata-comparison-single .mythic-eventing-metadata-pair-grid { + grid-template-columns: 1fr; +} +.mythic-eventing-metadata-pair { + min-width: 0; +} +.mythic-eventing-metadata-pair-title { + color: ${(props) => props.theme.palette.text.secondary}; + font-size: 0.72rem; + font-weight: 850; + line-height: 1.2; + margin-bottom: 0.35rem; +} +.mythic-eventing-metadata-pair-grid, +.mythic-eventing-metadata-static-grid { + display: grid; + gap: 0.55rem; + grid-template-columns: repeat(2, minmax(0, 1fr)); + min-width: 0; +} +.mythic-eventing-metadata-panel { + background-color: ${(props) => props.theme.palette.mode === "dark" ? "rgba(255,255,255,0.02)" : "rgba(0,0,0,0.012)"}; + border: 1px solid ${(props) => props.theme.table?.borderSoft || props.theme.borderColor}; + border-radius: ${(props) => props.theme.shape.borderRadius}px; + min-width: 0; + overflow: hidden; +} +.mythic-eventing-metadata-panel-title { + background-color: ${(props) => props.theme.palette.mode === "dark" ? "rgba(255,255,255,0.03)" : "rgba(0,0,0,0.018)"}; + border-bottom: 1px solid ${(props) => props.theme.table?.borderSoft || props.theme.borderColor}; + color: ${(props) => props.theme.palette.text.secondary}; + font-size: 0.68rem; + font-weight: 850; + line-height: 1.2; + padding: 0.45rem 0.55rem; +} +.mythic-eventing-code-block { + background-color: ${(props) => props.theme.palette.mode === "dark" ? "rgba(0,0,0,0.24)" : "rgba(255,255,255,0.72)"}; + color: ${(props) => props.theme.palette.text.primary}; + font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace; + font-size: 0.72rem; + line-height: 1.42; + margin: 0; + max-height: 18rem; + min-height: 3.4rem; + overflow: auto; + padding: 0.62rem; + white-space: pre-wrap; + word-break: break-word; +} +.mythic-eventing-code-block-empty { + align-items: center; + color: ${(props) => props.theme.palette.text.secondary}; + display: flex; + font-family: ${(props) => props.theme.typography.fontFamily}; + font-size: 0.74rem; + font-weight: 700; +} +.mythic-eventing-detail-table-wrap { + max-height: 18rem; + overflow: auto; +} +.mythic-eventing-detail-table-wrap .MuiTable-root { + min-width: 650px; +} +.mythic-eventing-detail-table-wrap .MuiTableCell-root { + vertical-align: middle; +} +.mythic-eventing-resource-link { + word-break: break-all; +} +.mythic-eventing-resource-secondary { + color: ${(props) => props.theme.palette.text.secondary}; + font-size: 0.78rem; + font-weight: 650; + line-height: 1.35; +} +.mythic-eventing-resource-command { + color: ${(props) => props.theme.palette.text.primary}; + font-weight: 850; +} +@media (max-width: 900px) { + .mythic-eventing-header-duration { + align-items: flex-start; + text-align: left; + } + .mythic-eventing-metadata-pair-grid, + .mythic-eventing-metadata-static-grid { + grid-template-columns: 1fr; + } +} +.mythic-service-actions { + gap: 0.55rem; +} +.mythic-split-action-group { + box-shadow: none !important; + display: inline-flex; + overflow: hidden; +} +.mythic-split-action-group .MuiButton-root, +.mythic-split-action-group .MuiIconButton-root { + border-radius: 0 !important; + height: 30px; + margin-left: -1px; +} +.mythic-split-action-group .MuiButton-root:first-of-type, +.mythic-split-action-group .MuiIconButton-root:first-of-type { + border-bottom-left-radius: ${(props) => props.theme.shape.borderRadius}px !important; + border-top-left-radius: ${(props) => props.theme.shape.borderRadius}px !important; + margin-left: 0; +} +.mythic-split-action-group .MuiButton-root:last-of-type, +.mythic-split-action-group .MuiIconButton-root:last-of-type { + border-bottom-right-radius: ${(props) => props.theme.shape.borderRadius}px !important; + border-top-right-radius: ${(props) => props.theme.shape.borderRadius}px !important; +} +.mythic-split-action-group .mythic-table-row-action { + min-width: 7.5rem; +} +.mythic-split-action-group .mythic-table-row-icon-action { + width: 34px; +} +.mythic-split-action-group .mythic-table-row-icon-action-success { + background-color: ${(props) => props.theme.palette.success.main + "1c"} !important; + border-color: ${(props) => props.theme.palette.success.main + "66"} !important; +} +.mythic-split-action-group .mythic-table-row-icon-action-success:hover { + background-color: ${(props) => props.theme.palette.success.main + "2b"} !important; + border-color: ${(props) => props.theme.palette.success.main + "88"} !important; +} +.mythic-split-action-group .mythic-table-row-icon-action-danger { + background-color: ${(props) => props.theme.palette.error.main + "1c"} !important; + border-color: ${(props) => props.theme.palette.error.main + "66"} !important; +} +.mythic-split-action-group .mythic-table-row-icon-action-danger:hover { + background-color: ${(props) => props.theme.palette.error.main + "2b"} !important; + border-color: ${(props) => props.theme.palette.error.main + "88"} !important; +} +.mythic-table-row-icon-action.Mui-disabled { + background-color: ${(props) => props.theme.palette.mode === "dark" ? "rgba(255,255,255,0.03)" : "rgba(0,0,0,0.02)"} !important; + border-color: ${(props) => props.theme.table?.borderSoft || props.theme.borderColor} !important; + color: ${(props) => props.theme.palette.text.disabled} !important; +} +.mythic-menu-item-hover-danger .MuiSvgIcon-root { + color: inherit !important; +} +.mythic-menu-item-hover-danger:hover, +.mythic-menu-item-hover-danger.Mui-focusVisible { + background-color: ${(props) => props.theme.palette.error.main + "16"} !important; + color: ${(props) => props.theme.palette.error.main} !important; +} +.mythic-status-stack { + align-items: center; + display: flex; + flex-wrap: wrap; + gap: 0.35rem; + min-width: 0; +} +.mythic-dialog-switch-row { + align-items: center; + display: flex; + justify-content: space-between; + margin: 0; + width: 100%; +} +.mythic-dialog-preview { + align-items: center; + border: 1px solid ${(props) => props.theme.table?.borderSoft || props.theme.borderColor}; + border-radius: ${(props) => props.theme.shape.borderRadius}px; + display: flex; + min-height: 38px; + min-width: 0; + padding: 0 0.75rem; +} +.mythic-dialog-file-target { + align-items: center; + background-color: ${(props) => props.theme.palette.background.paper}; + border: 1px dashed ${(props) => props.theme.table?.border || props.theme.borderColor}; + border-radius: ${(props) => props.theme.shape.borderRadius}px; + display: flex; + justify-content: center; + min-height: 7rem; + padding: 0.75rem; +} +.mythic-dialog-table-wrap { + max-height: min(45vh, 28rem); +} +.mythic-transfer-section { + margin-top: 0.75rem; +} +.mythic-transfer-list { + background-color: ${(props) => props.theme.palette.background.paper}; + border: 1px solid ${(props) => props.theme.table?.borderSoft || props.theme.borderColor}; + border-radius: ${(props) => props.theme.shape.borderRadius}px; + display: flex; + flex-direction: column; + min-height: 16rem; + min-width: 0; + overflow: hidden; + width: 100%; +} +.mythic-transfer-list-header { + background-color: ${(props) => props.theme.table?.header || props.theme.tableHeader}; + border-bottom: 1px solid ${(props) => props.theme.table?.border || props.theme.borderColor}; + color: ${(props) => props.theme.palette.text.primary}; + font-size: 0.76rem; + font-weight: 700; + line-height: 1.25; + padding: 0.45rem 0.6rem; + text-transform: uppercase; +} +.mythic-transfer-list-body { + flex: 1 1 auto; + max-height: 30vh; + min-height: 12rem; + overflow: auto; +} +.mythic-transfer-list .MuiList-root { + background: transparent; + border: 0; + border-radius: 0; +} +.mythic-transfer-controls { + align-items: center; + display: flex; + flex-direction: column; + gap: 0.25rem; + justify-content: center; + padding: 0 0.5rem; +} +.mythic-transfer-controls .MuiButton-root { + background-color: ${(props) => props.theme.palette.mode === "dark" ? "rgba(255,255,255,0.06)" : "rgba(0,0,0,0.035)"} !important; + border: 1px solid ${(props) => props.theme.table?.borderSoft || props.theme.borderColor} !important; + border-radius: ${(props) => props.theme.shape.borderRadius}px; + box-shadow: none !important; + color: ${(props) => props.theme.palette.text.primary} !important; + min-height: 30px; + min-width: 34px; + padding: 0.25rem 0.45rem; +} +.mythic-transfer-controls .MuiButton-root:hover { + background-color: ${(props) => props.theme.palette.mode === "dark" ? "rgba(255,255,255,0.1)" : "rgba(0,0,0,0.06)"} !important; +} +@media screen and (max-width: 700px) { + .mythic-dialog-choice-row { + grid-template-columns: 1fr; + } + .mythic-dialog-choice-divider { + text-align: left; + } +} .MuiTabs-root { min-height: 34px; }