updating operator settings and virtualized tables

This commit is contained in:
its-a-feature
2026-05-06 09:05:11 -05:00
parent 0580100dd8
commit e998ccd765
22 changed files with 1838 additions and 690 deletions
+56 -4
View File
@@ -18,6 +18,61 @@ export const taskTimestampDisplayFieldOptions = [
display: "When Agent Picked up Task",
}
]
export const taskingDisplayFieldOptions = [
{
name: "timestamp",
display: "Timestamp",
description: "Show the configured task timestamp."
},
{
name: "task",
display: "Task number",
description: "Show the T-number link for each task."
},
{
name: "username",
display: "Username",
description: "Show the operator that issued the task."
},
{
name: "callback",
display: "Callback number",
description: "Show the C-number link for the task callback."
},
{
name: "host",
display: "Host",
description: "Show the callback host."
},
{
name: "ip",
display: "IP address",
description: "Show the callback primary IP."
},
{
name: "groups",
display: "Callback groups",
description: "Show the callback's tree groups."
},
{
name: "payload_type",
display: "Payload type",
description: "Show the task payload type."
},
];
export const defaultTaskingDisplayFields = ["timestamp", "task", "username", "callback", "payload_type"];
export const normalizeTaskingDisplayFields = (fields) => {
if(!Array.isArray(fields)){
return [...defaultTaskingDisplayFields];
}
const validFieldNames = taskingDisplayFieldOptions.map((option) => option.name);
return fields.reduce( (prev, fieldName) => {
if(validFieldNames.includes(fieldName) && !prev.includes(fieldName)){
return [...prev, fieldName];
}
return prev;
}, []);
}
export const taskingContextFieldsOptions = ["impersonation_context", "cwd", "user", "host", "ip", "pid", "process_short_name", "extra_info", "architecture"].sort();
export const defaultShortcuts = [
"ActiveCallbacks", "Payloads", "PayloadTypesAndC2",
@@ -29,11 +84,8 @@ export const operatorSettingDefaults = {
navBarOpen: false,
fontFamily: 'Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif',
showMedia: true,
hideUsernames: false,
showIP: false,
showHostname: false,
showOPSECBypassUsername: false,
showCallbackGroups: false,
taskingDisplayFields: defaultTaskingDisplayFields,
useDisplayParamsForCLIHistory: true,
interactType: "interactSplit",
taskTimestampDisplayField: "timestamp",
@@ -1,19 +1,23 @@
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';
import Table from '@mui/material/Table';
import TableBody from '@mui/material/TableBody';
import TableContainer from '@mui/material/TableContainer';
import Paper from '@mui/material/Paper';
import { Divider, Input, MenuItem, Select } from '@mui/material';
import MythicTextField from './MythicTextField';
import FormControl from '@mui/material/FormControl';
import MenuItem from '@mui/material/MenuItem';
import Select from '@mui/material/Select';
import TextField from '@mui/material/TextField';
import {gql } from '@apollo/client';
import {snackActions} from '../utilities/Snackbar';
import {useMutation } from '@apollo/client';
import {
MythicDialogBody,
MythicDialogButton,
MythicDialogFooter,
MythicDialogGrid,
MythicDialogSection,
MythicForm,
MythicFormField,
MythicFormNote
} from './MythicDialogLayout';
const submitFeedbackMutation = gql`
mutation submitFeedback($webhookType: String!, $webhookData: jsonb!){
@@ -25,15 +29,15 @@ const submitFeedbackMutation = gql`
`;
export function MythicFeedbackDialog(props) {
const [message, setMessage] = React.useState("");
const [taskID, setTaskID] = React.useState(0);
const [taskID, setTaskID] = React.useState("");
const messageTypeOptions = [
{display: "Bug", value: "bug"},
{display: "Feature Request", value: "feature_request"},
{display: "Confusing UI", value: "confusing_ui"},
{display: "Detection", value: "detection"},
{display: "Bug", value: "bug", description: "Something is broken or behaving unexpectedly."},
{display: "Feature Request", value: "feature_request", description: "A workflow, capability, or quality-of-life improvement."},
{display: "Confusing UI", value: "confusing_ui", description: "A part of the interface is unclear or hard to use."},
{display: "Detection", value: "detection", description: "Detection, telemetry, or visibility feedback."},
];
const [messageType, setMessageType] = React.useState("bug");
const [submitFeedback] = useMutation(submitFeedbackMutation, {
const [submitFeedback, {loading}] = useMutation(submitFeedbackMutation, {
update: (cache, {data}) => {
if(data.sendExternalWebhook.status === "success"){
snackActions.success("Submitted Feedback!");
@@ -48,90 +52,111 @@ export function MythicFeedbackDialog(props) {
}
});
const handleSubmit = () => {
let webhookData = {};
if(taskID > 0){
webhookData["task_display_id"] = String(taskID);
const trimmedMessage = message.trim();
if(trimmedMessage.length === 0){
snackActions.warning("Please include feedback before submitting.");
return;
}
webhookData["message"] = message;
let webhookData = {};
if(Number(taskID) > 0){
webhookData["task_display_id"] = String(Number(taskID));
}
webhookData["message"] = trimmedMessage;
webhookData["feedback_type"] = messageType;
submitFeedback({variables: {webhookType: "new_feedback", webhookData: webhookData}});
}
const handleMessageTypeChange = (evt) => {
setMessageType(evt.target.value);
}
const handleTaskIDChange = (name, value, error) => {
setTaskID(value);
const handleTaskIDChange = (evt) => {
setTaskID(evt.target.value);
}
const handleMessageChange = (name, value, error) => {
setMessage(value);
const handleMessageChange = (evt) => {
setMessage(evt.target.value);
}
const handleOnEnter = (event) => {
if( event.shiftKey){
handleSubmit();
}
const onSubmitForm = (event) => {
event.preventDefault();
handleSubmit();
}
const selectedMessageType = messageTypeOptions.find((opt) => opt.value === messageType);
const canSubmit = message.trim().length > 0 && !loading;
return (
<React.Fragment>
<DialogTitle >{props.title}</DialogTitle>
<Divider></Divider>
<DialogContent style={{padding: "10px"}}>
Send a feedback report to the slack webhook configured for the current operation.
This provides a way to easily capture a bug, feedback requests, or comments without breaking operator flow too much. <br/>
Shift+Enter will auto-submit the form.
<TableContainer className="mythicElement">
<Table size="small" style={{ "maxWidth": "100%", "overflow": "scroll"}}>
<TableBody style={{whiteSpace: "pre"}}>
<TableRow hover >
<TableCell style={{width: "5rem"}}>
Feedback Type
</TableCell>
<TableCell>
<Select
labelId="demo-dialog-select-label"
id="demo-dialog-select"
value={messageType}
onChange={handleMessageTypeChange}
input={<Input style={{width: "100%"}}/>}
>
{messageTypeOptions.map( (opt) => (
<MenuItem value={opt.value} key={opt.value}>{opt.display}</MenuItem>
) )}
</Select>
</TableCell>
</TableRow>
<TableRow hover >
<TableCell style={{width: "5rem"}}>
Task (if applicable)
</TableCell>
<TableCell>
<MythicTextField value={taskID} type={"number"}
onChange={handleTaskIDChange} display="inline-block" name={"taskid"} showLabel={false}
/>
</TableCell>
</TableRow>
<TableRow hover>
<TableCell>
Feedback
</TableCell>
<TableCell>
<MythicTextField value={message} multiline={true} onEnter={handleOnEnter}
onChange={handleMessageChange} display="inline-block" name={"taskid"} showLabel={false}
/>
</TableCell>
</TableRow>
</TableBody>
</Table>
</TableContainer>
<DialogTitle>{props.title || "Submit Feedback"}</DialogTitle>
<DialogContent dividers={true}>
<MythicDialogBody>
<MythicDialogSection
title="Feedback Destination"
description="Send this report to the feedback webhook configured for the current operation."
>
<MythicFormNote>
Use this for bugs, workflow friction, feature ideas, detection notes, or anything operators should be able to capture without leaving Mythic.
</MythicFormNote>
</MythicDialogSection>
<MythicDialogSection
title="Report Details"
description="Capture the category, optional task context, and the feedback details."
>
<MythicForm onSubmit={onSubmitForm}>
<MythicDialogGrid minWidth="14rem">
<MythicFormField label="Feedback Type" description={selectedMessageType?.description} required>
<FormControl fullWidth size="small">
<Select
labelId="feedback-type-select-label"
id="feedback-type-select"
value={messageType}
onChange={handleMessageTypeChange}
>
{messageTypeOptions.map( (opt) => (
<MenuItem value={opt.value} key={opt.value}>{opt.display}</MenuItem>
) )}
</Select>
</FormControl>
</MythicFormField>
<MythicFormField
label="Task"
description="Optional task display ID for feedback tied to specific output or tasking."
>
<TextField
fullWidth
size="small"
type="number"
value={taskID}
onChange={handleTaskIDChange}
placeholder="Optional"
inputProps={{min: 0}}
/>
</MythicFormField>
</MythicDialogGrid>
<MythicFormField
label="Feedback"
description="Include the context an operator or maintainer would need to understand the issue."
required
>
<TextField
autoFocus
fullWidth
multiline
minRows={6}
maxRows={10}
value={message}
onChange={handleMessageChange}
placeholder="What happened, what did you expect, and what would make this easier?"
/>
</MythicFormField>
</MythicForm>
</MythicDialogSection>
</MythicDialogBody>
</DialogContent>
<DialogActions>
<Button onClick={props.onClose} variant="contained" color="primary">
<MythicDialogFooter>
<MythicDialogButton onClick={props.onClose}>
Close
</Button>
<Button onClick={handleSubmit} variant="contained" color="success">
Submit Feedback
</Button>
</DialogActions>
</MythicDialogButton>
<MythicDialogButton onClick={handleSubmit} intent="primary" disabled={!canSubmit}>
{loading ? "Submitting..." : "Submit Feedback"}
</MythicDialogButton>
</MythicDialogFooter>
</React.Fragment>
);
}
@@ -435,33 +435,35 @@ const FileBrowserVirtualTreePreMemo = ({
}
}, [selectedFolderData, flattenedNodes]);
return flattenedNodes.length > 0 ? (
<StyledAutoSizer>
{(AutoSizerProps) => (
<List
itemData={flattenedNodes}
layout="vertical"
height={AutoSizerProps.height}
width={AutoSizerProps.width}
itemCount={flattenedNodes.length}
itemKey={itemKey}
itemSize={24}
ref={gridRef}
>
{(ListProps) => (
<VirtualTreeRow
{...ListProps}
tabInfo={tabInfo}
selectedFolderData={selectedFolderData}
treeRootData={treeRootData}
onSelectNode={onSelectNode}
onExpandNode={onExpandNode}
onCollapseNode={onCollapseNode}
onContextMenu={onContextMenu}
/>
)}
</List>
)}
</StyledAutoSizer>
<div style={{height: "100%", width: "100%", minHeight: 0, minWidth: 0, overflow: "hidden"}}>
<StyledAutoSizer style={{height: "100%", width: "100%"}}>
{(AutoSizerProps) => (
<List
itemData={flattenedNodes}
layout="vertical"
height={AutoSizerProps.height}
width={AutoSizerProps.width}
itemCount={flattenedNodes.length}
itemKey={itemKey}
itemSize={24}
ref={gridRef}
>
{(ListProps) => (
<VirtualTreeRow
{...ListProps}
tabInfo={tabInfo}
selectedFolderData={selectedFolderData}
treeRootData={treeRootData}
onSelectNode={onSelectNode}
onExpandNode={onExpandNode}
onCollapseNode={onCollapseNode}
onContextMenu={onContextMenu}
/>
)}
</List>
)}
</StyledAutoSizer>
</div>
) : null;
};
export const FileBrowserVirtualTree = React.memo(FileBrowserVirtualTreePreMemo);
@@ -13,13 +13,22 @@ const CellPreMemo = ({ style, rowIndex, columnIndex, data }) => {
const cellStyle = item?.props?.cellData?.cellStyle || {};
const rowStyle = data.items[rowIndex][columnIndex]?.props?.rowData?.rowStyle || {};
const contextMenuLocationRef = React.useRef({x: 0, y: 0});
const ownsContextRowRef = React.useRef(false);
const updateRowClass = useCallback((className, shouldAdd) => {
const cells = document.getElementsByClassName(rowClassName);
if(cells.length > 0){
for(const cell of cells){
cell.classList.toggle(className, shouldAdd);
}
}
}, [rowClassName]);
const handleDoubleClick = useCallback(
(e) => {
e.preventDefault();
e.stopPropagation();
data.onDoubleClickRow(e, rowIndex - 1, data.items[rowIndex][columnIndex]?.props?.rowData); // minus 1 to account for header row
},
[data, rowIndex]
[columnIndex, data, rowIndex]
);
const handleClick = (event) => {
event.preventDefault();
@@ -29,22 +38,23 @@ const CellPreMemo = ({ style, rowIndex, columnIndex, data }) => {
}
};
const selectedClass = data.items[rowIndex][columnIndex]?.props?.rowData?.selected ? "selectedCallback" : "";
const onMouseEnter = () => {
const cells = document.getElementsByClassName(rowClassName);
if(cells.length > 0){
for(const cell of cells){
cell.classList.add(classes.hoveredRow);
}
const rowFirstCellClass = columnIndex === 0 ? classes.rowFirstCell : "";
const rowLastCellClass = columnIndex === data.items[rowIndex].length - 1 ? classes.rowLastCell : "";
const rowInteractiveClass = data.onRowClick ? classes.rowInteractive : "";
const setHoveredRow = useCallback((enabled) => {
updateRowClass(classes.hoveredRow, enabled);
}, [updateRowClass]);
const setContextRow = useCallback((enabled) => {
updateRowClass(classes.contextRow, enabled);
}, [updateRowClass]);
const onMouseEnter = useCallback(() => {
setHoveredRow(true);
}, [setHoveredRow]);
const onMouseLeave = useCallback(() => {
if(!ownsContextRowRef.current){
setHoveredRow(false);
}
}
const onMouseLeave = () => {
const cells = document.getElementsByClassName(rowClassName);
if(cells.length > 0){
for(const cell of cells){
cell.classList.remove(classes.hoveredRow);
}
}
}
}, [setHoveredRow]);
const handleMenuItemClick = (event, clickOption) => {
event.preventDefault();
event.stopPropagation();
@@ -52,10 +62,24 @@ const CellPreMemo = ({ style, rowIndex, columnIndex, data }) => {
setOpenContextMenu(false);
};
React.useEffect( () => {
if(!openContextMenu){
onMouseLeave();
if(openContextMenu){
ownsContextRowRef.current = true;
setContextRow(true);
setHoveredRow(true);
} else if(ownsContextRowRef.current){
ownsContextRowRef.current = false;
setContextRow(false);
setHoveredRow(false);
}
}, [openContextMenu]);
}, [openContextMenu, setContextRow, setHoveredRow]);
React.useEffect( () => {
return () => {
if(ownsContextRowRef.current){
setContextRow(false);
setHoveredRow(false);
}
};
}, [setContextRow, setHoveredRow]);
const handleContextClick = useCallback(
(event) => {
event.preventDefault();
@@ -83,7 +107,7 @@ const CellPreMemo = ({ style, rowIndex, columnIndex, data }) => {
);
return (
<div style={{...style, ...cellStyle, ...rowStyle}}
className={`${classes.cell} ${rowClassName} ${rowHighlight} ${selectedClass}`}
className={`${classes.cell} ${rowClassName} ${rowHighlight} ${selectedClass} ${rowFirstCellClass} ${rowLastCellClass} ${rowInteractiveClass}`}
onDoubleClick={handleDoubleClick}
onClick={handleClick}
onMouseEnter={onMouseEnter}
@@ -3,8 +3,9 @@ import { useCallback } from 'react';
import useSingleAndDoubleClick from '../../utilities/useSingleAndDoubleClick';
import {classes} from './styles';
import React from 'react';
import {FontAwesomeIcon} from '@fortawesome/react-fontawesome';
import {faFilter} from '@fortawesome/free-solid-svg-icons';
import FilterAltOutlinedIcon from '@mui/icons-material/FilterAltOutlined';
import KeyboardArrowDownIcon from '@mui/icons-material/KeyboardArrowDown';
import KeyboardArrowUpIcon from '@mui/icons-material/KeyboardArrowUp';
import {ContextMenu} from "./Cell";
const HeaderCell = ({
@@ -21,6 +22,9 @@ const HeaderCell = ({
const dropdownAnchorRef = React.useRef(null);
const item = data.items[rowIndex][columnIndex];
const isFiltered = item?.filtered || false;
const isSorted = sortIndicatorIndex === columnIndex;
const HeaderSortIcon = sortDirection === 'ASC' ? KeyboardArrowUpIcon : KeyboardArrowDownIcon;
const headerLabel = item?.[headerNameKey] || "";
const contextMenuLocationRef = React.useRef({x: 0, y: 0});
const handleClick = useCallback(
(e) => {
@@ -59,38 +63,69 @@ const HeaderCell = ({
[]
);
const [openContextMenu, setOpenContextMenu] = React.useState(false);
const menuContext = React.useMemo(() => {
return {columnIndex, rowIndex, data: data.items[rowIndex][columnIndex]?.props?.rowData || {}};
}, [columnIndex, data.items, rowIndex]);
const resolvedContextMenuOptions = React.useMemo(() => {
const resolveOption = (option) => {
const resolvedOption = {
...option,
disabled: typeof option.disabled === "function" ? option.disabled(menuContext) : option.disabled,
};
if(option.menuItems){
resolvedOption.menuItems = option.menuItems.map(resolveOption);
}
return resolvedOption;
};
return contextMenuOptions.map(resolveOption);
}, [contextMenuOptions, menuContext]);
const handleContextClick = useCallback(
(event) => {
event.preventDefault();
if(item.disableFilterMenu){
return;
}
if(contextMenuOptions && contextMenuOptions.length > 0){
if(resolvedContextMenuOptions && resolvedContextMenuOptions.length > 0){
contextMenuLocationRef.current.x = event.clientX;
contextMenuLocationRef.current.y = event.clientY;
setOpenContextMenu(true);
}
},
[contextMenuOptions, columnIndex] // eslint-disable-line react-hooks/exhaustive-deps
[resolvedContextMenuOptions]
);
const handleMenuItemClick = (event, clickOption) => {
event.preventDefault();
event.stopPropagation();
clickOption({event, columnIndex, rowIndex, data: data.items[rowIndex][columnIndex]?.props?.rowData || {}});
clickOption({event, ...menuContext});
setOpenContextMenu(false);
};
const handleClicks = useSingleAndDoubleClick(handleClick, handleDoubleClick);
const className = `${classes.headerCell} ${item.disableSort ? classes.headerCellNoSort : ""}`;
return (
<div style={style} className={classes.headerCell} onClick={handleClicks} onContextMenu={handleContextClick} ref={dropdownAnchorRef}>
<Box display='flex' alignItems='center' justifyContent='space-between' width='100%'>
<Typography className={classes.cellInner} variant='body1'>
{item[headerNameKey].toUpperCase()}
<div
style={style}
className={className}
onClick={handleClicks}
onContextMenu={handleContextClick}
ref={dropdownAnchorRef}
title={typeof headerLabel === "string" ? headerLabel : undefined}
>
<Box className={classes.headerContent}>
<Typography className={`${classes.cellInner} ${classes.headerLabel}`} component='div' variant='body1'>
{typeof headerLabel === "string" ? headerLabel : headerLabel}
</Typography>
{isFiltered && <FontAwesomeIcon icon={faFilter} />}
{sortIndicatorIndex === columnIndex && (sortDirection === 'ASC' ? <div></div> : <div></div>)}
<ContextMenu dropdownAnchorRef={dropdownAnchorRef} contextMenuOptions={contextMenuOptions}
<Box className={classes.headerActions}>
{isFiltered &&
<span className={`${classes.headerIndicator} ${classes.headerFilterIcon}`} title="Filtered">
<FilterAltOutlinedIcon fontSize="inherit" />
</span>
}
{isSorted &&
<span className={`${classes.headerIndicator} ${classes.headerSortIcon}`} title={`Sorted ${sortDirection === 'ASC' ? 'ascending' : 'descending'}`}>
<HeaderSortIcon fontSize="inherit" />
</span>
}
</Box>
<ContextMenu dropdownAnchorRef={dropdownAnchorRef} contextMenuOptions={resolvedContextMenuOptions}
disableFilterMenu={item?.disableFilterMenu} openContextMenu={openContextMenu}
contextMenuLocationRef={contextMenuLocationRef}
setOpenContextMenu={setOpenContextMenu} handleMenuItemClick={handleMenuItemClick}
@@ -100,7 +135,7 @@ const HeaderCell = ({
<div
role="separator"
aria-orientation="vertical"
aria-label={`Resize ${item[headerNameKey]} column`}
aria-label={`Resize ${typeof headerLabel === "string" ? headerLabel : "column"} column`}
className={`${classes.headerResizeHandle} ${isResizing ? classes.headerResizeHandleActive : ""}`}
onPointerDown={handleResizePointerDown}
onDoubleClick={handleResizeDoubleClick}
@@ -7,12 +7,183 @@ import HeaderCell from './HeaderCell';
import Cell from './Cell';
import {classes} from './styles';
import {GetMythicSetting, useSetMythicSetting} from "../MythicSavedUserSetting";
import FitScreenIcon from '@mui/icons-material/FitScreen';
import RestartAltIcon from '@mui/icons-material/RestartAlt';
import ViewColumnIcon from '@mui/icons-material/ViewColumn';
const HeaderCellContext = createContext({});
const MIN_COLUMN_WIDTH = 100;
const MIN_FLEX_COLUMN_WIDTH = 150;
const AUTOSIZE_HORIZONTAL_PADDING = 44;
const AUTOSIZE_HEADER_EXTRA_WIDTH = 28;
const headerMenuIconStyle = {fontSize: "1rem", marginRight: "8px"};
let autosizeCanvas;
const getNumericWidth = (value) => {
const width = Number(value);
return Number.isFinite(width) ? width : undefined;
};
const getColumnMinWidth = (column) => {
const configuredMinWidth = getNumericWidth(column?.minWidth);
return Math.max(configuredMinWidth || 0, column?.fillWidth ? MIN_FLEX_COLUMN_WIDTH : MIN_COLUMN_WIDTH);
};
const getColumnMaxWidth = (column) => {
const configuredMaxWidth = getNumericWidth(column?.maxWidth);
return configuredMaxWidth && configuredMaxWidth > 0 ? configuredMaxWidth : Infinity;
};
const clampColumnWidth = (column, width) => {
const minWidth = getColumnMinWidth(column);
const maxWidth = getColumnMaxWidth(column);
const numericWidth = getNumericWidth(width) || minWidth;
return Math.floor(Math.min(maxWidth, Math.max(numericWidth, minWidth)));
};
const getPreferredColumnWidth = (column) => {
return column?.width === undefined ? getColumnMinWidth(column) : column.width;
};
const distributeFillWidth = ({columns, columnWidths, totalWidth}) => {
const availableWidth = Math.max(0, Math.floor(totalWidth || 0));
const currentWidth = columnWidths.reduce((a, b) => a + b, 0);
const widthDifference = availableWidth - currentWidth;
if(widthDifference <= 0 || columns.length === 0){
return columnWidths;
}
const fillColumnIndexes = columns.reduce((previous, column, index) => {
return column.fillWidth ? [...previous, index] : previous;
}, []);
const targetIndexes = fillColumnIndexes.length > 0 ? fillColumnIndexes : [columns.length - 1];
let remainingWidth = widthDifference;
const updatedColumnWidths = [...columnWidths];
for(let i = 0; i < targetIndexes.length; i++){
const columnIndex = targetIndexes[i];
const share = i === targetIndexes.length - 1 ? remainingWidth : Math.floor(widthDifference / targetIndexes.length);
const currentColumnWidth = updatedColumnWidths[columnIndex];
const nextColumnWidth = clampColumnWidth(columns[columnIndex], currentColumnWidth + share);
updatedColumnWidths[columnIndex] = nextColumnWidth;
remainingWidth -= nextColumnWidth - currentColumnWidth;
}
return updatedColumnWidths;
};
const getInitialColumnWidths = ({columns, savedWidths = [], totalWidth = 0}) => {
const hasSavedWidths = savedWidths.length === columns.length;
const baseColumnWidths = columns.map((column, index) => {
return clampColumnWidth(column, hasSavedWidths ? savedWidths[index] : getPreferredColumnWidth(column));
});
if(hasSavedWidths){
return baseColumnWidths;
}
return distributeFillWidth({columns, columnWidths: baseColumnWidths, totalWidth});
};
const areColumnWidthsEqual = (first = [], second = []) => {
if(first.length !== second.length){
return false;
}
return first.every((width, index) => Math.floor(width || MIN_COLUMN_WIDTH) === Math.floor(second[index] || MIN_COLUMN_WIDTH));
};
const normalizeColumnWidths = (columnWidths = []) => {
return columnWidths.map((columnWidth) => Math.floor(getNumericWidth(columnWidth) || MIN_COLUMN_WIDTH));
};
const getMeasureContext = () => {
if(typeof document === "undefined"){
return null;
}
if(autosizeCanvas === undefined){
autosizeCanvas = document.createElement("canvas");
}
const context = autosizeCanvas.getContext("2d");
if(context === null){
return null;
}
const referenceElement = document.querySelector(`.${classes.cell}`) || document.body;
const computedStyle = window.getComputedStyle(referenceElement);
context.font = [
computedStyle.fontStyle,
computedStyle.fontVariant,
computedStyle.fontWeight,
computedStyle.fontSize,
computedStyle.fontFamily,
].join(" ");
return context;
};
const getDisplayText = (value) => {
if(value === undefined || value === null){
return "";
}
if(typeof value === "string"){
const trimmedValue = value.trim();
if(trimmedValue.startsWith("[")){
try{
const parsedValue = JSON.parse(trimmedValue);
if(Array.isArray(parsedValue) && parsedValue.length > 0){
return getDisplayText(parsedValue[0]);
}
}catch(error){
// Keep the original string when it is not a serialized array.
}
}
return value;
}
if(typeof value === "number" || typeof value === "boolean"){
return String(value);
}
if(Array.isArray(value)){
return value.map((entry) => getDisplayText(entry)).filter(Boolean).join(", ");
}
if(React.isValidElement(value)){
return getDisplayText(value.props?.cellData || value.props?.children);
}
if(typeof value === "object"){
if(value.plaintext !== undefined){
return getDisplayText(value.plaintext);
}
if(value.name !== undefined){
return getDisplayText(value.name);
}
if(value.button?.name !== undefined){
return getDisplayText(value.button.name);
}
if(value.display !== undefined){
return getDisplayText(value.display);
}
return "";
}
return "";
};
const getCellAutosizeText = ({column, cell}) => {
const cellProps = cell?.props || {};
const columnKey = column?.key || column?.plaintext;
const rowData = cellProps.rowData || {};
const cellDataText = getDisplayText(cellProps.cellData);
if(cellDataText !== ""){
return cellDataText;
}
if(columnKey !== undefined){
return getDisplayText(rowData?.[columnKey]);
}
return getDisplayText(cell);
};
const measureAutosizeText = (context, text) => {
const normalizedText = getDisplayText(text);
if(normalizedText === ""){
return 0;
}
const lines = normalizedText.split(/\r?\n/);
return lines.reduce((longestLineWidth, line) => {
const textWidth = context ? context.measureText(line).width : line.length * 8;
return Math.max(longestLineWidth, textWidth);
}, 0);
};
const getAutosizedColumnWidth = ({column, columnIndex, headerNameKey, items}) => {
const measureContext = getMeasureContext();
const headerText = getDisplayText(column?.[headerNameKey] || column?.name || column?.plaintext);
const headerWidth = measureAutosizeText(measureContext, headerText.toUpperCase()) + AUTOSIZE_HEADER_EXTRA_WIDTH;
const widestCellWidth = items.reduce((widestWidth, itemRow) => {
const cellText = getCellAutosizeText({column, cell: itemRow[columnIndex]});
return Math.max(widestWidth, measureAutosizeText(measureContext, cellText));
}, 0);
return clampColumnWidth(column, Math.max(headerWidth, widestCellWidth) + AUTOSIZE_HORIZONTAL_PADDING);
};
const CellRendererPreMemo = ({ style, rowIndex, columnIndex, data }) => {
return rowIndex === 0 ? null : <Cell style={style} rowIndex={rowIndex} columnIndex={columnIndex} data={data} />;
@@ -20,12 +191,16 @@ const CellRendererPreMemo = ({ style, rowIndex, columnIndex, data }) => {
const CellRenderer = React.memo(CellRendererPreMemo);
const innerElementType = React.forwardRef(({ children, style }, ref) => {
const HeaderCellData = useContext(HeaderCellContext);
const resizingColumnIndex = HeaderCellData.resizingColumnIndex;
const resizeGuideLeft = resizingColumnIndex >= 0 ?
HeaderCellData.columnWidths.slice(0, resizingColumnIndex + 1).reduce((a, b) => a + b, 0) :
-1;
const onHeaderDoubleClick = React.useCallback( (e, columnIndex) => {
if (HeaderCellData.columns[columnIndex].disableAutosize) return;
HeaderCellData.autosizeColumn({columnIndex});
}, [HeaderCellData.columns, HeaderCellData.autosizeColumn]);
return (
<div ref={ref} style={style}>
<div ref={ref} style={{...style, position: "relative"}}>
{/* always render header cells */}
<div
className={classes.headerCellRow}
@@ -63,6 +238,15 @@ const innerElementType = React.forwardRef(({ children, style }, ref) => {
</div>
{/* render other cells as usual */}
{children}
{resizingColumnIndex >= 0 &&
<div
className={classes.resizeGuide}
style={{
left: resizeGuideLeft,
height: style.height,
}}
/>
}
</div>
);
});
@@ -93,14 +277,11 @@ const ResizableGridWrapper = ({
const { width: scrollbarWidth } = useScrollbarSize();
const localColumnsRef = React.useRef(columns);
const initialSavedWidths = name === undefined ? [] : GetMythicSetting({setting_name: `${name}_column_widths`, default_value: {}, output: "json-array"});
const [columnWidths, setColumnWidths] = useState(columns.map((column, index) => {
if(initialSavedWidths.length > 0){
return initialSavedWidths[index];
}
if(column.fillWidth){
return Math.max(column?.width || 0, MIN_FLEX_COLUMN_WIDTH);
}
return Math.max(column?.width || 0, MIN_COLUMN_WIDTH);
const savedColumnWidths = Array.isArray(initialSavedWidths) ? initialSavedWidths : [];
const [columnWidths, setColumnWidths] = useState(() => getInitialColumnWidths({
columns,
savedWidths: savedColumnWidths,
totalWidth: AutoSizerProps.width - scrollbarWidth,
}));
const gridUUID = React.useMemo( () => GetShortRandomString(), []);
const gridRef = useRef(null);
@@ -112,9 +293,14 @@ const ResizableGridWrapper = ({
const [resizingColumnIndex, setResizingColumnIndex] = useState(-1);
const [updateSetting] = useSetMythicSetting();
const setColumnWidthsAndRef = useCallback( (newColumnWidths, resetColumnIndex=0) => {
const normalizedColumnWidths = normalizeColumnWidths(newColumnWidths);
if(areColumnWidthsEqual(columnWidthsRef.current, normalizedColumnWidths)){
return false;
}
lastResetColumnIndexRef.current = resetColumnIndex;
columnWidthsRef.current = newColumnWidths;
setColumnWidths(newColumnWidths);
columnWidthsRef.current = normalizedColumnWidths;
setColumnWidths(normalizedColumnWidths);
return true;
}, []);
const getColumnWidth = useCallback(
(index) => {
@@ -132,33 +318,17 @@ const ResizableGridWrapper = ({
[rowHeight, headerRowHeight]
);
useEffect(() => {
if(initialSavedWidths.length > 0 && localColumnsRef.current.length === columns.length){
if(savedColumnWidths.length === columns.length && localColumnsRef.current.length === columns.length){
return;
}
localColumnsRef.current = columns;
const totalWidth = AutoSizerProps.width - scrollbarWidth;
const updatedColumnWidths = columns.map((column, index) => {
return column.width || MIN_COLUMN_WIDTH
const updatedColumnWidths = getInitialColumnWidths({
columns,
savedWidths: [],
totalWidth: AutoSizerProps.width - scrollbarWidth,
});
const totalWidthDiff = totalWidth - updatedColumnWidths.reduce((a, b) => a + b, 0);
if (totalWidthDiff !== 0) {
let updatedWidthIndexs = [];
for(let i = 0; i < columns.length; i++){
// check if any of the columns have the `fillWidth` property to true
if(columns[i]["fillWidth"]){
updatedWidthIndexs.push(i);
}
}
if(updatedWidthIndexs.length === 0){
updatedWidthIndexs.push(columns.length - 1);
}
for(let i = 0; i < updatedWidthIndexs.length; i++){
updatedColumnWidths[updatedWidthIndexs[i]] += Math.max(totalWidthDiff / updatedWidthIndexs.length, MIN_FLEX_COLUMN_WIDTH);
}
//updatedColumnWidths[updatedWidthIndex] += totalWidthDiff;
}
setColumnWidthsAndRef(updatedColumnWidths);
if(name !== undefined){
const didUpdateWidths = setColumnWidthsAndRef(updatedColumnWidths);
if(didUpdateWidths && name !== undefined){
updateSetting({setting_name: `${name}_column_widths`, value: updatedColumnWidths});
}
@@ -197,7 +367,7 @@ const ResizableGridWrapper = ({
return;
}
const resizeDelta = currentResize.latestClientX - currentResize.startClientX;
const nextColumnWidth = Math.floor(Math.max(currentResize.startWidth + resizeDelta, MIN_COLUMN_WIDTH));
const nextColumnWidth = clampColumnWidth(columns[currentResize.columnIndex], currentResize.startWidth + resizeDelta);
const updatedColumnWidths = currentResize.initialWidths.map( (columnWidth, index) => {
return currentResize.columnIndex === index ? nextColumnWidth : columnWidth;
});
@@ -223,13 +393,13 @@ const ResizableGridWrapper = ({
resizeFrameRef.current = null;
}
const resizeDelta = clientX - currentResize.startClientX;
const nextColumnWidth = Math.floor(Math.max(currentResize.startWidth + resizeDelta, MIN_COLUMN_WIDTH));
const nextColumnWidth = clampColumnWidth(columns[currentResize.columnIndex], currentResize.startWidth + resizeDelta);
const updatedColumnWidths = currentResize.initialWidths.map( (columnWidth, index) => {
return currentResize.columnIndex === index ? nextColumnWidth : columnWidth;
});
activeResizeRef.current = null;
setColumnWidthsAndRef(updatedColumnWidths, currentResize.columnIndex);
if(name !== undefined){
const didUpdateWidths = setColumnWidthsAndRef(updatedColumnWidths, currentResize.columnIndex);
if(didUpdateWidths && name !== undefined){
updateSetting({setting_name: `${name}_column_widths`, value: updatedColumnWidths});
}
setResizingColumnIndex(-1);
@@ -270,100 +440,74 @@ const ResizableGridWrapper = ({
};
}, []);
const autosizeColumn = useCallback( ({columnIndex}) => {
if(columns[columnIndex].disableDoubleClick){
const column = columns[columnIndex];
if(column?.disableDoubleClick || column?.disableAutosize){
return
}
const longestElementInColumn = Math.max(...items.map((itemRow) => {
if(columns[columnIndex].key === undefined){
if(columns[columnIndex].plaintext){
columns[columnIndex].key = columns[columnIndex].plaintext;
} else {
return 30;
}
}
if(columns[columnIndex].key !== undefined){
if(columns[columnIndex].key.includes("time")){
return 30;
}
if(columns[columnIndex].key === "mythictree_groups"){
return itemRow[columnIndex]?.props?.cellData.length;
}
if(columns[columnIndex].type === "size"){
if(itemRow[columnIndex]?.props?.cellData !== undefined){
if(typeof itemRow[columnIndex]?.props?.cellData === 'number'){
return String(itemRow[columnIndex]?.props?.cellData)?.length;
}
return String(itemRow[columnIndex]?.props?.cellData?.plaintext)?.length;
}
return itemRow[columnIndex].length;
}
try{
items = JSON.parse(itemRow[columnIndex]?.props?.rowData?.[columns[columnIndex].key]);
if(Array.isArray(items) && items.length > 0){
return String(items[0]).length;
}
}catch(error){
//console.log(itemRow[columnIndex]?.props?.rowData?.[columns[columnIndex].key])
}
if(typeof itemRow[columnIndex]?.props?.cellData === 'string' ){
return itemRow[columnIndex]?.props?.cellData.length;
}
let data = itemRow[columnIndex]?.props?.rowData?.[columns[columnIndex].key];
if(columns[columnIndex].inMetadata){
return itemRow[columnIndex]?.props?.cellData.length;
}
if(data === undefined){
return MIN_COLUMN_WIDTH;
}
if(data.plaintext){
return String(data.plaintext)?.length;
} else if(data?.button?.name) {
return String(data?.button?.name)?.length ;
} else {
return MIN_COLUMN_WIDTH;
}
//return String(itemRow[columnIndex]?.props?.rowData?.[columns[columnIndex].key]).length || -1;
} else if(typeof(itemRow[columnIndex]?.props?.cellData) === "string") {
try {
items = JSON.parse(itemRow[columnIndex]?.props?.cellData);
if (Array.isArray(items) && items.length > 0) {
return String(items[0]).length;
}
} catch (error) {
return itemRow[columnIndex]?.props?.cellData.length;
}
} else if(Array.isArray(itemRow[columnIndex]?.props?.cellData)){
return itemRow[columnIndex]?.props?.cellData.join(", ").length;
}else {
return itemRow[columnIndex]?.props?.cellData?.length || -1;
}
}));
const updatedColumnWidths = columnWidths.map((columnWidth, index) => {
const currentColumnWidths = columnWidthsRef.current;
const updatedColumnWidths = currentColumnWidths.map((columnWidth, index) => {
if (columnIndex === index) {
if(isNaN(longestElementInColumn)){
return MIN_COLUMN_WIDTH;
}
return Math.floor(Math.max(longestElementInColumn * 10 + 40, MIN_COLUMN_WIDTH));
return getAutosizedColumnWidth({column, columnIndex, headerNameKey, items});
}
return Math.floor(columnWidth);
});
//console.log(updatedColumnWidths, columnWidths, longestElementInColumn);
let updatedValues = false;
for(let i = 0; i < updatedColumnWidths.length; i++){
if(updatedColumnWidths[i] !== columnWidths[i]){
updatedValues = true;
break;
}
}
if(updatedValues){
setColumnWidthsAndRef(updatedColumnWidths, columnIndex);
const didUpdateWidths = setColumnWidthsAndRef(updatedColumnWidths, columnIndex);
if(didUpdateWidths){
if(name !== undefined){
updateSetting({setting_name: `${name}_column_widths`, value: updatedColumnWidths});
}
}
}, [columnWidths, columns, items, name, setColumnWidthsAndRef, updateSetting]);
}, [columns, headerNameKey, items, name, setColumnWidthsAndRef, updateSetting]);
const autosizeAllColumns = useCallback( () => {
const currentColumnWidths = columnWidthsRef.current;
const updatedColumnWidths = currentColumnWidths.map((columnWidth, columnIndex) => {
const column = columns[columnIndex];
if(column?.disableDoubleClick || column?.disableAutosize){
return Math.floor(columnWidth);
}
return getAutosizedColumnWidth({column, columnIndex, headerNameKey, items});
});
const didUpdateWidths = setColumnWidthsAndRef(updatedColumnWidths, 0);
if(didUpdateWidths && name !== undefined){
updateSetting({setting_name: `${name}_column_widths`, value: updatedColumnWidths});
}
}, [columns, headerNameKey, items, name, setColumnWidthsAndRef, updateSetting]);
const resetColumnWidths = useCallback( () => {
const updatedColumnWidths = getInitialColumnWidths({
columns,
savedWidths: [],
totalWidth: AutoSizerProps.width - scrollbarWidth,
});
const didUpdateWidths = setColumnWidthsAndRef(updatedColumnWidths, 0);
if(didUpdateWidths && name !== undefined){
updateSetting({setting_name: `${name}_column_widths`, value: updatedColumnWidths});
}
}, [AutoSizerProps.width, columns, name, scrollbarWidth, setColumnWidthsAndRef, updateSetting]);
const headerContextMenuOptions = React.useMemo( () => {
const sizingOptions = [
{
name: "Autosize Column",
type: "item",
icon: <FitScreenIcon style={headerMenuIconStyle} />,
disabled: ({columnIndex}) => columns[columnIndex]?.disableDoubleClick || columns[columnIndex]?.disableAutosize,
click: ({columnIndex}) => autosizeColumn({columnIndex}),
},
{
name: "Autosize All Columns",
type: "item",
icon: <ViewColumnIcon style={headerMenuIconStyle} />,
disabled: columns.every((column) => column?.disableDoubleClick || column?.disableAutosize),
click: () => autosizeAllColumns(),
},
{
name: "Reset Column Widths",
type: "item",
icon: <RestartAltIcon style={headerMenuIconStyle} />,
click: () => resetColumnWidths(),
},
];
return [...sizingOptions, ...(contextMenuOptions || [])];
}, [autosizeAllColumns, autosizeColumn, columns, contextMenuOptions, resetColumnWidths]);
useEffect( () => {
if(callbackTableGridRef){
@@ -378,7 +522,7 @@ const ResizableGridWrapper = ({
"headerNameKey": headerNameKey,
"onClickHeader": onClickHeader,
"autosizeColumn": autosizeColumn,
"contextMenuOptions": contextMenuOptions,
"contextMenuOptions": headerContextMenuOptions,
"sortIndicatorIndex": sortIndicatorIndex,
"sortDirection": sortDirection,
"getColumnWidth": getColumnWidth,
@@ -390,6 +534,7 @@ const ResizableGridWrapper = ({
<>
<HeaderCellContext.Provider value={headerCellData}>
<VariableSizeGrid
className={classes.grid}
height={Math.max(1, AutoSizerProps.height)}
width={AutoSizerProps.width}
columnCount={columns.length}
@@ -432,29 +577,31 @@ const MythicResizableGrid = ({
name,
}) => {
return (
<AutoSizer style={{height: "100%"}}>
{(AutoSizerProps) => (
<ResizableGridWrapper
name={name}
columns={columns}
callbackTableGridRef={callbackTableGridRef}
headerNameKey={headerNameKey}
sortIndicatorIndex={sortIndicatorIndex}
sortDirection={sortDirection}
items={items}
widthMeasureKey={widthMeasureKey}
rowHeight={rowHeight}
headerRowHeight={headerRowHeight}
onClickHeader={onClickHeader}
onDoubleClickRow={onDoubleClickRow}
contextMenuOptions={contextMenuOptions}
rowContextMenuOptions={rowContextMenuOptions}
onRowContextMenuClick={onRowContextMenuClick}
onRowClick={onRowClick}
{...AutoSizerProps}
/>
)}
</AutoSizer>
<div className={classes.root}>
<AutoSizer style={{height: "100%", width: "100%"}}>
{(AutoSizerProps) => (
<ResizableGridWrapper
name={name}
columns={columns}
callbackTableGridRef={callbackTableGridRef}
headerNameKey={headerNameKey}
sortIndicatorIndex={sortIndicatorIndex}
sortDirection={sortDirection}
items={items}
widthMeasureKey={widthMeasureKey}
rowHeight={rowHeight}
headerRowHeight={headerRowHeight}
onClickHeader={onClickHeader}
onDoubleClickRow={onDoubleClickRow}
contextMenuOptions={contextMenuOptions}
rowContextMenuOptions={rowContextMenuOptions}
onRowContextMenuClick={onRowContextMenuClick}
onRowClick={onRowClick}
{...AutoSizerProps}
/>
)}
</AutoSizer>
</div>
);
};
@@ -1,10 +1,24 @@
const PREFIX = 'MythicResizableGrid';
export const classes = {
root: `${PREFIX}-root`,
grid: `${PREFIX}-grid`,
headerCellRow: `${PREFIX}-headerCellRow`,
headerCell: `${PREFIX}-headerCell`,
headerCellNoSort: `${PREFIX}-headerCellNoSort`,
headerContent: `${PREFIX}-headerContent`,
headerLabel: `${PREFIX}-headerLabel`,
headerActions: `${PREFIX}-headerActions`,
headerIndicator: `${PREFIX}-headerIndicator`,
headerFilterIcon: `${PREFIX}-headerFilterIcon`,
headerSortIcon: `${PREFIX}-headerSortIcon`,
headerResizeHandle: `${PREFIX}-headerResizeHandle`,
headerResizeHandleActive: `${PREFIX}-headerResizeHandleActive`,
resizeGuide: `${PREFIX}-resizeGuide`,
hoveredRow: `${PREFIX}-hoveredRow`,
contextRow: `${PREFIX}-contextRow`,
rowFirstCell: `${PREFIX}-rowFirstCell`,
rowLastCell: `${PREFIX}-rowLastCell`,
rowInteractive: `${PREFIX}-rowInteractive`,
cell: `${PREFIX}-cell`,
cellInner: `${PREFIX}-cellInner`,
}
@@ -19,10 +19,7 @@ export const GetComputedFontSize = () => {
}
/*
setting_name options:
hideUsernames
showIP
showHostname
showCallbackGroups
taskingDisplayFields
showMedia
interactType
@@ -80,4 +77,4 @@ export function useSetMythicSetting() {
updateSetting({variables: {preferences: operatorSettingDefaults}});
}
]
}
}
@@ -8,13 +8,9 @@ import FormControl from '@mui/material/FormControl';
import Select from '@mui/material/Select';
import InputLabel from '@mui/material/InputLabel';
import Input from '@mui/material/Input';
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 Paper from '@mui/material/Paper';
import MythicStyledTableCell from "./MythicTableCell";
import Box from '@mui/material/Box';
import Stack from '@mui/material/Stack';
import Typography from '@mui/material/Typography';
export function MythicSelectFromListDialog(props) {
const [options, setOptions] = React.useState([]);
@@ -78,10 +74,17 @@ export function MythicSelectFromListDialog(props) {
export function MythicSelectFromRawListDialog(props) {
const [options, setOptions] = React.useState([]);
const actionText = props.action || "Select";
const handleSubmit = (selected) => {
props.onSubmit(selected);
props.onClose();
}
const handleKeyDown = (event, selected) => {
if(event.key === "Enter" || event.key === " "){
event.preventDefault();
handleSubmit(selected);
}
}
useEffect( () => {
//expects options to be an array of dictionaries with a "display" field for what gets presented to the user
const opts = [...props.options];
@@ -90,22 +93,33 @@ export function MythicSelectFromRawListDialog(props) {
return (
<React.Fragment>
<DialogTitle >{props.title}</DialogTitle>
<div style={{height: "100%", display: "flex"}}>
<TableContainer component={Paper} className="mythicElement" style={{flexGrow: 1, overflowY: "auto"}}>
<Table size="small" style={{ "maxWidth": "100%", "overflow": "scroll"}}>
<TableBody style={{whiteSpace: "pre"}}>
{options.map( (choice, i) => (
<TableRow hover key={choice + i}>
<MythicStyledTableCell style={{width: "5rem"}}>
<Button onClick={() => handleSubmit(choice)} variant="contained" color="primary">Select</Button>
</MythicStyledTableCell>
<MythicStyledTableCell>{choice}</MythicStyledTableCell>
</TableRow>
))}
</TableBody>
</Table>
</TableContainer>
</div>
<DialogContent dividers={true} className="mythic-raw-select-dialog-content">
{options.length === 0 ? (
<Box className="mythic-raw-select-empty">
<Typography variant="body2">No options available</Typography>
</Box>
) : (
<Stack className="mythic-raw-select-list">
{options.map( (choice, i) => (
<Box
key={String(choice) + i}
className="mythic-raw-select-row"
role="button"
tabIndex={0}
onClick={() => handleSubmit(choice)}
onKeyDown={(event) => handleKeyDown(event, choice)}
>
<Typography className="mythic-raw-select-value" title={String(choice)}>
{String(choice)}
</Typography>
<Button className="mythic-dialog-button-info mythic-raw-select-action" variant="outlined" size="small" tabIndex={-1}>
{actionText}
</Button>
</Box>
))}
</Stack>
)}
</DialogContent>
<DialogActions>
<Button onClick={props.onClose} variant="contained" color="primary">
Close
@@ -192,6 +192,9 @@ export const taskingDataFragment = gql`
cmd
supported_ui_features
id
payloadtype {
name
}
}
command_name
response_count
@@ -114,7 +114,7 @@ export const ipCompare = (a, b) => {
}
}
const callbackTableInitialColumns = [
{key: "id", type: 'number', name: "Interact", width: 150, disableDoubleClick: true},
{key: "id", type: 'number', name: "Interact", width: 175, disableDoubleClick: true},
{key: "mythictree_groups", type: 'array', name: "Groups", width: 150},
{key: "ip", type: 'ip', name: "IP", width: 150},
{key: "external_ip",type: 'string', name: "External IP", width: 150},
@@ -1014,7 +1014,7 @@ function CallbacksTablePreMemo(props){
sortIndicatorIndex={sortColumn}
sortDirection={sortData.sortDirection}
items={sortedData}
rowHeight={GetComputedFontSize() + 7}
rowHeight={GetComputedFontSize() + 10}
onClickHeader={onClickHeader}
onDoubleClickRow={onRowDoubleClick}
contextMenuOptions={contextMenuOptions}
@@ -1213,4 +1213,4 @@ function CallbacksTablePreMemo(props){
</div>
)
}
export const CallbacksTable = React.memo(CallbacksTablePreMemo);
export const CallbacksTable = React.memo(CallbacksTablePreMemo);
@@ -34,7 +34,6 @@ import {faSocks} from '@fortawesome/free-solid-svg-icons';
import {TagsDisplay, ViewEditTags} from "../../MythicComponents/MythicTag";
export const CallbacksTableIDCell = React.memo(({rowData, callbackDropdown}) =>{
const theme = useTheme();
const dropdownAnchorRef = React.useRef(null);
const onOpenTab = useContext(OnOpenTabContext);
const interactType = GetMythicSetting({setting_name: "interactType", default_value: "interact"})
@@ -104,41 +103,53 @@ export const CallbacksTableIDCell = React.memo(({rowData, callbackDropdown}) =>{
const localOnOpenTab = () => {
onOpenTab({tabType: interactType, tabID: rowDataStatic.id + interactType, callbackID: rowDataStatic.id, displayID: rowDataStatic.display_id});
}
let defaultInteractIcon = <KeyboardIcon style={{paddingRight: "5px"}}/>;
let defaultInteractIcon = <KeyboardIcon fontSize="small" />;
if(interactType === "interactSplit"){
defaultInteractIcon = <VerticalSplitIcon style={{paddingRight: "5px"}}/>;
defaultInteractIcon = <VerticalSplitIcon fontSize="small" />;
} else if(interactType === "interactConsole"){
defaultInteractIcon = <TerminalIcon style={{paddingRight: "5px"}}/>;
defaultInteractIcon = <TerminalIcon fontSize="small" />;
}
const highIntegrity = rowDataStatic.integrity_level > 3;
const lockOwner = rowDataStatic.locked_operator?.username || "unknown operator";
return (
<div id={`callbacksTableID${rowDataStatic.id}`} style={{display: "inline-flex", alignItems: "flex-start"}}>
<IconButton style={{padding: 0, margin: 0}} color={rowDataStatic.integrity_level > 2 ? "error" : ""}
onClick={(evt) => {evt.stopPropagation();localOnOpenTab()}}
>
{rowDataStatic.locked ? (<LockIcon style={{marginRight: "10px"}} />):(defaultInteractIcon)}
</IconButton>
{rowDataStatic.display_id}
<div id={`callbacksTableID${rowDataStatic.id}`} className="mythic-callback-interactCell">
<MythicStyledTooltip title={`Open ${interactType === "interactSplit" ? "split" : interactType === "interactConsole" ? "console" : "tasking"} view${highIntegrity ? " - high integrity callback" : ""}`}>
<IconButton
className={`mythic-callback-iconButton mythic-callback-interactButton ${highIntegrity ? "mythic-callback-interactButtonHighIntegrity" : ""}`}
onClick={(evt) => {evt.stopPropagation();localOnOpenTab()}}
>
{defaultInteractIcon}
</IconButton>
</MythicStyledTooltip>
<span className="mythic-callback-displayId">{rowDataStatic.display_id}</span>
<IconButton
style={{margin: 0, padding: 0}}
color={rowDataStatic.integrity_level > 2 ? "error" : ""}
className={`mythic-callback-iconButton mythic-callback-menuButton ${highIntegrity ? "mythic-callback-menuButtonHighIntegrity" : ""}`}
aria-haspopup="menu"
onClick={handleDropdownToggle}
ref={dropdownAnchorRef}
>
<ArrowDropDownIcon/>
<ArrowDropDownIcon fontSize="small" />
</IconButton>
{rowDataStatic.locked &&
<MythicStyledTooltip title={`Locked by ${lockOwner}`}>
<span className="mythic-callback-lockBadge">
<LockIcon fontSize="inherit" />
<span>Locked</span>
</span>
</MythicStyledTooltip>
}
{rowDataStatic.trigger_on_checkin_after_time > 0 &&
<MythicStyledTooltip title={`Alert on callback after no checkin for ${rowDataStatic.trigger_on_checkin_after_time} minutes`}>
<NotificationsActiveTwoToneIcon color={"success"}/>
<NotificationsActiveTwoToneIcon className="mythic-callback-statusIcon mythic-callback-statusIconSuccess" fontSize="small" />
</MythicStyledTooltip>
}
{rowDataStatic.callbackports.length > 0 &&
<MythicStyledTooltip title={proxyMessage()}>
<FontAwesomeIcon icon={faSocks} size="lg" style={{color: theme.palette.success.main}}/>
<FontAwesomeIcon icon={faSocks} className="mythic-callback-statusIcon mythic-callback-statusIconSuccess" />
</MythicStyledTooltip>
}
</div>
</div>
)
},
areEqual)
@@ -483,4 +494,4 @@ export const CallbacksTableTagsCell = React.memo(({rowData, cellData}) => {
<TagsDisplay tags={cellData} expand={false} />
</div>
)
}, areEqual)
}, areEqual)
@@ -681,7 +681,7 @@ export const CallbacksTabsCustomFileBasedBrowserPanel = ({ index, value, tabInfo
return (
<MythicTabPanel index={index} value={value}>
<Split direction="horizontal" style={{width: "100%", height: "100%", display: "flex", overflow: "hidden"}} minSize={[0,0]} sizes={[30, 70]} >
<div className="bg-gray-base" style={{display: "inline-flex"}}>
<div className="bg-gray-base" style={{display: "inline-flex", height: "100%", minHeight: 0, minWidth: 0, overflow: "hidden"}}>
<Backdrop open={backdropOpen} style={{zIndex: 2, position: "absolute"}} invisible={true}>
<CircularProgress color="inherit" />
</Backdrop>
@@ -701,8 +701,8 @@ export const CallbacksTabsCustomFileBasedBrowserPanel = ({ index, value, tabInfo
/>
</div>
<div className="bg-gray-light" style={{display: "inline-flex"}}>
<div style={{ display: 'flex', flexDirection: 'column', flexGrow: 1, overflow: "hidden" }}>
<div className="bg-gray-light" style={{display: "inline-flex", height: "100%", minHeight: 0, minWidth: 0, overflow: "hidden"}}>
<div style={{ display: 'flex', flexDirection: 'column', flexGrow: 1, minHeight: 0, minWidth: 0, overflow: "hidden" }}>
<div style={{ flexGrow: 0 }}>
<FileBrowserTableTop
tabInfo={tabInfo}
@@ -720,7 +720,7 @@ export const CallbacksTabsCustomFileBasedBrowserPanel = ({ index, value, tabInfo
treeConfig={treeConfig}
/>
</div>
<div style={{ flexGrow: 1 }}>
<div style={{ flex: "1 1 auto", minHeight: 0, minWidth: 0, overflow: "hidden" }}>
<Backdrop open={backdropOpen} style={{zIndex: 2, position: "absolute"}} invisible={true}>
<CircularProgress color="inherit" />
</Backdrop>
@@ -987,7 +987,7 @@ const FileBrowserTableTop = ({
autoTaskLsOnEmptyDirectoriesRef.current = autoTaskLsOnEmptyDirectories;
}, [autoTaskLsOnEmptyDirectories]);
return (
<Grid container spacing={0} style={{ paddingTop: '5px' }}>
<Grid container spacing={0} className="mythic-file-browser-tableTop">
<Grid size={12}>
{openEditHostDialog && (
<MythicDialog
@@ -1017,62 +1017,69 @@ const FileBrowserTableTop = ({
name={<>
{placeHolder}
<MythicStyledTooltip title={`Edit the supplied Host field`}>
<IconButton style={{ padding: '3px' }}
onClick={() => {setOpenEditHostDialog(true);}}
size="large">
<EditIcon color='info' />
<IconButton
className="mythic-file-browser-iconButton mythic-file-browser-labelButton mythic-file-browser-hoverInfo"
onClick={() => {setOpenEditHostDialog(true);}}
size="small">
<EditIcon fontSize="small" />
</IconButton>
</MythicStyledTooltip>
{placeHolderGroups}
</>}
marginTop="0"
marginBottom="0"
InputProps={{
className: "mythic-file-browser-pathInput",
endAdornment: (
<React.Fragment>
<div className="mythic-file-browser-toolbarGroup mythic-file-browser-toolbarGroupEnd">
<MythicStyledTooltip title={`Task current callback (${tabInfo["displayID"]}) to list contents`}>
<IconButton style={{ padding: '3px' }}
onClick={onLocalListFilesButton}
size="large">
<RefreshIcon color='info' />
<IconButton
className="mythic-file-browser-iconButton mythic-file-browser-hoverInfo"
onClick={onLocalListFilesButton}
size="small">
<RefreshIcon fontSize="small" />
</IconButton>
</MythicStyledTooltip>
{autoTaskLsOnEmptyDirectories ? (
<MythicStyledTooltip title={"Currently tasking listing on empty directories, click to toggle off"} >
<IconButton style={{padding: "3px"}}
onClick={onToggleAutoTaskLsOnEmptyDirectories}
disabled={!treeConfig.show_current_path}
size={"large"}>
<PlaylistAddIcon color={"success"} ></PlaylistAddIcon>
<IconButton
className="mythic-file-browser-iconButton mythic-file-browser-activeSuccess mythic-file-browser-hoverSuccess"
onClick={onToggleAutoTaskLsOnEmptyDirectories}
disabled={!treeConfig.show_current_path}
size="small">
<PlaylistAddIcon fontSize="small" />
</IconButton>
</MythicStyledTooltip>
) : (
<MythicStyledTooltip title={"Currently not tasking listing on empty directories, click to toggle on"} >
<IconButton style={{padding: "3px"}}
disabled={!treeConfig.show_current_path}
onClick={onToggleAutoTaskLsOnEmptyDirectories}
size={"large"}>
<PlaylistRemoveIcon color={"secondary"} ></PlaylistRemoveIcon>
<IconButton
className="mythic-file-browser-iconButton mythic-file-browser-hoverWarning"
disabled={!treeConfig.show_current_path}
onClick={onToggleAutoTaskLsOnEmptyDirectories}
size="small">
<PlaylistRemoveIcon fontSize="small" />
</IconButton>
</MythicStyledTooltip>
)}
<MythicStyledTooltip title={showDeletedFiles ? 'Hide Deleted Entries' : 'Show Deleted Entries'}>
<IconButton
style={{ padding: '3px' }}
className={`mythic-file-browser-iconButton mythic-file-browser-hoverWarning ${showDeletedFiles ? "mythic-file-browser-activeWarning" : ""}`}
onClick={onLocalToggleShowDeletedFiles}
size="large">
size="small">
{showDeletedFiles ? (
<VisibilityIcon color="success" />
<VisibilityIcon fontSize="small" />
) : (
<VisibilityOffIcon color={"secondary"} />
<VisibilityOffIcon fontSize="small" />
)}
</IconButton>
</MythicStyledTooltip>
<MythicStyledTooltip title={`Export Current Path and Children`}>
<IconButton style={{ padding: '3px' }}
disabled={treeConfig.export_function === ""}
onClick={onLocalExportButton}
color="info"
size="large">
<IosShareIcon/>
<IconButton
className="mythic-file-browser-iconButton mythic-file-browser-hoverInfo"
disabled={treeConfig.export_function === ""}
onClick={onLocalExportButton}
size="small">
<IosShareIcon fontSize="small" />
</IconButton>
</MythicStyledTooltip>
{treeConfig.extra_table_inputs?.length > 0 &&
@@ -1081,12 +1088,13 @@ const FileBrowserTableTop = ({
showExtraInputs ?
`Hide Extra Browser Inputs ${extraDataSet ? "( Extra Data Currently Set )" : ""}` :
`Show Extra Browser Inputs ${extraDataSet ? "( Extra Data Currently Set )" : ""}`}>
<IconButton style={{ padding: '3px' }}
onClick={() => {setShowExtraInputs(!showExtraInputs)}}
disableFocusRipple={true}
disableRipple={true}
size="large">
<SettingsInputComponentRoundedIcon color={extraDataRequired ? "error" : extraDataSet ? "warning" : "info"} />
<IconButton
className={`mythic-file-browser-iconButton mythic-file-browser-iconButtonCompound ${extraDataRequired ? "mythic-file-browser-activeError mythic-file-browser-hoverError" : extraDataSet ? "mythic-file-browser-activeWarning mythic-file-browser-hoverWarning" : "mythic-file-browser-hoverInfo"}`}
onClick={() => {setShowExtraInputs(!showExtraInputs)}}
disableFocusRipple={true}
disableRipple={true}
size="small">
<SettingsInputComponentRoundedIcon fontSize="small" />
{showExtraInputs ? (
<KeyboardArrowDownRoundedIcon style={{rotate: "180deg"}} />
) : (
@@ -1095,47 +1103,45 @@ const FileBrowserTableTop = ({
</IconButton>
</MythicStyledTooltip>
}
</React.Fragment>
</div>
),
startAdornment: (
<div style={{display: "inline-flex",
alignItems: "center",
borderRight: "1px solid grey",
marginRight: "10px",
padding: "0 5px 0 0"}}>
<div className="mythic-file-browser-toolbarGroup mythic-file-browser-toolbarGroupStart">
{tokenOptions.length > 0 &&
<CallbacksTabsTaskingInputTokenSelect width={"100%"}
options={tokenOptions} changeSelectedToken={changeSelectedToken}/>
<div className="mythic-file-browser-tokenSelect">
<CallbacksTabsTaskingInputTokenSelect width={"100%"}
options={tokenOptions} changeSelectedToken={changeSelectedToken}/>
</div>
}
<MythicStyledTooltip title={`Move back to previous listing`}>
<IconButton style={{ padding: '3px' }}
disabled={historyIndex >= history.length -1 }
onClick={moveIndexToPreviousListing}
color='info'
size="large">
<ArrowBackIcon />
<IconButton
className="mythic-file-browser-iconButton mythic-file-browser-hoverInfo"
disabled={historyIndex >= history.length -1 }
onClick={moveIndexToPreviousListing}
size="small">
<ArrowBackIcon fontSize="small" />
</IconButton>
</MythicStyledTooltip>
<MythicStyledTooltip title={`Move to next listing`}>
<IconButton style={{ padding: '3px' }}
disabled={historyIndex <= 0}
onClick={moveIndexToNextListing}
size="large"
color='info'>
<ArrowForwardIcon />
<IconButton
className="mythic-file-browser-iconButton mythic-file-browser-hoverInfo"
disabled={historyIndex <= 0}
onClick={moveIndexToNextListing}
size="small">
<ArrowForwardIcon fontSize="small" />
</IconButton>
</MythicStyledTooltip>
<MythicStyledTooltip title={"Move up a directory"} >
<IconButton style={{padding: "0 0 0 0"}}
onClick={onLocalMoveUpDirectoryButton}
disabled={!selectedFolderData?.parent_path_text || selectedFolderData?.parent_path_text?.length === 0 || selectedFolderData.root || fullPath === ""}
<IconButton
className="mythic-file-browser-iconButton mythic-file-browser-hoverInfo"
onClick={onLocalMoveUpDirectoryButton}
disabled={!selectedFolderData?.parent_path_text || selectedFolderData?.parent_path_text?.length === 0 || selectedFolderData.root || fullPath === ""}
>
<KeyboardReturnIcon style={{rotate: "90deg"}} ></KeyboardReturnIcon>
<KeyboardReturnIcon fontSize="small" style={{rotate: "90deg"}} />
</IconButton>
</MythicStyledTooltip>
</div>),
style: { },
}}
/>
{showExtraInputs &&
@@ -683,7 +683,7 @@ export const CallbacksTabsFileBrowserPanel = ({ index, value, tabInfo, me, setNe
return (
<MythicTabPanel index={index} value={value}>
<Split direction="horizontal" style={{width: "100%", height: "100%", display: "flex", overflow: "hidden"}} sizes={[30, 70]} >
<div className="bg-gray-base" style={{display: "inline-flex"}}>
<div className="bg-gray-base" style={{display: "inline-flex", height: "100%", minHeight: 0, minWidth: 0, overflow: "hidden"}}>
<Backdrop open={backdropOpen} style={{zIndex: 2, position: "absolute"}} invisible={true}>
<CircularProgress color="inherit" />
</Backdrop>
@@ -702,8 +702,8 @@ export const CallbacksTabsFileBrowserPanel = ({ index, value, tabInfo, me, setNe
/>
</div>
<div className="bg-gray-light" style={{display: "inline-flex"}}>
<div style={{ display: 'flex', flexDirection: 'column', flexGrow: 1, overflow: "hidden" }}>
<div className="bg-gray-light" style={{display: "inline-flex", height: "100%", minHeight: 0, minWidth: 0, overflow: "hidden"}}>
<div style={{ display: 'flex', flexDirection: 'column', flexGrow: 1, minHeight: 0, minWidth: 0, overflow: "hidden" }}>
<div style={{ flexGrow: 0 }}>
<FileBrowserTableTop
tabInfo={tabInfo}
@@ -720,7 +720,7 @@ export const CallbacksTabsFileBrowserPanel = ({ index, value, tabInfo, me, setNe
baseUIFeature={baseUIFeature}
/>
</div>
<div style={{ flexGrow: 1 }}>
<div style={{ flex: "1 1 auto", minHeight: 0, minWidth: 0, overflow: "hidden" }}>
<Backdrop open={backdropOpen} style={{zIndex: 2, position: "absolute"}} invisible={true}>
<CircularProgress color="inherit" />
</Backdrop>
@@ -917,7 +917,7 @@ const FileBrowserTableTop = ({
autoTaskLsOnEmptyDirectoriesRef.current = autoTaskLsOnEmptyDirectories;
}, [autoTaskLsOnEmptyDirectories]);
return (
<Grid container spacing={0} style={{ paddingTop: '10px' }}>
<Grid container spacing={0} className="mythic-file-browser-tableTop">
<Grid size={12}>
<MythicTextField
placeholder={selectedFolderData.host}
@@ -925,93 +925,98 @@ const FileBrowserTableTop = ({
onEnter={goToDirectory}
onChange={onChangePath}
name={placeHolder}
marginTop="0"
marginBottom="0"
InputProps={{
className: "mythic-file-browser-pathInput",
endAdornment: (
<React.Fragment>
<div className="mythic-file-browser-toolbarGroup mythic-file-browser-toolbarGroupEnd">
<MythicStyledTooltip title={`Task current callback (${tabInfo["displayID"]}) to list contents`}>
<IconButton style={{ padding: '3px' }}
onClick={onLocalListFilesButton}
size="large">
<RefreshIcon color='info' />
<IconButton
className="mythic-file-browser-iconButton mythic-file-browser-hoverInfo"
onClick={onLocalListFilesButton}
size="small">
<RefreshIcon fontSize="small" />
</IconButton>
</MythicStyledTooltip>
<MythicStyledTooltip title={`Upload file to folder via current callback (${tabInfo["displayID"]})`}>
<IconButton style={{ padding: '3px' }}
onClick={onLocalUploadFileButton}
size="large">
<CloudUploadIcon color="info" />
<IconButton
className="mythic-file-browser-iconButton mythic-file-browser-hoverInfo"
onClick={onLocalUploadFileButton}
size="small">
<CloudUploadIcon fontSize="small" />
</IconButton>
</MythicStyledTooltip>
{autoTaskLsOnEmptyDirectories ? (
<MythicStyledTooltip title={"Currently tasking listing on empty directories, click to toggle off"} >
<IconButton style={{padding: "3px"}}
onClick={onToggleAutoTaskLsOnEmptyDirectories}
size={"large"}>
<PlaylistAddIcon color={"success"} ></PlaylistAddIcon>
<IconButton
className="mythic-file-browser-iconButton mythic-file-browser-activeSuccess mythic-file-browser-hoverSuccess"
onClick={onToggleAutoTaskLsOnEmptyDirectories}
size="small">
<PlaylistAddIcon fontSize="small" />
</IconButton>
</MythicStyledTooltip>
) : (
<MythicStyledTooltip title={"Currently not tasking listing on empty directories, click to toggle on"} >
<IconButton style={{padding: "3px"}}
onClick={onToggleAutoTaskLsOnEmptyDirectories}
size={"large"}>
<PlaylistRemoveIcon color={"secondary"} ></PlaylistRemoveIcon>
<IconButton
className="mythic-file-browser-iconButton mythic-file-browser-hoverWarning"
onClick={onToggleAutoTaskLsOnEmptyDirectories}
size="small">
<PlaylistRemoveIcon fontSize="small" />
</IconButton>
</MythicStyledTooltip>
)}
<MythicStyledTooltip title={showDeletedFiles ? 'Hide Deleted Files' : 'Show Deleted Files'}>
<IconButton
style={{ padding: '3px' }}
className={`mythic-file-browser-iconButton mythic-file-browser-hoverWarning ${showDeletedFiles ? "mythic-file-browser-activeWarning" : ""}`}
onClick={onLocalToggleShowDeletedFiles}
size="large">
size="small">
{showDeletedFiles ? (
<VisibilityIcon color="success" />
<VisibilityIcon fontSize="small" />
) : (
<VisibilityOffIcon color="error" />
<VisibilityOffIcon fontSize="small" />
)}
</IconButton>
</MythicStyledTooltip>
</React.Fragment>
</div>
),
startAdornment: (
<div style={{display: "inline-flex",
alignItems: "center",
borderRight: "1px solid grey",
marginRight: "10px",
padding: "0 5px 0 0"}}>
<div className="mythic-file-browser-toolbarGroup mythic-file-browser-toolbarGroupStart">
{tokenOptions.length > 0 &&
<CallbacksTabsTaskingInputTokenSelect width={"100%"}
options={tokenOptions} changeSelectedToken={changeSelectedToken}/>
<div className="mythic-file-browser-tokenSelect">
<CallbacksTabsTaskingInputTokenSelect width={"100%"}
options={tokenOptions} changeSelectedToken={changeSelectedToken}/>
</div>
}
<MythicStyledTooltip title={`Move back to previous listing`}>
<IconButton style={{ padding: '3px' }}
disabled={historyIndex >= history.length -1 }
onClick={moveIndexToPreviousListing}
color='info'
size="large">
<ArrowBackIcon />
<IconButton
className="mythic-file-browser-iconButton mythic-file-browser-hoverInfo"
disabled={historyIndex >= history.length -1 }
onClick={moveIndexToPreviousListing}
size="small">
<ArrowBackIcon fontSize="small" />
</IconButton>
</MythicStyledTooltip>
<MythicStyledTooltip title={`Move to next listing`}>
<IconButton style={{ padding: '3px' }}
disabled={historyIndex <= 0}
onClick={moveIndexToNextListing}
size="large"
color='info'>
<ArrowForwardIcon />
<IconButton
className="mythic-file-browser-iconButton mythic-file-browser-hoverInfo"
disabled={historyIndex <= 0}
onClick={moveIndexToNextListing}
size="small">
<ArrowForwardIcon fontSize="small" />
</IconButton>
</MythicStyledTooltip>
<MythicStyledTooltip title={"Move up a directory"} >
<IconButton style={{padding: "0 0 0 0"}}
onClick={onLocalMoveUpDirectoryButton}
disabled={!selectedFolderData?.parent_path_text || selectedFolderData?.parent_path_text?.length === 0 || selectedFolderData.root || fullPath === ""}
<IconButton
className="mythic-file-browser-iconButton mythic-file-browser-hoverInfo"
onClick={onLocalMoveUpDirectoryButton}
disabled={!selectedFolderData?.parent_path_text || selectedFolderData?.parent_path_text?.length === 0 || selectedFolderData.root || fullPath === ""}
>
<KeyboardReturnIcon style={{rotate: "90deg"}} ></KeyboardReturnIcon>
<KeyboardReturnIcon fontSize="small" style={{rotate: "90deg"}} />
</IconButton>
</MythicStyledTooltip>
</div>),
style: { },
}}
/>
</Grid>
@@ -418,10 +418,10 @@ export const CallbacksTabsTaskingSplitPanel = ({tabInfo, index, value, onCloseTa
return (
<MythicTabPanel index={index} value={value} >
<Split direction="horizontal" minSize={[0,0]} sizes={[30, 70]}
style={{width: "100%", height: "100%", overflow: "auto", display: "flex", justifyContent: "flex-end"}} >
<div className="bg-gray-base" style={{display: "inline-flex", flexDirection: "column"}} >
style={{width: "100%", height: "100%", minWidth: 0, overflow: "hidden", display: "flex"}} >
<div className="bg-gray-base" style={{display: "inline-flex", flexDirection: "column", minWidth: 0, overflow: "hidden"}} >
<div style={{overflowY: "auto", flexGrow: 1}} id={`taskingPanelSplit${tabInfo.callbackID}`}>
<div style={{overflowY: "auto", overflowX: "hidden", flexGrow: 1, minWidth: 0}} id={`taskingPanelSplit${tabInfo.callbackID}`}>
{!fetchedAllTasks &&
<MythicStyledTooltip title="Fetch Older Tasks">
@@ -450,7 +450,7 @@ export const CallbacksTabsTaskingSplitPanel = ({tabInfo, index, value, onCloseTa
</div>
</div>
<div className="bg-gray-light" style={{display: "inline-flex", flexDirection: "column", height: "100%", width: "100%"}}>
<div className="bg-gray-light" style={{display: "inline-flex", flexDirection: "column", height: "100%", minWidth: 0, overflow: "hidden", width: "100%"}}>
{selectedTask.id > 0 && <TaskDisplayFlat key={"taskinteractdisplaysplit" + selectedTask.id} me={me} task={selectedTask}
command_id={selectedTask.command == null ? 0 : selectedTask.command.id}
filterOptions={filterOptions}
@@ -483,7 +483,7 @@ export const CallbacksTabsTaskingSplitPanel = ({tabInfo, index, value, onCloseTa
}
const CallbacksTabsTaskingSplitTable = ({selectedTask, me}) => {
return (
<div style={{width: "100%", height: "100%", overflowY: "auto", display: "flex", flexDirection: "column"}} >
<div style={{width: "100%", flex: "1 1 auto", minHeight: 0, minWidth: 0, overflow: "hidden", display: "flex", flexDirection: "column"}} >
{selectedTask.id > 0 &&
<TaskDisplayContainerFlat key={selectedTask.id} me={me} task={selectedTask} />
}
@@ -364,7 +364,7 @@ const NonInteractiveResponseDisplay = (props) => {
{props.searchOutput &&
<SearchBar onSubmitSearch={onSubmitSearch} />
}
<div style={{overflowY: "auto", flexGrow: 1, width: "100%", height: props.expand ? "100%": undefined, display: "flex", flexDirection: "column"}} ref={props.responseRef}>
<div style={{overflowY: "auto", overflowX: "hidden", flexGrow: 1, minWidth: 0, width: "100%", height: props.expand ? "100%": undefined, display: "flex", flexDirection: "column"}} ref={props.responseRef}>
<ResponseDisplayComponent rawResponses={rawResponses} viewBrowserScript={props.viewBrowserScript}
output={output} command_id={props.command_id} displayType={"accordion"}
task={props.task} search={search} expand={props.expand}/>
@@ -430,7 +430,7 @@ export const NonInteractiveResponseDisplayConsole = (props) => {
return (
<React.Fragment>
<div style={{overflowY: "auto", width: "100%", height: props.expand ? "100%" : undefined}}
<div style={{overflowY: "auto", overflowX: "hidden", minWidth: 0, width: "100%", height: props.expand ? "100%" : undefined}}
ref={props.responseRef}>
<ResponseDisplayComponent rawResponses={rawResponses} viewBrowserScript={props.viewBrowserScript}
output={output} command_id={props.command_id} displayType={"console"}
@@ -808,10 +808,10 @@ export const ResponseDisplayTable = ({table, callback_id, expand, task}) =>{
return expand ? {flexGrow: 1,
minHeight: gridData.length > 0 ? Math.min(maxHeight, dataHeight) : headerHeight,
width: "100%", position: "relative"} :
{height: dataHeight, position: "relative"}
{height: dataHeight, position: "relative", width: "100%"}
}, [expand, dataHeight, gridData]);
return (
<div style={{height: "100%", display: "flex", flexDirection: "column", position: "relative", width: "100%"}}>
<div className="mythic-response-table" style={{height: "100%", position: "relative"}}>
{table?.title && (
<MythicSectionHeader
dense
@@ -820,7 +820,7 @@ export const ResponseDisplayTable = ({table, callback_id, expand, task}) =>{
/>
)}
<div style={tableStyle}>
<div className="mythic-response-table-grid" style={tableStyle}>
<MythicResizableGrid
columns={columns}
sortIndicatorIndex={sortColumn}
@@ -14,11 +14,11 @@ import {gql, useSubscription } from '@apollo/client';
import {TaskDisplayContainer, TaskDisplayContainerConsole} from './TaskDisplayContainer';
import {TagsDisplay} from '../../MythicComponents/MythicTag';
import {taskingDataFragment} from './CallbackMutations';
import {GetMythicSetting} from "../../MythicComponents/MythicSavedUserSetting";
import {GetMythicSetting, useGetMythicSetting} from "../../MythicComponents/MythicSavedUserSetting";
import PlayCircleFilledTwoToneIcon from '@mui/icons-material/PlayCircleFilledTwoTone';
import CropRotateTwoToneIcon from '@mui/icons-material/CropRotateTwoTone';
import {MythicStyledTooltip} from "../../MythicComponents/MythicStyledTooltip";
import {operatorSettingDefaults} from "../../../cache";
import {normalizeTaskingDisplayFields, operatorSettingDefaults} from "../../../cache";
import {TaskFromUIButton} from "./TaskFromUIButton";
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import {faSkullCrossbones} from '@fortawesome/free-solid-svg-icons';
@@ -406,6 +406,66 @@ const getCallbackPrimaryIP = (task) => {
return "";
}
}
const useTaskingDisplayFields = () => {
const taskingDisplayFields = useGetMythicSetting({setting_name: "taskingDisplayFields", default_value: operatorSettingDefaults.taskingDisplayFields});
return React.useMemo(() => normalizeTaskingDisplayFields(taskingDisplayFields), [taskingDisplayFields]);
}
const TaskingMetadataField = ({fieldName, task, displayTimestamp, me, compactTimestamp=false}) => {
const ipValue = getCallbackPrimaryIP(task);
const callbackGroups = task.callback.mythictree_groups?.join(', ') || "";
switch(fieldName){
case "timestamp":
return (
<TaskMetaItem title={"Task timestamp"} icon={<AccessTimeIcon />}>
{compactTimestamp ? toLocalTimeShort(displayTimestamp, me?.user?.view_utc_time || false) : toLocalTime(displayTimestamp, me?.user?.view_utc_time || false)}
</TaskMetaItem>
);
case "task":
return (
<TaskMetaItem title={"View Task in separate page"} icon={<NumbersIcon />}>
<Link underline="hover" target="_blank" href={"/new/task/" + task.display_id}>T-{task.display_id}</Link>
</TaskMetaItem>
);
case "username":
return (
<TaskMetaItem title={"Operator"} icon={<PersonOutlineIcon />}>
{task.operator.username}
</TaskMetaItem>
);
case "callback":
return (
<TaskMetaItem title={"View Callback in separate page"}>
<Link underline="hover" target="_blank" href={"/new/callbacks/" + task.callback.display_id}>C-{task.callback.display_id}</Link>
</TaskMetaItem>
);
case "host":
return (
<TaskMetaItem title={"Host"} icon={<ComputerIcon />}>
{task.callback.host}
</TaskMetaItem>
);
case "ip":
return ipValue !== "" ? (
<TaskMetaItem title={"IP address"} icon={<PublicIcon />}>
{ipValue}
</TaskMetaItem>
) : null;
case "groups":
return callbackGroups !== "" ? (
<TaskMetaItem title={"Callback groups"}>
{callbackGroups}
</TaskMetaItem>
) : null;
case "payload_type":
return task?.command?.payloadtype?.name ? (
<TaskMetaItem title={"Payload type"}>
{task.command.payloadtype.name}
</TaskMetaItem>
) : null;
default:
return null;
}
}
const TaskMetaItem = ({children, icon, title, style}) => {
const item = (
<span className={classes.taskMetaItem} style={style}>
@@ -450,18 +510,13 @@ const ColoredTaskDisplay = ({task, theme, children, expanded}) => {
}
export const ColoredTaskLabel = ({task, theme, me, taskDivID, onClick, displayChildren, toggleDisplayChildren, expanded, compact=false }) => {
const [displayComment, setDisplayComment] = React.useState(false);
const initialHideUsernameValue = GetMythicSetting({setting_name: "hideUsernames", default_value: operatorSettingDefaults.hideUsernames});
const initialShowIPValue = GetMythicSetting({setting_name: "showIP", default_value: operatorSettingDefaults.showIP});
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 taskingDisplayFields = useTaskingDisplayFields();
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,
@@ -499,40 +554,15 @@ export const ColoredTaskLabel = ({task, theme, me, taskDivID, onClick, displayCh
</div>
)}
<div className={classes.taskMetaRow} onClick={preventPropagation}>
<TaskMetaItem title={"Task timestamp"} icon={<AccessTimeIcon />}>
{toLocalTime(displayTimestamp, me?.user?.view_utc_time || false)}
</TaskMetaItem>
<TaskMetaItem title={"View Task in separate page"} icon={<NumbersIcon />}>
<Link underline="hover" target="_blank" href={"/new/task/" + task.display_id}>T-{task.display_id}</Link>
</TaskMetaItem>
{!initialHideUsernameValue &&
<TaskMetaItem title={"Operator"} icon={<PersonOutlineIcon />}>
{task.operator.username}
</TaskMetaItem>
}
<TaskMetaItem title={"View Callback in separate page"}>
<Link underline="hover" target="_blank" href={"/new/callbacks/" + task.callback.display_id}>C-{task.callback.display_id}</Link>
</TaskMetaItem>
{initialShowHostnameValue &&
<TaskMetaItem title={"Host"} icon={<ComputerIcon />}>
{task.callback.host}
</TaskMetaItem>
}
{initialShowIPValue && ipValue !== "" &&
<TaskMetaItem title={"IP address"} icon={<PublicIcon />}>
{ipValue}
</TaskMetaItem>
}
{initialShowCallbackGroupsValue && callbackGroups !== "" &&
<TaskMetaItem title={"Callback groups"}>
{callbackGroups}
</TaskMetaItem>
}
{task?.command?.payloadtype?.name &&
<TaskMetaItem title={"Payload type"}>
{task.command.payloadtype.name}
</TaskMetaItem>
}
{taskingDisplayFields.map((fieldName) => (
<TaskingMetadataField
displayTimestamp={displayTimestamp}
fieldName={fieldName}
key={fieldName}
me={me}
task={task}
/>
))}
{initialShowOPSECBypassUsername && opsecBypassUsers !== "" &&
<TaskMetaItem title={"The specified usernames approved OPSEC bypasses for this task"} icon={<LockOpenIcon />}>
{opsecBypassUsers}
@@ -1155,13 +1185,10 @@ const TaskRowConsole = ({task, filterOptions, me, newlyIssuedTasks, indentLevel}
)
}
export const ColoredTaskLabelConsole = ({task, theme, me, taskDivID, onClick, displayChildren, toggleDisplayChildren, expanded }) => {
const initialHideUsernameValue = GetMythicSetting({setting_name: "hideUsernames", default_value: operatorSettingDefaults.hideUsernames});
const taskingDisplayFields = useTaskingDisplayFields();
const initialTaskTimestampDisplayField = GetMythicSetting({setting_name: "taskTimestampDisplayField", default_value: operatorSettingDefaults.taskTimestampDisplayField});
const displayTimestamp = task[initialTaskTimestampDisplayField] ? task[initialTaskTimestampDisplayField] : task.timestamp;
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) => {
@@ -1178,27 +1205,16 @@ export const ColoredTaskLabelConsole = ({task, theme, me, taskDivID, onClick, di
<ColoredTaskDisplayConsole task={task} theme={theme} expanded={expanded}>
<div id={taskDivID} className={classes.taskHeaderBody}>
<div className={classes.taskMetaRow} onClick={preventPropagation}>
<TaskMetaItem title={"Task timestamp"} icon={<AccessTimeIcon />}>
{toLocalTimeShort(displayTimestamp, me?.user?.view_utc_time || false)}
</TaskMetaItem>
{!initialHideUsernameValue &&
<TaskMetaItem title={"Operator"} icon={<PersonOutlineIcon />}>
{task.operator.username}
</TaskMetaItem>
}
<TaskMetaItem title={"View Task in separate page"} icon={<NumbersIcon />}>
<Link underline="hover" target="_blank" href={"/new/task/" + task.display_id}>T-{task.display_id}</Link>
</TaskMetaItem>
{initialShowHostnameValue &&
<TaskMetaItem title={"Host"} icon={<ComputerIcon />}>
{task.callback.host}
</TaskMetaItem>
}
{initialShowIPValue && ipValue !== "" &&
<TaskMetaItem title={"IP address"} icon={<PublicIcon />}>
{ipValue}
</TaskMetaItem>
}
{taskingDisplayFields.map((fieldName) => (
<TaskingMetadataField
compactTimestamp
displayTimestamp={displayTimestamp}
fieldName={fieldName}
key={fieldName}
me={me}
task={task}
/>
))}
{(task.opsec_pre_blocked && !task.opsec_pre_bypassed) &&
<TaskMetaItem title={"OPSEC blocked before tasking"} icon={<WarningAmberIcon />}>
OPSEC pre
@@ -138,7 +138,8 @@ export const TaskDisplayContainerFlat = ({task, me}) => {
}, [searchOutput]);
return (
<div style={{ height: "100%", position: "relative", display: "flex", overflowY: "auto", justifyContent: "flex-end" }}>
<div style={{height: "100%", minWidth: 0, overflow: "hidden", position: "relative", display: "flex", width: "100%"}}>
<div style={{display: "flex", flex: "1 1 auto", height: "100%", minWidth: 0, overflow: "hidden"}}>
<ResponseDisplay
key={task.id}
task={task}
@@ -150,6 +151,8 @@ export const TaskDisplayContainerFlat = ({task, me}) => {
expand={true}
responseRef={responseRef}
/>
</div>
<div style={{flex: "0 0 30px", height: "100%", minWidth: "30px", overflow: "hidden", zIndex: 2}}>
<SideDisplayGeneric toggleViewBrowserScript={toggleViewBrowserScript}
toggleSelectAllOutput={toggleSelectAllOutput}
toggleOpenSearch={toggleOpenSearch}
@@ -159,6 +162,7 @@ export const TaskDisplayContainerFlat = ({task, me}) => {
style={{position: "absolute", bottom: "15px", zIndex: 2}}
fabStyle={{ }}
viewAllOutput={selectAllOutput}/>
</div>
</div>
)
@@ -1,11 +1,13 @@
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 MythicTextField from '../../MythicComponents/MythicTextField';
import TableRow from '@mui/material/TableRow';
import Switch from '@mui/material/Switch';
import Box from '@mui/material/Box';
import IconButton from '@mui/material/IconButton';
import Table from '@mui/material/Table';
import TableBody from '@mui/material/TableBody';
import Link from '@mui/material/Link';
@@ -15,11 +17,11 @@ import {GetMythicSetting, useSetMythicSetting} from "../../MythicComponents/Myth
import MenuItem from '@mui/material/MenuItem';
import Select from '@mui/material/Select';
import Input from '@mui/material/Input';
import IconButton from '@mui/material/IconButton';
import MythicStyledTableCell from "../../MythicComponents/MythicTableCell";
import {operatorSettingDefaults, taskingContextFieldsOptions, taskTimestampDisplayFieldOptions} from "../../../cache";
import {normalizeTaskingDisplayFields, operatorSettingDefaults, taskingContextFieldsOptions, taskingDisplayFieldOptions, taskTimestampDisplayFieldOptions} from "../../../cache";
import CloudDownloadIcon from '@mui/icons-material/CloudDownload';
import CloudUploadIcon from '@mui/icons-material/CloudUpload';
import DragHandleIcon from '@mui/icons-material/DragHandle';
import {MythicStyledTooltip} from "../../MythicComponents/MythicStyledTooltip";
import {snackActions} from "../../utilities/Snackbar";
import {userSettingsQuery} from "../../App";
@@ -28,6 +30,21 @@ import {useLazyQuery } from '@apollo/client';
import PhoneCallbackIcon from '@mui/icons-material/PhoneCallback';
import ColorLensIcon from '@mui/icons-material/ColorLens';
import {MythicColorSwatchInput} from "../../MythicComponents/MythicColorInput";
import VisibilityOffIcon from '@mui/icons-material/VisibilityOff';
import VisibilityIcon from '@mui/icons-material/Visibility';
import {MythicDialog} from "../../MythicComponents/MythicDialog";
import {
MythicDialogBody,
MythicDialogButton,
MythicDialogFooter,
MythicDialogSection,
} from "../../MythicComponents/MythicDialogLayout";
import {reorder} from "../../MythicComponents/MythicDraggableList";
import {
Draggable,
DragDropContext,
Droppable,
} from "@hello-pangea/dnd";
const interactTypeOptions = [
{value: "interact", display: "Accordions"},
@@ -39,6 +56,200 @@ const commonFontFamilies = [
"-apple-system,BlinkMacSystemFont,\"Segoe UI\",Roboto,\"Helvetica Neue\",Arial,sans-serif,\"Apple Color Emoji\",\"Segoe UI Emoji\",\"Segoe UI Symbol\"",
"Monaco"
];
const getTaskingDisplayOption = (fieldName) => {
return taskingDisplayFieldOptions.find((option) => option.name === fieldName);
}
const getTaskingDisplayLayoutItems = (visibleFields) => {
const visibleSet = new Set(visibleFields);
const orderedVisibleItems = visibleFields.reduce((prev, fieldName) => {
const option = getTaskingDisplayOption(fieldName);
if(option){
return [...prev, {...option, visible: true}];
}
return prev;
}, []);
const hiddenItems = taskingDisplayFieldOptions.reduce((prev, option) => {
if(visibleSet.has(option.name)){
return prev;
}
return [...prev, {...option, visible: false}];
}, []);
return [...orderedVisibleItems, ...hiddenItems];
}
const TaskingMetadataSummary = ({value, onChange}) => {
const [openLayoutDialog, setOpenLayoutDialog] = React.useState(false);
const selectedOptions = value.map(getTaskingDisplayOption).filter(Boolean);
const hiddenCount = taskingDisplayFieldOptions.length - selectedOptions.length;
const onSubmitLayout = (items) => {
onChange(normalizeTaskingDisplayFields(items.reduce((prev, item) => {
if(item.visible){
return [...prev, item.name];
}
return prev;
}, [])));
setOpenLayoutDialog(false);
}
return (
<>
<Box className="mythic-tasking-visibility-panel mythic-tasking-visibility-summary-panel">
<Box>
<Typography component="div" className="mythic-tasking-visibility-title">
Tasking metadata
</Typography>
<Typography component="div" className="mythic-tasking-visibility-description">
Selected chips appear in this order above task commands.
</Typography>
</Box>
<Box className="mythic-tasking-visibility-summary-actions">
<Typography component="div" className="mythic-tasking-visibility-count">
{selectedOptions.length} shown{hiddenCount > 0 ? `, ${hiddenCount} hidden` : ""}
</Typography>
<Button
className="mythic-dialog-title-action mythic-tasking-visibility-manage-button"
onClick={() => setOpenLayoutDialog(true)}
size="small"
variant="outlined"
>
Manage
</Button>
</Box>
<Box className="mythic-tasking-visibility-chip-row">
{selectedOptions.length > 0 ? (
selectedOptions.map((option, index) => (
<span className="mythic-tasking-visibility-chip" key={option.name}>
<span className="mythic-tasking-visibility-chip-index">{index + 1}</span>
{option.display}
</span>
))
) : (
<Typography component="div" className="mythic-tasking-visibility-empty">
No tasking metadata selected.
</Typography>
)}
</Box>
</Box>
{openLayoutDialog &&
<MythicDialog
open={openLayoutDialog}
onClose={() => setOpenLayoutDialog(false)}
maxWidth="sm"
innerDialog={
<TaskingMetadataLayoutDialog
initialItems={getTaskingDisplayLayoutItems(value)}
onClose={() => setOpenLayoutDialog(false)}
onReset={() => onSubmitLayout(getTaskingDisplayLayoutItems(operatorSettingDefaults.taskingDisplayFields))}
onSubmit={onSubmitLayout}
/>
}
/>
}
</>
)
}
const TaskingMetadataLayoutDialog = ({initialItems, onClose, onReset, onSubmit}) => {
const [items, setItems] = React.useState(initialItems);
const onDragEnd = ({destination, source}) => {
if(!destination){
return;
}
setItems(reorder(items, source.index, destination.index));
}
const onToggleVisibility = (index) => {
setItems(items.map((item, itemIndex) => {
if(itemIndex === index){
return {...item, visible: !item.visible};
}
return item;
}));
}
return (
<>
<DialogTitle id="form-dialog-title">Tasking Metadata Layout</DialogTitle>
<DialogContent dividers={true} sx={{p: 0}}>
<MythicDialogBody sx={{height: "min(70vh, 38rem)", p: 1}}>
<MythicDialogSection
title="Metadata Chips"
description="Drag to set display order. Toggle visibility to choose what appears in tasking."
sx={{display: "flex", flexDirection: "column", flex: "1 1 auto", minHeight: 0}}
>
<TaskingMetadataDraggableList
items={items}
onDragEnd={onDragEnd}
onToggleVisibility={onToggleVisibility}
/>
</MythicDialogSection>
</MythicDialogBody>
</DialogContent>
<MythicDialogFooter>
<MythicDialogButton onClick={onClose}>
Cancel
</MythicDialogButton>
<MythicDialogButton onClick={onReset} intent="warning">
Reset
</MythicDialogButton>
<MythicDialogButton onClick={() => onSubmit(items)} intent="primary">
Save
</MythicDialogButton>
</MythicDialogFooter>
</>
)
}
const TaskingMetadataDraggableList = ({items, onDragEnd, onToggleVisibility}) => {
return (
<DragDropContext onDragEnd={onDragEnd}>
<Droppable droppableId="tasking-metadata-layout-list">
{(provided) => (
<div className="mythic-reorder-list" ref={provided.innerRef} {...provided.droppableProps}>
{items.map((item, index) => (
<TaskingMetadataDraggableListItem
item={item}
index={index}
key={item.name}
onToggleVisibility={onToggleVisibility}
/>
))}
{provided.placeholder}
</div>
)}
</Droppable>
</DragDropContext>
)
}
const TaskingMetadataDraggableListItem = ({item, index, onToggleVisibility}) => {
return (
<Draggable draggableId={item.name} index={index}>
{(provided, snapshot) => (
<div
ref={provided.innerRef}
className={`mythic-reorder-row mythic-tasking-metadata-row${snapshot.isDragging ? " mythic-reorder-row-dragging" : ""}${item.visible ? "" : " mythic-reorder-row-disabled"}`}
{...provided.draggableProps}
>
<span className="mythic-reorder-drag-handle" {...provided.dragHandleProps}>
<DragHandleIcon fontSize="small" />
</span>
<div className="mythic-reorder-row-main">
<span className="mythic-reorder-row-title">{item.display}</span>
<span className="mythic-reorder-row-description">{item.description}</span>
</div>
<div className="mythic-reorder-row-actions">
<IconButton
aria-label={item.visible ? `Hide ${item.display}` : `Show ${item.display}`}
className={`mythic-table-row-icon-action ${item.visible ? "mythic-table-row-icon-action-hover-danger" : "mythic-table-row-icon-action-hover-info"}`}
size="small"
onClick={() => onToggleVisibility(index)}
>
{item.visible ? (
<VisibilityIcon fontSize="small" />
) : (
<VisibilityOffIcon fontSize="small" />
)}
</IconButton>
</div>
</div>
)}
</Draggable>
)
}
const isValidColor = (color) =>{
if(typeof color !== "string"){
return false;
@@ -502,17 +713,8 @@ export function SettingsOperatorUIConfigDialog(props) {
const initialShowMediaValue = GetMythicSetting({setting_name: "showMedia", default_value: operatorSettingDefaults.showMedia});
const [showMedia, setShowMedia] = React.useState(initialShowMediaValue);
const initialHideUsernameValue = GetMythicSetting({setting_name: "hideUsernames", default_value: operatorSettingDefaults.hideUsernames});
const [hideUsernames, setHideUsernames] = React.useState(initialHideUsernameValue);
const initialShowIPValue = GetMythicSetting({setting_name: "showIP", default_value: operatorSettingDefaults.showIP});
const [showIP, setShowIP] = React.useState(initialShowIPValue);
const initialShowHostnameValue = GetMythicSetting({setting_name: "showHostname", default_value: operatorSettingDefaults.showHostname});
const [showHostname, setShowHostname] = React.useState(initialShowHostnameValue);
const initialShowCallbackGroupsValue = GetMythicSetting({setting_name: "showCallbackGroups", default_value: operatorSettingDefaults.showCallbackGroups});
const [showCallbackGroups, setShowCallbackGroups] = React.useState(initialShowCallbackGroupsValue);
const initialTaskingDisplayFields = normalizeTaskingDisplayFields(GetMythicSetting({setting_name: "taskingDisplayFields", default_value: operatorSettingDefaults.taskingDisplayFields}));
const [taskingDisplayFields, setTaskingDisplayFields] = React.useState(initialTaskingDisplayFields);
const initialUseDisplayParamsForCLIHistory = GetMythicSetting({setting_name: "useDisplayParamsForCLIHistory", default_value: operatorSettingDefaults.useDisplayParamsForCLIHistory});
const [useDisplayParamsForCLIHistory, setUseDisplayParamsForCLIHistory] = React.useState(initialUseDisplayParamsForCLIHistory);
@@ -652,12 +854,6 @@ export function SettingsOperatorUIConfigDialog(props) {
const onChangeFontFamily = (name, value, error) => {
setFontFamily(value);
}
const onHideUsernamesChanged = (evt) => {
setHideUsernames(!hideUsernames);
}
const onShowIPChanged = (evt) => {
setShowIP(!showIP);
}
const onHideBrowserTaskingChanged = (evt) => {
setHideBrowserTasking(!hideBrowserTasking);
}
@@ -667,12 +863,6 @@ export function SettingsOperatorUIConfigDialog(props) {
const onHideTaskingContextChanged = (evt) => {
setHideTaskingContext(!hideTaskingContext);
}
const onShowHostnameChanged = (evt) => {
setShowHostname(!showHostname);
}
const onShowCallbackGroupsChanged = (evt) => {
setShowCallbackGroups(!showCallbackGroups);
}
const onShowMediaChanged = (evt) => {
setShowMedia(!showMedia);
}
@@ -703,10 +893,7 @@ export function SettingsOperatorUIConfigDialog(props) {
}))
}
updateSettings({settings: {
hideUsernames,
showIP,
showHostname,
showCallbackGroups,
taskingDisplayFields: normalizeTaskingDisplayFields(taskingDisplayFields),
fontSize: parseInt(fontSize),
fontFamily,
showMedia,
@@ -730,10 +917,7 @@ export function SettingsOperatorUIConfigDialog(props) {
const setDefaults = () => {
setFontSize(operatorSettingDefaults.fontSize);
setFontFamily(operatorSettingDefaults.fontFamily);
setHideUsernames(operatorSettingDefaults.hideUsernames);
setShowIP(operatorSettingDefaults.showIP);
setShowHostname(operatorSettingDefaults.showHostname);
setShowCallbackGroups(operatorSettingDefaults.showCallbackGroups);
setTaskingDisplayFields(operatorSettingDefaults.taskingDisplayFields);
setShowMedia(operatorSettingDefaults.showMedia);
setInteractType(operatorSettingDefaults.interactType);
setUseDisplayParamsForCLIHistory(operatorSettingDefaults.useDisplayParamsForCLIHistory);
@@ -762,10 +946,7 @@ export function SettingsOperatorUIConfigDialog(props) {
try{
let jsonData = JSON.parse(String(contents));
let currentSettings = {
hideUsernames,
showIP,
showHostname,
showCallbackGroups,
taskingDisplayFields,
fontSize: parseInt(fontSize),
fontFamily,
showMedia,
@@ -778,7 +959,9 @@ export function SettingsOperatorUIConfigDialog(props) {
showOPSECBypassUsername,
palette: palette
}
updateSettings({settings: {...currentSettings, ...jsonData}});
const mergedSettings = {...currentSettings, ...jsonData};
mergedSettings.taskingDisplayFields = normalizeTaskingDisplayFields(mergedSettings.taskingDisplayFields);
updateSettings({settings: mergedSettings});
snackActions.info("Updating settings");
props.onClose();
}catch(error){
@@ -818,29 +1001,54 @@ export function SettingsOperatorUIConfigDialog(props) {
return (
<React.Fragment>
<DialogTitle id="form-dialog-title">
Configure UI Settings
<div style={{float: "right", display: "flex", alignItems: "center"}}>
<MythicStyledTooltip title={"Export Preferences"} tooltipStyle={{float: "right"}}>
<IconButton onClick={getCurrentPreferences}>
<CloudDownloadIcon />
</IconButton>
</MythicStyledTooltip>
<MythicStyledTooltip title={"Export Only Color Preferences"} tooltipStyle={{float: "right"}}>
<IconButton onClick={getCurrentColorPreferences}>
<CloudDownloadIcon fontSize={"medium"} />
<ColorLensIcon color={"success"} fontSize={"small"} style={{marginLeft: "-8px", marginTop: "7px"}} />
</IconButton>
</MythicStyledTooltip>
<MythicStyledTooltip title={"Import Preferences"} tooltipStyle={{float: "right"}}>
<IconButton onClick={()=>fileInputRef.current.click()} >
<CloudUploadIcon color={"success"}/>
<input ref={fileInputRef} onChange={onFileChange} type="file" hidden />
</IconButton>
</MythicStyledTooltip>
</div>
<Typography variant={"body2"}>
Community themes are located on <Link target={"_blank"} href={"https://github.com/MythicMeta/CommunityThemes"}>GitHub</Link>
</Typography>
<Box className="mythic-dialog-title-row mythic-ui-settings-title-row">
<Box className="mythic-ui-settings-title-copy">
<Typography component="div" className="mythic-ui-settings-title">
Configure UI Settings
</Typography>
<Typography variant={"body2"} className="mythic-ui-settings-subtitle">
Community themes are located on <Link target={"_blank"} href={"https://github.com/MythicMeta/CommunityThemes"}>GitHub</Link>
</Typography>
</Box>
<Box className="mythic-ui-settings-title-actions">
<MythicStyledTooltip title={"Copy all preferences as JSON"}>
<Button
className="mythic-dialog-title-action mythic-ui-settings-title-button mythic-ui-settings-title-button-info"
onClick={getCurrentPreferences}
size="small"
variant="outlined"
startIcon={<CloudDownloadIcon fontSize="small" />}
>
Export
</Button>
</MythicStyledTooltip>
<MythicStyledTooltip title={"Copy only color preferences as JSON"}>
<Button
className="mythic-dialog-title-action mythic-ui-settings-title-button mythic-ui-settings-title-button-info"
onClick={getCurrentColorPreferences}
size="small"
variant="outlined"
startIcon={
<ColorLensIcon fontSize="small" />
}
>
Export Colors
</Button>
</MythicStyledTooltip>
<MythicStyledTooltip title={"Import preferences from a JSON file"}>
<Button
className="mythic-dialog-title-action mythic-ui-settings-title-button mythic-ui-settings-title-button-success"
onClick={()=>fileInputRef.current.click()}
size="small"
variant="outlined"
startIcon={<CloudUploadIcon fontSize="small" />}
>
Import
<input ref={fileInputRef} onChange={onFileChange} type="file" hidden />
</Button>
</MythicStyledTooltip>
</Box>
</Box>
</DialogTitle>
<TableContainer className="mythicElement" style={{paddingLeft: "10px", paddingRight: "10px"}}>
<Table size="small" style={{ "maxWidth": "100%", "overflow": "scroll"}}>
@@ -862,52 +1070,9 @@ export function SettingsOperatorUIConfigDialog(props) {
</Select>
</MythicStyledTableCell>
</TableRow>
<TableRow hover>
<MythicStyledTableCell>Hide Usernames In Tasking</MythicStyledTableCell>
<MythicStyledTableCell>
<Switch
checked={hideUsernames}
onChange={onHideUsernamesChanged}
color="info"
inputProps={{ 'aria-label': 'info checkbox' }}
name="hide_usernames"
/>
</MythicStyledTableCell>
</TableRow>
<TableRow hover>
<MythicStyledTableCell>Show Callback IP In Tasking</MythicStyledTableCell>
<MythicStyledTableCell>
<Switch
checked={showIP}
onChange={onShowIPChanged}
color="info"
inputProps={{ 'aria-label': 'info checkbox' }}
name="show_ip"
/>
</MythicStyledTableCell>
</TableRow>
<TableRow hover>
<MythicStyledTableCell>Show Callback Hostname In Tasking</MythicStyledTableCell>
<MythicStyledTableCell>
<Switch
checked={showHostname}
onChange={onShowHostnameChanged}
color="info"
inputProps={{ 'aria-label': 'info checkbox' }}
name="show_hostname"
/>
</MythicStyledTableCell>
</TableRow>
<TableRow hover>
<MythicStyledTableCell>Show Callback Groups In Tasking</MythicStyledTableCell>
<MythicStyledTableCell>
<Switch
checked={showCallbackGroups}
onChange={onShowCallbackGroupsChanged}
color="info"
inputProps={{ 'aria-label': 'info checkbox' }}
name="show_callback_groups"
/>
<TableRow>
<MythicStyledTableCell colSpan={2} style={{paddingTop: "16px", paddingBottom: "16px"}}>
<TaskingMetadataSummary value={taskingDisplayFields} onChange={setTaskingDisplayFields} />
</MythicStyledTableCell>
</TableRow>
<TableRow hover>
@@ -1,25 +1,35 @@
import { useState, useEffect } from 'react';
import { useState, useEffect, useRef, useCallback } from 'react';
const useSingleAndDoubleClick = (onSingleClick, onDoubleClick, delay = 250) => {
const [click, setClick] = useState(0);
const onSingleClickRef = useRef(onSingleClick);
const onDoubleClickRef = useRef(onDoubleClick);
useEffect(() => {
onSingleClickRef.current = onSingleClick;
onDoubleClickRef.current = onDoubleClick;
}, [onSingleClick, onDoubleClick]);
useEffect(() => {
if(click === 0){
return;
}
const timer = setTimeout(() => {
// simple click
if (click === 1) onSingleClick();
if (click === 1) onSingleClickRef.current();
setClick(0);
}, delay);
// the duration between this click and the previous one
// is less than the value of delay = double-click
if (click === 2){
onDoubleClick();
setClick(0);
onDoubleClickRef.current();
}
return () => clearTimeout(timer);
}, [delay, onSingleClick, onDoubleClick, click]);
}, [delay, click]);
return () => setClick((prev) => prev + 1);
return useCallback(() => setClick((prev) => prev + 1), []);
};
export default useSingleAndDoubleClick;
+655 -37
View File
@@ -227,7 +227,133 @@ tspan {
.selectedTask {
background-color: ${(props) => props.theme.selectedCallbackColor + "DD"} !important;
}
.mythic-file-browser-tableTop {
background-color: ${(props) => alpha(props.theme.palette.background.paper, props.theme.palette.mode === "dark" ? 0.96 : 0.98)};
border-bottom: 1px solid ${(props) => props.theme.table?.borderSoft || props.theme.borderColor};
padding: 0.45rem 0.55rem 0.4rem;
}
.mythic-file-browser-pathInput.MuiInputBase-root {
background-color: ${(props) => props.theme.palette.background.paper};
}
.mythic-file-browser-pathInput.MuiInputBase-root.MuiOutlinedInput-adornedStart {
padding-left: 0.22rem;
}
.mythic-file-browser-pathInput.MuiInputBase-root.MuiOutlinedInput-adornedEnd {
padding-right: 0.22rem;
}
.mythic-file-browser-pathInput .MuiInputBase-input {
min-width: 8rem;
padding-bottom: 10px;
padding-top: 10px;
}
.mythic-file-browser-toolbarGroup {
align-items: center;
background-color: ${(props) => alpha(props.theme.palette.text.primary, props.theme.palette.mode === "dark" ? 0.045 : 0.035)};
border: 1px solid ${(props) => props.theme.table?.borderSoft || props.theme.borderColor};
border-radius: ${(props) => props.theme.shape.borderRadius}px;
display: inline-flex;
flex: 0 0 auto;
gap: 0.15rem;
min-height: 32px;
min-width: 0;
padding: 0.12rem;
}
.mythic-file-browser-toolbarGroupStart {
margin-right: 0.45rem;
}
.mythic-file-browser-toolbarGroupEnd {
margin-left: 0.45rem;
}
.mythic-file-browser-tokenSelect {
max-width: 12rem;
min-width: 8.5rem;
overflow: hidden;
}
.mythic-file-browser-tokenSelect .MuiInputBase-root,
.mythic-file-browser-tokenSelect .MuiSelect-select {
font-size: 0.78rem;
min-height: 0;
}
.mythic-file-browser-iconButton.MuiIconButton-root {
border-radius: ${(props) => props.theme.shape.borderRadius}px;
color: ${(props) => props.theme.palette.text.secondary};
height: 28px;
padding: 0;
transition: background-color 120ms ease, color 120ms ease, opacity 120ms ease;
width: 28px;
}
.mythic-file-browser-iconButton.MuiIconButton-root:hover {
background-color: ${(props) => alpha(props.theme.palette.text.primary, props.theme.palette.mode === "dark" ? 0.09 : 0.075)};
color: ${(props) => props.theme.palette.text.primary};
}
.mythic-file-browser-iconButton.MuiIconButton-root.Mui-disabled {
opacity: 0.42;
}
.mythic-file-browser-iconButton.mythic-file-browser-hoverInfo:hover {
background-color: ${(props) => alpha(props.theme.palette.info.main, props.theme.palette.mode === "dark" ? 0.18 : 0.1)};
color: ${(props) => props.theme.palette.info.main};
}
.mythic-file-browser-iconButton.mythic-file-browser-hoverSuccess:hover {
background-color: ${(props) => alpha(props.theme.palette.success.main, props.theme.palette.mode === "dark" ? 0.18 : 0.1)};
color: ${(props) => props.theme.palette.success.main};
}
.mythic-file-browser-iconButton.mythic-file-browser-hoverWarning:hover {
background-color: ${(props) => alpha(props.theme.palette.warning.main, props.theme.palette.mode === "dark" ? 0.2 : 0.12)};
color: ${(props) => props.theme.palette.warning.main};
}
.mythic-file-browser-iconButton.mythic-file-browser-hoverError:hover {
background-color: ${(props) => alpha(props.theme.palette.error.main, props.theme.palette.mode === "dark" ? 0.18 : 0.1)};
color: ${(props) => props.theme.palette.error.main};
}
.mythic-file-browser-iconButton.mythic-file-browser-activeSuccess {
background-color: ${(props) => alpha(props.theme.palette.success.main, props.theme.palette.mode === "dark" ? 0.2 : 0.12)};
color: ${(props) => props.theme.palette.success.main};
}
.mythic-file-browser-iconButton.mythic-file-browser-activeWarning {
background-color: ${(props) => alpha(props.theme.palette.warning.main, props.theme.palette.mode === "dark" ? 0.22 : 0.14)};
color: ${(props) => props.theme.palette.warning.main};
}
.mythic-file-browser-iconButton.mythic-file-browser-activeError {
background-color: ${(props) => alpha(props.theme.palette.error.main, props.theme.palette.mode === "dark" ? 0.2 : 0.12)};
color: ${(props) => props.theme.palette.error.main};
}
.mythic-file-browser-iconButtonCompound.MuiIconButton-root {
gap: 0;
padding: 0 0.15rem;
width: auto;
}
.mythic-file-browser-iconButtonCompound .MuiSvgIcon-root + .MuiSvgIcon-root {
font-size: 1rem;
margin-left: -0.15rem;
}
.mythic-file-browser-labelButton.MuiIconButton-root {
height: 20px;
margin: 0 0.1rem;
width: 20px;
}
.mythic-file-browser-labelButton .MuiSvgIcon-root {
font-size: 0.9rem;
}
.MythicResizableGrid-root {
background-color: ${(props) => props.theme.palette.background.paper};
border: 1px solid ${(props) => props.theme.table?.border || props.theme.borderColor};
border-radius: ${(props) => props.theme.shape.borderRadius}px;
box-shadow: ${(props) => props.theme.palette.mode === "dark" ? "none" : "0 8px 18px rgba(15, 23, 42, 0.035)"};
flex: 1 1 auto;
height: 100%;
min-height: 0;
min-width: 0;
overflow: hidden;
position: relative;
width: 100%;
}
.MythicResizableGrid-grid {
background-color: ${(props) => props.theme.palette.background.paper};
outline: none;
}
.MythicResizableGrid-headerCellRow {
background-color: ${(props) => props.theme.tableHeader};
box-shadow: inset 0 -1px 0 ${(props) => props.theme.table?.border || props.theme.borderColor};
display: flex;
flex-direction: row;
position: sticky;
@@ -237,89 +363,296 @@ tspan {
z-index: 10;
}
.MythicResizableGrid-headerCell {
display: flex;
align-items: center;
position: relative;
padding: 0 8px;
box-sizing: border-box;
justify-content: space-between;
user-select: none;
border-right: 1px solid ${(props) => props.theme.table?.borderSoft || props.theme.borderColor};
border-bottom: 1px solid ${(props) => props.theme.table?.border || props.theme.borderColor};
background-color: ${(props) => props.theme.tableHeader} !important;
border-bottom: 1px solid ${(props) => props.theme.table?.border || props.theme.borderColor};
border-right: 1px solid ${(props) => props.theme.table?.borderSoft || props.theme.borderColor};
box-sizing: border-box;
color: ${(props) => props.theme.palette.text.primary};
cursor: pointer;
display: flex;
font-size: 0.76rem;
font-weight: 700;
justify-content: space-between;
letter-spacing: 0;
min-width: 0;
overflow: hidden;
padding: 0 0.45rem 0 0.65rem;
position: relative;
transition: background-color 120ms ease, color 120ms ease;
user-select: none;
&:first-child-of-type {
border-left: 0;
}
&:hover {
background-color: ${(props) => props.theme.table?.headerHover || props.theme.tableHover};
cursor: pointer;
}
}
.MythicResizableGrid-headerCellNoSort {
cursor: default;
}
.MythicResizableGrid-headerContent {
align-items: center;
display: flex;
gap: 0.4rem;
justify-content: space-between;
min-width: 0;
width: 100%;
}
.MythicResizableGrid-headerCell .MythicResizableGrid-cellInner {
font-size: 0.76rem;
font-weight: 700;
line-height: 1.2;
}
.MythicResizableGrid-headerLabel {
color: ${(props) => props.theme.palette.text.primary};
display: block;
letter-spacing: 0;
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
text-transform: uppercase;
white-space: nowrap;
}
.MythicResizableGrid-headerActions {
align-items: center;
display: inline-flex;
flex: 0 0 auto;
gap: 0.25rem;
min-width: 0;
}
.MythicResizableGrid-headerIndicator {
align-items: center;
background-color: ${(props) => props.theme.palette.background.paper};
border: 1px solid ${(props) => alpha(props.theme.palette.text.primary, props.theme.palette.mode === "dark" ? 0.28 : 0.22)};
border-radius: ${(props) => props.theme.shape.borderRadius}px;
box-shadow: ${(props) => props.theme.palette.mode === "dark" ? "0 0 0 1px rgba(255,255,255,0.04)" : "0 1px 2px rgba(15, 23, 42, 0.08)"};
display: inline-flex;
flex: 0 0 auto;
font-size: 0.95rem;
height: 18px;
justify-content: center;
line-height: 1;
width: 18px;
}
.MythicResizableGrid-headerFilterIcon {
background-color: ${(props) => alpha(props.theme.palette.mode === "dark" ? props.theme.palette.info.light : props.theme.palette.info.dark || props.theme.palette.info.main, props.theme.palette.mode === "dark" ? 0.24 : 0.14)};
border-color: ${(props) => alpha(props.theme.palette.mode === "dark" ? props.theme.palette.info.light : props.theme.palette.info.dark || props.theme.palette.info.main, props.theme.palette.mode === "dark" ? 0.72 : 0.5)};
color: ${(props) => props.theme.palette.mode === "dark" ? props.theme.palette.info.light : props.theme.palette.info.dark || props.theme.palette.info.main};
}
.MythicResizableGrid-headerSortIcon {
background-color: ${(props) => alpha(props.theme.palette.mode === "dark" ? props.theme.palette.primary.light : props.theme.palette.primary.dark || props.theme.palette.primary.main, props.theme.palette.mode === "dark" ? 0.24 : 0.14)};
border-color: ${(props) => alpha(props.theme.palette.mode === "dark" ? props.theme.palette.primary.light : props.theme.palette.primary.dark || props.theme.palette.primary.main, props.theme.palette.mode === "dark" ? 0.72 : 0.5)};
color: ${(props) => props.theme.palette.mode === "dark" ? props.theme.palette.primary.light : props.theme.palette.primary.dark || props.theme.palette.primary.main};
}
.MythicResizableGrid-headerResizeHandle {
position: absolute;
top: 0;
right: 0;
width: 5px;
height: 100%;
bottom: 0;
cursor: col-resize;
user-select: none;
position: absolute;
right: 0;
top: 0;
touch-action: none;
user-select: none;
width: 9px;
z-index: 2;
}
.MythicResizableGrid-headerResizeHandle::before {
content: "";
position: absolute;
right: 2px;
width: 0px;
height: 100%;
border-left: 1px solid ${(props) => props.theme.palette.text.secondary};
border-right: 1px solid ${(props) => props.theme.palette.text.secondary};
opacity: 0.55;
bottom: 18%;
right: 4px;
top: 18%;
width: 2px;
border-radius: 999px;
background-color: ${(props) => alpha(props.theme.palette.text.primary, props.theme.palette.mode === "dark" ? 0.5 : 0.38)};
opacity: 0.72;
pointer-events: none;
transition: opacity 120ms ease, background-color 120ms ease;
}
.MythicResizableGrid-headerResizeHandle::after {
content: "";
inset: 0 -2px;
position: absolute;
}
.MythicResizableGrid-headerCell:hover .MythicResizableGrid-headerResizeHandle::before {
opacity: 1;
}
.MythicResizableGrid-headerResizeHandle:hover,
.MythicResizableGrid-headerResizeHandleActive {
background-color: ${(props) => props.theme.palette.info.main + "22"};
}
.MythicResizableGrid-headerResizeHandle:hover::after,
.MythicResizableGrid-headerResizeHandleActive::after {
background-color: ${(props) => props.theme.palette.info.main};
opacity: 1;
background-color: ${(props) => alpha(props.theme.palette.info.main, props.theme.palette.mode === "dark" ? 0.24 : 0.14)};
}
.MythicResizableGrid-headerResizeHandle:hover::before,
.MythicResizableGrid-headerResizeHandleActive::before {
border-color: ${(props) => props.theme.palette.info.main};
background-color: ${(props) => props.theme.palette.info.main};
opacity: 1;
}
.MythicResizableGrid-hoveredRow {
background-color: ${(props) => props.theme.table?.rowHover || props.theme.tableHover + "CC"};
.MythicResizableGrid-resizeGuide {
background-color: ${(props) => props.theme.palette.info.main};
box-shadow: 0 0 0 1px ${(props) => alpha(props.theme.palette.info.main, props.theme.palette.mode === "dark" ? 0.32 : 0.22)},
0 0 10px ${(props) => alpha(props.theme.palette.info.main, props.theme.palette.mode === "dark" ? 0.45 : 0.28)};
pointer-events: none;
position: absolute;
top: 0;
transform: translateX(-1px);
width: 2px;
z-index: 12;
}
.MythicResizableGrid-cell {
display: flex;
align-items: center;
padding: 0 8px;
background-image: none;
background-color: ${(props) => props.theme.palette.background.paper};
border-bottom: 1px solid ${(props) => props.theme.table?.borderSoft || props.theme.borderColor};
border-right: 1px solid ${(props) => props.theme.table?.borderSoft || props.theme.borderColor};
box-sizing: border-box;
color: ${(props) => props.theme.palette.text.primary};
cursor: default !important;
display: flex;
font-family: ${(props) => props.theme.typography.fontFamily};
font-size: 0.86rem;
font-variant-numeric: tabular-nums;
border-right: 1px solid ${(props) => props.theme.table?.borderSoft || props.theme.borderColor};
border-bottom: 1px solid ${(props) => props.theme.table?.borderSoft || props.theme.borderColor};
cursor: default !important;
min-width: 0;
padding: 0 0.65rem;
position: relative;
transition: background-color 120ms ease, background-image 120ms ease, border-color 120ms ease, box-shadow 120ms ease, color 120ms ease;
}
.MythicResizableGrid-cell.MythicResizableGridRowHighlight {
background-color: ${(props) => props.theme.table?.rowStripe || props.theme.tableHover + "66"};
}
.MythicResizableGrid-cell.MythicResizableGrid-hoveredRow {
background-image: linear-gradient(0deg,
${(props) => alpha(props.theme.palette.primary.main, props.theme.palette.mode === "dark" ? 0.11 : 0.07)},
${(props) => alpha(props.theme.palette.primary.main, props.theme.palette.mode === "dark" ? 0.11 : 0.07)}) !important;
border-bottom-color: ${(props) => alpha(props.theme.palette.primary.main, props.theme.palette.mode === "dark" ? 0.3 : 0.2)};
color: ${(props) => props.theme.palette.text.primary} !important;
}
.MythicResizableGrid-cell.MythicResizableGrid-contextRow {
background-image: linear-gradient(0deg,
${(props) => alpha(props.theme.palette.info.main, props.theme.palette.mode === "dark" ? 0.18 : 0.1)},
${(props) => alpha(props.theme.palette.info.main, props.theme.palette.mode === "dark" ? 0.18 : 0.1)}) !important;
border-bottom-color: ${(props) => alpha(props.theme.palette.info.main, props.theme.palette.mode === "dark" ? 0.52 : 0.34)};
box-shadow: inset 0 1px 0 ${(props) => alpha(props.theme.palette.info.main, props.theme.palette.mode === "dark" ? 0.52 : 0.34)};
color: ${(props) => props.theme.palette.text.primary} !important;
}
.MythicResizableGrid-cell.selectedCallback {
background-color: ${(props) => props.theme.table?.selected || props.theme.selectedCallbackColor + "CC"} !important;
}
.MythicResizableGrid-cell.selectedCallbackHierarchy {
background-color: ${(props) => props.theme.table?.selectedHierarchy || props.theme.selectedCallbackHierarchyColor + "CC"} !important;
}
.MythicResizableGrid-cell.MythicResizableGrid-rowInteractive {
cursor: pointer !important;
}
.MythicResizableGrid-cell.MythicResizableGrid-rowFirstCell::before {
background-color: ${(props) => alpha(props.theme.palette.primary.main, props.theme.palette.mode === "dark" ? 0.72 : 0.58)};
border-radius: 999px;
bottom: 5px;
content: "";
left: 3px;
opacity: 0;
pointer-events: none;
position: absolute;
top: 5px;
transition: background-color 120ms ease, opacity 120ms ease, width 120ms ease;
width: 3px;
}
.MythicResizableGrid-cell.MythicResizableGrid-rowFirstCell.MythicResizableGrid-hoveredRow::before {
opacity: 0.72;
}
.MythicResizableGrid-cell.MythicResizableGrid-rowFirstCell.MythicResizableGrid-contextRow::before {
background-color: ${(props) => props.theme.palette.info.main};
opacity: 1;
width: 4px;
}
.MythicResizableGrid-cell.MythicResizableGrid-rowLastCell {
border-right-color: ${(props) => props.theme.table?.border || props.theme.borderColor};
}
.MythicResizableGrid-cellInner {
width: 100%;
white-space: nowrap;
align-items: center;
display: flex;
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
width: 100%;
}
.mythic-callback-interactCell {
align-items: center;
display: inline-flex;
gap: 0.18rem;
height: 100%;
min-width: 0;
overflow: hidden;
width: 100%;
}
.mythic-callback-iconButton.MuiIconButton-root {
border: 1px solid transparent;
border-radius: ${(props) => props.theme.shape.borderRadius}px;
color: ${(props) => props.theme.palette.text.secondary};
flex: 0 0 auto;
height: 22px;
padding: 0;
transition: background-color 120ms ease, border-color 120ms ease, color 120ms ease;
width: 22px;
}
.mythic-callback-iconButton.MuiIconButton-root:hover {
background-color: ${(props) => alpha(props.theme.palette.primary.main, props.theme.palette.mode === "dark" ? 0.16 : 0.09)};
color: ${(props) => props.theme.palette.primary.main};
}
.mythic-callback-interactButtonHighIntegrity.MuiIconButton-root {
background-color: ${(props) => alpha(props.theme.palette.error.main, props.theme.palette.mode === "dark" ? 0.18 : 0.1)};
border-color: ${(props) => alpha(props.theme.palette.error.main, props.theme.palette.mode === "dark" ? 0.48 : 0.32)};
color: ${(props) => props.theme.palette.error.main};
}
.mythic-callback-interactButtonHighIntegrity.MuiIconButton-root:hover,
.mythic-callback-menuButtonHighIntegrity.MuiIconButton-root:hover {
background-color: ${(props) => alpha(props.theme.palette.error.main, props.theme.palette.mode === "dark" ? 0.24 : 0.14)};
color: ${(props) => props.theme.palette.error.main};
}
.mythic-callback-menuButton.MuiIconButton-root {
margin-left: -0.12rem;
}
.mythic-callback-menuButtonHighIntegrity.MuiIconButton-root {
color: ${(props) => props.theme.palette.error.main};
}
.mythic-callback-displayId {
color: ${(props) => props.theme.palette.text.primary};
flex: 0 0 auto;
font-weight: 700;
line-height: 1;
max-width: 3.8rem;
overflow: hidden;
text-overflow: ellipsis;
}
.mythic-callback-lockBadge {
align-items: center;
background-color: ${(props) => alpha(props.theme.palette.warning.main, props.theme.palette.mode === "dark" ? 0.18 : 0.12)};
border: 1px solid ${(props) => alpha(props.theme.palette.warning.main, props.theme.palette.mode === "dark" ? 0.46 : 0.34)};
border-radius: ${(props) => props.theme.shape.borderRadius}px;
color: ${(props) => props.theme.palette.warning.main};
display: inline-flex;
flex: 0 1 auto;
font-size: 0.68rem;
font-weight: 700;
gap: 0.12rem;
height: 18px;
line-height: 1;
min-width: 0;
overflow: hidden;
padding: 0 0.28rem;
text-overflow: ellipsis;
}
.mythic-callback-lockBadge span {
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
}
.mythic-callback-statusIcon {
flex: 0 0 auto;
font-size: 0.9rem;
margin-left: 0.08rem;
}
.mythic-callback-statusIconSuccess {
color: ${(props) => props.theme.palette.success.main};
}
.MythicInteractiveTerminal {
background-color: ${(props) => props.theme.outputBackgroundColor};
@@ -589,6 +922,212 @@ tspan {
min-height: 30px;
text-transform: none;
}
.mythic-ui-settings-title-row {
align-items: flex-start;
}
.mythic-ui-settings-title-copy {
display: flex;
flex: 1 1 22rem;
flex-direction: column;
gap: 0.15rem;
min-width: 0;
}
.mythic-ui-settings-title {
color: ${(props) => props.theme.palette.text.primary};
font-size: 1.05rem;
font-weight: 750;
line-height: 1.25;
}
.mythic-ui-settings-subtitle {
color: ${(props) => props.theme.palette.text.secondary};
line-height: 1.35;
}
.mythic-ui-settings-title-actions {
align-items: center;
display: flex;
flex: 0 1 auto;
flex-wrap: wrap;
gap: 0.4rem;
justify-content: flex-end;
min-width: 0;
}
.mythic-ui-settings-title-button.MuiButton-root {
min-height: 31px;
min-width: auto;
padding: 0.35rem 0.65rem;
}
.mythic-ui-settings-title-button-info.MuiButton-root:hover {
background-color: ${(props) => alpha(props.theme.palette.info.main, props.theme.palette.mode === "dark" ? 0.18 : 0.1)} !important;
border-color: ${(props) => alpha(props.theme.palette.info.main, props.theme.palette.mode === "dark" ? 0.58 : 0.42)} !important;
color: ${(props) => props.theme.palette.info.main} !important;
}
.mythic-ui-settings-title-button-success.MuiButton-root:hover {
background-color: ${(props) => alpha(props.theme.palette.success.main, props.theme.palette.mode === "dark" ? 0.18 : 0.1)} !important;
border-color: ${(props) => alpha(props.theme.palette.success.main, props.theme.palette.mode === "dark" ? 0.58 : 0.42)} !important;
color: ${(props) => props.theme.palette.success.main} !important;
}
.mythic-tasking-visibility-panel {
background-color: ${(props) => props.theme.palette.mode === "dark" ? "rgba(255,255,255,0.035)" : "rgba(15,23,42,0.025)"};
border: 1px solid ${(props) => props.theme.table?.borderSoft || props.theme.borderColor};
border-radius: ${(props) => props.theme.shape.borderRadius}px;
display: flex;
flex-direction: column;
gap: 0.75rem;
padding: 0.8rem;
}
.mythic-tasking-visibility-summary-panel {
display: grid;
grid-template-columns: minmax(12rem, 0.85fr) minmax(12rem, 1.15fr);
}
.mythic-tasking-visibility-header {
align-items: flex-start;
display: flex;
flex-wrap: wrap;
gap: 0.65rem;
justify-content: space-between;
min-width: 0;
}
.mythic-tasking-visibility-title {
color: ${(props) => props.theme.palette.text.primary};
font-size: 0.92rem;
font-weight: 800;
line-height: 1.25;
}
.mythic-tasking-visibility-description {
color: ${(props) => props.theme.palette.text.secondary};
font-size: 0.78rem;
line-height: 1.35;
}
.mythic-tasking-visibility-count {
background-color: ${(props) => alpha(props.theme.palette.info.main, props.theme.palette.mode === "dark" ? 0.16 : 0.1)};
border: 1px solid ${(props) => alpha(props.theme.palette.info.main, props.theme.palette.mode === "dark" ? 0.34 : 0.24)};
border-radius: ${(props) => props.theme.shape.borderRadius}px;
color: ${(props) => props.theme.palette.info.main};
font-size: 0.72rem;
font-weight: 800;
line-height: 1;
padding: 0.35rem 0.5rem;
white-space: nowrap;
}
.mythic-tasking-visibility-summary-actions {
align-items: center;
display: flex;
flex-wrap: wrap;
gap: 0.45rem;
justify-content: flex-end;
min-width: 0;
}
.mythic-tasking-visibility-manage-button.MuiButton-root {
min-height: 30px;
padding: 0.3rem 0.65rem;
}
.mythic-tasking-visibility-chip-row {
align-items: center;
display: flex;
flex: 1 1 auto;
flex-wrap: wrap;
gap: 0.35rem;
grid-column: 1 / -1;
min-width: 0;
}
.mythic-tasking-visibility-chip {
align-items: center;
background-color: ${(props) => alpha(props.theme.palette.info.main, props.theme.palette.mode === "dark" ? 0.14 : 0.08)};
border: 1px solid ${(props) => alpha(props.theme.palette.info.main, props.theme.palette.mode === "dark" ? 0.38 : 0.24)};
border-radius: ${(props) => props.theme.shape.borderRadius}px;
color: ${(props) => props.theme.palette.text.primary};
display: inline-flex;
font-size: 0.76rem;
font-weight: 750;
gap: 0.35rem;
line-height: 1.2;
min-height: 28px;
min-width: 0;
padding: 0.22rem 0.52rem 0.22rem 0.3rem;
}
.mythic-tasking-visibility-chip-index {
align-items: center;
background-color: ${(props) => props.theme.palette.info.main};
border-radius: ${(props) => Math.max(4, props.theme.shape.borderRadius - 1)}px;
color: ${(props) => props.theme.palette.info.contrastText};
display: inline-flex;
font-size: 0.68rem;
font-weight: 850;
height: 19px;
justify-content: center;
min-width: 19px;
padding: 0 0.24rem;
}
.mythic-tasking-visibility-empty {
color: ${(props) => props.theme.palette.text.secondary};
font-size: 0.78rem;
font-style: italic;
}
.mythic-tasking-visibility-grid {
display: grid;
gap: 0.5rem;
grid-template-columns: repeat(auto-fit, minmax(13rem, 1fr));
}
.mythic-tasking-visibility-option.MuiButton-root {
align-items: flex-start;
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: none;
color: ${(props) => props.theme.palette.text.secondary};
display: flex;
flex-direction: column;
gap: 0.25rem;
justify-content: flex-start;
min-height: 4.2rem;
padding: 0.65rem 0.75rem;
text-align: left;
text-transform: none;
}
.mythic-tasking-visibility-option.MuiButton-root:hover {
background-color: ${(props) => alpha(props.theme.palette.info.main, props.theme.palette.mode === "dark" ? 0.13 : 0.07)};
border-color: ${(props) => alpha(props.theme.palette.info.main, props.theme.palette.mode === "dark" ? 0.46 : 0.28)};
}
.mythic-tasking-visibility-option.MuiButton-root.selected {
background-color: ${(props) => alpha(props.theme.palette.info.main, props.theme.palette.mode === "dark" ? 0.18 : 0.1)};
border-color: ${(props) => alpha(props.theme.palette.info.main, props.theme.palette.mode === "dark" ? 0.62 : 0.42)};
color: ${(props) => props.theme.palette.text.primary};
box-shadow: inset 3px 0 0 ${(props) => props.theme.palette.info.main};
}
.mythic-tasking-visibility-option-title {
color: inherit;
font-size: 0.8rem;
font-weight: 800;
line-height: 1.2;
}
.mythic-tasking-visibility-option-description {
color: ${(props) => props.theme.palette.text.secondary};
font-size: 0.72rem;
font-weight: 600;
line-height: 1.25;
}
.mythic-tasking-metadata-row .mythic-reorder-row-main {
align-items: flex-start;
flex-direction: column;
gap: 0.12rem;
}
.mythic-reorder-row-description {
color: ${(props) => props.theme.palette.text.secondary};
font-size: 0.72rem;
font-weight: 600;
line-height: 1.25;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
@media (max-width: 760px) {
.mythic-tasking-visibility-summary-panel {
grid-template-columns: 1fr;
}
.mythic-tasking-visibility-summary-actions {
justify-content: flex-start;
}
}
.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;
@@ -1018,6 +1557,65 @@ tspan {
border-color: ${(props) => props.theme.table?.borderSoft || props.theme.borderColor} !important;
color: ${(props) => props.theme.palette.text.disabled} !important;
}
.mythic-raw-select-dialog-content.MuiDialogContent-root {
background-color: ${(props) => props.theme.palette.background.paper};
padding: 0;
}
.mythic-raw-select-list {
gap: 0.45rem;
max-height: min(55vh, 28rem);
overflow-y: auto;
padding: 0.6rem;
}
.mythic-raw-select-row {
align-items: center;
background-color: ${(props) => props.theme.surfaces?.muted || alpha(props.theme.palette.text.primary, props.theme.palette.mode === "dark" ? 0.045 : 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.primary};
cursor: pointer;
display: flex;
gap: 0.75rem;
justify-content: space-between;
min-height: 42px;
min-width: 0;
padding: 0.45rem 0.55rem 0.45rem 0.75rem;
transition: background-color 120ms ease, border-color 120ms ease, box-shadow 120ms ease;
width: 100%;
}
.mythic-raw-select-row:hover {
background-color: ${(props) => alpha(props.theme.palette.info.main, props.theme.palette.mode === "dark" ? 0.13 : 0.075)};
border-color: ${(props) => alpha(props.theme.palette.info.main, props.theme.palette.mode === "dark" ? 0.44 : 0.32)};
}
.mythic-raw-select-row:focus-visible {
border-color: ${(props) => props.theme.palette.info.main};
box-shadow: 0 0 0 2px ${(props) => alpha(props.theme.palette.info.main, props.theme.palette.mode === "dark" ? 0.28 : 0.18)};
outline: none;
}
.mythic-raw-select-value.MuiTypography-root {
color: ${(props) => props.theme.palette.text.primary};
font-size: 0.86rem;
font-variant-numeric: tabular-nums;
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.mythic-raw-select-action.MuiButton-root {
flex: 0 0 auto;
min-height: 28px;
min-width: 5rem;
padding: 0.25rem 0.65rem;
}
.mythic-raw-select-empty {
align-items: center;
color: ${(props) => props.theme.palette.text.secondary};
display: flex;
justify-content: center;
min-height: 8rem;
padding: 1rem;
text-align: center;
}
.mythic-dialog-grid {
align-items: start;
display: grid;
@@ -1516,9 +2114,15 @@ tspan {
display: flex;
flex-direction: column;
gap: 0.55rem;
max-width: 100%;
min-width: 0;
overflow-x: hidden;
width: 100%;
}
.mythic-browser-script-response > * {
max-width: 100%;
min-width: 0;
}
.mythic-browser-script-response-expanded {
flex: 1 1 auto;
min-height: 0;
@@ -1645,6 +2249,20 @@ tspan {
padding: 0.5rem;
width: 100%;
}
.mythic-response-table {
display: flex;
flex-direction: column;
max-width: 100%;
min-width: 0;
overflow: hidden;
width: 100%;
}
.mythic-response-table-grid {
max-width: 100%;
min-width: 0;
overflow: hidden;
width: 100%;
}
.mythic-browser-scripts-table {
max-width: 100%;
min-width: 60rem;