updated table filtering

This commit is contained in:
its-a-feature
2026-05-07 17:22:24 -05:00
parent fa8955e4ba
commit 4c628c1974
11 changed files with 430 additions and 65 deletions
@@ -0,0 +1,180 @@
import React from 'react';
import Box from '@mui/material/Box';
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 FormControlLabel from '@mui/material/FormControlLabel';
import Switch from '@mui/material/Switch';
import TextField from '@mui/material/TextField';
import ToggleButton from '@mui/material/ToggleButton';
import ToggleButtonGroup from '@mui/material/ToggleButtonGroup';
import Typography from '@mui/material/Typography';
const DEFAULT_FILTER = {
include: [],
exclude: [],
includeMode: "any",
caseSensitive: false,
};
const splitFilterTerms = (value) => {
if(Array.isArray(value)){
return value.map((entry) => String(entry).trim()).filter(Boolean);
}
if(value === undefined || value === null){
return [];
}
return String(value)
.split(/[\n,]+/)
.map((entry) => entry.trim())
.filter(Boolean);
};
export const normalizeGridColumnFilter = (value) => {
if(value === undefined || value === null){
return {...DEFAULT_FILTER};
}
if(typeof value === "string" || typeof value === "number" || typeof value === "boolean"){
return {
...DEFAULT_FILTER,
include: splitFilterTerms(value),
};
}
if(typeof value === "object"){
return {
include: splitFilterTerms(value.include),
exclude: splitFilterTerms(value.exclude),
includeMode: value.includeMode === "all" ? "all" : "any",
caseSensitive: Boolean(value.caseSensitive),
};
}
return {...DEFAULT_FILTER};
};
export const isGridColumnFilterActive = (value) => {
const normalizedFilter = normalizeGridColumnFilter(value);
return normalizedFilter.include.length > 0 || normalizedFilter.exclude.length > 0;
};
export const getUpdatedGridFilterOptions = (filterOptions, key, value) => {
const nextFilterOptions = {...filterOptions};
if(!key){
return nextFilterOptions;
}
const normalizedFilter = normalizeGridColumnFilter(value);
if(isGridColumnFilterActive(normalizedFilter)){
nextFilterOptions[key] = normalizedFilter;
}else{
delete nextFilterOptions[key];
}
return nextFilterOptions;
};
export const gridValuePassesFilter = (value, filterOption) => {
const normalizedFilter = normalizeGridColumnFilter(filterOption);
if(!isGridColumnFilterActive(normalizedFilter)){
return true;
}
const cellText = String(value ?? "");
const haystack = normalizedFilter.caseSensitive ? cellText : cellText.toLowerCase();
const normalizeTerm = (term) => normalizedFilter.caseSensitive ? term : term.toLowerCase();
const termMatches = (term) => haystack.includes(normalizeTerm(term));
const includeMatches = normalizedFilter.include.length === 0 ? true :
normalizedFilter.includeMode === "all" ?
normalizedFilter.include.every(termMatches) :
normalizedFilter.include.some(termMatches);
if(!includeMatches){
return false;
}
return !normalizedFilter.exclude.some(termMatches);
};
export function GridColumnFilterDialog({filterValue, onClose, onSubmit, selectedColumn}) {
const normalizedFilter = React.useMemo(() => normalizeGridColumnFilter(filterValue), [filterValue]);
const [includeText, setIncludeText] = React.useState(normalizedFilter.include.join("\n"));
const [excludeText, setExcludeText] = React.useState(normalizedFilter.exclude.join("\n"));
const [includeMode, setIncludeMode] = React.useState(normalizedFilter.includeMode);
const [caseSensitive, setCaseSensitive] = React.useState(normalizedFilter.caseSensitive);
const columnName = selectedColumn?.name || selectedColumn?.plaintext || selectedColumn?.key || "Column";
const buildFilter = () => ({
include: splitFilterTerms(includeText),
exclude: splitFilterTerms(excludeText),
includeMode,
caseSensitive,
});
const handleSubmit = () => {
onSubmit(buildFilter());
onClose();
};
const handleClear = () => {
onSubmit({...DEFAULT_FILTER});
onClose();
};
const onIncludeModeChange = (event, value) => {
if(value !== null){
setIncludeMode(value);
}
};
return (
<div className="mythic-grid-filter-dialog">
<DialogTitle className="mythic-grid-filter-dialog-title">
Filter {columnName}
</DialogTitle>
<DialogContent className="mythic-grid-filter-dialog-content">
<Typography className="mythic-grid-filter-dialog-copy" component="div">
Add one term per line, or separate terms with commas. Include terms decide what can stay visible; exclude terms remove matching rows.
</Typography>
<Box className="mythic-grid-filter-dialog-mode-row">
<Typography className="mythic-grid-filter-dialog-label" component="div">
Include matching
</Typography>
<ToggleButtonGroup
exclusive
onChange={onIncludeModeChange}
size="small"
value={includeMode}
>
<ToggleButton value="any">Any</ToggleButton>
<ToggleButton value="all">All</ToggleButton>
</ToggleButtonGroup>
</Box>
<div className="mythic-grid-filter-dialog-fields">
<TextField
fullWidth
label="Include rows matching"
minRows={4}
multiline
onChange={(event) => setIncludeText(event.target.value)}
placeholder={"admin\nprod"}
value={includeText}
/>
<TextField
fullWidth
label="Exclude rows matching"
minRows={4}
multiline
onChange={(event) => setExcludeText(event.target.value)}
placeholder={"debug\ntest"}
value={excludeText}
/>
</div>
<FormControlLabel
control={
<Switch
checked={caseSensitive}
onChange={(event) => setCaseSensitive(event.target.checked)}
size="small"
/>
}
label="Case sensitive"
/>
</DialogContent>
<DialogActions className="mythic-grid-filter-dialog-actions">
<Button onClick={handleClear}>Clear</Button>
<Button onClick={onClose}>Cancel</Button>
<Button onClick={handleSubmit} variant="contained">Apply Filter</Button>
</DialogActions>
</div>
);
}
@@ -96,6 +96,22 @@ const HeaderCell = ({
clickOption({event, ...menuContext});
setOpenContextMenu(false);
};
const filterMenuOption = React.useMemo(
() => resolvedContextMenuOptions.find((option) => option.type === "item" && option.name === "Filter Column"),
[resolvedContextMenuOptions]
);
const handleFilterIndicatorClick = useCallback(
(event) => {
event.preventDefault();
event.stopPropagation();
if(filterMenuOption?.disabled || !filterMenuOption?.click){
return;
}
filterMenuOption.click({event, ...menuContext});
setOpenContextMenu(false);
},
[filterMenuOption, menuContext]
);
const handleClicks = useSingleAndDoubleClick(handleClick, handleDoubleClick);
const className = `${classes.headerCell} ${item.disableSort ? classes.headerCellNoSort : ""}`;
@@ -115,9 +131,15 @@ const HeaderCell = ({
</Typography>
<Box className={classes.headerActions}>
{isFiltered &&
<span className={`${classes.headerIndicator} ${classes.headerFilterIcon}`} title="Filtered">
<button
aria-label={`Edit filter for ${typeof headerLabel === "string" ? headerLabel : "column"}`}
className={`${classes.headerIndicator} ${classes.headerFilterIcon}`}
onClick={handleFilterIndicatorClick}
title="Edit Filter"
type="button"
>
<FilterAltOutlinedIcon fontSize="inherit" />
</span>
</button>
}
{isSorted &&
<span className={`${classes.headerIndicator} ${classes.headerSortIcon}`} title={`Sorted ${sortDirection === 'ASC' ? 'ascending' : 'descending'}`}>
@@ -10,6 +10,7 @@ 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';
import FilterAltOutlinedIcon from '@mui/icons-material/FilterAltOutlined';
const HeaderCellContext = createContext({});
@@ -18,6 +19,21 @@ const MIN_FLEX_COLUMN_WIDTH = 150;
const AUTOSIZE_HORIZONTAL_PADDING = 44;
const AUTOSIZE_HEADER_EXTRA_WIDTH = 28;
const headerMenuIconStyle = {fontSize: "1rem", marginRight: "8px"};
const normalizeHeaderContextMenuOption = (option) => {
if(option?.name === "Filter Column"){
return {
...option,
icon: <FilterAltOutlinedIcon style={headerMenuIconStyle} />,
};
}
if(option?.menuItems){
return {
...option,
menuItems: option.menuItems.map(normalizeHeaderContextMenuOption),
};
}
return option;
};
let autosizeCanvas;
@@ -506,7 +522,7 @@ const ResizableGridWrapper = ({
click: () => resetColumnWidths(),
},
];
return [...sizingOptions, ...(contextMenuOptions || [])];
return [...sizingOptions, ...(contextMenuOptions || []).map(normalizeHeaderContextMenuOption)];
}, [autosizeAllColumns, autosizeColumn, columns, contextMenuOptions, resetColumnWidths]);
useEffect( () => {
@@ -1,5 +1,5 @@
import React, { useEffect, useMemo, useContext} from 'react';
import {MythicDialog, MythicModifyStringDialog} from '../../MythicComponents/MythicDialog';
import {MythicDialog} from '../../MythicComponents/MythicDialog';
import {
exportCallbackConfigQuery,
hideCallbackMutation, lockCallbackMutation, unlockCallbackMutation, updateCallbackTriggerMutation,
@@ -56,6 +56,12 @@ import {EventTriggerContextSelectDialog} from "../Eventing/EventTriggerContextSe
import {useMythicLazyQuery} from "../../utilities/useMythicLazyQuery";
import {faFilter} from '@fortawesome/free-solid-svg-icons';
import {getReadableTextColor, isValidHexColor} from "../../MythicComponents/MythicColorInput";
import {
GridColumnFilterDialog,
getUpdatedGridFilterOptions,
gridValuePassesFilter,
isGridColumnFilterActive
} from "../../MythicComponents/MythicResizableGrid/GridColumnFilterDialog";
export const getCustomBrowsersQuery = gql(`
query getCustomBrowsersQuery{
@@ -749,7 +755,7 @@ function CallbacksTablePreMemo(props){
() =>
columnOrder.reduce( (prev, cur) => {
if(columnVisibility.visible.includes(cur.name) || cur.name === "Interact"){
if(filterOptions[cur.key] && String(filterOptions[cur.key]).length > 0){
if(isGridColumnFilterActive(filterOptions[cur.key])){
return [...prev, {...cur, filtered: true}];
}else{
return [...prev, {...cur}];
@@ -820,16 +826,19 @@ function CallbacksTablePreMemo(props){
}, [])
const filterRow = (row) => {
for(const [key,value] of Object.entries(filterOptions)){
if(key === "agent"){
if(!String(row.payload.payloadtype.name).toLowerCase().includes(String(value).toLowerCase())){
return true;
}
if(key === "agent") {
if (!gridValuePassesFilter(row.payload.payloadtype.name, value)) {
return true;
}
}else if(key === "id"){
if (!gridValuePassesFilter(row.display_id, value)) {
return true;
}
}else{
if(!String(row[key]).toLowerCase().includes(String(value).toLowerCase())){
if(!gridValuePassesFilter(row[key], value)){
return true;
}
}
}
return false;
}
@@ -1001,9 +1010,10 @@ function CallbacksTablePreMemo(props){
}, [])
}, [callbacks, callbackLocalUpdates, sortData, filterOptions, columnVisibility, clickedCallbackID]);
const onSubmitFilterOptions = (value) => {
setFilterOptions({...filterOptions, [selectedColumn.key]: value });
const nextFilterOptions = getUpdatedGridFilterOptions(filterOptions, selectedColumn.key, value);
setFilterOptions(nextFilterOptions);
try{
updateSetting({setting_name: "callbacks_table_filter_options", value: {...filterOptions, [selectedColumn.key]: value }});
updateSetting({setting_name: "callbacks_table_filter_options", value: nextFilterOptions});
}catch(error){
console.log("failed to save filter options");
}
@@ -1072,7 +1082,7 @@ function CallbacksTablePreMemo(props){
sortIndicatorIndex={sortColumn}
sortDirection={sortData.sortDirection}
items={sortedData}
rowHeight={GetComputedFontSize() + 10}
rowHeight={GetComputedFontSize() + 15}
onClickHeader={onClickHeader}
onDoubleClickRow={onRowDoubleClick}
contextMenuOptions={contextMenuOptions}
@@ -1081,10 +1091,10 @@ function CallbacksTablePreMemo(props){
{openContextMenu &&
<MythicDialog fullWidth={true} maxWidth="sm" open={openContextMenu}
onClose={()=>{setOpenContextMenu(false);}}
innerDialog={<MythicModifyStringDialog
title='Filter Column'
innerDialog={<GridColumnFilterDialog
onSubmit={onSubmitFilterOptions}
value={filterOptions[selectedColumn.key]}
filterValue={filterOptions[selectedColumn.key]}
selectedColumn={selectedColumn}
onClose={() => {
setOpenContextMenu(false);
}}
@@ -36,6 +36,12 @@ import {
} from "../../MythicComponents/MythicSavedUserSetting";
import {getIconColor, getIconName} from "./ResponseDisplayTable";
import {CallbacksTableColumnsReorderDialog} from "./CallbacksTableColumnsReorderDialog";
import {
GridColumnFilterDialog,
getUpdatedGridFilterOptions,
gridValuePassesFilter,
isGridColumnFilterActive
} from "../../MythicComponents/MythicResizableGrid/GridColumnFilterDialog";
const updateFileComment = gql`
mutation updateCommentMutation($mythictree_id: Int!, $comment: String!) {
@@ -85,7 +91,7 @@ export const CallbacksTabsCustomFileBasedBrowserTable = (props) => {
() =>
columnOrder.reduce( (prev, cur) => {
if(columnVisibility.visible.includes(cur.name)){
if(filterOptions[cur.key] && String(filterOptions[cur.key]).length > 0){
if(isGridColumnFilterActive(filterOptions[cur.key])){
return [...prev, {...cur, filtered: true}];
}else{
return [...prev, {...cur}];
@@ -176,9 +182,10 @@ export const CallbacksTabsCustomFileBasedBrowserTable = (props) => {
return tempData;
}, [allData, sortData]);
const onSubmitFilterOptions = (value) => {
setFilterOptions({...filterOptions, [selectedColumn.key]: value });
const nextFilterOptions = getUpdatedGridFilterOptions(filterOptions, selectedColumn.key, value);
setFilterOptions(nextFilterOptions);
try{
updateSetting({setting_name: `${props.treeConfig.name}_browser_filter_options`, value: {...filterOptions, [selectedColumn.key]: value }});
updateSetting({setting_name: `${props.treeConfig.name}_browser_filter_options`, value: nextFilterOptions});
}catch(error){
console.log("failed to save filter options");
}
@@ -194,11 +201,11 @@ export const CallbacksTabsCustomFileBasedBrowserTable = (props) => {
}
for(const [key,value] of Object.entries(filterOptions)){
if(["name_text", "comment"].includes(key)){
if(!String(props.treeRootData[props.selectedFolderData.group][props.selectedFolderData.host][row][key]).toLowerCase().includes(value)){
if(!gridValuePassesFilter(props.treeRootData[props.selectedFolderData.group][props.selectedFolderData.host][row][key], value)){
return true;
}
} else {
if(!String(props.treeRootData[props.selectedFolderData.group][props.selectedFolderData.host][row].metadata[key]).toLowerCase().includes(value)){
if(!gridValuePassesFilter(props.treeRootData[props.selectedFolderData.group][props.selectedFolderData.host][row].metadata[key], value)){
return true;
}
}
@@ -846,10 +853,10 @@ export const CallbacksTabsCustomFileBasedBrowserTable = (props) => {
setOpenContextMenu(false);
}}
innerDialog={
<MythicModifyStringDialog
title='Filter Column'
<GridColumnFilterDialog
onSubmit={onSubmitFilterOptions}
value={filterOptions[selectedColumn.key]}
filterValue={filterOptions[selectedColumn.key]}
selectedColumn={selectedColumn}
onClose={() => {
setOpenContextMenu(false);
}}
@@ -41,6 +41,12 @@ import {
useSetMythicSetting
} from "../../MythicComponents/MythicSavedUserSetting";
import {CallbacksTableColumnsReorderDialog} from "./CallbacksTableColumnsReorderDialog";
import {
GridColumnFilterDialog,
getUpdatedGridFilterOptions,
gridValuePassesFilter,
isGridColumnFilterActive
} from "../../MythicComponents/MythicResizableGrid/GridColumnFilterDialog";
const getFileDownloadHistory = gql`
query getFileDownloadHistory($full_path_text: String!, $host: String!, $group: [String!]) {
@@ -107,7 +113,7 @@ export const CallbacksTabsFileBrowserTable = (props) => {
() =>
columnOrder.reduce( (prev, cur) => {
if(columnVisibility.visible.includes(cur.name)){
if(filterOptions[cur.key] && String(filterOptions[cur.key]).length > 0){
if(isGridColumnFilterActive(filterOptions[cur.key])){
return [...prev, {...cur, filtered: true}];
}else{
return [...prev, {...cur}];
@@ -197,9 +203,10 @@ export const CallbacksTabsFileBrowserTable = (props) => {
return tempData;
}, [allData, sortData]);
const onSubmitFilterOptions = (value) => {
setFilterOptions({...filterOptions, [selectedColumn.key]: value });
const nextFilterOptions = getUpdatedGridFilterOptions(filterOptions, selectedColumn.key, value);
setFilterOptions(nextFilterOptions);
try{
updateSetting({setting_name: `file_browser_filter_options`, value: {...filterOptions,[selectedColumn.key]: value }});
updateSetting({setting_name: `file_browser_filter_options`, value: nextFilterOptions});
}catch(error){
console.log("failed to save filter options");
}
@@ -214,7 +221,7 @@ export const CallbacksTabsFileBrowserTable = (props) => {
return true;
}
for(const [key,value] of Object.entries(filterOptions)){
if(!String(props.treeRootData[props.selectedFolderData.group][props.selectedFolderData.host][row][key]).toLowerCase().includes(value)){
if(!gridValuePassesFilter(props.treeRootData[props.selectedFolderData.group][props.selectedFolderData.host][row][key], value)){
return true;
}
}
@@ -735,10 +742,10 @@ export const CallbacksTabsFileBrowserTable = (props) => {
<MythicDialog fullWidth={true} maxWidth="sm" open={openContextMenu}
onClose={()=>{setOpenContextMenu(false);}}
innerDialog={
<MythicModifyStringDialog
title='Filter Column'
<GridColumnFilterDialog
onSubmit={onSubmitFilterOptions}
value={filterOptions[selectedColumn.key]}
filterValue={filterOptions[selectedColumn.key]}
selectedColumn={selectedColumn}
onClose={() => {
setOpenContextMenu(false);
}}
@@ -26,6 +26,12 @@ import {
useSetMythicSetting
} from "../../MythicComponents/MythicSavedUserSetting";
import {CallbacksTableColumnsReorderDialog} from "./CallbacksTableColumnsReorderDialog";
import {
GridColumnFilterDialog,
getUpdatedGridFilterOptions,
gridValuePassesFilter,
isGridColumnFilterActive
} from "../../MythicComponents/MythicResizableGrid/GridColumnFilterDialog";
const getPermissionsDataQuery = gql`
query getPermissionsQuery($mythictree_id: Int!) {
@@ -216,7 +222,7 @@ export const CallbacksTabsProcessBrowserTable = ({treeAdjMatrix, treeRootData, m
() =>
columnOrder.reduce( (prev, cur) => {
if(columnVisibility.visible.includes(cur.name)){
if(filterOptions[cur.key] && String(filterOptions[cur.key]).length > 0){
if(isGridColumnFilterActive(filterOptions[cur.key])){
return [...prev, {...cur, filtered: true}];
}else{
return [...prev, {...cur}];
@@ -393,9 +399,10 @@ export const CallbacksTabsProcessBrowserTable = ({treeAdjMatrix, treeRootData, m
return tempData;
}, [allData, sortData]);
const onSubmitFilterOptions = (value) => {
setFilterOptions({...filterOptions, [selectedColumn.current.key]: value });
const nextFilterOptions = getUpdatedGridFilterOptions(filterOptions, selectedColumn.current?.key, value);
setFilterOptions(nextFilterOptions);
try{
updateSetting({setting_name: `process_browser_filter_options`, value: {...filterOptions,[selectedColumn.current.key]: value }});
updateSetting({setting_name: `process_browser_filter_options`, value: nextFilterOptions});
}catch(error){
console.log("failed to save filter options");
}
@@ -422,11 +429,11 @@ export const CallbacksTabsProcessBrowserTable = ({treeAdjMatrix, treeRootData, m
for(const [key,value] of Object.entries(filterOptions)){
if(treeRootData[group][host][rowData.full_path_text] === undefined){return true}
if(filterOptionInMetadata[key]){
if(!String(treeRootData[group][host][rowData.full_path_text ]?.metadata[key]).toLowerCase().includes(value)){
if(!gridValuePassesFilter(treeRootData[group][host][rowData.full_path_text ]?.metadata[key], value)){
return true;
}
}else{
if(!String(treeRootData[group][host][rowData.full_path_text][key]).toLowerCase().includes(value)){
if(!gridValuePassesFilter(treeRootData[group][host][rowData.full_path_text][key], value)){
return true;
}
}
@@ -764,10 +771,10 @@ export const CallbacksTabsProcessBrowserTable = ({treeAdjMatrix, treeRootData, m
<MythicDialog fullWidth={true} maxWidth="sm" open={openContextMenu}
onClose={()=>{setOpenContextMenu(false);}}
innerDialog={
<MythicModifyStringDialog
title='Filter Column'
<GridColumnFilterDialog
onSubmit={onSubmitFilterOptions}
value={filterOptions[selectedColumn.current]}
filterValue={filterOptions[selectedColumn.current?.key]}
selectedColumn={selectedColumn.current}
onClose={() => {
setOpenContextMenu(false);
}}
@@ -1,6 +1,6 @@
import React, {useEffect, useRef} from 'react';
import {Button} from '@mui/material';
import {MythicViewJSONAsTableDialog, MythicDialog, MythicModifyStringDialog} from '../../MythicComponents/MythicDialog';
import {MythicViewJSONAsTableDialog, MythicDialog} from '../../MythicComponents/MythicDialog';
import { MythicDisplayTextDialog } from '../../MythicComponents/MythicDisplayTextDialog';
import { ResponseDisplayTableDialogTable } from './ResponseDisplayTableDialogTable';
import {useTheme} from '@mui/material/styles';
@@ -18,9 +18,14 @@ import {faList, faTrashAlt, faSkullCrossbones, faCamera, faSyringe, faFolder, fa
faFileImage, faCopy, faBoxOpen, faFileAlt, faCirclePlus, faCheck, faSquareXmark, faRotate } from '@fortawesome/free-solid-svg-icons';
import {Dropdown, DropdownMenuItem} from "../../MythicComponents/MythicNestedMenus";
import {GetComputedFontSize} from "../../MythicComponents/MythicSavedUserSetting";
import {TableFilterDialog} from "./TableFilterDialog";
import ArrowDropDownIcon from '@mui/icons-material/ArrowDropDown';
import {MythicSectionHeader} from "../../MythicComponents/MythicPageHeader";
import {
GridColumnFilterDialog,
getUpdatedGridFilterOptions,
gridValuePassesFilter,
isGridColumnFilterActive
} from "../../MythicComponents/MythicResizableGrid/GridColumnFilterDialog";
const onCopyToClipboard = (data) => {
let result = copyStringToClipboard(data);
@@ -142,7 +147,7 @@ const ResponseDisplayTableFontAwesomeEndIcon = ({cellData}) => {
}
const ResponseDisplayTableStringCell = ({cellData, rowData}) => {
return (
<div style={{...(cellData?.cellStyle || null), height: "100%"}}>
<div className="mythic-response-table-cell" style={{...(cellData?.cellStyle || null), height: "100%"}}>
<ResponseDisplayTableStringCellCopy cellData={cellData} />
<ResponseDisplayTableFontAwesomeStartIcon cellData={cellData} />
{cellData?.plaintextHoverText? (
@@ -163,7 +168,7 @@ const ResponseDisplayTableStringCell = ({cellData, rowData}) => {
}
const ResponseDisplayTableNumberCell = ({cellData, rowData}) => {
return (
<div style={{...(cellData?.cellStyle || null), height: "100%"}}>
<div className="mythic-response-table-cell" style={{...(cellData?.cellStyle || null), height: "100%"}}>
{cellData?.copyIcon?
<MythicStyledTooltip title={"Copy to clipboard"}>
<IconButton onClick={() => onCopyToClipboard(cellData["plaintext"])} size="small">
@@ -220,7 +225,7 @@ export const getStringSize = ({cellData}) => {
}
const ResponseDisplayTableSizeCell = ({cellData, rowData}) => {
return (
<div style={{...(cellData?.cellStyle || null), height: "100%"}}>
<div className="mythic-response-table-cell" style={{...(cellData?.cellStyle || null), height: "100%"}}>
{cellData?.plaintextHoverText? (
<MythicStyledTooltip title={cellData.plaintextHoverText} >
<pre style={{display: "inline-block", margin: 0}}>
@@ -242,7 +247,7 @@ const getActionButtonClassName = (intent = "info") => {
}
const ResponseDisplayTableActionCell = ({cellData, callback_id, rowData}) => {
return (
<div style={{ height: "100%"}}>
<div className="mythic-response-table-cell mythic-response-table-action-cell" style={{ height: "100%"}}>
{cellData?.plaintext && cellData.plaintext}
{cellData?.button && <ResponseDisplayTableActionCellButton cellData={cellData} callback_id={callback_id} />}
</div>
@@ -466,8 +471,8 @@ const createRowCells = ({row, rowIndex, headers, callback_id}) => {
})
}
export const ResponseDisplayTable = ({table, callback_id, expand, task}) =>{
const rowHeight = GetComputedFontSize() + 7;
const headerHeight = GetComputedFontSize() + 32;
const rowHeight = GetComputedFontSize() + 15;
const headerHeight = GetComputedFontSize() + 35;
const maxHeight = 375;
const [dataHeight, setDataHeight] = React.useState(maxHeight);
const [allData, setAllData] = React.useState([]);
@@ -477,7 +482,7 @@ export const ResponseDisplayTable = ({table, callback_id, expand, task}) =>{
const columns = React.useMemo(
() =>
table?.headers?.reduce( (prev, cur) => {
if(filterOptions[cur.plaintext] && String(filterOptions[cur.plaintext]).length > 0){
if(isGridColumnFilterActive(filterOptions[cur.plaintext])){
return [...prev, {...cur, filtered: true}];
}else{
return [...prev, {...cur}];
@@ -488,7 +493,7 @@ export const ResponseDisplayTable = ({table, callback_id, expand, task}) =>{
const [sortData, setSortData] = React.useState({sortKey: null, sortType: null, sortDirection: "ASC"})
const filterRow = (row) => {
for(const [key,value] of Object.entries(filterOptions)){
if(!String(row[key].plaintext).toLowerCase().includes(String(value).toLowerCase())){
if(!gridValuePassesFilter(row[key]?.plaintext, value)){
return true;
}
}
@@ -592,7 +597,7 @@ export const ResponseDisplayTable = ({table, callback_id, expand, task}) =>{
}, [sortedData, table?.headers, callback_id]
);
const onSubmitFilterOptions = (value) => {
setFilterOptions({...filterOptions, [selectedColumn.key]: value });
setFilterOptions(getUpdatedGridFilterOptions(filterOptions, selectedColumn.key, value));
}
const filterOutButtonsFromRowData = (data) => {
@@ -838,10 +843,10 @@ export const ResponseDisplayTable = ({table, callback_id, expand, task}) =>{
<MythicDialog fullWidth={true} maxWidth="sm" open={openContextMenu}
onClose={()=>{setOpenContextMenu(false);}}
innerDialog={
<MythicModifyStringDialog
title='Filter Column'
<GridColumnFilterDialog
onSubmit={onSubmitFilterOptions}
value={filterOptions[selectedColumn.key]}
filterValue={filterOptions[selectedColumn.key]}
selectedColumn={selectedColumn}
onClose={() => {
setOpenContextMenu(false);
}}
@@ -15,7 +15,8 @@ function ResponseDisplayTabsLabel(props) {
</span>
}
className="mythic-response-tab"
wrapped={true}
title={typeof label === "string" ? label : undefined}
wrapped={false}
{...a11yProps(index)}
{...other}
/>
@@ -64,7 +65,7 @@ export function ResponseDisplayTabs({ tabs, task, expand, displayType, output })
TabIndicatorProps={{style: {
display: "none",
}}}
aria-label='scrollable tabs'>
aria-label='browser script response tabs'>
{tabs.map((tab, index) => (
<ResponseDisplayTabsLabel
key={'tablabel' + task.id + index}
@@ -13,7 +13,7 @@ import Moment from 'react-moment';
import moment from 'moment';
import CastConnectedTwoToneIcon from '@mui/icons-material/CastConnectedTwoTone';
import {snackActions} from "../../utilities/Snackbar";
import {MythicDialog, MythicModifyStringDialog} from "../../MythicComponents/MythicDialog";
import {MythicDialog} from "../../MythicComponents/MythicDialog";
import OpenInNewTwoToneIcon from '@mui/icons-material/OpenInNewTwoTone';
import IosShareIcon from '@mui/icons-material/IosShare';
import {copyStringToClipboard} from "../../utilities/Clipboard";
@@ -21,6 +21,12 @@ import {ipCompare} from "../Callbacks/CallbacksTable";
import MythicResizableGrid from "../../MythicComponents/MythicResizableGrid";
import {CallbacksTableStringCell} from "../Callbacks/CallbacksTableRow";
import {GetComputedFontSize} from "../../MythicComponents/MythicSavedUserSetting";
import {
GridColumnFilterDialog,
getUpdatedGridFilterOptions,
gridValuePassesFilter,
isGridColumnFilterActive
} from "../../MythicComponents/MythicResizableGrid/GridColumnFilterDialog";
const cancelEventGroupInstanceMutation = gql(`
mutation cancelEventGroupInstanceMutation($eventgroupinstance_id: Int!){
@@ -183,7 +189,7 @@ function EventGroupInstancesTableMaterialReactTablePreMemo({eventgroups, me, set
{key: "operator", type: 'string', name: "Operator", inMetadata: true, fillWidth: true, disableSort: true,},
{key: "cancel", type: 'string', name: "Action", width: 70, disableSort: true, disableFilter: true},
]?.reduce( (prev, cur) => {
if(filterOptions[cur.key] && String(filterOptions[cur.key]).length > 0){
if(isGridColumnFilterActive(filterOptions[cur.key])){
return [...prev, {...cur, filtered: true}];
}else{
return [...prev, {...cur}];
@@ -212,11 +218,20 @@ function EventGroupInstancesTableMaterialReactTablePreMemo({eventgroups, me, set
const filterRow = (row) => {
for(const [key,value] of Object.entries(filterOptions)){
if(key === "operator"){
if(!String(row?.operator?.username).toLowerCase().includes(String(value).toLowerCase())){
if(!gridValuePassesFilter(row?.operator?.username, value)){
return true;
}
}else if(key === "eventgroup"){
if(!gridValuePassesFilter(row?.eventgroup?.name, value)){
return true;
}
}else if(key === "time"){
const timeFilterValue = `${row?.created_at || ""} ${row?.created_at ? toLocalTime(row.created_at, me?.user?.view_utc_time) : ""} ${row?.end_timestamp || ""}`;
if(!gridValuePassesFilter(timeFilterValue, value)){
return true;
}
}else {
if(!String(row[key]).toLowerCase().includes(String(value).toLowerCase())){
if(!gridValuePassesFilter(row[key], value)){
return true;
}
}
@@ -380,7 +395,7 @@ function EventGroupInstancesTableMaterialReactTablePreMemo({eventgroups, me, set
const [openContextMenu, setOpenContextMenu] = React.useState(false);
const [selectedColumn, setSelectedColumn] = React.useState({});
const onSubmitFilterOptions = (value) => {
setFilterOptions({...filterOptions, [selectedColumn.key]: value });
setFilterOptions(getUpdatedGridFilterOptions(filterOptions, selectedColumn.key, value));
}
const contextMenuOptions = [{
name: 'Filter Column', type: "item", icon: null,
@@ -420,10 +435,10 @@ function EventGroupInstancesTableMaterialReactTablePreMemo({eventgroups, me, set
<MythicDialog fullWidth={true} maxWidth="sm" open={openContextMenu}
onClose={()=>{setOpenContextMenu(false);}}
innerDialog={
<MythicModifyStringDialog
title='Filter Column'
<GridColumnFilterDialog
onSubmit={onSubmitFilterOptions}
value={filterOptions[selectedColumn.key]}
filterValue={filterOptions[selectedColumn.key]}
selectedColumn={selectedColumn}
onClose={() => {
setOpenContextMenu(false);
}}
+96 -1
View File
@@ -423,6 +423,7 @@ tspan {
}
.MythicResizableGrid-headerIndicator {
align-items: center;
appearance: none;
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;
@@ -430,15 +431,23 @@ tspan {
display: inline-flex;
flex: 0 0 auto;
font-size: 0.95rem;
font-family: inherit;
height: 18px;
justify-content: center;
line-height: 1;
margin: 0;
padding: 0;
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};
cursor: pointer;
}
.MythicResizableGrid-headerFilterIcon:hover {
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.34 : 0.22)};
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.86 : 0.68)};
}
.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)};
@@ -593,6 +602,61 @@ tspan {
white-space: nowrap;
width: 100%;
}
.mythic-grid-filter-dialog {
background-color: ${(props) => props.theme.surfaces?.raised || props.theme.palette.background.paper};
color: ${(props) => props.theme.palette.text.primary};
}
.mythic-grid-filter-dialog-title {
background-color: ${(props) => props.theme.pageHeader?.main || props.theme.surfaces?.muted || props.theme.palette.background.default};
background-image: ${(props) => `linear-gradient(90deg, ${alpha(props.theme.palette.primary.main, props.theme.palette.mode === "dark" ? 0.18 : 0.1)} 0%, ${alpha(props.theme.palette.text.primary, props.theme.palette.mode === "dark" ? 0.045 : 0.035)} 100%)`};
border-bottom: 1px solid ${(props) => props.theme.table?.borderSoft || props.theme.borderColor};
color: ${(props) => props.theme.palette.text.primary};
font-size: 0.98rem !important;
font-weight: 850 !important;
line-height: 1.2 !important;
padding: 0.85rem 1rem !important;
}
.mythic-grid-filter-dialog-content {
display: flex;
flex-direction: column;
gap: 0.9rem;
min-width: min(38rem, calc(100vw - 3rem));
padding: 1rem !important;
}
.mythic-grid-filter-dialog-copy {
color: ${(props) => props.theme.palette.text.secondary};
font-size: 0.78rem;
font-weight: 650;
line-height: 1.35;
}
.mythic-grid-filter-dialog-mode-row {
align-items: center;
display: flex;
gap: 0.75rem;
justify-content: space-between;
}
.mythic-grid-filter-dialog-label {
color: ${(props) => props.theme.palette.text.primary};
font-size: 0.8rem;
font-weight: 800;
}
.mythic-grid-filter-dialog-fields {
display: grid;
gap: 0.85rem;
grid-template-columns: repeat(2, minmax(0, 1fr));
}
.mythic-grid-filter-dialog-actions {
border-top: 1px solid ${(props) => props.theme.table?.borderSoft || props.theme.borderColor};
padding: 0.7rem 1rem !important;
}
@media (max-width: 720px) {
.mythic-grid-filter-dialog-content {
min-width: 0;
}
.mythic-grid-filter-dialog-fields {
grid-template-columns: 1fr;
}
}
.mythic-callback-interactCell {
align-items: center;
display: inline-flex;
@@ -2674,15 +2738,22 @@ tspan {
padding: 0.35rem;
}
.mythic-response-tabs-list {
max-height: 34px;
min-height: 34px;
}
.mythic-response-tabs-list .MuiTabs-flexContainer {
flex-wrap: nowrap;
gap: 0.35rem;
}
.mythic-response-tabs-list .MuiTabs-scroller {
overflow-x: auto !important;
overflow-y: hidden !important;
}
.mythic-response-tabs-list .MuiTabs-scrollButtons {
border: 1px solid ${(props) => props.theme.table?.borderSoft || props.theme.borderColor};
border-radius: ${(props) => props.theme.shape.borderRadius}px;
color: ${(props) => props.theme.palette.text.secondary};
flex: 0 0 30px;
min-height: 30px;
width: 30px;
}
@@ -2691,12 +2762,14 @@ tspan {
border: 1px solid ${(props) => props.theme.table?.borderSoft || props.theme.borderColor} !important;
border-radius: ${(props) => props.theme.shape.borderRadius}px !important;
color: ${(props) => props.theme.palette.text.secondary} !important;
flex: 0 0 auto;
font-size: 0.76rem;
font-weight: 750;
line-height: 1.2;
max-width: min(18rem, 60vw);
max-width: min(16rem, 55vw);
min-height: 30px;
min-width: 0;
overflow: hidden;
padding: 0.35rem 0.7rem;
text-transform: none;
}
@@ -2711,6 +2784,7 @@ tspan {
color: ${(props) => props.theme.palette.primary.main} !important;
}
.mythic-response-tab-label {
display: block;
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
@@ -2740,6 +2814,27 @@ tspan {
overflow: hidden;
width: 100%;
}
.mythic-response-table-cell {
align-items: center;
display: inline-flex;
gap: 0.3rem;
max-width: 100%;
min-width: 0;
vertical-align: middle;
white-space: nowrap;
}
.mythic-response-table-action-cell {
gap: 0.45rem;
}
.mythic-response-table-cell pre {
line-height: 1.2;
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
}
.mythic-response-table-cell .MuiIconButton-root {
flex: 0 0 auto;
}
.mythic-browser-scripts-table {
max-width: 100%;
min-width: 60rem;