mirror of
https://github.com/its-a-feature/Mythic
synced 2026-06-08 14:55:38 +00:00
delegate check improvements and process table ui updates
This commit is contained in:
@@ -633,8 +633,11 @@ export function App(props) {
|
||||
const preferences = useReactiveVar(mePreferences);
|
||||
const [loadingPreference, setLoadingPreferences] = React.useState(true);
|
||||
const [themeMode, themeToggler] = useDarkMode();
|
||||
const themePalette = preferences?.palette;
|
||||
const themeFontFamily = preferences?.fontFamily;
|
||||
const theme = React.useMemo(
|
||||
() => {
|
||||
const preferences = {palette: themePalette, fontFamily: themeFontFamily};
|
||||
try{
|
||||
return createTheme({
|
||||
transitions: {
|
||||
@@ -864,7 +867,7 @@ export function App(props) {
|
||||
...getModernThemeAdditions(themeMode, operatorSettingDefaults),
|
||||
})
|
||||
}
|
||||
},[themeMode, preferences]
|
||||
},[themeMode, themePalette, themeFontFamily]
|
||||
);
|
||||
const mountedRef = React.useRef(true);
|
||||
const [openRefreshDialog, setOpenRefreshDialog] = React.useState(false);
|
||||
|
||||
@@ -38,6 +38,7 @@ const CellPreMemo = ({ style, rowIndex, columnIndex, data }) => {
|
||||
}
|
||||
};
|
||||
const selectedClass = data.items[rowIndex][columnIndex]?.props?.rowData?.selected ? "selectedCallback" : "";
|
||||
const customRowClass = data.items[rowIndex][columnIndex]?.props?.rowData?.rowClassName || "";
|
||||
const rowFirstCellClass = columnIndex === 0 ? classes.rowFirstCell : "";
|
||||
const rowLastCellClass = columnIndex === data.items[rowIndex].length - 1 ? classes.rowLastCell : "";
|
||||
const rowInteractiveClass = data.onRowClick ? classes.rowInteractive : "";
|
||||
@@ -107,7 +108,7 @@ const CellPreMemo = ({ style, rowIndex, columnIndex, data }) => {
|
||||
);
|
||||
return (
|
||||
<div style={{...style, ...cellStyle, ...rowStyle}}
|
||||
className={`${classes.cell} ${rowClassName} ${rowHighlight} ${selectedClass} ${rowFirstCellClass} ${rowLastCellClass} ${rowInteractiveClass}`}
|
||||
className={`${classes.cell} ${rowClassName} ${rowHighlight} ${selectedClass} ${customRowClass} ${rowFirstCellClass} ${rowLastCellClass} ${rowInteractiveClass}`}
|
||||
onDoubleClick={handleDoubleClick}
|
||||
onClick={handleClick}
|
||||
onMouseEnter={onMouseEnter}
|
||||
|
||||
+5
-5
@@ -345,7 +345,7 @@ const ResizableGridWrapper = ({
|
||||
});
|
||||
const didUpdateWidths = setColumnWidthsAndRef(updatedColumnWidths);
|
||||
if(didUpdateWidths && name !== undefined){
|
||||
updateSetting({setting_name: `${name}_column_widths`, value: updatedColumnWidths});
|
||||
updateSetting({setting_name: `${name}_column_widths`, value: updatedColumnWidths, broadcast: false});
|
||||
}
|
||||
|
||||
}, [scrollbarWidth, columns, AutoSizerProps.width, localColumnsRef.current, setColumnWidthsAndRef]); // eslint-disable-line react-hooks/exhaustive-deps
|
||||
@@ -416,7 +416,7 @@ const ResizableGridWrapper = ({
|
||||
activeResizeRef.current = null;
|
||||
const didUpdateWidths = setColumnWidthsAndRef(updatedColumnWidths, currentResize.columnIndex);
|
||||
if(didUpdateWidths && name !== undefined){
|
||||
updateSetting({setting_name: `${name}_column_widths`, value: updatedColumnWidths});
|
||||
updateSetting({setting_name: `${name}_column_widths`, value: updatedColumnWidths, broadcast: false});
|
||||
}
|
||||
setResizingColumnIndex(-1);
|
||||
cleanupResizeListeners();
|
||||
@@ -470,7 +470,7 @@ const ResizableGridWrapper = ({
|
||||
const didUpdateWidths = setColumnWidthsAndRef(updatedColumnWidths, columnIndex);
|
||||
if(didUpdateWidths){
|
||||
if(name !== undefined){
|
||||
updateSetting({setting_name: `${name}_column_widths`, value: updatedColumnWidths});
|
||||
updateSetting({setting_name: `${name}_column_widths`, value: updatedColumnWidths, broadcast: false});
|
||||
}
|
||||
}
|
||||
}, [columns, headerNameKey, items, name, setColumnWidthsAndRef, updateSetting]);
|
||||
@@ -485,7 +485,7 @@ const ResizableGridWrapper = ({
|
||||
});
|
||||
const didUpdateWidths = setColumnWidthsAndRef(updatedColumnWidths, 0);
|
||||
if(didUpdateWidths && name !== undefined){
|
||||
updateSetting({setting_name: `${name}_column_widths`, value: updatedColumnWidths});
|
||||
updateSetting({setting_name: `${name}_column_widths`, value: updatedColumnWidths, broadcast: false});
|
||||
}
|
||||
}, [columns, headerNameKey, items, name, setColumnWidthsAndRef, updateSetting]);
|
||||
const resetColumnWidths = useCallback( () => {
|
||||
@@ -496,7 +496,7 @@ const ResizableGridWrapper = ({
|
||||
});
|
||||
const didUpdateWidths = setColumnWidthsAndRef(updatedColumnWidths, 0);
|
||||
if(didUpdateWidths && name !== undefined){
|
||||
updateSetting({setting_name: `${name}_column_widths`, value: updatedColumnWidths});
|
||||
updateSetting({setting_name: `${name}_column_widths`, value: updatedColumnWidths, broadcast: false});
|
||||
}
|
||||
}, [AutoSizerProps.width, columns, name, scrollbarWidth, setColumnWidthsAndRef, updateSetting]);
|
||||
const headerContextMenuOptions = React.useMemo( () => {
|
||||
|
||||
@@ -17,6 +17,23 @@ export const GetComputedFontSize = () => {
|
||||
const fontSizeString = window.getComputedStyle(element).fontSize;
|
||||
return parseFloat(fontSizeString);
|
||||
}
|
||||
let latestPreferenceSnapshot = null;
|
||||
const getPreferenceSnapshot = () => {
|
||||
return latestPreferenceSnapshot || mePreferences() || {};
|
||||
};
|
||||
const areSettingValuesEqual = (first, second) => {
|
||||
if(first === second){
|
||||
return true;
|
||||
}
|
||||
if(typeof first !== "object" || typeof second !== "object" || first === null || second === null){
|
||||
return false;
|
||||
}
|
||||
try{
|
||||
return JSON.stringify(first) === JSON.stringify(second);
|
||||
}catch(error){
|
||||
return false;
|
||||
}
|
||||
};
|
||||
/*
|
||||
setting_name options:
|
||||
taskingDisplayFields
|
||||
@@ -40,7 +57,7 @@ export function useGetMythicSetting({setting_name, default_value}){
|
||||
return setting;
|
||||
}
|
||||
export function GetMythicSetting({setting_name, default_value}){
|
||||
const preferences = mePreferences();
|
||||
const preferences = getPreferenceSnapshot();
|
||||
return preferences?.[setting_name] === undefined ? default_value : preferences?.[setting_name];
|
||||
}
|
||||
export function useSetMythicSetting() {
|
||||
@@ -54,25 +71,33 @@ export function useSetMythicSetting() {
|
||||
}
|
||||
});
|
||||
return [
|
||||
({setting_name, value}) => {
|
||||
if(mePreferences()?.[setting_name] !== value){
|
||||
({setting_name, value, broadcast=true}) => {
|
||||
const currentPreferences = getPreferenceSnapshot();
|
||||
if(!areSettingValuesEqual(currentPreferences?.[setting_name], value)){
|
||||
const updatedPreferences = {
|
||||
...mePreferences(),
|
||||
...currentPreferences,
|
||||
[setting_name]: value,
|
||||
};
|
||||
mePreferences(updatedPreferences);
|
||||
latestPreferenceSnapshot = updatedPreferences;
|
||||
if(broadcast){
|
||||
mePreferences(updatedPreferences);
|
||||
}
|
||||
updateSetting({variables: {preferences: updatedPreferences}});
|
||||
}
|
||||
},
|
||||
({settings}) => {
|
||||
({settings, broadcast=true}) => {
|
||||
const updatedPreferences = {
|
||||
...mePreferences(),
|
||||
...getPreferenceSnapshot(),
|
||||
...settings,
|
||||
};
|
||||
mePreferences(updatedPreferences);
|
||||
latestPreferenceSnapshot = updatedPreferences;
|
||||
if(broadcast){
|
||||
mePreferences(updatedPreferences);
|
||||
}
|
||||
updateSetting({variables: {preferences: updatedPreferences}});
|
||||
},
|
||||
() => {
|
||||
latestPreferenceSnapshot = operatorSettingDefaults;
|
||||
mePreferences(operatorSettingDefaults);
|
||||
updateSetting({variables: {preferences: operatorSettingDefaults}});
|
||||
}
|
||||
|
||||
@@ -52,7 +52,7 @@ import {useQuery, gql} from '@apollo/client';
|
||||
import ConfirmationNumberIcon from '@mui/icons-material/ConfirmationNumber';
|
||||
import AccountTreeIcon from '@mui/icons-material/AccountTree';
|
||||
import AssignmentIcon from '@mui/icons-material/Assignment';
|
||||
import {GetMythicSetting, useGetMythicSetting, useSetMythicSetting} from "./MythicComponents/MythicSavedUserSetting";
|
||||
import {GetMythicSetting, useSetMythicSetting} from "./MythicComponents/MythicSavedUserSetting";
|
||||
import AddCircleIcon from '@mui/icons-material/AddCircle';
|
||||
import ManageSearchIcon from '@mui/icons-material/ManageSearch';
|
||||
import MoreHorizIcon from '@mui/icons-material/MoreHoriz';
|
||||
@@ -591,8 +591,7 @@ const AllSettingOptions = [
|
||||
"BrowserScripts"
|
||||
].sort();
|
||||
|
||||
const TopAppBarVerticalAdjustShortcutsDialog = ({onClose}) => {
|
||||
const sideShortcuts = useGetMythicSetting({setting_name: "sideShortcuts", default_value: defaultShortcuts})
|
||||
const TopAppBarVerticalAdjustShortcutsDialog = ({onClose, onSave, sideShortcuts}) => {
|
||||
const [currentShortcuts, setCurrentShortcuts] = React.useState(sideShortcuts);
|
||||
const [updateSetting] = useSetMythicSetting();
|
||||
const reset = () => {
|
||||
@@ -620,7 +619,8 @@ const TopAppBarVerticalAdjustShortcutsDialog = ({onClose}) => {
|
||||
setCurrentShortcuts(newShortcuts);
|
||||
}
|
||||
const onUpdate = () => {
|
||||
updateSetting({setting_name: "sideShortcuts", value: currentShortcuts});
|
||||
updateSetting({setting_name: "sideShortcuts", value: currentShortcuts, broadcast: false});
|
||||
onSave(currentShortcuts);
|
||||
snackActions.success("Updated shortcuts!");
|
||||
onClose();
|
||||
}
|
||||
@@ -720,11 +720,12 @@ export function TopAppBarVertical(props) {
|
||||
const me = props.me;
|
||||
const navigate = useNavigate();
|
||||
const initialNavBarOpen = GetMythicSetting({setting_name: 'navBarOpen', default_value: operatorSettingDefaults.navBarOpen});
|
||||
const initialSideShortcuts = GetMythicSetting({setting_name: "sideShortcuts", default_value: defaultShortcuts});
|
||||
const [updateSetting] = useSetMythicSetting();
|
||||
const [menuOpen, setMenuOpen] = React.useState(initialNavBarOpen);
|
||||
const [sideShortcuts, setSideShortcuts] = React.useState(initialSideShortcuts);
|
||||
const [openExtra, setOpenExtra] = React.useState(false);
|
||||
const [openEditDialog, setOpenEditDialog ] = React.useState(false);
|
||||
const sideShortcuts = useGetMythicSetting({setting_name: "sideShortcuts", default_value: defaultShortcuts})
|
||||
const [serverVersion, setServerVersion] = React.useState("...");
|
||||
const [serverName, setServerName] = React.useState("...");
|
||||
useQuery(GET_SETTINGS, {fetchPolicy: "no-cache",
|
||||
@@ -736,12 +737,13 @@ export function TopAppBarVertical(props) {
|
||||
const toggleDrawerOpen = (e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
setMenuOpen(!menuOpen);
|
||||
updateSetting({setting_name: "navBarOpen", value: !menuOpen})
|
||||
const nextMenuOpen = !menuOpen;
|
||||
setMenuOpen(nextMenuOpen);
|
||||
updateSetting({setting_name: "navBarOpen", value: nextMenuOpen, broadcast: false});
|
||||
};
|
||||
const handleDrawerClose = () => {
|
||||
setMenuOpen(false);
|
||||
updateSetting({setting_name: "navBarOpen", value: false})
|
||||
updateSetting({setting_name: "navBarOpen", value: false, broadcast: false})
|
||||
}
|
||||
const handleToggleExtra = () => {
|
||||
setOpenExtra(!openExtra);
|
||||
@@ -875,6 +877,8 @@ export function TopAppBarVertical(props) {
|
||||
<MythicDialog open={openEditDialog} fullWidth={true} maxWidth={"sm"}
|
||||
onClose={()=>{setOpenEditDialog(false);}}
|
||||
innerDialog={<TopAppBarVerticalAdjustShortcutsDialog
|
||||
sideShortcuts={sideShortcuts}
|
||||
onSave={setSideShortcuts}
|
||||
onClose={()=>{setOpenEditDialog(false);}} />}
|
||||
/>
|
||||
}
|
||||
|
||||
@@ -385,7 +385,7 @@ function CallbacksTablePreMemo(props){
|
||||
const eventingDataRef = React.useRef({});
|
||||
const [openEventingDialog, setOpenEventingDialog] = React.useState(false);
|
||||
const [openEventingMultipleDialog, setOpenEventingMultipleDialog] = React.useState(false);
|
||||
const [updateSetting] = useSetMythicSetting();
|
||||
const [updateSetting, updateSettings] = useSetMythicSetting();
|
||||
const onUpdateTrigger = (newTriggerValue) => {
|
||||
updateTrigger({variables: {callback_display_id: openTriggerDialog.display_id, trigger_on_checkin_after_time: newTriggerValue}})
|
||||
setOpenTriggerDialog({...openTriggerDialog, open: false});
|
||||
@@ -1039,7 +1039,7 @@ function CallbacksTablePreMemo(props){
|
||||
const nextFilterOptions = getUpdatedGridFilterOptions(filterOptions, selectedColumn.key, value);
|
||||
setFilterOptions(nextFilterOptions);
|
||||
try{
|
||||
updateSetting({setting_name: "callbacks_table_filter_options", value: nextFilterOptions});
|
||||
updateSetting({setting_name: "callbacks_table_filter_options", value: nextFilterOptions, broadcast: false});
|
||||
}catch(error){
|
||||
console.log("failed to save filter options");
|
||||
}
|
||||
@@ -1083,10 +1083,15 @@ function CallbacksTablePreMemo(props){
|
||||
newHidden.push(newOrder[i].name);
|
||||
}
|
||||
}
|
||||
updateSetting({setting_name: "callbacks_table_column_order", value: newOrder.map(c => c.name)});
|
||||
setColumnOrder(newOrder);
|
||||
setColumnVisibility({visible: newVisible, hidden: newHidden});
|
||||
updateSetting({setting_name: "callbacks_table_columns", value: newVisible});
|
||||
updateSettings({
|
||||
settings: {
|
||||
callbacks_table_column_order: newOrder.map(c => c.name),
|
||||
callbacks_table_columns: newVisible,
|
||||
},
|
||||
broadcast: false,
|
||||
});
|
||||
setOpenReorderDialog(false);
|
||||
}
|
||||
const onResetColumnReorder = () => {
|
||||
|
||||
@@ -304,8 +304,6 @@ export const CallbacksTableC2Cell = React.memo(({rowData}) => {
|
||||
const [localRowData, setLocalRowData] = React.useState(rowData);
|
||||
const initialCallbackGraphEdges = useContext(CallbackGraphEdgesContext);
|
||||
const onOpenTab = useContext(OnOpenTabContext);
|
||||
const [egressActive, setEgressActive] = React.useState(true);
|
||||
const [hasEgressRoute, setHasEgressRoute] = React.useState(true);
|
||||
const [openC2Dialog, setOpenC2Dialog] = React.useState(false);
|
||||
const [callbackgraphedges, setCallbackgraphedges] = React.useState([]);
|
||||
const [callbackgraphedgesAll, setCallbackgraphedgesAll] = React.useState([]);
|
||||
@@ -313,19 +311,6 @@ export const CallbacksTableC2Cell = React.memo(({rowData}) => {
|
||||
event.stopPropagation();
|
||||
setOpenC2Dialog(true);
|
||||
}
|
||||
useEffect( () => {
|
||||
const routes = callbackgraphedgesAll.filter( (edge) => {
|
||||
if(!edge.c2profile.is_p2p && edge.source.id === localRowData.id && edge.destination.id === localRowData.id){
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}).length;
|
||||
if(routes > 0 && !hasEgressRoute){
|
||||
setHasEgressRoute(true);
|
||||
}else if(routes === 0 && hasEgressRoute){
|
||||
setHasEgressRoute(false);
|
||||
}
|
||||
}, [callbackgraphedgesAll, localRowData]);
|
||||
useEffect( () => {
|
||||
const getEdges = (activeOnly) => {
|
||||
//update our aggregate of callbackgraphedges for both src and dst that involve us
|
||||
@@ -378,21 +363,6 @@ export const CallbacksTableC2Cell = React.memo(({rowData}) => {
|
||||
setCallbackgraphedgesAll(myEdges);
|
||||
|
||||
}, [initialCallbackGraphEdges, localRowData]);
|
||||
useEffect( () => {
|
||||
//determine if there are any active routes left at all
|
||||
//console.log(localRowData.display_id, callbackgraphedges)
|
||||
const activeRoutes = callbackgraphedges.filter( (edge) => {
|
||||
if(!edge.c2profile.is_p2p && edge.end_timestamp === null){
|
||||
return true;
|
||||
}
|
||||
return false
|
||||
});
|
||||
if(activeRoutes.length === 0){
|
||||
setEgressActive(false);
|
||||
}else{
|
||||
setEgressActive(true);
|
||||
}
|
||||
}, [callbackgraphedges]);
|
||||
useEffect( () => {
|
||||
if(rowData.id !== localRowData.id){
|
||||
setLocalRowData(rowData);
|
||||
@@ -401,15 +371,26 @@ export const CallbacksTableC2Cell = React.memo(({rowData}) => {
|
||||
if(rowData?.payload?.payloadtype?.agent_type !== "agent"){
|
||||
return null
|
||||
}
|
||||
const c2ButtonClass = `mythic-callback-iconButton mythic-callback-cellIconButton ${egressActive && hasEgressRoute ? "mythic-callback-cellIconButtonSuccess" : "mythic-callback-cellIconButtonError"}`;
|
||||
const c2Tooltip = hasEgressRoute ?
|
||||
(egressActive ? "View C2 path information" : "No active egress route. View C2 path information") :
|
||||
"No egress route detected. View C2 path information";
|
||||
const directEgressRoutes = callbackgraphedgesAll.filter((edge) =>
|
||||
!edge.c2profile?.is_p2p &&
|
||||
edge.source.id === localRowData.id &&
|
||||
edge.destination.id === localRowData.id
|
||||
);
|
||||
const hasDirectEgressRoute = directEgressRoutes.length > 0;
|
||||
const directEgressActive = directEgressRoutes.some((edge) => edge.end_timestamp === null);
|
||||
const p2pRouteActive = callbackgraphedges.some((edge) =>
|
||||
!edge.c2profile?.is_p2p && edge.end_timestamp === null
|
||||
);
|
||||
const c2RouteActive = hasDirectEgressRoute ? directEgressActive : p2pRouteActive;
|
||||
const c2ButtonClass = `mythic-callback-iconButton mythic-callback-cellIconButton ${c2RouteActive ? "mythic-callback-cellIconButtonSuccess" : "mythic-callback-cellIconButtonError"}`;
|
||||
const c2Tooltip = hasDirectEgressRoute ?
|
||||
(directEgressActive ? "Direct C2 route active. View C2 path information" : "Direct C2 route inactive. View C2 path information") :
|
||||
(p2pRouteActive ? "Active P2P route to Mythic. View C2 path information" : "No active route to Mythic. View C2 path information");
|
||||
return (
|
||||
<div className="mythic-callback-cellInline mythic-callback-cellInlineCenter">
|
||||
<MythicStyledTooltip title={c2Tooltip}>
|
||||
<IconButton className={c2ButtonClass} onClick={onOpenC2Dialog}>
|
||||
{hasEgressRoute ?
|
||||
{hasDirectEgressRoute ?
|
||||
<WifiIcon fontSize="small" /> :
|
||||
<InsertLinkTwoToneIcon fontSize="small" />
|
||||
}
|
||||
|
||||
+12
-5
@@ -24,7 +24,7 @@ import ArrowForwardIcon from '@mui/icons-material/ArrowForward';
|
||||
import ArrowBackIcon from '@mui/icons-material/ArrowBack';
|
||||
import PlaylistAddIcon from '@mui/icons-material/PlaylistAdd';
|
||||
import PlaylistRemoveIcon from '@mui/icons-material/PlaylistRemove';
|
||||
import {useGetMythicSetting, useSetMythicSetting} from "../../MythicComponents/MythicSavedUserSetting";
|
||||
import {GetMythicSetting, useSetMythicSetting} from "../../MythicComponents/MythicSavedUserSetting";
|
||||
import {RenderSingleTask} from "../SingleTaskView/SingleTaskView";
|
||||
import {loadedCommandsQuery} from "./CallbacksTabsProcessBrowser";
|
||||
import {getSkewedNow} from "../../utilities/Time";
|
||||
@@ -772,9 +772,9 @@ const FileBrowserTableTop = ({
|
||||
tabInfo,
|
||||
treeConfig
|
||||
}) => {
|
||||
const autoTaskLsOnEmptyDirectories = useGetMythicSetting({
|
||||
const [autoTaskLsOnEmptyDirectories, setAutoTaskLsOnEmptyDirectories] = React.useState(() => GetMythicSetting({
|
||||
setting_name: "autoTaskLsOnEmptyDirectories", default_value: false
|
||||
});
|
||||
}));
|
||||
const [updateMythicSetting] = useSetMythicSetting();
|
||||
const [openEditHostDialog, setOpenEditHostDialog] = React.useState(false);
|
||||
const [fullPath, setFullPath] = React.useState('');
|
||||
@@ -948,8 +948,15 @@ const FileBrowserTableTop = ({
|
||||
}});
|
||||
};
|
||||
const onToggleAutoTaskLsOnEmptyDirectories = () => {
|
||||
updateMythicSetting({setting_name: "autoTaskLsOnEmptyDirectories", value: !autoTaskLsOnEmptyDirectories});
|
||||
if(autoTaskLsOnEmptyDirectories){
|
||||
const nextAutoTaskLsOnEmptyDirectories = !autoTaskLsOnEmptyDirectories;
|
||||
setAutoTaskLsOnEmptyDirectories(nextAutoTaskLsOnEmptyDirectories);
|
||||
autoTaskLsOnEmptyDirectoriesRef.current = nextAutoTaskLsOnEmptyDirectories;
|
||||
updateMythicSetting({
|
||||
setting_name: "autoTaskLsOnEmptyDirectories",
|
||||
value: nextAutoTaskLsOnEmptyDirectories,
|
||||
broadcast: false,
|
||||
});
|
||||
if(!nextAutoTaskLsOnEmptyDirectories){
|
||||
snackActions.info("No longer auto issuing listings for empty paths");
|
||||
} else {
|
||||
snackActions.success("Now starting to auto issue listings for empty paths");
|
||||
|
||||
+9
-4
@@ -54,7 +54,7 @@ const updateFileComment = gql`
|
||||
|
||||
export const CallbacksTabsCustomFileBasedBrowserTable = (props) => {
|
||||
const theme = useTheme();
|
||||
const [updateSetting] = useSetMythicSetting();
|
||||
const [updateSetting, updateSettings] = useSetMythicSetting();
|
||||
const [allData, setAllData] = React.useState([]);
|
||||
const [openReorderDialog, setOpenReorderDialog] = React.useState(false);
|
||||
const [openContextMenu, setOpenContextMenu] = React.useState(false);
|
||||
@@ -185,7 +185,7 @@ export const CallbacksTabsCustomFileBasedBrowserTable = (props) => {
|
||||
const nextFilterOptions = getUpdatedGridFilterOptions(filterOptions, selectedColumn.key, value);
|
||||
setFilterOptions(nextFilterOptions);
|
||||
try{
|
||||
updateSetting({setting_name: `${props.treeConfig.name}_browser_filter_options`, value: nextFilterOptions});
|
||||
updateSetting({setting_name: `${props.treeConfig.name}_browser_filter_options`, value: nextFilterOptions, broadcast: false});
|
||||
}catch(error){
|
||||
console.log("failed to save filter options");
|
||||
}
|
||||
@@ -687,10 +687,15 @@ export const CallbacksTabsCustomFileBasedBrowserTable = (props) => {
|
||||
snackActions.error("Can't update to show no fields");
|
||||
return;
|
||||
}
|
||||
updateSetting({setting_name: `${props.treeConfig.name}_browser_column_order`, value: newOrder.map(c => c.name)});
|
||||
setColumnOrder(newOrder);
|
||||
setColumnVisibility({visible: newVisible, hidden: newHidden});
|
||||
updateSetting({setting_name: `${props.treeConfig.name}_browser_table_columns`, value: newVisible});
|
||||
updateSettings({
|
||||
settings: {
|
||||
[`${props.treeConfig.name}_browser_column_order`]: newOrder.map(c => c.name),
|
||||
[`${props.treeConfig.name}_browser_table_columns`]: newVisible,
|
||||
},
|
||||
broadcast: false,
|
||||
});
|
||||
setOpenReorderDialog(false);
|
||||
}
|
||||
const onResetColumnReorder = () => {
|
||||
|
||||
@@ -26,7 +26,7 @@ import ArrowBackIcon from '@mui/icons-material/ArrowBack';
|
||||
import {b64DecodeUnicode} from "./ResponseDisplay";
|
||||
import PlaylistAddIcon from '@mui/icons-material/PlaylistAdd';
|
||||
import PlaylistRemoveIcon from '@mui/icons-material/PlaylistRemove';
|
||||
import {useGetMythicSetting, useSetMythicSetting} from "../../MythicComponents/MythicSavedUserSetting";
|
||||
import {GetMythicSetting, useSetMythicSetting} from "../../MythicComponents/MythicSavedUserSetting";
|
||||
import {RenderSingleTask} from "../SingleTaskView/SingleTaskView";
|
||||
import {loadedCommandsQuery} from "./CallbacksTabsProcessBrowser";
|
||||
import {getSkewedNow} from "../../utilities/Time";
|
||||
@@ -772,9 +772,9 @@ const FileBrowserTableTop = ({
|
||||
tabInfo,
|
||||
baseUIFeature
|
||||
}) => {
|
||||
const autoTaskLsOnEmptyDirectories = useGetMythicSetting({
|
||||
const [autoTaskLsOnEmptyDirectories, setAutoTaskLsOnEmptyDirectories] = React.useState(() => GetMythicSetting({
|
||||
setting_name: "autoTaskLsOnEmptyDirectories", default_value: false
|
||||
});
|
||||
}));
|
||||
const [updateMythicSetting] = useSetMythicSetting();
|
||||
const [fullPath, setFullPath] = React.useState('');
|
||||
const selectedToken = React.useRef("Default Token");
|
||||
@@ -831,16 +831,15 @@ const FileBrowserTableTop = ({
|
||||
return;
|
||||
}
|
||||
if(selectedFolderData.id !== ""){
|
||||
if(history[0]?.full_path_text !== selectedFolderData.full_path_text){
|
||||
// always add newest things to the bottom of the stack
|
||||
setHistory([selectedFolderData, ...history]);
|
||||
if(history.length > 20){
|
||||
// pop off from the top (the oldest) if we get more than 50
|
||||
setHistory([selectedFolderData, ...history.splice(history.length-1, 1)])
|
||||
setHistory((currentHistory) => {
|
||||
if(currentHistory[0]?.full_path_text === selectedFolderData.full_path_text){
|
||||
return currentHistory;
|
||||
}
|
||||
}
|
||||
return [selectedFolderData, ...currentHistory].slice(0, 20);
|
||||
});
|
||||
setHistoryIndex(0);
|
||||
}
|
||||
}, [selectedFolderData, history])
|
||||
}, [selectedFolderData])
|
||||
const moveIndexToPreviousListing = () => {
|
||||
// we're getting closer to the end of the historyRef.current, the oldest listing
|
||||
if(historyIndex >= history.length -1){
|
||||
@@ -886,7 +885,14 @@ const FileBrowserTableTop = ({
|
||||
}
|
||||
};
|
||||
const onToggleAutoTaskLsOnEmptyDirectories = () => {
|
||||
updateMythicSetting({setting_name: "autoTaskLsOnEmptyDirectories", value: !autoTaskLsOnEmptyDirectories});
|
||||
const nextAutoTaskSetting = !autoTaskLsOnEmptyDirectories;
|
||||
setAutoTaskLsOnEmptyDirectories(nextAutoTaskSetting);
|
||||
autoTaskLsOnEmptyDirectoriesRef.current = nextAutoTaskSetting;
|
||||
updateMythicSetting({
|
||||
setting_name: "autoTaskLsOnEmptyDirectories",
|
||||
value: nextAutoTaskSetting,
|
||||
broadcast: false,
|
||||
});
|
||||
if(autoTaskLsOnEmptyDirectories){
|
||||
snackActions.info("No longer auto issuing listings for empty paths");
|
||||
} else {
|
||||
|
||||
@@ -96,7 +96,7 @@ const columnDefaults = [
|
||||
];
|
||||
export const CallbacksTabsFileBrowserTable = (props) => {
|
||||
const theme = useTheme();
|
||||
const [updateSetting] = useSetMythicSetting();
|
||||
const [updateSetting, updateSettings] = useSetMythicSetting();
|
||||
const [loading, setLoading] = React.useState(true);
|
||||
const [allData, setAllData] = React.useState([]);
|
||||
const [openContextMenu, setOpenContextMenu] = React.useState(false);
|
||||
@@ -207,7 +207,7 @@ export const CallbacksTabsFileBrowserTable = (props) => {
|
||||
const nextFilterOptions = getUpdatedGridFilterOptions(filterOptions, selectedColumn.key, value);
|
||||
setFilterOptions(nextFilterOptions);
|
||||
try{
|
||||
updateSetting({setting_name: `file_browser_filter_options`, value: nextFilterOptions});
|
||||
updateSetting({setting_name: `file_browser_filter_options`, value: nextFilterOptions, broadcast: false});
|
||||
}catch(error){
|
||||
console.log("failed to save filter options");
|
||||
}
|
||||
@@ -414,11 +414,15 @@ export const CallbacksTabsFileBrowserTable = (props) => {
|
||||
snackActions.error("Can't update to show no fields");
|
||||
return;
|
||||
}
|
||||
console.log("newOrder", newOrder)
|
||||
updateSetting({setting_name: `file_browser_column_order`, value: newOrder.map(c => c.name)});
|
||||
setColumnOrder(newOrder);
|
||||
setColumnVisibility({visible: newVisible, hidden: newHidden});
|
||||
updateSetting({setting_name: `file_browser_table_columns`, value: newVisible});
|
||||
updateSettings({
|
||||
settings: {
|
||||
file_browser_column_order: newOrder.map(c => c.name),
|
||||
file_browser_table_columns: newVisible,
|
||||
},
|
||||
broadcast: false,
|
||||
});
|
||||
setOpenReorderDialog(false);
|
||||
}
|
||||
const onResetColumnReorder = () => {
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
import {MythicTabPanel, MythicTabLabel} from '../../MythicComponents/MythicTabPanel';
|
||||
import React, {useEffect, useRef} from 'react';
|
||||
import {gql, useQuery, useSubscription, useLazyQuery } from '@apollo/client';
|
||||
import React, {useEffect} from 'react';
|
||||
import {gql, useQuery, useSubscription } from '@apollo/client';
|
||||
import { MythicDialog } from '../../MythicComponents/MythicDialog';
|
||||
import {useTheme} from '@mui/material/styles';
|
||||
import Grid from '@mui/material/Grid';
|
||||
import RefreshIcon from '@mui/icons-material/Refresh';
|
||||
import IconButton from '@mui/material/IconButton';
|
||||
import {CallbacksTabsProcessBrowserTable} from './CallbacksTabsProcessBrowserTable';
|
||||
@@ -13,14 +12,18 @@ import { MythicStyledTooltip } from '../../MythicComponents/MythicStyledTooltip'
|
||||
import MenuItem from '@mui/material/MenuItem';
|
||||
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 TextField from '@mui/material/TextField';
|
||||
import InputAdornment from '@mui/material/InputAdornment';
|
||||
import {ViewCallbackMythicTreeGroupsDialog} from "./ViewCallbackMythicTreeGroupsDialog";
|
||||
import WidgetsIcon from '@mui/icons-material/Widgets';
|
||||
import {snackActions} from "../../utilities/Snackbar";
|
||||
import { Backdrop, Typography } from '@mui/material';
|
||||
import {CircularProgress} from '@mui/material';
|
||||
import ExpandIcon from '@mui/icons-material/Expand';
|
||||
import VisibilityIcon from '@mui/icons-material/Visibility';
|
||||
import VisibilityOffIcon from '@mui/icons-material/VisibilityOff';
|
||||
import SearchIcon from '@mui/icons-material/Search';
|
||||
import ClearIcon from '@mui/icons-material/Clear';
|
||||
import {useMythicLazyQuery} from "../../utilities/useMythicLazyQuery";
|
||||
|
||||
const treeFragment = gql`
|
||||
@@ -145,7 +148,7 @@ export const CallbacksTabsProcessBrowserPanel = ({index, value, tabInfo, me, set
|
||||
const theme = useTheme();
|
||||
const fromNow = React.useRef((new Date()));
|
||||
const [backdropOpen, setBackdropOpen] = React.useState(false);
|
||||
const [expandOrCollapseAll, setExpandOrCollapseAll] = React.useState(false);
|
||||
const [expandOrCollapseAll, setExpandOrCollapseAll] = React.useState(true);
|
||||
const treeRootDataRef = React.useRef({}); // hold all the actual data
|
||||
const [treeAdjMtx, setTreeAdjMtx] = React.useState({}); // hold the simple adjacency matrix for parent/child relationships
|
||||
const [openTaskingButton, setOpenTaskingButton] = React.useState(false);
|
||||
@@ -154,6 +157,7 @@ export const CallbacksTabsProcessBrowserPanel = ({index, value, tabInfo, me, set
|
||||
const [showDeletedFiles, setShowDeletedFiles] = React.useState(false);
|
||||
const [selectedHost, setSelectedHost] = React.useState("");
|
||||
const [selectedGroup, setSelectedGroup] = React.useState("");
|
||||
const [quickFilter, setQuickFilter] = React.useState("");
|
||||
const loadedCommandsRef = React.useRef({});
|
||||
const loadingCommandsRef = React.useRef(false);
|
||||
const getNewMatrix = () => {
|
||||
@@ -350,9 +354,9 @@ export const CallbacksTabsProcessBrowserPanel = ({index, value, tabInfo, me, set
|
||||
callback_display_id: tabInfo["displayID"]});
|
||||
setOpenTaskingButton(true);
|
||||
}
|
||||
const onTaskRowAction = ({process_id, architecture, uifeature, openDialog, getConfirmation, callback_id, callback_display_id}) => {
|
||||
const onTaskRowAction = ({process_id, architecture, uifeature, openDialog, getConfirmation, callback_id, callback_display_id, display_id}) => {
|
||||
taskingData.current = {"parameters": {host: selectedHost, process_id, architecture},
|
||||
"ui_feature": uifeature, openDialog, getConfirmation, callback_id, callback_display_id};
|
||||
"ui_feature": uifeature, openDialog, getConfirmation, callback_id, callback_display_id: callback_display_id || display_id};
|
||||
setOpenTaskingButton(true);
|
||||
}
|
||||
const toggleShowDeletedFiles = (showStatus) => {
|
||||
@@ -434,6 +438,8 @@ export const CallbacksTabsProcessBrowserPanel = ({index, value, tabInfo, me, set
|
||||
hostOptions={treeRootDataRef.current[selectedGroup] || {}}
|
||||
expandOrCollapseAll={expandOrCollapseAll}
|
||||
setExpandOrCollapseAll={setExpandOrCollapseAll}
|
||||
quickFilter={quickFilter}
|
||||
setQuickFilter={setQuickFilter}
|
||||
/>
|
||||
<CallbacksTabsProcessBrowserTable
|
||||
showDeletedFiles={showDeletedFiles}
|
||||
@@ -443,6 +449,7 @@ export const CallbacksTabsProcessBrowserPanel = ({index, value, tabInfo, me, set
|
||||
treeAdjMatrix={treeAdjMtx}
|
||||
host={selectedHost}
|
||||
group={selectedGroup}
|
||||
quickFilter={quickFilter}
|
||||
expandOrCollapseAll={expandOrCollapseAll}
|
||||
onTaskRowAction={onTaskRowAction}
|
||||
getLoadedCommandForUIFeature={getLoadedCommandForUIFeature}
|
||||
@@ -473,13 +480,12 @@ const ProcessBrowserTableTop = ({
|
||||
hostOptions,
|
||||
groupOptions,
|
||||
expandOrCollapseAll,
|
||||
setExpandOrCollapseAll
|
||||
setExpandOrCollapseAll,
|
||||
quickFilter,
|
||||
setQuickFilter
|
||||
}) => {
|
||||
const theme = useTheme();
|
||||
const [showDeletedFiles, setLocalShowDeletedFiles] = React.useState(false);
|
||||
const [openViewGroupsDialog, setOpenViewGroupDialog] = React.useState(false);
|
||||
const inputRef = useRef(null);
|
||||
const inputGroupRef = useRef(null);
|
||||
const onLocalListFilesButton = () => {
|
||||
onListFilesButton()
|
||||
}
|
||||
@@ -496,58 +502,104 @@ const ProcessBrowserTableTop = ({
|
||||
const onLocalExpandOrCollapseAllButton = () => {
|
||||
setExpandOrCollapseAll(!expandOrCollapseAll);
|
||||
}
|
||||
const onClearQuickFilter = () => {
|
||||
setQuickFilter("");
|
||||
}
|
||||
return (
|
||||
<Grid container spacing={0} style={{paddingTop: "10px"}}>
|
||||
<Grid size={12}>
|
||||
<FormControl style={{width: "30%"}}>
|
||||
<InputLabel ref={inputGroupRef}>Available Groups</InputLabel>
|
||||
<Select
|
||||
labelId="demo-dialog-select-label"
|
||||
id="demo-dialog-select"
|
||||
value={group}
|
||||
onChange={handleGroupChange}
|
||||
input={<Input style={{width: "100%"}}/>}
|
||||
endAdornment={
|
||||
<React.Fragment>
|
||||
<MythicStyledTooltip title="View Callbacks associated with this group">
|
||||
<IconButton style={{padding: "3px", paddingRight: "25px"}} onClick={() => {setOpenViewGroupDialog(true);}} size="large"><WidgetsIcon style={{color: theme.palette.info.main}}/></IconButton>
|
||||
</MythicStyledTooltip>
|
||||
</React.Fragment>
|
||||
}
|
||||
>
|
||||
{Object.keys(groupOptions).sort().map( (opt) => (
|
||||
<MenuItem value={opt} key={opt}>{opt}</MenuItem>
|
||||
) )}
|
||||
</Select>
|
||||
</FormControl>
|
||||
<FormControl style={{width: "70%"}}>
|
||||
<InputLabel ref={inputRef}>Available Hosts</InputLabel>
|
||||
<Select
|
||||
labelId="demo-dialog-select-label"
|
||||
id="demo-dialog-select"
|
||||
value={host}
|
||||
onChange={handleChange}
|
||||
input={<Input style={{width: "100%"}}/>}
|
||||
endAdornment={
|
||||
<React.Fragment>
|
||||
<MythicStyledTooltip title="Task Callback to List Processes">
|
||||
<IconButton style={{padding: "3px"}} onClick={onLocalListFilesButton} size="large"><RefreshIcon style={{color: theme.palette.info.main}}/></IconButton>
|
||||
</MythicStyledTooltip>
|
||||
<MythicStyledTooltip title={expandOrCollapseAll ? "Collapse all processes" : "Expand all processes"} >
|
||||
<IconButton style={{padding: "3px"}}
|
||||
onClick={onLocalExpandOrCollapseAllButton} size={"large"}>
|
||||
<ExpandIcon color={expandOrCollapseAll ? "info" : "success"} />
|
||||
<div className="mythic-file-browser-tableTop mythic-process-browser-tableTop">
|
||||
<div className="mythic-process-browser-toolbar">
|
||||
<div className="mythic-process-browser-control mythic-process-browser-controlGroup">
|
||||
<span className="mythic-process-browser-controlLabel">Group</span>
|
||||
<FormControl size="small" className="mythic-process-browser-selectControl">
|
||||
<Select
|
||||
value={group}
|
||||
onChange={handleGroupChange}
|
||||
displayEmpty
|
||||
className="mythic-process-browser-select"
|
||||
>
|
||||
{Object.keys(groupOptions).sort().map( (opt) => (
|
||||
<MenuItem value={opt} key={opt}>{opt}</MenuItem>
|
||||
) )}
|
||||
</Select>
|
||||
</FormControl>
|
||||
<MythicStyledTooltip title="View callbacks associated with this group">
|
||||
<IconButton
|
||||
className="mythic-file-browser-iconButton mythic-file-browser-hoverInfo"
|
||||
onClick={() => {setOpenViewGroupDialog(true);}}
|
||||
size="small">
|
||||
<WidgetsIcon fontSize="small" />
|
||||
</IconButton>
|
||||
</MythicStyledTooltip>
|
||||
<div style={{paddingRight: "20px"}} />
|
||||
</React.Fragment>
|
||||
}
|
||||
>
|
||||
{Object.keys(hostOptions).sort().map( (opt) => (
|
||||
<MenuItem value={opt} key={opt}>{opt}</MenuItem>
|
||||
) )}
|
||||
</Select>
|
||||
</FormControl>
|
||||
</div>
|
||||
<div className="mythic-process-browser-control mythic-process-browser-controlHost">
|
||||
<span className="mythic-process-browser-controlLabel">Host</span>
|
||||
<FormControl size="small" className="mythic-process-browser-selectControl">
|
||||
<Select
|
||||
value={host}
|
||||
onChange={handleChange}
|
||||
displayEmpty
|
||||
className="mythic-process-browser-select"
|
||||
>
|
||||
{Object.keys(hostOptions).sort().map( (opt) => (
|
||||
<MenuItem value={opt} key={opt}>{opt}</MenuItem>
|
||||
) )}
|
||||
</Select>
|
||||
</FormControl>
|
||||
</div>
|
||||
<TextField
|
||||
className="mythic-process-browser-searchInput"
|
||||
size="small"
|
||||
value={quickFilter}
|
||||
onChange={(event) => setQuickFilter(event.target.value)}
|
||||
placeholder="Filter name, PID, user, arch, session, command..."
|
||||
InputProps={{
|
||||
startAdornment: (
|
||||
<InputAdornment position="start">
|
||||
<SearchIcon fontSize="small" />
|
||||
</InputAdornment>
|
||||
),
|
||||
endAdornment: quickFilter ? (
|
||||
<InputAdornment position="end">
|
||||
<IconButton
|
||||
className="mythic-file-browser-iconButton mythic-file-browser-hoverError"
|
||||
onClick={onClearQuickFilter}
|
||||
size="small">
|
||||
<ClearIcon fontSize="small" />
|
||||
</IconButton>
|
||||
</InputAdornment>
|
||||
) : null
|
||||
}}
|
||||
/>
|
||||
<div className="mythic-file-browser-toolbarGroup mythic-process-browser-actions">
|
||||
<MythicStyledTooltip title="Task current callback to list processes">
|
||||
<IconButton
|
||||
className="mythic-file-browser-iconButton mythic-file-browser-hoverInfo"
|
||||
onClick={onLocalListFilesButton}
|
||||
size="small">
|
||||
<RefreshIcon fontSize="small" />
|
||||
</IconButton>
|
||||
</MythicStyledTooltip>
|
||||
<MythicStyledTooltip title={expandOrCollapseAll ? "Collapse all processes" : "Expand all processes"} >
|
||||
<IconButton
|
||||
className={`mythic-file-browser-iconButton mythic-file-browser-hoverInfo ${expandOrCollapseAll ? "mythic-file-browser-activeSuccess" : ""}`}
|
||||
onClick={onLocalExpandOrCollapseAllButton}
|
||||
size="small">
|
||||
<ExpandIcon fontSize="small" />
|
||||
</IconButton>
|
||||
</MythicStyledTooltip>
|
||||
<MythicStyledTooltip title={showDeletedFiles ? 'Hide deleted processes' : 'Show deleted processes'}>
|
||||
<IconButton
|
||||
className={`mythic-file-browser-iconButton mythic-file-browser-hoverWarning ${showDeletedFiles ? "mythic-file-browser-activeWarning" : ""}`}
|
||||
onClick={onLocalToggleShowDeletedFiles}
|
||||
size="small">
|
||||
{showDeletedFiles ? (
|
||||
<VisibilityIcon fontSize="small" />
|
||||
) : (
|
||||
<VisibilityOffIcon fontSize="small" />
|
||||
)}
|
||||
</IconButton>
|
||||
</MythicStyledTooltip>
|
||||
</div>
|
||||
{openViewGroupsDialog &&
|
||||
<MythicDialog
|
||||
fullWidth={true}
|
||||
@@ -560,7 +612,7 @@ const ProcessBrowserTableTop = ({
|
||||
}
|
||||
/>
|
||||
}
|
||||
</Grid>
|
||||
</Grid>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
+738
-372
File diff suppressed because it is too large
Load Diff
+235
-164
@@ -1,34 +1,32 @@
|
||||
import React from 'react';
|
||||
import TableRow from '@mui/material/TableRow';
|
||||
import Table from '@mui/material/Table';
|
||||
import TableCell from '@mui/material/TableCell';
|
||||
import TableBody from '@mui/material/TableBody';
|
||||
import TableContainer from '@mui/material/TableContainer';
|
||||
import TableHead from '@mui/material/TableHead';
|
||||
import {useQuery, gql} from '@apollo/client';
|
||||
import DialogActions from '@mui/material/DialogActions';
|
||||
import DialogContent from '@mui/material/DialogContent';
|
||||
import DialogTitle from '@mui/material/DialogTitle';
|
||||
import {Button} from '@mui/material';
|
||||
import {Backdrop, Button, CircularProgress, IconButton} from '@mui/material';
|
||||
import Table from '@mui/material/Table';
|
||||
import TableBody from '@mui/material/TableBody';
|
||||
import TableContainer from '@mui/material/TableContainer';
|
||||
import TableHead from '@mui/material/TableHead';
|
||||
import TableRow from '@mui/material/TableRow';
|
||||
import TableSortLabel from '@mui/material/TableSortLabel';
|
||||
import VisibilityOffIcon from '@mui/icons-material/VisibilityOff';
|
||||
import VisibilityIcon from '@mui/icons-material/Visibility';
|
||||
import MythicStyledTableCell from "../../MythicComponents/MythicTableCell";
|
||||
import {MythicStyledTooltip} from "../../MythicComponents/MythicStyledTooltip";
|
||||
import { useTheme } from '@mui/material/styles';
|
||||
import LayersIcon from '@mui/icons-material/Layers';
|
||||
import Paper from '@mui/material/Paper';
|
||||
import {IconButton, Typography} from '@mui/material';
|
||||
import {MythicDialog} from "../../MythicComponents/MythicDialog";
|
||||
import LinearProgress from '@mui/material/LinearProgress';
|
||||
import { Backdrop } from '@mui/material';
|
||||
import {CircularProgress} from '@mui/material';
|
||||
import {snackActions} from "../../utilities/Snackbar";
|
||||
import {MythicAgentSVGIcon} from "../../MythicComponents/MythicAgentSVGIcon";
|
||||
import MythicStyledTableCell from "../../MythicComponents/MythicTableCell";
|
||||
import {MythicTableEmptyState} from "../../MythicComponents/MythicStateDisplay";
|
||||
import {MythicClientSideTablePagination, useMythicClientPagination} from "../../MythicComponents/MythicTablePagination";
|
||||
|
||||
|
||||
const getCallbackMythicTreeGroups = gql`
|
||||
query getCallbackMythicTreeGroups($group_name: [String!]!) {
|
||||
callback(where: {mythictree_groups: {_contains: $group_name}}, order_by: {id: asc}) {
|
||||
id
|
||||
display_id
|
||||
user
|
||||
host
|
||||
@@ -48,6 +46,7 @@ query getCallbackMythicTreeGroups($group_name: [String!]!) {
|
||||
const getAllCallbackMythicTreeGroups = gql`
|
||||
query getCallbackMythicTreeGroups {
|
||||
callback(order_by: {id: asc}) {
|
||||
id
|
||||
display_id
|
||||
user
|
||||
host
|
||||
@@ -65,8 +64,169 @@ query getCallbackMythicTreeGroups {
|
||||
}
|
||||
}
|
||||
`;
|
||||
const normalizeCallbackIP = (callback) => {
|
||||
try{
|
||||
const parsed = JSON.parse(callback.ip);
|
||||
if(Array.isArray(parsed) && parsed.length > 0){
|
||||
return {...callback, ip: parsed[0]};
|
||||
}
|
||||
return {...callback, ip: ""};
|
||||
}catch(error){
|
||||
return {...callback};
|
||||
}
|
||||
};
|
||||
const compareValues = (left, right) => {
|
||||
if(left === right){
|
||||
return 0;
|
||||
}
|
||||
if(left === undefined || left === null){
|
||||
return -1;
|
||||
}
|
||||
if(right === undefined || right === null){
|
||||
return 1;
|
||||
}
|
||||
if(typeof left === "number" && typeof right === "number"){
|
||||
return left - right;
|
||||
}
|
||||
return String(left).localeCompare(String(right), undefined, {numeric: true, sensitivity: "base"});
|
||||
};
|
||||
const callbackGroupColumns = [
|
||||
{field: "status", headerName: "", width: "5rem", disableSort: true},
|
||||
{field: "display_id", headerName: "Callback", width: "6rem", sortValue: (row) => row.display_id},
|
||||
{field: "user", headerName: "User", sortValue: (row) => row.user},
|
||||
{field: "host", headerName: "Host", sortValue: (row) => row.host},
|
||||
{field: "domain", headerName: "Domain", sortValue: (row) => row.domain},
|
||||
{field: "ip", headerName: "IP", sortValue: (row) => row.ip},
|
||||
{field: "pid", headerName: "PID", width: "5rem", sortValue: (row) => row.pid},
|
||||
{field: "description", headerName: "Description", sortValue: (row) => row.description},
|
||||
];
|
||||
const CallbackGroupStatusCell = ({callback}) => {
|
||||
const payloadType = callback.payload?.payloadtype?.name || "unknown";
|
||||
return (
|
||||
<div className="mythic-tree-groups-callback-icons">
|
||||
<MythicStyledTooltip title={callback.active ? "Callback is active" : "Callback is not active"}>
|
||||
<span className={`mythic-tree-groups-status ${callback.active ? "mythic-tree-groups-statusActive" : "mythic-tree-groups-statusInactive"}`}>
|
||||
{callback.active ? <VisibilityIcon fontSize="small" /> : <VisibilityOffIcon fontSize="small" />}
|
||||
</span>
|
||||
</MythicStyledTooltip>
|
||||
<MythicStyledTooltip title={payloadType}>
|
||||
<span className="mythic-tree-groups-agent-icon">
|
||||
<MythicAgentSVGIcon payload_type={payloadType} style={{width: "28px", height: "28px"}} />
|
||||
</span>
|
||||
</MythicStyledTooltip>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
const CallbackGroupCellValue = ({value, className = ""}) => (
|
||||
<span className={`mythic-tree-groups-table-value ${className}`.trim()} title={value || ""}>
|
||||
{value || "-"}
|
||||
</span>
|
||||
);
|
||||
const getSafeTableId = (value) => String(value || "tree-group-callbacks").replace(/[^a-zA-Z0-9_-]/g, "-");
|
||||
const CallbackGroupTable = ({callbacks, emptyMessage, tableId}) => {
|
||||
const [orderBy, setOrderBy] = React.useState("display_id");
|
||||
const [order, setOrder] = React.useState("asc");
|
||||
const safeTableId = getSafeTableId(tableId);
|
||||
const sortedCallbacks = React.useMemo(() => {
|
||||
const sortColumn = callbackGroupColumns.find((column) => column.field === orderBy);
|
||||
if(!sortColumn || sortColumn.disableSort){
|
||||
return callbacks;
|
||||
}
|
||||
const direction = order === "desc" ? -1 : 1;
|
||||
return [...callbacks].sort((left, right) => direction * compareValues(sortColumn.sortValue(left), sortColumn.sortValue(right)));
|
||||
}, [callbacks, order, orderBy]);
|
||||
const pagination = useMythicClientPagination({
|
||||
items: sortedCallbacks,
|
||||
resetKey: `${safeTableId}:${orderBy}:${order}`,
|
||||
rowsPerPage: 25,
|
||||
});
|
||||
const handleSort = (column) => {
|
||||
if(column.disableSort){
|
||||
return;
|
||||
}
|
||||
if(orderBy === column.field){
|
||||
setOrder((current) => current === "asc" ? "desc" : "asc");
|
||||
}else{
|
||||
setOrderBy(column.field);
|
||||
setOrder("asc");
|
||||
}
|
||||
};
|
||||
return (
|
||||
<>
|
||||
<TableContainer
|
||||
className="mythicElement mythic-dialog-table-wrap mythic-fixed-row-table-wrap mythic-tree-groups-table-wrap"
|
||||
data-testid={`${safeTableId}-scroll-region`}
|
||||
id={`${safeTableId}-scroll-region`}
|
||||
role="region"
|
||||
>
|
||||
<Table aria-label="Callback tree group table" data-testid={safeTableId} id={safeTableId} stickyHeader size="small" style={{height: "auto"}}>
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
{callbackGroupColumns.map((column) => (
|
||||
<MythicStyledTableCell key={column.field} sortDirection={orderBy === column.field ? order : false} style={{width: column.width}}>
|
||||
{column.disableSort ? column.headerName : (
|
||||
<TableSortLabel
|
||||
active={orderBy === column.field}
|
||||
direction={orderBy === column.field ? order : "asc"}
|
||||
onClick={() => handleSort(column)}
|
||||
>
|
||||
{column.headerName}
|
||||
</TableSortLabel>
|
||||
)}
|
||||
</MythicStyledTableCell>
|
||||
))}
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
{sortedCallbacks.length === 0 ? (
|
||||
<MythicTableEmptyState
|
||||
colSpan={callbackGroupColumns.length}
|
||||
compact
|
||||
description={emptyMessage}
|
||||
minHeight={150}
|
||||
title="No callbacks"
|
||||
/>
|
||||
) : (
|
||||
pagination.pageData.map((callback) => (
|
||||
<TableRow hover key={callback.id || `callback-group-${callback.display_id}-${callback.host}`}>
|
||||
<MythicStyledTableCell>
|
||||
<CallbackGroupStatusCell callback={callback} />
|
||||
</MythicStyledTableCell>
|
||||
<MythicStyledTableCell>
|
||||
<CallbackGroupCellValue className="mythic-tree-groups-callback-id" value={`#${callback.display_id}`} />
|
||||
</MythicStyledTableCell>
|
||||
<MythicStyledTableCell>
|
||||
<CallbackGroupCellValue value={callback.user} />
|
||||
</MythicStyledTableCell>
|
||||
<MythicStyledTableCell>
|
||||
<CallbackGroupCellValue value={callback.host} />
|
||||
</MythicStyledTableCell>
|
||||
<MythicStyledTableCell>
|
||||
<CallbackGroupCellValue value={callback.domain} />
|
||||
</MythicStyledTableCell>
|
||||
<MythicStyledTableCell>
|
||||
<CallbackGroupCellValue value={callback.ip} />
|
||||
</MythicStyledTableCell>
|
||||
<MythicStyledTableCell>
|
||||
<CallbackGroupCellValue value={callback.pid === undefined || callback.pid === null ? "" : `${callback.pid}`} />
|
||||
</MythicStyledTableCell>
|
||||
<MythicStyledTableCell>
|
||||
<CallbackGroupCellValue className="mythic-tree-groups-description-cell" value={callback.description} />
|
||||
</MythicStyledTableCell>
|
||||
</TableRow>
|
||||
))
|
||||
)}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
<MythicClientSideTablePagination
|
||||
id={`${safeTableId}-pagination`}
|
||||
pagination={pagination}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
export function ViewCallbackMythicTreeGroupsDialog(props){
|
||||
const theme = useTheme();
|
||||
const [backdropOpen, setBackdropOpen] = React.useState(true);
|
||||
const [groups, setGroups] = React.useState([]);
|
||||
const [openViewAllCallbacksDialog, setOpenViewAllCallbacksDialog] = React.useState(false);
|
||||
@@ -77,19 +237,7 @@ export function ViewCallbackMythicTreeGroupsDialog(props){
|
||||
fetchPolicy: "no-cache",
|
||||
variables: {group_name: [props.group_name]},
|
||||
onCompleted: data => {
|
||||
|
||||
const groupData = data.callback.map( c => {
|
||||
try{
|
||||
let cIP = JSON.parse(c.ip);
|
||||
if(cIP.length > 0){
|
||||
return {...c, ip:cIP[0] };
|
||||
}
|
||||
return {...c, ip: ""};
|
||||
|
||||
}catch(error){
|
||||
return {...c};
|
||||
}
|
||||
})
|
||||
const groupData = data.callback.map(normalizeCallbackIP);
|
||||
setGroups(groupData);
|
||||
setBackdropOpen(false);
|
||||
}
|
||||
@@ -99,67 +247,34 @@ export function ViewCallbackMythicTreeGroupsDialog(props){
|
||||
}, [groups]);
|
||||
return (
|
||||
<React.Fragment>
|
||||
<DialogTitle id="form-dialog-title" style={{display: "flex", justifyContent: "space-between"}}>
|
||||
Viewing Callbacks for group: {props.group_name}
|
||||
<MythicStyledTooltip title="View all groups" >
|
||||
<IconButton size="small" onClick={()=>{setOpenViewAllCallbacksDialog(true);}} style={{color: theme.palette.info.main}} variant="contained"><LayersIcon/></IconButton>
|
||||
</MythicStyledTooltip>
|
||||
<DialogTitle id="form-dialog-title" className="mythic-tree-groups-title">
|
||||
<div className="mythic-dialog-title-row">
|
||||
<div className="mythic-tree-groups-title-copy">
|
||||
<span>Callbacks for {props.group_name}</span>
|
||||
<span>These callbacks contribute aggregated process data for this group.</span>
|
||||
</div>
|
||||
<MythicStyledTooltip title="View all groups" >
|
||||
<IconButton
|
||||
className="mythic-file-browser-iconButton mythic-file-browser-hoverInfo"
|
||||
size="small"
|
||||
onClick={()=>{setOpenViewAllCallbacksDialog(true);}}>
|
||||
<LayersIcon fontSize="small" />
|
||||
</IconButton>
|
||||
</MythicStyledTooltip>
|
||||
</div>
|
||||
</DialogTitle>
|
||||
<div style={{paddingLeft: "20px"}}>
|
||||
All of these callbacks are contributing data that's aggregated together for the "{props.group_name}" group.
|
||||
</div>
|
||||
<DialogContent dividers={true} style={{padding: 0}}>
|
||||
<DialogContent dividers={true} className="mythic-tree-groups-content">
|
||||
<Backdrop open={backdropOpen} style={{zIndex: 2, position: "absolute"}} invisible={false}>
|
||||
<CircularProgress color="inherit" />
|
||||
</Backdrop>
|
||||
<TableContainer className="mythicElement">
|
||||
<Table stickyHeader={true} size="small" style={{ "overflowWrap": "break-word", width: "100%"}}>
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
<TableCell></TableCell>
|
||||
<TableCell>Callback</TableCell>
|
||||
<TableCell>User</TableCell>
|
||||
<TableCell>Host</TableCell>
|
||||
<TableCell>Domain</TableCell>
|
||||
<TableCell>IP</TableCell>
|
||||
<TableCell>PID</TableCell>
|
||||
<TableCell>Description</TableCell>
|
||||
</TableRow>
|
||||
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
{groups.map( (a, i) => (
|
||||
<TableRow key={'array' + props.group_name + i} hover>
|
||||
<MythicStyledTableCell style={{width: "90px"}}>
|
||||
{!a.active ?
|
||||
<MythicStyledTooltip title={"Callback is not active"}>
|
||||
<VisibilityOffIcon style={{color: theme.palette.error.main, marginRight: "15px"}}/>
|
||||
</MythicStyledTooltip>
|
||||
:
|
||||
<MythicStyledTooltip title={"Callback is active"}>
|
||||
<VisibilityIcon style={{color: theme.palette.success.main, marginRight: "15px"}}/>
|
||||
</MythicStyledTooltip>
|
||||
}
|
||||
<MythicStyledTooltip title={a.payload.payloadtype.name}>
|
||||
<MythicAgentSVGIcon payload_type={a.payload.payloadtype.name} style={{width: "35px", height: "35px"}} />
|
||||
</MythicStyledTooltip>
|
||||
</MythicStyledTableCell>
|
||||
|
||||
<MythicStyledTableCell>{a.display_id}</MythicStyledTableCell>
|
||||
<MythicStyledTableCell>{a.user}</MythicStyledTableCell>
|
||||
<MythicStyledTableCell>{a.host}</MythicStyledTableCell>
|
||||
<MythicStyledTableCell>{a.domain}</MythicStyledTableCell>
|
||||
<MythicStyledTableCell style={{wordBreak: "break-all"}}>{a.ip}</MythicStyledTableCell>
|
||||
<MythicStyledTableCell >{a.pid}</MythicStyledTableCell>
|
||||
<MythicStyledTableCell style={{wordBreak: "break-all"}}>{a.description}</MythicStyledTableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
<CallbackGroupTable
|
||||
callbacks={groups}
|
||||
emptyMessage={`No callbacks are contributing to ${props.group_name}.`}
|
||||
tableId={`tree-group-${props.group_name}-callbacks`}
|
||||
/>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button onClick={props.onClose} variant="contained" color="primary">
|
||||
<Button onClick={props.onClose}>
|
||||
Close
|
||||
</Button>
|
||||
</DialogActions>
|
||||
@@ -179,37 +294,24 @@ export function ViewCallbackMythicTreeGroupsDialog(props){
|
||||
}
|
||||
|
||||
export function ViewAllCallbackMythicTreeGroupsDialog(props){
|
||||
const theme = useTheme();
|
||||
const [groups, setGroups] = React.useState([]);
|
||||
const {loading} = useQuery(getAllCallbackMythicTreeGroups, {
|
||||
fetchPolicy: "no-cache",
|
||||
onCompleted: data => {
|
||||
let groupDict = {};
|
||||
const callbacks = data.callback.map( c => {
|
||||
try{
|
||||
let cIP = JSON.parse(c.ip);
|
||||
if(cIP.length > 0){
|
||||
return {...c, ip:cIP[0] };
|
||||
}
|
||||
return {...c, ip: ""};
|
||||
|
||||
}catch(error){
|
||||
return {...c};
|
||||
}
|
||||
});
|
||||
const callbacks = data.callback.map(normalizeCallbackIP);
|
||||
for(let i = 0; i < callbacks.length; i++){
|
||||
if (callbacks[i].mythictree_groups.length > 0){
|
||||
for(let j = 0; j < callbacks[i].mythictree_groups.length; j++){
|
||||
if(callbacks[i].mythictree_groups[j] === "Default"){
|
||||
const treeGroups = callbacks[i].mythictree_groups || [];
|
||||
if (treeGroups.length > 0){
|
||||
for(let j = 0; j < treeGroups.length; j++){
|
||||
if(treeGroups[j] === "Default"){
|
||||
continue;
|
||||
}
|
||||
if(groupDict[callbacks[i].mythictree_groups[j]] === undefined){
|
||||
groupDict[callbacks[i].mythictree_groups[j]] = [];
|
||||
if(groupDict[treeGroups[j]] === undefined){
|
||||
groupDict[treeGroups[j]] = [];
|
||||
}
|
||||
groupDict[callbacks[i].mythictree_groups[j]].push(callbacks[i]);
|
||||
groupDict[treeGroups[j]].push(callbacks[i]);
|
||||
}
|
||||
} else {
|
||||
|
||||
}
|
||||
}
|
||||
const keys = Object.keys(groupDict).sort();
|
||||
@@ -229,71 +331,40 @@ export function ViewAllCallbackMythicTreeGroupsDialog(props){
|
||||
}
|
||||
return (
|
||||
<React.Fragment>
|
||||
<DialogTitle id="form-dialog-title">Viewing Callbacks for every group
|
||||
<DialogTitle id="form-dialog-title" className="mythic-tree-groups-title">Callback tree groups
|
||||
</DialogTitle>
|
||||
<div style={{paddingLeft: "20px"}}>
|
||||
Callbacks with no groups or with only the "Default" group are not shown.
|
||||
</div>
|
||||
<DialogContent dividers={true} style={{paddingLeft: 0, paddingRight: 0}}>
|
||||
|
||||
{groups.map( (g, i) => (
|
||||
<div key={g.group}>
|
||||
<Paper elevation={5} style={{backgroundColor: theme.pageHeader.main, color: theme.pageHeaderText.main,marginBottom: "5px", marginTop: "10px"}} variant={"elevation"}>
|
||||
<Typography variant="h6" style={{textAlign: "left", display: "inline-block", marginLeft: "20px", color: theme.pageHeaderColor}}>
|
||||
{g.group}
|
||||
</Typography>
|
||||
</Paper>
|
||||
<TableContainer className="mythicElement">
|
||||
<Table size="small" aria-label="details" style={{ "overflowWrap": "break-word", width: "100%"}}>
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
<MythicStyledTableCell></MythicStyledTableCell>
|
||||
<MythicStyledTableCell>Callback</MythicStyledTableCell>
|
||||
<MythicStyledTableCell>User</MythicStyledTableCell>
|
||||
<MythicStyledTableCell>Host</MythicStyledTableCell>
|
||||
<MythicStyledTableCell>Domain</MythicStyledTableCell>
|
||||
<MythicStyledTableCell>IP</MythicStyledTableCell>
|
||||
<MythicStyledTableCell>PID</MythicStyledTableCell>
|
||||
<MythicStyledTableCell>Description</MythicStyledTableCell>
|
||||
</TableRow>
|
||||
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
{g.callbacks.map( (a, i) => (
|
||||
<TableRow key={'array' + g.group + i} hover>
|
||||
<MythicStyledTableCell style={{width: "90px"}}>
|
||||
{!a.active ?
|
||||
<MythicStyledTooltip title={"Callback is not active"}>
|
||||
<VisibilityOffIcon style={{color: theme.palette.error.main, marginRight: "15px"}}/>
|
||||
</MythicStyledTooltip>
|
||||
:
|
||||
<MythicStyledTooltip title={"Callback is active"}>
|
||||
<VisibilityIcon style={{color: theme.palette.success.main, marginRight: "15px"}}/>
|
||||
</MythicStyledTooltip>
|
||||
}
|
||||
<MythicStyledTooltip title={a.payload.payloadtype.name}>
|
||||
<MythicAgentSVGIcon payload_type={a.payload.payloadtype.name} style={{width: "35px", height: "35px"}} />
|
||||
</MythicStyledTooltip>
|
||||
</MythicStyledTableCell>
|
||||
|
||||
<MythicStyledTableCell>{a.display_id}</MythicStyledTableCell>
|
||||
<MythicStyledTableCell>{a.user}</MythicStyledTableCell>
|
||||
<MythicStyledTableCell>{a.host}</MythicStyledTableCell>
|
||||
<MythicStyledTableCell>{a.domain}</MythicStyledTableCell>
|
||||
<MythicStyledTableCell style={{wordBreak: "break-all"}}>{a.ip}</MythicStyledTableCell>
|
||||
<MythicStyledTableCell >{a.pid}</MythicStyledTableCell>
|
||||
<MythicStyledTableCell style={{wordBreak: "break-all"}}>{a.description}</MythicStyledTableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
<DialogContent dividers={true} className="mythic-tree-groups-content">
|
||||
<div className="mythic-tree-groups-help">
|
||||
Callbacks with no groups or with only the "Default" group are not shown.
|
||||
</div>
|
||||
{groups.length === 0 ? (
|
||||
<div className="mythic-tree-groups-empty">
|
||||
No callback tree groups found.
|
||||
</div>
|
||||
|
||||
))}
|
||||
) : (
|
||||
groups.map( (g, i) => (
|
||||
<div key={g.group} className="mythic-dialog-section mythic-tree-groups-section">
|
||||
<div className="mythic-dialog-section-header">
|
||||
<div>
|
||||
<div className="mythic-dialog-section-title">
|
||||
{g.group}
|
||||
</div>
|
||||
<div className="mythic-dialog-section-description">
|
||||
{g.callbacks.length} callback{g.callbacks.length === 1 ? "" : "s"}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<CallbackGroupTable
|
||||
callbacks={g.callbacks}
|
||||
emptyMessage={`No callbacks are contributing to ${g.group}.`}
|
||||
tableId={`tree-group-${g.group}-callbacks`}
|
||||
/>
|
||||
</div>
|
||||
))
|
||||
)}
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button onClick={props.onClose} variant="contained" color="primary">
|
||||
<Button onClick={props.onClose}>
|
||||
Close
|
||||
</Button>
|
||||
</DialogActions>
|
||||
|
||||
@@ -1983,7 +1983,7 @@ const CustomDashboard = ({me, setLoading, loading, editing}) => {
|
||||
setDashboards(dashboards.toSpliced(i, 1));
|
||||
}
|
||||
React.useEffect( () => {
|
||||
updateSetting({setting_name: "customDashboardElements", value: dashboards});
|
||||
updateSetting({setting_name: "customDashboardElements", value: dashboards, broadcast: false});
|
||||
}, [dashboards]);
|
||||
const getDashboardElement = React.useCallback( (e, i, j) => {
|
||||
switch(e){
|
||||
@@ -2122,7 +2122,7 @@ export function CallbacksCard({me}) {
|
||||
return;
|
||||
}
|
||||
setDashboard(value);
|
||||
updateSetting({setting_name: "dashboard", value: value});
|
||||
updateSetting({setting_name: "dashboard", value: value, broadcast: false});
|
||||
setLoading(true);
|
||||
}
|
||||
return (
|
||||
|
||||
+2
-2
@@ -26,9 +26,9 @@ export function SettingsOperatorExperimentalUIConfigDialog(props) {
|
||||
|
||||
const onAccept = () => {
|
||||
if(newResponseStreamLimit < 0){
|
||||
updateSetting({setting_name: "experiment-responseStreamLimit", value: 0});
|
||||
updateSetting({setting_name: "experiment-responseStreamLimit", value: 0, broadcast: false});
|
||||
}else{
|
||||
updateSetting({setting_name: "experiment-responseStreamLimit", value: newResponseStreamLimit});
|
||||
updateSetting({setting_name: "experiment-responseStreamLimit", value: newResponseStreamLimit, broadcast: false});
|
||||
}
|
||||
snackActions.success("Updated settings!");
|
||||
props.onClose();
|
||||
|
||||
@@ -330,6 +330,592 @@ tspan {
|
||||
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-process-browser-tableTop {
|
||||
padding: 0.5rem 0.6rem;
|
||||
}
|
||||
.mythic-process-browser-toolbar {
|
||||
align-items: center;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.45rem;
|
||||
min-width: 0;
|
||||
width: 100%;
|
||||
}
|
||||
.mythic-process-browser-control {
|
||||
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;
|
||||
gap: 0.35rem;
|
||||
min-height: 32px;
|
||||
min-width: 0;
|
||||
padding: 0.15rem 0.18rem 0.15rem 0.55rem;
|
||||
}
|
||||
.mythic-process-browser-controlGroup {
|
||||
flex: 1 1 18rem;
|
||||
max-width: 30rem;
|
||||
}
|
||||
.mythic-process-browser-controlHost {
|
||||
flex: 1 1 16rem;
|
||||
max-width: 28rem;
|
||||
padding-right: 0.35rem;
|
||||
}
|
||||
.mythic-process-browser-searchInput {
|
||||
flex: 2 1 24rem;
|
||||
min-width: 16rem !important;
|
||||
}
|
||||
.mythic-process-browser-searchInput .MuiInputBase-root {
|
||||
background-color: ${(props) => props.theme.palette.background.paper};
|
||||
font-size: 0.78rem;
|
||||
min-height: 32px;
|
||||
}
|
||||
.mythic-process-browser-searchInput .MuiInputBase-input {
|
||||
padding-bottom: 6px;
|
||||
padding-top: 6px;
|
||||
}
|
||||
.mythic-process-browser-searchInput .MuiInputAdornment-root {
|
||||
color: ${(props) => props.theme.palette.text.secondary};
|
||||
}
|
||||
.mythic-process-browser-controlLabel {
|
||||
color: ${(props) => props.theme.palette.text.secondary};
|
||||
flex: 0 0 auto;
|
||||
font-size: 0.72rem;
|
||||
font-weight: 750;
|
||||
}
|
||||
.mythic-process-browser-selectControl {
|
||||
flex: 1 1 auto;
|
||||
min-width: 8rem !important;
|
||||
}
|
||||
.mythic-process-browser-select.MuiInputBase-root {
|
||||
background-color: ${(props) => props.theme.palette.background.paper};
|
||||
font-size: 0.78rem;
|
||||
min-height: 28px;
|
||||
}
|
||||
.mythic-process-browser-select .MuiSelect-select {
|
||||
min-height: 0 !important;
|
||||
overflow: hidden;
|
||||
padding-bottom: 5px;
|
||||
padding-top: 5px;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.mythic-process-browser-actions {
|
||||
margin-left: auto;
|
||||
}
|
||||
.mythic-process-browser-table-shell {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
min-height: 0;
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
width: 100%;
|
||||
}
|
||||
.mythic-process-browser-grid-shell {
|
||||
flex: 1 1 auto;
|
||||
min-height: 0;
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
.mythic-process-summary-strip {
|
||||
align-items: center;
|
||||
background-color: ${(props) => alpha(props.theme.palette.background.paper, props.theme.palette.mode === "dark" ? 0.94 : 0.98)};
|
||||
border-bottom: 1px solid ${(props) => props.theme.table?.borderSoft || props.theme.borderColor};
|
||||
display: flex;
|
||||
flex: 0 0 auto;
|
||||
flex-wrap: nowrap;
|
||||
gap: 0.35rem;
|
||||
min-height: 34px;
|
||||
overflow-x: auto;
|
||||
overflow-y: hidden;
|
||||
padding: 0.35rem 0.6rem;
|
||||
}
|
||||
.mythic-process-summary-chip,
|
||||
.mythic-process-indicator {
|
||||
align-items: center;
|
||||
border: 1px solid ${(props) => props.theme.table?.borderSoft || props.theme.borderColor};
|
||||
border-radius: ${(props) => props.theme.shape.borderRadius}px;
|
||||
color: ${(props) => props.theme.palette.text.secondary};
|
||||
display: inline-flex;
|
||||
flex: 0 0 auto;
|
||||
font-size: 0.68rem;
|
||||
font-weight: 800;
|
||||
gap: 0.2rem;
|
||||
line-height: 1;
|
||||
max-width: 12rem;
|
||||
min-height: 20px;
|
||||
overflow: hidden;
|
||||
padding: 0.22rem 0.4rem;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.mythic-process-summary-chip-muted {
|
||||
opacity: 0.58;
|
||||
}
|
||||
.mythic-process-summary-chip-info,
|
||||
.mythic-process-indicator-info {
|
||||
background-color: ${(props) => alpha(props.theme.palette.info.main, props.theme.palette.mode === "dark" ? 0.16 : 0.08)};
|
||||
border-color: ${(props) => alpha(props.theme.palette.info.main, props.theme.palette.mode === "dark" ? 0.42 : 0.28)};
|
||||
color: ${(props) => props.theme.palette.info.main};
|
||||
}
|
||||
.mythic-process-summary-chip-warning,
|
||||
.mythic-process-indicator-warning {
|
||||
background-color: ${(props) => alpha(props.theme.palette.warning.main, props.theme.palette.mode === "dark" ? 0.18 : 0.1)};
|
||||
border-color: ${(props) => alpha(props.theme.palette.warning.main, props.theme.palette.mode === "dark" ? 0.46 : 0.3)};
|
||||
color: ${(props) => props.theme.palette.warning.main};
|
||||
}
|
||||
.mythic-process-indicator-deleted {
|
||||
background-color: ${(props) => alpha(props.theme.palette.error.main, props.theme.palette.mode === "dark" ? 0.16 : 0.08)};
|
||||
border-color: ${(props) => alpha(props.theme.palette.error.main, props.theme.palette.mode === "dark" ? 0.42 : 0.28)};
|
||||
color: ${(props) => props.theme.palette.error.main};
|
||||
}
|
||||
.mythic-process-indicator {
|
||||
font-size: 0.62rem;
|
||||
min-height: 18px;
|
||||
padding: 0.18rem 0.32rem;
|
||||
}
|
||||
.mythic-process-indicator svg {
|
||||
font-size: 0.78rem;
|
||||
}
|
||||
.mythic-process-match-chips {
|
||||
align-items: center;
|
||||
display: inline-flex;
|
||||
flex: 0 0 auto;
|
||||
gap: 0.18rem;
|
||||
min-width: 0;
|
||||
}
|
||||
.mythic-process-match-chip {
|
||||
align-items: center;
|
||||
background-color: ${(props) => alpha(props.theme.palette.info.main, props.theme.palette.mode === "dark" ? 0.18 : 0.1)};
|
||||
border: 1px solid ${(props) => alpha(props.theme.palette.info.main, props.theme.palette.mode === "dark" ? 0.45 : 0.3)};
|
||||
border-radius: ${(props) => props.theme.shape.borderRadius}px;
|
||||
color: ${(props) => props.theme.palette.info.main};
|
||||
display: inline-flex;
|
||||
flex: 0 0 auto;
|
||||
font-size: 0.6rem;
|
||||
font-weight: 850;
|
||||
line-height: 1;
|
||||
min-height: 17px;
|
||||
padding: 0.16rem 0.28rem;
|
||||
}
|
||||
.mythic-process-match-chip-ancestor {
|
||||
background-color: ${(props) => alpha(props.theme.palette.text.primary, props.theme.palette.mode === "dark" ? 0.08 : 0.05)};
|
||||
border-color: ${(props) => props.theme.table?.borderSoft || props.theme.borderColor};
|
||||
color: ${(props) => props.theme.palette.text.secondary};
|
||||
}
|
||||
.mythic-process-inspector {
|
||||
background-color: ${(props) => props.theme.surfaces?.muted || props.theme.palette.background.paper};
|
||||
border-top: 1px solid ${(props) => props.theme.table?.borderSoft || props.theme.borderColor};
|
||||
box-shadow: ${(props) => props.theme.palette.mode === "dark" ? "0 -10px 24px rgba(0,0,0,0.22)" : "0 -10px 24px rgba(15,23,42,0.06)"};
|
||||
flex: 0 0 auto;
|
||||
min-width: 0;
|
||||
padding: 0.55rem 0.65rem 0.65rem;
|
||||
}
|
||||
.mythic-process-inspector-header {
|
||||
align-items: center;
|
||||
display: flex;
|
||||
gap: 0.75rem;
|
||||
justify-content: space-between;
|
||||
min-width: 0;
|
||||
}
|
||||
.mythic-process-inspector-title {
|
||||
align-items: center;
|
||||
color: ${(props) => props.theme.palette.text.primary};
|
||||
display: flex;
|
||||
flex: 1 1 auto;
|
||||
font-size: 0.86rem;
|
||||
font-weight: 800;
|
||||
gap: 0.35rem;
|
||||
min-width: 0;
|
||||
}
|
||||
.mythic-process-inspector-title > span:first-of-type {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.mythic-process-inspector-title svg {
|
||||
color: ${(props) => props.theme.palette.text.secondary};
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
.mythic-process-inspector-actions {
|
||||
align-items: center;
|
||||
display: flex;
|
||||
flex: 0 0 auto;
|
||||
gap: 0.3rem;
|
||||
}
|
||||
.mythic-process-inspector-body {
|
||||
display: grid;
|
||||
gap: 0.65rem;
|
||||
grid-template-columns: minmax(22rem, 1.25fr) minmax(18rem, 1fr);
|
||||
margin-top: 0.5rem;
|
||||
min-width: 0;
|
||||
}
|
||||
.mythic-process-inspector-details {
|
||||
display: grid;
|
||||
gap: 0.35rem;
|
||||
grid-template-columns: repeat(4, minmax(0, 1fr));
|
||||
min-width: 0;
|
||||
}
|
||||
.mythic-process-inspector-detail {
|
||||
background-color: ${(props) => props.theme.palette.background.paper};
|
||||
border: 1px solid ${(props) => props.theme.table?.borderSoft || props.theme.borderColor};
|
||||
border-radius: ${(props) => props.theme.shape.borderRadius}px;
|
||||
min-width: 0;
|
||||
padding: 0.34rem 0.45rem;
|
||||
}
|
||||
.mythic-process-inspector-detailWide {
|
||||
grid-column: span 2;
|
||||
}
|
||||
.mythic-process-inspector-detail span {
|
||||
color: ${(props) => props.theme.palette.text.secondary};
|
||||
display: block;
|
||||
font-size: 0.66rem;
|
||||
font-weight: 800;
|
||||
line-height: 1.1;
|
||||
}
|
||||
.mythic-process-inspector-detail strong {
|
||||
color: ${(props) => props.theme.palette.text.primary};
|
||||
display: block;
|
||||
font-size: 0.76rem;
|
||||
font-weight: 700;
|
||||
line-height: 1.25;
|
||||
margin-top: 0.16rem;
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.mythic-process-inspector-side {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.4rem;
|
||||
min-width: 0;
|
||||
}
|
||||
.mythic-process-inspector-tags {
|
||||
align-items: center;
|
||||
background-color: ${(props) => props.theme.palette.background.paper};
|
||||
border: 1px solid ${(props) => props.theme.table?.borderSoft || props.theme.borderColor};
|
||||
border-radius: ${(props) => props.theme.shape.borderRadius}px;
|
||||
display: flex;
|
||||
gap: 0.35rem;
|
||||
min-height: 32px;
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
padding: 0.28rem 0.35rem;
|
||||
}
|
||||
.mythic-process-inspector-tags .MuiIconButton-root {
|
||||
align-items: center !important;
|
||||
background-color: ${(props) => props.theme.palette.mode === "dark" ? "rgba(255,255,255,0.045)" : "rgba(0,0,0,0.025)"} !important;
|
||||
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} !important;
|
||||
display: inline-flex !important;
|
||||
flex: 0 0 auto;
|
||||
height: 22px;
|
||||
justify-content: center !important;
|
||||
padding: 0 !important;
|
||||
width: 22px;
|
||||
}
|
||||
.mythic-process-inspector-tags .MuiIconButton-root:hover {
|
||||
background-color: ${(props) => alpha(props.theme.palette.primary.main, props.theme.palette.mode === "dark" ? 0.16 : 0.09)} !important;
|
||||
color: ${(props) => props.theme.palette.primary.main} !important;
|
||||
}
|
||||
.mythic-process-inspector-command {
|
||||
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;
|
||||
color: ${(props) => props.theme.palette.text.primary};
|
||||
font-family: ${(props) => props.theme.typography.fontFamilyMonospace || "ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace"};
|
||||
font-size: 0.72rem;
|
||||
line-height: 1.35;
|
||||
max-height: 3.3rem;
|
||||
min-height: 2.1rem;
|
||||
overflow: auto;
|
||||
padding: 0.4rem 0.5rem;
|
||||
white-space: pre-wrap;
|
||||
word-break: break-word;
|
||||
}
|
||||
.mythic-process-name-cell,
|
||||
.mythic-process-string-cell,
|
||||
.mythic-process-tags-cell {
|
||||
align-items: center;
|
||||
display: flex;
|
||||
height: 100%;
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
width: 100%;
|
||||
}
|
||||
.mythic-process-name-cell {
|
||||
gap: 0.25rem;
|
||||
}
|
||||
.mythic-process-row-deleted {
|
||||
color: ${(props) => props.theme.palette.text.disabled};
|
||||
text-decoration: line-through;
|
||||
}
|
||||
.mythic-process-indent {
|
||||
flex: 0 0 auto;
|
||||
height: 1px;
|
||||
}
|
||||
.mythic-process-expand-button.MuiIconButton-root {
|
||||
align-items: center;
|
||||
border-radius: ${(props) => props.theme.shape.borderRadius}px;
|
||||
color: ${(props) => props.theme.palette.text.secondary};
|
||||
display: inline-flex;
|
||||
flex: 0 0 auto;
|
||||
height: 22px;
|
||||
justify-content: center;
|
||||
padding: 0;
|
||||
transition: background-color 120ms ease, color 120ms ease;
|
||||
width: 22px;
|
||||
}
|
||||
.mythic-process-expand-button.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-process-expand-spacer {
|
||||
flex: 0 0 22px;
|
||||
height: 22px;
|
||||
}
|
||||
.mythic-process-name-icon {
|
||||
color: ${(props) => props.theme.palette.text.secondary};
|
||||
flex: 0 0 auto;
|
||||
opacity: 0.85;
|
||||
}
|
||||
.mythic-process-name-text,
|
||||
.mythic-process-string-cell {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
flex: 1 1 auto;
|
||||
}
|
||||
.mythic-process-name-text {
|
||||
min-width: 0;
|
||||
}
|
||||
.mythic-process-string-cell {
|
||||
color: ${(props) => props.theme.palette.text.primary};
|
||||
font-size: 0.78rem;
|
||||
padding-left: 0.25rem;
|
||||
padding-right: 0.25rem;
|
||||
}
|
||||
.mythic-process-tags-cell {
|
||||
gap: 0.35rem;
|
||||
}
|
||||
.mythic-process-tags-cell .MuiIconButton-root {
|
||||
align-items: center !important;
|
||||
background-color: ${(props) => props.theme.palette.mode === "dark" ? "rgba(255,255,255,0.045)" : "rgba(0,0,0,0.025)"} !important;
|
||||
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} !important;
|
||||
display: inline-flex !important;
|
||||
flex: 0 0 auto;
|
||||
float: none !important;
|
||||
height: 22px;
|
||||
justify-content: center !important;
|
||||
padding: 0 !important;
|
||||
transition: background-color 120ms ease, border-color 120ms ease, color 120ms ease;
|
||||
width: 22px;
|
||||
}
|
||||
.mythic-process-tags-cell .MuiIconButton-root:hover {
|
||||
background-color: ${(props) => alpha(props.theme.palette.primary.main, props.theme.palette.mode === "dark" ? 0.16 : 0.09)} !important;
|
||||
border-color: ${(props) => alpha(props.theme.palette.primary.main, props.theme.palette.mode === "dark" ? 0.44 : 0.3)};
|
||||
color: ${(props) => props.theme.palette.primary.main} !important;
|
||||
}
|
||||
.mythic-process-tags-cell .MuiIconButton-root svg {
|
||||
font-size: 0.92rem;
|
||||
}
|
||||
.mythic-process-tags-list {
|
||||
align-items: center;
|
||||
display: flex;
|
||||
flex: 1 1 auto;
|
||||
flex-wrap: nowrap;
|
||||
gap: 0.2rem;
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
.mythic-process-tags-list .MuiChip-root {
|
||||
flex: 0 0 auto;
|
||||
float: none !important;
|
||||
height: 18px !important;
|
||||
max-width: 5.5rem;
|
||||
}
|
||||
.mythic-process-tags-list .MuiChip-label {
|
||||
font-size: 0.68rem;
|
||||
font-weight: 800;
|
||||
min-width: 0;
|
||||
overflow: hidden !important;
|
||||
padding-left: 0.35rem;
|
||||
padding-right: 0.35rem;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
.mythic-process-action-button.MuiIconButton-root {
|
||||
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};
|
||||
height: 24px;
|
||||
padding: 0;
|
||||
transition: background-color 120ms ease, border-color 120ms ease, color 120ms ease;
|
||||
width: 24px;
|
||||
}
|
||||
.mythic-process-action-button.MuiIconButton-root:hover,
|
||||
.mythic-process-action-button.MuiIconButton-root[aria-expanded="true"] {
|
||||
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.46 : 0.3)};
|
||||
color: ${(props) => props.theme.palette.info.main};
|
||||
}
|
||||
.mythic-process-menu-icon {
|
||||
align-items: center;
|
||||
color: ${(props) => props.theme.palette.text.secondary};
|
||||
display: inline-flex;
|
||||
flex: 0 0 1.25rem;
|
||||
justify-content: center;
|
||||
margin-right: 0.45rem;
|
||||
}
|
||||
.mythic-process-menu-icon svg,
|
||||
.mythic-process-menu-icon .svg-inline--fa {
|
||||
font-size: 0.95rem;
|
||||
}
|
||||
.mythic-process-menu-icon-success {
|
||||
color: ${(props) => props.theme.palette.success.main};
|
||||
}
|
||||
.mythic-process-menu-icon-warning {
|
||||
color: ${(props) => props.theme.palette.warning.main};
|
||||
}
|
||||
.mythic-process-menu-icon-error {
|
||||
color: ${(props) => props.theme.palette.error.main};
|
||||
}
|
||||
@media (max-width: 1100px) {
|
||||
.mythic-process-inspector-body {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
.mythic-process-inspector-details {
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
}
|
||||
}
|
||||
.mythic-tree-groups-title.MuiDialogTitle-root {
|
||||
background: ${getSectionHeaderGradient};
|
||||
border-bottom: 1px solid ${(props) => props.theme.table?.borderSoft || props.theme.borderColor};
|
||||
color: ${(props) => props.theme.pageHeaderText?.main || props.theme.palette.text.primary};
|
||||
font-size: 0.98rem;
|
||||
font-weight: 800;
|
||||
padding: 0.75rem 0.9rem;
|
||||
position: relative;
|
||||
}
|
||||
.mythic-tree-groups-title.MuiDialogTitle-root::before {
|
||||
background: ${getSectionHeaderAccent};
|
||||
content: "";
|
||||
height: 100%;
|
||||
left: 0;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
width: 3px;
|
||||
}
|
||||
.mythic-tree-groups-title-copy {
|
||||
display: flex;
|
||||
flex: 1 1 18rem;
|
||||
flex-direction: column;
|
||||
gap: 0.14rem;
|
||||
min-width: 0;
|
||||
}
|
||||
.mythic-tree-groups-title-copy span:first-child {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.mythic-tree-groups-title-copy span:last-child,
|
||||
.mythic-tree-groups-help {
|
||||
color: ${(props) => props.theme.palette.text.secondary};
|
||||
font-size: 0.76rem;
|
||||
font-weight: 550;
|
||||
line-height: 1.35;
|
||||
}
|
||||
.mythic-tree-groups-content.MuiDialogContent-root {
|
||||
background-color: ${(props) => props.theme.palette.background.default};
|
||||
padding: 0.75rem;
|
||||
}
|
||||
.mythic-tree-groups-content .MuiBackdrop-root {
|
||||
background-color: ${(props) => alpha(props.theme.palette.background.default, props.theme.palette.mode === "dark" ? 0.7 : 0.52)};
|
||||
}
|
||||
.mythic-tree-groups-help {
|
||||
background-color: ${(props) => props.theme.surfaces?.muted || props.theme.palette.background.paper};
|
||||
border: 1px solid ${(props) => props.theme.table?.borderSoft || props.theme.borderColor};
|
||||
border-radius: ${(props) => props.theme.shape.borderRadius}px;
|
||||
margin-bottom: 0.65rem;
|
||||
padding: 0.55rem 0.7rem;
|
||||
}
|
||||
.mythic-tree-groups-section + .mythic-tree-groups-section {
|
||||
margin-top: 0.65rem;
|
||||
}
|
||||
.mythic-tree-groups-table-wrap {
|
||||
max-height: min(48vh, 32rem);
|
||||
overflow: auto;
|
||||
}
|
||||
.mythic-tree-groups-section .mythic-tree-groups-table-wrap {
|
||||
max-height: min(36vh, 24rem);
|
||||
}
|
||||
.mythic-tree-groups-callback-icons {
|
||||
align-items: center;
|
||||
display: flex;
|
||||
flex: 0 0 auto;
|
||||
gap: 0.4rem;
|
||||
}
|
||||
.mythic-tree-groups-status {
|
||||
align-items: center;
|
||||
background-color: ${(props) => props.theme.palette.mode === "dark" ? "rgba(255,255,255,0.045)" : "rgba(0,0,0,0.025)"};
|
||||
border: 1px solid ${(props) => props.theme.table?.borderSoft || props.theme.borderColor};
|
||||
border-radius: ${(props) => props.theme.shape.borderRadius}px;
|
||||
color: ${(props) => props.theme.palette.text.secondary};
|
||||
display: inline-flex;
|
||||
height: 26px;
|
||||
justify-content: center;
|
||||
width: 26px;
|
||||
}
|
||||
.mythic-tree-groups-statusActive {
|
||||
background-color: ${(props) => alpha(props.theme.palette.success.main, props.theme.palette.mode === "dark" ? 0.16 : 0.09)};
|
||||
border-color: ${(props) => alpha(props.theme.palette.success.main, props.theme.palette.mode === "dark" ? 0.42 : 0.28)};
|
||||
color: ${(props) => props.theme.palette.success.main};
|
||||
}
|
||||
.mythic-tree-groups-statusInactive {
|
||||
background-color: ${(props) => alpha(props.theme.palette.warning.main, props.theme.palette.mode === "dark" ? 0.18 : 0.1)};
|
||||
border-color: ${(props) => alpha(props.theme.palette.warning.main, props.theme.palette.mode === "dark" ? 0.46 : 0.32)};
|
||||
color: ${(props) => props.theme.palette.warning.main};
|
||||
}
|
||||
.mythic-tree-groups-agent-icon {
|
||||
align-items: center;
|
||||
display: inline-flex;
|
||||
height: 30px;
|
||||
justify-content: center;
|
||||
width: 30px;
|
||||
}
|
||||
.mythic-tree-groups-callback-id {
|
||||
color: ${(props) => props.theme.palette.text.primary};
|
||||
font-size: 0.84rem;
|
||||
font-weight: 800;
|
||||
}
|
||||
.mythic-tree-groups-table-value {
|
||||
display: block;
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.mythic-tree-groups-description-cell {
|
||||
max-width: 24rem;
|
||||
}
|
||||
.mythic-tree-groups-empty {
|
||||
align-items: center;
|
||||
background-color: ${(props) => props.theme.palette.mode === "dark" ? "rgba(255,255,255,0.035)" : "rgba(0,0,0,0.02)"};
|
||||
border: 1px dashed ${(props) => props.theme.table?.border || props.theme.borderColor};
|
||||
border-radius: ${(props) => props.theme.shape.borderRadius}px;
|
||||
color: ${(props) => props.theme.palette.text.secondary};
|
||||
display: flex;
|
||||
font-size: 0.8rem;
|
||||
justify-content: center;
|
||||
min-height: 6rem;
|
||||
padding: 0.9rem;
|
||||
text-align: center;
|
||||
}
|
||||
.mythic-tasking-composer {
|
||||
background-color: ${(props) => alpha(props.theme.palette.background.paper, props.theme.palette.mode === "dark" ? 0.96 : 0.98)};
|
||||
border-top: 1px solid ${(props) => props.theme.table?.borderSoft || props.theme.borderColor};
|
||||
@@ -1706,6 +2292,27 @@ tspan {
|
||||
opacity: 1;
|
||||
width: 4px;
|
||||
}
|
||||
.MythicResizableGrid-cell.mythic-process-filter-match-row {
|
||||
background-image: linear-gradient(0deg,
|
||||
${(props) => alpha(props.theme.palette.info.main, props.theme.palette.mode === "dark" ? 0.16 : 0.09)},
|
||||
${(props) => alpha(props.theme.palette.info.main, props.theme.palette.mode === "dark" ? 0.16 : 0.09)}) !important;
|
||||
border-bottom-color: ${(props) => alpha(props.theme.palette.info.main, props.theme.palette.mode === "dark" ? 0.46 : 0.3)};
|
||||
}
|
||||
.MythicResizableGrid-cell.mythic-process-filter-ancestor-row {
|
||||
background-image: linear-gradient(0deg,
|
||||
${(props) => alpha(props.theme.palette.text.primary, props.theme.palette.mode === "dark" ? 0.055 : 0.035)},
|
||||
${(props) => alpha(props.theme.palette.text.primary, props.theme.palette.mode === "dark" ? 0.055 : 0.035)}) !important;
|
||||
}
|
||||
.MythicResizableGrid-cell.MythicResizableGrid-rowFirstCell.mythic-process-filter-match-row::before {
|
||||
background-color: ${(props) => props.theme.palette.info.main};
|
||||
opacity: 1;
|
||||
width: 4px;
|
||||
}
|
||||
.MythicResizableGrid-cell.MythicResizableGrid-rowFirstCell.mythic-process-filter-ancestor-row::before {
|
||||
background-color: ${(props) => props.theme.palette.text.secondary};
|
||||
opacity: 0.35;
|
||||
width: 3px;
|
||||
}
|
||||
.MythicResizableGrid-cell.MythicResizableGrid-rowLastCell {
|
||||
border-right-color: ${(props) => props.theme.table?.border || props.theme.borderColor};
|
||||
}
|
||||
|
||||
@@ -100,7 +100,7 @@ func Initialize() {
|
||||
InvalidateOperationEventLogCacheMap()
|
||||
go listenForOperationsMessages()
|
||||
go listenForWriteDownloadChunkToLocalDisk()
|
||||
go listenForFileBrowserData()
|
||||
go listenForMythicTreeData()
|
||||
go listenForAsyncAgentMessagePostResponseContent()
|
||||
go updateCheckinTimeEverySecond()
|
||||
for {
|
||||
|
||||
@@ -270,8 +270,9 @@ func getDelegateProxyMessages(callbackID int, agentUUIDLength int, updateCheckin
|
||||
return nil
|
||||
}
|
||||
delegateMessages := []delegateMessageResponse{}
|
||||
// get a list of all the other callbacks with proxy ports open
|
||||
if callbackIds := proxyPorts.GetOtherCallbackIds(callbackID); len(callbackIds) > 0 {
|
||||
// get callbacks that have proxy data or interactive tasking waiting so we
|
||||
// avoid route lookups for idle proxy ports on every check-in.
|
||||
if callbackIds := proxyPorts.GetOtherCallbackIdsWithPendingData(callbackID); len(callbackIds) > 0 {
|
||||
// check if there's a route between our callback and the callback with a task
|
||||
for _, targetCallbackId := range callbackIds {
|
||||
if routablePath := callbackGraph.GetBFSPath(callbackID, targetCallbackId); routablePath != nil && len(routablePath) > 0 {
|
||||
|
||||
@@ -627,16 +627,10 @@ func handleAgentMessagePostResponse(incoming *map[string]interface{}, uUIDInfo *
|
||||
currentTask.Stderr += *agentMessage.Responses[i].Stderr
|
||||
}
|
||||
if agentMessage.Responses[i].FileBrowser != nil {
|
||||
// do it in the background - the agent doesn't need the result of this directly
|
||||
FileBrowserChannel <- FileBrowserChannelMessage{
|
||||
Task: ¤tTask,
|
||||
NewFileBrowserData: agentMessage.Responses[i].FileBrowser,
|
||||
APITokenID: 0,
|
||||
}
|
||||
//go HandleAgentMessagePostResponseFileBrowser(currentTask, agentMessage.Responses[i].FileBrowser, 0)
|
||||
enqueueMythicTreeFileBrowserResponse(currentTask, agentMessage.Responses[i].FileBrowser, 0)
|
||||
}
|
||||
if agentMessage.Responses[i].Processes != nil {
|
||||
go HandleAgentMessagePostResponseProcesses(currentTask, agentMessage.Responses[i].Processes, 0)
|
||||
enqueueMythicTreeProcessResponse(currentTask, agentMessage.Responses[i].Processes, 0)
|
||||
}
|
||||
if agentMessage.Responses[i].RemovedFiles != nil {
|
||||
go handleAgentMessagePostResponseRemovedFiles(currentTask, agentMessage.Responses[i].RemovedFiles)
|
||||
@@ -682,7 +676,7 @@ func handleAgentMessagePostResponse(incoming *map[string]interface{}, uUIDInfo *
|
||||
go handleAgentMessagePostResponseEvent(currentTask, agentMessage.Responses[i].Events)
|
||||
}
|
||||
if agentMessage.Responses[i].CustomBrowser != nil {
|
||||
go handleAgentMessagePostResponseCustomBrowser(currentTask, agentMessage.Responses[i].CustomBrowser)
|
||||
enqueueMythicTreeCustomBrowserResponse(currentTask, agentMessage.Responses[i].CustomBrowser)
|
||||
}
|
||||
// this section always happens
|
||||
reflectBackOtherKeys(&mythicResponse, &agentMessage.Responses[i].Other)
|
||||
@@ -702,10 +696,7 @@ func handleAgentMessagePostResponse(incoming *map[string]interface{}, uUIDInfo *
|
||||
}
|
||||
if transitionedToCompleted {
|
||||
go CheckAndProcessTaskCompletionHandlers(currentTask.ID)
|
||||
FileBrowserChannel <- FileBrowserChannelMessage{
|
||||
Task: ¤tTask,
|
||||
NewFileBrowserData: nil,
|
||||
}
|
||||
enqueueMythicTreeFileBrowserFlush(currentTask, 0)
|
||||
go emitTaskLog(currentTask.ID)
|
||||
go func(task databaseStructs.Task) {
|
||||
EventingChannel <- EventNotification{
|
||||
@@ -1934,23 +1925,10 @@ type FileBrowserChannelMessage struct {
|
||||
APITokenID int
|
||||
}
|
||||
|
||||
var fileBrowserUpdateDeletedChannel = make(chan FileBrowserChannelMessage)
|
||||
var FileBrowserChannel = make(chan FileBrowserChannelMessage, 1000)
|
||||
// fileBrowserUpdateDeletedChannel is buffered so slower update_deleted cleanup
|
||||
// does not block the shared MythicTree ingest workers from accepting more work.
|
||||
var fileBrowserUpdateDeletedChannel = make(chan FileBrowserChannelMessage, defaultAgentMessagePostResponseQueueSize)
|
||||
|
||||
func listenForFileBrowserData() {
|
||||
go listenForFileBrowserUpdateDeleted()
|
||||
for {
|
||||
select {
|
||||
case message := <-FileBrowserChannel:
|
||||
//logging.LogInfo("[*] got message from FileBrowserChannel", "task", message.Task.ID, "completed", message.Task.Completed,
|
||||
// "nil data", message.NewFileBrowserData == nil)
|
||||
err := HandleAgentMessagePostResponseFileBrowser(*message.Task, message.NewFileBrowserData, message.APITokenID)
|
||||
if err != nil {
|
||||
logging.LogError(err, "Failed to handle file browser post response")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
func HandleAgentMessagePostResponseFileBrowser(task databaseStructs.Task, fileBrowser *agentMessagePostResponseFileBrowser,
|
||||
apitokensId int) error {
|
||||
// given a FileBrowser object, need to insert it into database and potentially insert parents along the way
|
||||
@@ -2084,10 +2062,8 @@ func listenForFileBrowserUpdateDeleted() {
|
||||
if err != nil {
|
||||
logging.LogError(err, "Failed to group file browser update_deleted entries")
|
||||
} else {
|
||||
for _, group := range groups {
|
||||
if err := reconcileFileBrowserUpdateDeletedGroup(updateMessage.Task, group, updateMessage.APITokenID); err != nil {
|
||||
logging.LogError(err, "Failed to update file browser children")
|
||||
}
|
||||
if err := reconcileFileBrowserUpdateDeletedGroups(updateMessage.Task, groups, updateMessage.APITokenID); err != nil {
|
||||
logging.LogError(err, "Failed to update file browser children")
|
||||
}
|
||||
}
|
||||
delete(fileBrowserUpdateDeletedCache, updateMessage.Task.ID)
|
||||
@@ -2097,22 +2073,26 @@ func listenForFileBrowserUpdateDeleted() {
|
||||
}
|
||||
func HandleAgentMessagePostResponseProcesses(task databaseStructs.Task, processes *[]agentMessagePostResponseProcesses,
|
||||
apitokensId int) error {
|
||||
if processes == nil || len(*processes) == 0 {
|
||||
return nil
|
||||
}
|
||||
// process data is also represented in a tree format with a full path of the process_id
|
||||
updateDeleted := false
|
||||
for indx, _ := range *processes {
|
||||
for indx := range *processes {
|
||||
if (*processes)[indx].UpdateDeleted != nil && *(*processes)[indx].UpdateDeleted {
|
||||
updateDeleted = true
|
||||
}
|
||||
}
|
||||
if len(*processes) == 0 {
|
||||
return nil
|
||||
}
|
||||
// Get the host (default to callback.Host if per-process host not specified)
|
||||
host := task.Callback.Host
|
||||
if (*processes)[0].Host != nil && *(*processes)[0].Host != "" {
|
||||
host = strings.ToUpper(*(*processes)[0].Host)
|
||||
}
|
||||
if updateDeleted {
|
||||
if err := ensureTaskCallbackMythicTreeGroups(&task); err != nil {
|
||||
logging.LogError(err, "Failed to fetch callback mythictree groups for process update_deleted")
|
||||
return err
|
||||
}
|
||||
idsToDelete := []int{}
|
||||
var existingTreeEntries []databaseStructs.MythicTree
|
||||
err := database.DB.Select(&existingTreeEntries, `SELECT
|
||||
@@ -2156,29 +2136,9 @@ func HandleAgentMessagePostResponseProcesses(task databaseStructs.Task, processe
|
||||
logging.LogError(err, "Failed to upsert process MythicTree entries")
|
||||
return err
|
||||
}
|
||||
// delete all the ids marked for deletion
|
||||
if len(idsToDelete) > 0 {
|
||||
deleteQuery, args, err := sqlx.Named(`UPDATE mythictree SET
|
||||
deleted=true, "timestamp"=now(), task_id=:task_id
|
||||
WHERE id IN (:ids)`, map[string]interface{}{
|
||||
"ids": idsToDelete,
|
||||
"task_id": task.ID,
|
||||
})
|
||||
if err != nil {
|
||||
logging.LogError(err, "Failed to make named statement when updated deleted process status")
|
||||
return err
|
||||
}
|
||||
deleteQuery, args, err = sqlx.In(deleteQuery, args...)
|
||||
if err != nil {
|
||||
logging.LogError(err, "Failed to do sqlx.In")
|
||||
return err
|
||||
}
|
||||
deleteQuery = database.DB.Rebind(deleteQuery)
|
||||
_, err = database.DB.Exec(deleteQuery, args...)
|
||||
if err != nil {
|
||||
logging.LogError(err, "Failed to exec sqlx.IN modified statement")
|
||||
return err
|
||||
}
|
||||
if err := deleteMythicTreeNodes(task.ID, idsToDelete, nil); err != nil {
|
||||
logging.LogError(err, "Failed to update deleted process status")
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
treeNodesToUpsert := make([]*databaseStructs.MythicTree, 0, len(*processes))
|
||||
@@ -2590,6 +2550,8 @@ func handleAgentMessagePostResponseCustomBrowser(task databaseStructs.Task, agen
|
||||
logging.LogError(errors.New("unknown custom browser"), "Failed to get custom browser", "browser", agentCustomBrowser.BrowserName)
|
||||
return
|
||||
}
|
||||
customBrowserUpdateDeleted := agentCustomBrowser.UpdateDeleted && customBrowserData.Type == databaseStructs.TREE_TYPE_FILE
|
||||
updateDeletedReconciliation := mythicTreeChildrenReconciliation{}
|
||||
for _, entry := range *agentCustomBrowser.Entries {
|
||||
pathData, err := utils.SplitCustomPath(entry.ParentPath, entry.Name, customBrowserData.Separator)
|
||||
if err != nil {
|
||||
@@ -2650,103 +2612,46 @@ func handleAgentMessagePostResponseCustomBrowser(task databaseStructs.Task, agen
|
||||
newTree.CallbackID.Valid = true
|
||||
newTree.CallbackID.Int64 = int64(task.Callback.ID)
|
||||
treeNodesToUpsert = append(treeNodesToUpsert, &newTree)
|
||||
if agentCustomBrowser.UpdateDeleted {
|
||||
if customBrowserData.Type == databaseStructs.TREE_TYPE_FILE {
|
||||
// we need to iterate over the children for this entry and potentially remove any that the database know of but that aren't in our `files` list
|
||||
var existingTreeEntries []databaseStructs.MythicTree
|
||||
err = database.DB.Select(&existingTreeEntries, `SELECT
|
||||
id, "name", success, full_path, parent_path, operation_id, host, tree_type, callback_id
|
||||
FROM mythictree WHERE
|
||||
parent_path=$1 AND operation_id=$2 AND host=$3 AND tree_type=$4`,
|
||||
fullPath, task.OperationID, pathData.Host, customBrowserData.Name)
|
||||
if err != nil {
|
||||
logging.LogError(err, "Failed to fetch existing children when trying to update deleted")
|
||||
continue
|
||||
}
|
||||
incomingChildrenByName := make(map[string]agentMessagePostResponseCustomBrowserChildren)
|
||||
if entry.Children != nil {
|
||||
for _, newEntry := range *entry.Children {
|
||||
incomingChildrenByName[newEntry.Name] = newEntry
|
||||
}
|
||||
}
|
||||
namesToDeleteAndUpdate := make(map[string]struct{}, len(existingTreeEntries)+len(incomingChildrenByName))
|
||||
for _, existingEntry := range existingTreeEntries {
|
||||
if newEntry, ok := incomingChildrenByName[string(existingEntry.Name)]; ok {
|
||||
namesToDeleteAndUpdate[newEntry.Name] = struct{}{}
|
||||
newTreeChild := databaseStructs.MythicTree{
|
||||
Host: pathData.Host,
|
||||
TaskID: task.ID,
|
||||
OperationID: task.OperationID,
|
||||
Name: []byte(newEntry.Name),
|
||||
ParentPath: existingEntry.ParentPath,
|
||||
FullPath: existingEntry.FullPath,
|
||||
TreeType: customBrowserData.Name,
|
||||
CanHaveChildren: newEntry.CanHaveChildren,
|
||||
DisplayPath: []byte(newEntry.DisplayPath),
|
||||
Deleted: false,
|
||||
Success: existingEntry.Success,
|
||||
ID: existingEntry.ID,
|
||||
Os: newTree.Os,
|
||||
}
|
||||
newTreeChild.Metadata = GetMythicJSONTextFromStruct(newEntry.Metadata)
|
||||
newTreeChild.CallbackID.Valid = true
|
||||
newTreeChild.CallbackID.Int64 = int64(task.Callback.ID)
|
||||
treeNodesToUpsert = append(treeNodesToUpsert, &newTreeChild)
|
||||
} else {
|
||||
namesToDeleteAndUpdate[string(existingEntry.Name)] = struct{}{}
|
||||
existingEntry.Deleted = true
|
||||
existingEntry.TaskID = task.ID
|
||||
deleteTreeNode(existingEntry, true)
|
||||
}
|
||||
}
|
||||
// now all existing ones have been updated or deleted, so it's time to add new ones
|
||||
for name, newEntry := range incomingChildrenByName {
|
||||
if _, ok := namesToDeleteAndUpdate[name]; !ok {
|
||||
newTreeChild := databaseStructs.MythicTree{
|
||||
Host: pathData.Host,
|
||||
TaskID: task.ID,
|
||||
OperationID: task.OperationID,
|
||||
Name: []byte(newEntry.Name),
|
||||
DisplayPath: []byte(newEntry.DisplayPath),
|
||||
ParentPath: fullPath,
|
||||
TreeType: customBrowserData.Name,
|
||||
CanHaveChildren: newEntry.CanHaveChildren,
|
||||
Deleted: false,
|
||||
Os: newTree.Os,
|
||||
}
|
||||
newTreeChild.FullPath = treeNodeGetFullPath(fullPath, []byte(newEntry.Name), []byte(pathData.PathSeparator), customBrowserData.Name)
|
||||
newTreeChild.Metadata = GetMythicJSONTextFromStruct(newEntry.Metadata)
|
||||
newTreeChild.CallbackID.Valid = true
|
||||
newTreeChild.CallbackID.Int64 = int64(task.Callback.ID)
|
||||
treeNodesToUpsert = append(treeNodesToUpsert, &newTreeChild)
|
||||
}
|
||||
}
|
||||
if customBrowserUpdateDeleted {
|
||||
updateDeletedReconciliation.TreeNodesToUpsert = append(updateDeletedReconciliation.TreeNodesToUpsert, treeNodesToUpsert...)
|
||||
childReconciliation, err := buildMythicTreeChildrenReconciliation(
|
||||
&task,
|
||||
pathData.Host,
|
||||
fullPath,
|
||||
pathData.PathSeparator,
|
||||
customBrowserData.Name,
|
||||
newTree.Os,
|
||||
customBrowserChildrenByName(entry.Children),
|
||||
0,
|
||||
false)
|
||||
if err != nil {
|
||||
logging.LogError(err, "Failed to build custom browser update_deleted changes")
|
||||
continue
|
||||
}
|
||||
|
||||
} else if entry.Children != nil {
|
||||
appendMythicTreeChildrenReconciliation(&updateDeletedReconciliation, childReconciliation)
|
||||
continue
|
||||
}
|
||||
if !agentCustomBrowser.UpdateDeleted && entry.Children != nil {
|
||||
// we're not automatically updating deleted children, so just iterate over the files and insert/update them
|
||||
for _, newEntry := range *entry.Children {
|
||||
newTreeChild := databaseStructs.MythicTree{
|
||||
Host: pathData.Host,
|
||||
TaskID: task.ID,
|
||||
OperationID: task.OperationID,
|
||||
Name: []byte(newEntry.Name),
|
||||
DisplayPath: []byte(newEntry.DisplayPath),
|
||||
ParentPath: fullPath,
|
||||
TreeType: customBrowserData.Name,
|
||||
CanHaveChildren: newEntry.CanHaveChildren,
|
||||
Deleted: false,
|
||||
Os: newTree.Os,
|
||||
}
|
||||
newTreeChild.FullPath = treeNodeGetFullPath(fullPath, []byte(newEntry.Name), []byte(pathData.PathSeparator), customBrowserData.Name)
|
||||
newTreeChild.Metadata = GetMythicJSONTextFromStruct(newEntry.Metadata)
|
||||
newTreeChild.CallbackID.Valid = true
|
||||
newTreeChild.CallbackID.Int64 = int64(task.Callback.ID)
|
||||
treeNodesToUpsert = append(treeNodesToUpsert, &newTreeChild)
|
||||
treeNodesToUpsert = append(treeNodesToUpsert, buildCustomBrowserChildMythicTreeNode(
|
||||
task,
|
||||
pathData.Host,
|
||||
fullPath,
|
||||
pathData.PathSeparator,
|
||||
customBrowserData.Name,
|
||||
newEntry,
|
||||
newTree.Os,
|
||||
0))
|
||||
}
|
||||
}
|
||||
if err := upsertMythicTreeNodes(treeNodesToUpsert); err != nil {
|
||||
logging.LogError(err, "Failed to upsert custom browser MythicTree entries")
|
||||
}
|
||||
}
|
||||
if customBrowserUpdateDeleted {
|
||||
if err := applyMythicTreeChildrenReconciliation(task.ID, updateDeletedReconciliation); err != nil {
|
||||
logging.LogError(err, "Failed to apply custom browser update_deleted changes")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -118,12 +118,11 @@ func isCallbackStreaming(callbackID int) bool {
|
||||
}
|
||||
|
||||
type interceptProxyToAgentMessage struct {
|
||||
MessagesToAgent chan proxyToAgentMessage
|
||||
InteractiveMessagesToAgent chan agentMessagePostResponseInteractive
|
||||
Message proxyToAgentMessage
|
||||
InteractiveMessage agentMessagePostResponseInteractive
|
||||
ProxyType string
|
||||
CallbackID int
|
||||
CallbackPort *callbackPortUsage
|
||||
Message proxyToAgentMessage
|
||||
InteractiveMessage agentMessagePostResponseInteractive
|
||||
ProxyType string
|
||||
CallbackID int
|
||||
}
|
||||
|
||||
var interceptProxyToAgentMessageChan = make(chan interceptProxyToAgentMessage, 2000)
|
||||
@@ -185,20 +184,21 @@ func interceptProxyDataToAgentForPushC2() {
|
||||
if !attemptedToSend {
|
||||
//logging.LogInfo("Couldn't send to push c2, saving to msg")
|
||||
// we don't have a PushC2 client available, so save it like normal
|
||||
if msg.CallbackPort == nil {
|
||||
logging.LogError(nil, "dropping proxy message because callback port is missing", "type", msg.ProxyType)
|
||||
continue
|
||||
}
|
||||
switch msg.ProxyType {
|
||||
case CALLBACK_PORT_TYPE_INTERACTIVE:
|
||||
select {
|
||||
case msg.InteractiveMessagesToAgent <- msg.InteractiveMessage:
|
||||
default:
|
||||
if !msg.CallbackPort.queueInteractiveDataForAgent(msg.InteractiveMessage) {
|
||||
logging.LogError(nil, "dropping interactive message because channel is full", "type", msg.ProxyType, "len(msg.interactiveMessagesToAgent)", len(msg.CallbackPort.interactiveMessagesToAgent))
|
||||
}
|
||||
//.LogInfo("saved to msg")
|
||||
case CALLBACK_PORT_TYPE_SOCKS:
|
||||
fallthrough
|
||||
case CALLBACK_PORT_TYPE_RPORTFWD:
|
||||
select {
|
||||
case msg.MessagesToAgent <- msg.Message:
|
||||
default:
|
||||
logging.LogError(nil, "dropping message because channel is full", "type", msg.ProxyType, "len(msg.MessagesToAgent)", len(msg.MessagesToAgent))
|
||||
if !msg.CallbackPort.queueProxyDataForAgent(msg.Message) {
|
||||
logging.LogError(nil, "dropping message because channel is full", "type", msg.ProxyType, "len(msg.messagesToAgent)", len(msg.CallbackPort.messagesToAgent))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -352,6 +352,29 @@ func (s *submittedTasksForAgents) getInteractiveTasksForCallbackId(callbackId in
|
||||
defer s.RUnlock()
|
||||
return cloneTaskIDs(s.interactiveTasksByCallbackID[callbackId])
|
||||
}
|
||||
func (s *submittedTasksForAgents) hasInteractiveTasksForCallbackId(callbackId int) bool {
|
||||
s.RLock()
|
||||
defer s.RUnlock()
|
||||
return len(s.interactiveTasksByCallbackID[callbackId]) > 0
|
||||
}
|
||||
func (s *submittedTasksForAgents) getCallbackIdsWithInteractiveTasks(callbackIds []int) map[int]bool {
|
||||
var callbacksWithInteractiveTasks map[int]bool
|
||||
if len(callbackIds) == 0 {
|
||||
return callbacksWithInteractiveTasks
|
||||
}
|
||||
s.RLock()
|
||||
defer s.RUnlock()
|
||||
for _, callbackId := range callbackIds {
|
||||
if len(s.interactiveTasksByCallbackID[callbackId]) == 0 {
|
||||
continue
|
||||
}
|
||||
if callbacksWithInteractiveTasks == nil {
|
||||
callbacksWithInteractiveTasks = make(map[int]bool)
|
||||
}
|
||||
callbacksWithInteractiveTasks[callbackId] = true
|
||||
}
|
||||
return callbacksWithInteractiveTasks
|
||||
}
|
||||
func (s *submittedTasksForAgents) getOtherCallbackIds(callbackId int) []int {
|
||||
callbacks := []int{}
|
||||
s.RLock()
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package rabbitmq
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"strconv"
|
||||
@@ -10,6 +11,7 @@ import (
|
||||
databaseStructs "github.com/its-a-feature/Mythic/database/structs"
|
||||
"github.com/its-a-feature/Mythic/logging"
|
||||
"github.com/its-a-feature/Mythic/utils"
|
||||
"github.com/jmoiron/sqlx"
|
||||
)
|
||||
|
||||
// MythicTree ingest can happen on busy callback paths for file browser,
|
||||
@@ -33,6 +35,8 @@ const upsertMythicTreeNodeQuery = `INSERT INTO mythictree
|
||||
success=(mythictree.success OR :success)
|
||||
RETURNING id`
|
||||
|
||||
const mythicTreeDeleteBatchSize = 500
|
||||
|
||||
// mythicTreeNodeKey represents the database uniqueness key used by the
|
||||
// MythicTree upsert. The callback validity flag is included so an invalid
|
||||
// callback id cannot accidentally collapse with callback_id=0.
|
||||
@@ -48,7 +52,37 @@ type mythicTreeNodeKey struct {
|
||||
type fileBrowserUpdateDeletedGroup struct {
|
||||
PathData utils.AnalyzedPath
|
||||
FullPath []byte
|
||||
ChildrenByName map[string]agentMessagePostResponseFileBrowserChildren
|
||||
ChildrenByName map[string]mythicTreeChildNodeData
|
||||
}
|
||||
|
||||
// mythicTreeChildNodeData is the normalized child payload used by both file
|
||||
// browser children and file-like custom browser children before they are mapped
|
||||
// to MythicTree database rows.
|
||||
type mythicTreeChildNodeData struct {
|
||||
Name string
|
||||
DisplayPath []byte
|
||||
CanHaveChildren bool
|
||||
Metadata databaseStructs.MythicJSONText
|
||||
}
|
||||
|
||||
// mythicTreeCascadeDeleteTarget describes one missing file-like node whose
|
||||
// descendants should be marked deleted along with the node itself.
|
||||
type mythicTreeCascadeDeleteTarget struct {
|
||||
Host string
|
||||
OperationID int
|
||||
TreeType string
|
||||
CallbackID int64
|
||||
CallbackIDValid bool
|
||||
FullPath []byte
|
||||
PathSeparator string
|
||||
}
|
||||
|
||||
// mythicTreeChildrenReconciliation holds the DB writes calculated for an
|
||||
// update_deleted parent path without applying them immediately.
|
||||
type mythicTreeChildrenReconciliation struct {
|
||||
TreeNodesToUpsert []*databaseStructs.MythicTree
|
||||
IDsToDelete []int
|
||||
CascadeDeleteTargets []mythicTreeCascadeDeleteTarget
|
||||
}
|
||||
|
||||
// upsertMythicTreeNode is the compatibility wrapper for callers that still need
|
||||
@@ -107,6 +141,175 @@ func upsertMythicTreeNodes(treeNodes []*databaseStructs.MythicTree) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// deleteMythicTreeNodes marks MythicTree rows deleted in bulk. File-like trees
|
||||
// can pass cascade targets so descendants are deleted with chunked SQL updates
|
||||
// instead of one deleteTreeNode call per missing child.
|
||||
func deleteMythicTreeNodes(taskID int, idsToDelete []int, cascadeTargets []mythicTreeCascadeDeleteTarget) error {
|
||||
idsToDelete = dedupeMythicTreeDeleteIDs(idsToDelete)
|
||||
cascadeTargets = dedupeMythicTreeCascadeDeleteTargets(cascadeTargets)
|
||||
if len(idsToDelete) == 0 && len(cascadeTargets) == 0 {
|
||||
return nil
|
||||
}
|
||||
transaction, err := database.DB.Beginx()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
committed := false
|
||||
defer func() {
|
||||
if !committed {
|
||||
_ = transaction.Rollback()
|
||||
}
|
||||
}()
|
||||
for start := 0; start < len(cascadeTargets); start += mythicTreeDeleteBatchSize {
|
||||
end := start + mythicTreeDeleteBatchSize
|
||||
if end > len(cascadeTargets) {
|
||||
end = len(cascadeTargets)
|
||||
}
|
||||
if err = deleteMythicTreeDescendantBatch(transaction, taskID, cascadeTargets[start:end]); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
for start := 0; start < len(idsToDelete); start += mythicTreeDeleteBatchSize {
|
||||
end := start + mythicTreeDeleteBatchSize
|
||||
if end > len(idsToDelete) {
|
||||
end = len(idsToDelete)
|
||||
}
|
||||
if err = deleteMythicTreeIDBatch(transaction, taskID, idsToDelete[start:end]); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if err = transaction.Commit(); err != nil {
|
||||
return err
|
||||
}
|
||||
committed = true
|
||||
return nil
|
||||
}
|
||||
|
||||
// deleteMythicTreeIDBatch marks exact MythicTree IDs deleted inside the caller's
|
||||
// transaction.
|
||||
func deleteMythicTreeIDBatch(transaction *sqlx.Tx, taskID int, idsToDelete []int) error {
|
||||
if len(idsToDelete) == 0 {
|
||||
return nil
|
||||
}
|
||||
deleteQuery, args, err := sqlx.Named(`UPDATE mythictree SET
|
||||
deleted=true, "timestamp"=now(), task_id=:task_id
|
||||
WHERE id IN (:ids)`, map[string]interface{}{
|
||||
"ids": idsToDelete,
|
||||
"task_id": taskID,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
deleteQuery, args, err = sqlx.In(deleteQuery, args...)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
deleteQuery = database.DB.Rebind(deleteQuery)
|
||||
_, err = transaction.Exec(deleteQuery, args...)
|
||||
return err
|
||||
}
|
||||
|
||||
// deleteMythicTreeDescendantBatch marks descendants of missing file-like nodes
|
||||
// deleted in one query. It uses exact parent_path plus separator-aware byte
|
||||
// prefix matching so /tmp/foo does not delete /tmp/foobar, and paths containing
|
||||
// SQL wildcard characters are treated as literal bytes.
|
||||
func deleteMythicTreeDescendantBatch(transaction *sqlx.Tx, taskID int, cascadeTargets []mythicTreeCascadeDeleteTarget) error {
|
||||
if len(cascadeTargets) == 0 {
|
||||
return nil
|
||||
}
|
||||
args := map[string]interface{}{
|
||||
"task_id": taskID,
|
||||
}
|
||||
clauses := make([]string, 0, len(cascadeTargets))
|
||||
for index, target := range cascadeTargets {
|
||||
callbackClause := "callback_id IS NULL"
|
||||
if target.CallbackIDValid {
|
||||
callbackKey := fmt.Sprintf("callback_id_%d", index)
|
||||
callbackClause = "callback_id=:" + callbackKey
|
||||
args[callbackKey] = target.CallbackID
|
||||
}
|
||||
args[fmt.Sprintf("host_%d", index)] = target.Host
|
||||
args[fmt.Sprintf("operation_id_%d", index)] = target.OperationID
|
||||
args[fmt.Sprintf("tree_type_%d", index)] = target.TreeType
|
||||
args[fmt.Sprintf("child_parent_path_%d", index)] = cloneByteSliceForDatabase(target.FullPath)
|
||||
args[fmt.Sprintf("descendant_parent_path_%d", index)] = getMythicTreeDescendantParentPathPrefix(target.FullPath, target.PathSeparator)
|
||||
clauses = append(clauses, fmt.Sprintf(
|
||||
`(host=:host_%d AND operation_id=:operation_id_%d AND tree_type=:tree_type_%d AND %s AND (parent_path=:child_parent_path_%d OR substring(parent_path from 1 for length(:descendant_parent_path_%d))=:descendant_parent_path_%d))`,
|
||||
index, index, index, callbackClause, index, index, index))
|
||||
}
|
||||
_, err := transaction.NamedExec(`UPDATE mythictree SET
|
||||
deleted=true, "timestamp"=now(), task_id=:task_id
|
||||
WHERE `+strings.Join(clauses, " OR "), args)
|
||||
return err
|
||||
}
|
||||
|
||||
// getMythicTreeDescendantParentPathPrefix returns the byte prefix for
|
||||
// descendant parent paths below a deleted node.
|
||||
func getMythicTreeDescendantParentPathPrefix(fullPath []byte, pathSeparator string) []byte {
|
||||
prefix := cloneByteSliceForDatabase(fullPath)
|
||||
separatorBytes := []byte(pathSeparator)
|
||||
if len(separatorBytes) > 0 && len(prefix) > 0 && !bytes.HasSuffix(prefix, separatorBytes) {
|
||||
prefix = append(prefix, separatorBytes...)
|
||||
}
|
||||
return prefix
|
||||
}
|
||||
|
||||
// dedupeMythicTreeDeleteIDs avoids redundant exact-row updates when multiple
|
||||
// reconciliation groups point at the same row.
|
||||
func dedupeMythicTreeDeleteIDs(idsToDelete []int) []int {
|
||||
if len(idsToDelete) == 0 {
|
||||
return idsToDelete
|
||||
}
|
||||
deduped := make([]int, 0, len(idsToDelete))
|
||||
seen := make(map[int]struct{}, len(idsToDelete))
|
||||
for _, id := range idsToDelete {
|
||||
if _, ok := seen[id]; ok {
|
||||
continue
|
||||
}
|
||||
seen[id] = struct{}{}
|
||||
deduped = append(deduped, id)
|
||||
}
|
||||
return deduped
|
||||
}
|
||||
|
||||
// dedupeMythicTreeCascadeDeleteTargets removes duplicate cascade scopes before
|
||||
// building the chunked SQL OR clauses.
|
||||
func dedupeMythicTreeCascadeDeleteTargets(targets []mythicTreeCascadeDeleteTarget) []mythicTreeCascadeDeleteTarget {
|
||||
if len(targets) == 0 {
|
||||
return targets
|
||||
}
|
||||
deduped := make([]mythicTreeCascadeDeleteTarget, 0, len(targets))
|
||||
seen := make(map[string]struct{}, len(targets))
|
||||
for _, target := range targets {
|
||||
key := strings.Join([]string{
|
||||
target.Host,
|
||||
strconv.Itoa(target.OperationID),
|
||||
target.TreeType,
|
||||
strconv.FormatBool(target.CallbackIDValid),
|
||||
strconv.FormatInt(target.CallbackID, 10),
|
||||
string(target.FullPath),
|
||||
target.PathSeparator,
|
||||
}, "\x00")
|
||||
if _, ok := seen[key]; ok {
|
||||
continue
|
||||
}
|
||||
seen[key] = struct{}{}
|
||||
target.FullPath = cloneByteSliceForDatabase(target.FullPath)
|
||||
deduped = append(deduped, target)
|
||||
}
|
||||
return deduped
|
||||
}
|
||||
|
||||
// ensureTaskCallbackMythicTreeGroups lazily fills callback mythictree_groups for
|
||||
// direct RPC/webhook paths that only loaded the callback id. Agent message
|
||||
// post-response tasks already include the groups in their initial task query.
|
||||
func ensureTaskCallbackMythicTreeGroups(task *databaseStructs.Task) error {
|
||||
if task == nil || len(task.Callback.MythicTreeGroups) > 0 || task.Callback.ID <= 0 {
|
||||
return nil
|
||||
}
|
||||
return database.DB.Get(&task.Callback.MythicTreeGroups, `SELECT mythictree_groups FROM callback WHERE id=$1`, task.Callback.ID)
|
||||
}
|
||||
|
||||
// normalizeMythicTreeNodesForBatch applies required-field defaults while
|
||||
// preserving the old per-row behavior for invalid entries: skip the bad node and
|
||||
// continue writing the rest of the batch.
|
||||
@@ -279,21 +482,33 @@ func buildMythicTreeParentNodes(pathData utils.AnalyzedPath, displayPathData uti
|
||||
// exact MythicTree row shape used by both normal file browser handling and
|
||||
// update_deleted reconciliation.
|
||||
func buildFileBrowserChildMythicTreeNode(task databaseStructs.Task, host string, parentFullPath []byte, pathSeparator string, child agentMessagePostResponseFileBrowserChildren, os string, apitokensId int) *databaseStructs.MythicTree {
|
||||
return buildMythicTreeChildNode(task, host, parentFullPath, pathSeparator, databaseStructs.TREE_TYPE_FILE, fileBrowserChildNodeData(child), os, apitokensId)
|
||||
}
|
||||
|
||||
// buildCustomBrowserChildMythicTreeNode maps file-like custom browser children
|
||||
// into the same MythicTree child shape used by file browser responses.
|
||||
func buildCustomBrowserChildMythicTreeNode(task databaseStructs.Task, host string, parentFullPath []byte, pathSeparator string, treeType string, child agentMessagePostResponseCustomBrowserChildren, os string, apitokensId int) *databaseStructs.MythicTree {
|
||||
return buildMythicTreeChildNode(task, host, parentFullPath, pathSeparator, treeType, customBrowserChildNodeData(child), os, apitokensId)
|
||||
}
|
||||
|
||||
// buildMythicTreeChildNode owns the common child-row mechanics for file browser
|
||||
// and file-like custom browser data: path construction, callback/api-token
|
||||
// attribution, and metadata assignment.
|
||||
func buildMythicTreeChildNode(task databaseStructs.Task, host string, parentFullPath []byte, pathSeparator string, treeType string, child mythicTreeChildNodeData, os string, apitokensId int) *databaseStructs.MythicTree {
|
||||
newTreeChild := databaseStructs.MythicTree{
|
||||
Host: host,
|
||||
TaskID: task.ID,
|
||||
OperationID: task.OperationID,
|
||||
Name: []byte(child.Name),
|
||||
ParentPath: parentFullPath,
|
||||
TreeType: databaseStructs.TREE_TYPE_FILE,
|
||||
CanHaveChildren: !child.IsFile,
|
||||
TreeType: treeType,
|
||||
CanHaveChildren: child.CanHaveChildren,
|
||||
Deleted: false,
|
||||
Os: os,
|
||||
DisplayPath: []byte{},
|
||||
DisplayPath: child.DisplayPath,
|
||||
}
|
||||
newTreeChild.FullPath = treeNodeGetFullPath(parentFullPath, []byte(child.Name), []byte(pathSeparator), databaseStructs.TREE_TYPE_FILE)
|
||||
fileMetaData := addChildFilePermissions(&child)
|
||||
newTreeChild.Metadata = GetMythicJSONTextFromStruct(fileMetaData)
|
||||
newTreeChild.FullPath = treeNodeGetFullPath(parentFullPath, []byte(child.Name), []byte(pathSeparator), treeType)
|
||||
newTreeChild.Metadata = child.Metadata
|
||||
newTreeChild.CallbackID.Valid = true
|
||||
newTreeChild.CallbackID.Int64 = int64(task.Callback.ID)
|
||||
if apitokensId > 0 {
|
||||
@@ -303,6 +518,41 @@ func buildFileBrowserChildMythicTreeNode(task databaseStructs.Task, host string,
|
||||
return &newTreeChild
|
||||
}
|
||||
|
||||
// customBrowserChildNodeData keeps custom-browser display path and metadata
|
||||
// mapping separate from the shared child-node construction logic.
|
||||
func customBrowserChildNodeData(child agentMessagePostResponseCustomBrowserChildren) mythicTreeChildNodeData {
|
||||
return mythicTreeChildNodeData{
|
||||
Name: child.Name,
|
||||
DisplayPath: []byte(child.DisplayPath),
|
||||
CanHaveChildren: child.CanHaveChildren,
|
||||
Metadata: GetMythicJSONTextFromStruct(child.Metadata),
|
||||
}
|
||||
}
|
||||
|
||||
// fileBrowserChildNodeData keeps the file-browser-specific metadata conversion
|
||||
// separate from the common MythicTree child builder.
|
||||
func fileBrowserChildNodeData(child agentMessagePostResponseFileBrowserChildren) mythicTreeChildNodeData {
|
||||
return mythicTreeChildNodeData{
|
||||
Name: child.Name,
|
||||
CanHaveChildren: !child.IsFile,
|
||||
Metadata: GetMythicJSONTextFromStruct(addChildFilePermissions(&child)),
|
||||
}
|
||||
}
|
||||
|
||||
// customBrowserChildrenByName converts a custom browser child slice into the
|
||||
// common reconciliation map. The last entry for a duplicate child name wins,
|
||||
// matching the existing single-row upsert behavior.
|
||||
func customBrowserChildrenByName(children *[]agentMessagePostResponseCustomBrowserChildren) map[string]mythicTreeChildNodeData {
|
||||
if children == nil {
|
||||
return map[string]mythicTreeChildNodeData{}
|
||||
}
|
||||
childrenByName := make(map[string]mythicTreeChildNodeData, len(*children))
|
||||
for _, child := range *children {
|
||||
childrenByName[child.Name] = customBrowserChildNodeData(child)
|
||||
}
|
||||
return childrenByName
|
||||
}
|
||||
|
||||
// groupFileBrowserUpdateDeletedEntries keeps chunked listings for the same
|
||||
// directory together while preventing entries for different directories in the
|
||||
// same task from being reconciled against the first path only.
|
||||
@@ -328,14 +578,14 @@ func groupFileBrowserUpdateDeletedEntries(task *databaseStructs.Task, fileBrowse
|
||||
groups = append(groups, fileBrowserUpdateDeletedGroup{
|
||||
PathData: pathData,
|
||||
FullPath: fullPath,
|
||||
ChildrenByName: make(map[string]agentMessagePostResponseFileBrowserChildren),
|
||||
ChildrenByName: make(map[string]mythicTreeChildNodeData),
|
||||
})
|
||||
}
|
||||
if fileBrowser.Files == nil {
|
||||
continue
|
||||
}
|
||||
for _, newEntry := range *fileBrowser.Files {
|
||||
groups[groupIndex].ChildrenByName[newEntry.Name] = newEntry
|
||||
groups[groupIndex].ChildrenByName[newEntry.Name] = fileBrowserChildNodeData(newEntry)
|
||||
}
|
||||
}
|
||||
return groups, nil
|
||||
@@ -372,63 +622,150 @@ func getFileBrowserUpdateDeletedPath(task *databaseStructs.Task, fileBrowser *ag
|
||||
// mark DB-only children deleted. Each group is independent so a task can report
|
||||
// multiple directories without cross-contaminating their child sets.
|
||||
func reconcileFileBrowserUpdateDeletedGroup(task *databaseStructs.Task, group fileBrowserUpdateDeletedGroup, apitokensId int) error {
|
||||
var existingTreeEntries []databaseStructs.MythicTree
|
||||
err := database.DB.Select(&existingTreeEntries, `SELECT
|
||||
id, "name", success, full_path, parent_path, operation_id, host, tree_type, callback_id, os, display_path
|
||||
FROM mythictree WHERE
|
||||
parent_path=$1 AND operation_id=$2 AND host=$3 AND tree_type=$4`,
|
||||
group.FullPath, task.OperationID, group.PathData.Host, databaseStructs.TREE_TYPE_FILE)
|
||||
return reconcileFileBrowserUpdateDeletedGroups(task, []fileBrowserUpdateDeletedGroup{group}, apitokensId)
|
||||
}
|
||||
|
||||
// reconcileFileBrowserUpdateDeletedGroups batches update_deleted writes across
|
||||
// all file-browser chunks for a task completion flush.
|
||||
func reconcileFileBrowserUpdateDeletedGroups(task *databaseStructs.Task, groups []fileBrowserUpdateDeletedGroup, apitokensId int) error {
|
||||
reconciliation := mythicTreeChildrenReconciliation{}
|
||||
for _, group := range groups {
|
||||
groupReconciliation, err := buildMythicTreeChildrenReconciliation(
|
||||
task,
|
||||
group.PathData.Host,
|
||||
group.FullPath,
|
||||
group.PathData.PathSeparator,
|
||||
databaseStructs.TREE_TYPE_FILE,
|
||||
getOSTypeBasedOnPathSeparator(group.PathData.PathSeparator, databaseStructs.TREE_TYPE_FILE),
|
||||
group.ChildrenByName,
|
||||
apitokensId,
|
||||
true)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
appendMythicTreeChildrenReconciliation(&reconciliation, groupReconciliation)
|
||||
}
|
||||
return applyMythicTreeChildrenReconciliation(task.ID, reconciliation)
|
||||
}
|
||||
|
||||
// reconcileMythicTreeChildren is the single-parent convenience wrapper around
|
||||
// the batch reconciliation builder and applier.
|
||||
func reconcileMythicTreeChildren(task *databaseStructs.Task, host string, parentFullPath []byte, pathSeparator string, treeType string, os string, childrenByName map[string]mythicTreeChildNodeData, apitokensId int, preserveExistingDisplayPathWhenEmpty bool) error {
|
||||
reconciliation, err := buildMythicTreeChildrenReconciliation(task, host, parentFullPath, pathSeparator, treeType, os, childrenByName, apitokensId, preserveExistingDisplayPathWhenEmpty)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
namesToDeleteAndUpdate := make(map[string]struct{}, len(existingTreeEntries)+len(group.ChildrenByName))
|
||||
treeNodesToUpsert := make([]*databaseStructs.MythicTree, 0, len(group.ChildrenByName))
|
||||
return applyMythicTreeChildrenReconciliation(task.ID, reconciliation)
|
||||
}
|
||||
|
||||
// buildMythicTreeChildrenReconciliation calculates update_deleted changes for
|
||||
// one parent path without applying them. It scopes existing rows the same way
|
||||
// process update_deleted does: same operation/host/tree and overlapping
|
||||
// callback mythictree_groups. Callers can combine many parent paths and then
|
||||
// apply one batch of upserts/deletes.
|
||||
func buildMythicTreeChildrenReconciliation(task *databaseStructs.Task, host string, parentFullPath []byte, pathSeparator string, treeType string, os string, childrenByName map[string]mythicTreeChildNodeData, apitokensId int, preserveExistingDisplayPathWhenEmpty bool) (mythicTreeChildrenReconciliation, error) {
|
||||
reconciliation := mythicTreeChildrenReconciliation{
|
||||
TreeNodesToUpsert: make([]*databaseStructs.MythicTree, 0, len(childrenByName)),
|
||||
}
|
||||
if err := ensureTaskCallbackMythicTreeGroups(task); err != nil {
|
||||
return reconciliation, err
|
||||
}
|
||||
var existingTreeEntries []databaseStructs.MythicTree
|
||||
err := database.DB.Select(&existingTreeEntries, `SELECT
|
||||
mythictree.id, mythictree."name", mythictree.success, mythictree.full_path, mythictree.parent_path,
|
||||
mythictree.operation_id, mythictree.host, mythictree.tree_type, mythictree.callback_id,
|
||||
mythictree.os, mythictree.display_path
|
||||
FROM mythictree
|
||||
JOIN callback ON mythictree.callback_id = callback.id
|
||||
WHERE
|
||||
mythictree.parent_path=$1 AND mythictree.operation_id=$2 AND mythictree.host=$3 AND
|
||||
mythictree.tree_type=$4 AND callback.mythictree_groups && $5`,
|
||||
parentFullPath, task.OperationID, host, treeType, task.Callback.MythicTreeGroups)
|
||||
if err != nil {
|
||||
return reconciliation, err
|
||||
}
|
||||
namesToDeleteAndUpdate := make(map[string]struct{}, len(existingTreeEntries)+len(childrenByName))
|
||||
for _, existingEntry := range existingTreeEntries {
|
||||
if newEntry, ok := group.ChildrenByName[string(existingEntry.Name)]; ok {
|
||||
if newEntry, ok := childrenByName[string(existingEntry.Name)]; ok {
|
||||
namesToDeleteAndUpdate[newEntry.Name] = struct{}{}
|
||||
displayPath := newEntry.DisplayPath
|
||||
if preserveExistingDisplayPathWhenEmpty && len(displayPath) == 0 {
|
||||
displayPath = existingEntry.DisplayPath
|
||||
}
|
||||
newTreeChild := databaseStructs.MythicTree{
|
||||
Host: group.PathData.Host,
|
||||
Host: host,
|
||||
TaskID: task.ID,
|
||||
OperationID: task.OperationID,
|
||||
Name: []byte(newEntry.Name),
|
||||
ParentPath: existingEntry.ParentPath,
|
||||
FullPath: existingEntry.FullPath,
|
||||
TreeType: databaseStructs.TREE_TYPE_FILE,
|
||||
CanHaveChildren: !newEntry.IsFile,
|
||||
TreeType: treeType,
|
||||
CanHaveChildren: newEntry.CanHaveChildren,
|
||||
Deleted: false,
|
||||
Success: existingEntry.Success,
|
||||
ID: existingEntry.ID,
|
||||
Os: existingEntry.Os,
|
||||
DisplayPath: existingEntry.DisplayPath,
|
||||
DisplayPath: displayPath,
|
||||
CallbackID: existingEntry.CallbackID,
|
||||
}
|
||||
fileMetaData := addChildFilePermissions(&newEntry)
|
||||
newTreeChild.Metadata = GetMythicJSONTextFromStruct(fileMetaData)
|
||||
if newTreeChild.Os == "" {
|
||||
newTreeChild.Os = os
|
||||
}
|
||||
newTreeChild.Metadata = newEntry.Metadata
|
||||
if !newTreeChild.CallbackID.Valid {
|
||||
newTreeChild.CallbackID.Valid = true
|
||||
newTreeChild.CallbackID.Int64 = int64(task.Callback.ID)
|
||||
}
|
||||
treeNodesToUpsert = append(treeNodesToUpsert, &newTreeChild)
|
||||
if apitokensId > 0 {
|
||||
newTreeChild.APITokensID.Valid = true
|
||||
newTreeChild.APITokensID.Int64 = int64(apitokensId)
|
||||
}
|
||||
reconciliation.TreeNodesToUpsert = append(reconciliation.TreeNodesToUpsert, &newTreeChild)
|
||||
} else {
|
||||
namesToDeleteAndUpdate[string(existingEntry.Name)] = struct{}{}
|
||||
existingEntry.Deleted = true
|
||||
existingEntry.TaskID = task.ID
|
||||
deleteTreeNode(existingEntry, true)
|
||||
reconciliation.IDsToDelete = append(reconciliation.IDsToDelete, existingEntry.ID)
|
||||
reconciliation.CascadeDeleteTargets = append(reconciliation.CascadeDeleteTargets, mythicTreeCascadeDeleteTarget{
|
||||
Host: existingEntry.Host,
|
||||
OperationID: existingEntry.OperationID,
|
||||
TreeType: existingEntry.TreeType,
|
||||
CallbackID: existingEntry.CallbackID.Int64,
|
||||
CallbackIDValid: existingEntry.CallbackID.Valid,
|
||||
FullPath: existingEntry.FullPath,
|
||||
PathSeparator: pathSeparator,
|
||||
})
|
||||
}
|
||||
}
|
||||
for name, newEntry := range group.ChildrenByName {
|
||||
for name, newEntry := range childrenByName {
|
||||
if _, ok := namesToDeleteAndUpdate[name]; !ok {
|
||||
treeNodesToUpsert = append(treeNodesToUpsert, buildFileBrowserChildMythicTreeNode(
|
||||
reconciliation.TreeNodesToUpsert = append(reconciliation.TreeNodesToUpsert, buildMythicTreeChildNode(
|
||||
*task,
|
||||
group.PathData.Host,
|
||||
group.FullPath,
|
||||
group.PathData.PathSeparator,
|
||||
host,
|
||||
parentFullPath,
|
||||
pathSeparator,
|
||||
treeType,
|
||||
newEntry,
|
||||
getOSTypeBasedOnPathSeparator(group.PathData.PathSeparator, databaseStructs.TREE_TYPE_FILE),
|
||||
os,
|
||||
apitokensId))
|
||||
}
|
||||
}
|
||||
return upsertMythicTreeNodes(treeNodesToUpsert)
|
||||
return reconciliation, nil
|
||||
}
|
||||
|
||||
// appendMythicTreeChildrenReconciliation merges independently calculated
|
||||
// update_deleted changes so callers can issue one write batch.
|
||||
func appendMythicTreeChildrenReconciliation(target *mythicTreeChildrenReconciliation, incoming mythicTreeChildrenReconciliation) {
|
||||
target.TreeNodesToUpsert = append(target.TreeNodesToUpsert, incoming.TreeNodesToUpsert...)
|
||||
target.IDsToDelete = append(target.IDsToDelete, incoming.IDsToDelete...)
|
||||
target.CascadeDeleteTargets = append(target.CascadeDeleteTargets, incoming.CascadeDeleteTargets...)
|
||||
}
|
||||
|
||||
// applyMythicTreeChildrenReconciliation writes all queued update_deleted
|
||||
// upserts first, then marks stale exact rows and descendants deleted in bulk.
|
||||
func applyMythicTreeChildrenReconciliation(taskID int, reconciliation mythicTreeChildrenReconciliation) error {
|
||||
if err := upsertMythicTreeNodes(reconciliation.TreeNodesToUpsert); err != nil {
|
||||
return err
|
||||
}
|
||||
return deleteMythicTreeNodes(taskID, reconciliation.IDsToDelete, reconciliation.CascadeDeleteTargets)
|
||||
}
|
||||
|
||||
// buildProcessMythicTreeNode centralizes process-to-MythicTree normalization so
|
||||
|
||||
@@ -137,6 +137,85 @@ func TestBuildFileBrowserChildMythicTreeNodeDoesNotAliasSiblingFullPaths(t *test
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildCustomBrowserChildMythicTreeNodePreservesDisplayPath(t *testing.T) {
|
||||
task := databaseStructs.Task{
|
||||
ID: 5,
|
||||
OperationID: 6,
|
||||
}
|
||||
task.Callback.ID = 7
|
||||
|
||||
child := agentMessagePostResponseCustomBrowserChildren{
|
||||
Name: "123",
|
||||
DisplayPath: "sshd",
|
||||
CanHaveChildren: true,
|
||||
Metadata: map[string]interface{}{"name": "sshd"},
|
||||
}
|
||||
node := buildCustomBrowserChildMythicTreeNode(task, "HOST", []byte("/processes"), "/", databaseStructs.TREE_TYPE_FILE, child, "linux", 0)
|
||||
|
||||
if string(node.FullPath) != "/processes/123" {
|
||||
t.Fatalf("expected custom child full path to include its name, got %q", string(node.FullPath))
|
||||
}
|
||||
if string(node.DisplayPath) != "sshd" {
|
||||
t.Fatalf("expected custom child display path to be preserved, got %q", string(node.DisplayPath))
|
||||
}
|
||||
if !node.CanHaveChildren {
|
||||
t.Fatalf("expected custom child can_have_children to be preserved")
|
||||
}
|
||||
if node.Metadata.StructValue()["name"] != "sshd" {
|
||||
t.Fatalf("expected custom child metadata to be preserved, got %#v", node.Metadata.StructValue())
|
||||
}
|
||||
}
|
||||
|
||||
func TestCustomBrowserChildrenByNameUsesLastDuplicate(t *testing.T) {
|
||||
children := []agentMessagePostResponseCustomBrowserChildren{
|
||||
{Name: "alpha", DisplayPath: "old"},
|
||||
{Name: "alpha", DisplayPath: "new"},
|
||||
{Name: "bravo", DisplayPath: "bravo"},
|
||||
}
|
||||
|
||||
childrenByName := customBrowserChildrenByName(&children)
|
||||
|
||||
if len(childrenByName) != 2 {
|
||||
t.Fatalf("expected duplicate custom child names to collapse to two entries, got %d", len(childrenByName))
|
||||
}
|
||||
if string(childrenByName["alpha"].DisplayPath) != "new" {
|
||||
t.Fatalf("expected duplicate custom child to keep last display path, got %q", string(childrenByName["alpha"].DisplayPath))
|
||||
}
|
||||
}
|
||||
|
||||
func TestMythicTreeDescendantParentPathPrefixIsSeparatorAware(t *testing.T) {
|
||||
if prefix := getMythicTreeDescendantParentPathPrefix([]byte("/tmp/foo"), "/"); string(prefix) != "/tmp/foo/" {
|
||||
t.Fatalf("expected slash descendant prefix to include separator, got %q", string(prefix))
|
||||
}
|
||||
if prefix := getMythicTreeDescendantParentPathPrefix([]byte("/tmp/foo/"), "/"); string(prefix) != "/tmp/foo/" {
|
||||
t.Fatalf("expected existing trailing separator to be preserved, got %q", string(prefix))
|
||||
}
|
||||
if prefix := getMythicTreeDescendantParentPathPrefix([]byte(`C:\Windows`), `\`); string(prefix) != `C:\Windows\` {
|
||||
t.Fatalf("expected windows descendant prefix to include separator, got %q", string(prefix))
|
||||
}
|
||||
if prefix := getMythicTreeDescendantParentPathPrefix([]byte(`/tmp/100%_literal`), `/`); string(prefix) != `/tmp/100%_literal/` {
|
||||
t.Fatalf("expected SQL wildcard bytes to remain literal, got %q", string(prefix))
|
||||
}
|
||||
}
|
||||
|
||||
func TestDedupeMythicTreeCascadeDeleteTargets(t *testing.T) {
|
||||
targets := []mythicTreeCascadeDeleteTarget{
|
||||
{Host: "HOST", OperationID: 1, TreeType: databaseStructs.TREE_TYPE_FILE, CallbackID: 2, CallbackIDValid: true, FullPath: []byte("/tmp/alpha"), PathSeparator: "/"},
|
||||
{Host: "HOST", OperationID: 1, TreeType: databaseStructs.TREE_TYPE_FILE, CallbackID: 2, CallbackIDValid: true, FullPath: []byte("/tmp/alpha"), PathSeparator: "/"},
|
||||
{Host: "HOST", OperationID: 1, TreeType: databaseStructs.TREE_TYPE_FILE, CallbackID: 3, CallbackIDValid: true, FullPath: []byte("/tmp/alpha"), PathSeparator: "/"},
|
||||
}
|
||||
|
||||
deduped := dedupeMythicTreeCascadeDeleteTargets(targets)
|
||||
|
||||
if len(deduped) != 2 {
|
||||
t.Fatalf("expected duplicate cascade targets to collapse to two entries, got %d", len(deduped))
|
||||
}
|
||||
targets[0].FullPath[0] = 'X'
|
||||
if string(deduped[0].FullPath) != "/tmp/alpha" {
|
||||
t.Fatalf("expected deduped cascade target to own its full path bytes, got %q", string(deduped[0].FullPath))
|
||||
}
|
||||
}
|
||||
|
||||
func TestGroupFileBrowserUpdateDeletedEntriesMergesChunksForSameDirectory(t *testing.T) {
|
||||
task := databaseStructs.Task{}
|
||||
task.Callback.Host = "HOST"
|
||||
|
||||
@@ -0,0 +1,151 @@
|
||||
package rabbitmq
|
||||
|
||||
import (
|
||||
"sync"
|
||||
|
||||
databaseStructs "github.com/its-a-feature/Mythic/database/structs"
|
||||
"github.com/its-a-feature/Mythic/logging"
|
||||
)
|
||||
|
||||
// mythicTreeIngestMessageKind tells the shared MythicTree worker how to route
|
||||
// one queued ingest message without needing separate queues for each browser
|
||||
// type.
|
||||
type mythicTreeIngestMessageKind int
|
||||
|
||||
const (
|
||||
mythicTreeIngestFileBrowser mythicTreeIngestMessageKind = iota
|
||||
mythicTreeIngestFileBrowserFlush
|
||||
mythicTreeIngestProcess
|
||||
mythicTreeIngestCustomBrowser
|
||||
)
|
||||
|
||||
// mythicTreeIngestMessage is the bounded-queue payload for all MythicTree write
|
||||
// paths. Only the field that matches Kind is populated.
|
||||
type mythicTreeIngestMessage struct {
|
||||
Kind mythicTreeIngestMessageKind
|
||||
Task databaseStructs.Task
|
||||
FileBrowser *agentMessagePostResponseFileBrowser
|
||||
Processes *[]agentMessagePostResponseProcesses
|
||||
CustomBrowser *agentMessagePostResponseCustomBrowser
|
||||
APITokenID int
|
||||
}
|
||||
|
||||
var mythicTreeWorkerChannels []chan mythicTreeIngestMessage
|
||||
var mythicTreeIngestWorkersOnce sync.Once
|
||||
|
||||
// listenForMythicTreeData initializes one bounded worker pool for MythicTree
|
||||
// ingest. File browser, process browser, and custom browser all write the same
|
||||
// table, so one sharded queue gives us one place to control DB pressure during
|
||||
// high-rate callback check-ins.
|
||||
func listenForMythicTreeData() {
|
||||
mythicTreeIngestWorkersOnce.Do(initializeMythicTreeIngestWorkers)
|
||||
}
|
||||
|
||||
// initializeMythicTreeIngestWorkers starts the update_deleted coordinator and a
|
||||
// sharded worker pool sized from the existing post-response worker settings.
|
||||
func initializeMythicTreeIngestWorkers() {
|
||||
go listenForFileBrowserUpdateDeleted()
|
||||
workerCount := getAgentMessagePostResponseWorkerCount()
|
||||
queueSize := getAgentMessagePostResponseQueueSize()
|
||||
mythicTreeWorkerChannels = make([]chan mythicTreeIngestMessage, workerCount)
|
||||
for i := 0; i < workerCount; i++ {
|
||||
mythicTreeWorkerChannels[i] = make(chan mythicTreeIngestMessage, queueSize)
|
||||
go listenForMythicTreeWorker(i, mythicTreeWorkerChannels[i])
|
||||
}
|
||||
}
|
||||
|
||||
// enqueueMythicTreeFileBrowserResponse queues a single file browser response so
|
||||
// agent post-response processing is not blocked on MythicTree DB writes.
|
||||
func enqueueMythicTreeFileBrowserResponse(task databaseStructs.Task, fileBrowser *agentMessagePostResponseFileBrowser, apitokensId int) {
|
||||
enqueueMythicTreeIngestMessage(mythicTreeIngestMessage{
|
||||
Kind: mythicTreeIngestFileBrowser,
|
||||
Task: task,
|
||||
FileBrowser: fileBrowser,
|
||||
APITokenID: apitokensId,
|
||||
})
|
||||
}
|
||||
|
||||
// enqueueMythicTreeFileBrowserFlush queues the completion marker that tells the
|
||||
// file-browser update_deleted coordinator it can reconcile cached chunks.
|
||||
func enqueueMythicTreeFileBrowserFlush(task databaseStructs.Task, apitokensId int) {
|
||||
enqueueMythicTreeIngestMessage(mythicTreeIngestMessage{
|
||||
Kind: mythicTreeIngestFileBrowserFlush,
|
||||
Task: task,
|
||||
APITokenID: apitokensId,
|
||||
})
|
||||
}
|
||||
|
||||
// enqueueMythicTreeProcessResponse queues process MythicTree data on the shared
|
||||
// ingest pool, preserving the direct RPC path for callers that need it.
|
||||
func enqueueMythicTreeProcessResponse(task databaseStructs.Task, processes *[]agentMessagePostResponseProcesses, apitokensId int) {
|
||||
if processes == nil {
|
||||
return
|
||||
}
|
||||
enqueueMythicTreeIngestMessage(mythicTreeIngestMessage{
|
||||
Kind: mythicTreeIngestProcess,
|
||||
Task: task,
|
||||
Processes: processes,
|
||||
APITokenID: apitokensId,
|
||||
})
|
||||
}
|
||||
|
||||
// enqueueMythicTreeCustomBrowserResponse queues custom-browser MythicTree data
|
||||
// on the same bounded pool as file and process browser data.
|
||||
func enqueueMythicTreeCustomBrowserResponse(task databaseStructs.Task, customBrowser *agentMessagePostResponseCustomBrowser) {
|
||||
if customBrowser == nil {
|
||||
return
|
||||
}
|
||||
enqueueMythicTreeIngestMessage(mythicTreeIngestMessage{
|
||||
Kind: mythicTreeIngestCustomBrowser,
|
||||
Task: task,
|
||||
CustomBrowser: customBrowser,
|
||||
})
|
||||
}
|
||||
|
||||
// enqueueMythicTreeIngestMessage lazily starts the pool and consistently shards
|
||||
// all MythicTree work for a task to the same worker. That keeps per-task flush
|
||||
// ordering intact while still allowing unrelated tasks to run in parallel.
|
||||
func enqueueMythicTreeIngestMessage(msg mythicTreeIngestMessage) {
|
||||
mythicTreeIngestWorkersOnce.Do(initializeMythicTreeIngestWorkers)
|
||||
workerID := getMythicTreeWorkerID(msg.Task.ID, len(mythicTreeWorkerChannels))
|
||||
mythicTreeWorkerChannels[workerID] <- msg
|
||||
}
|
||||
|
||||
// getMythicTreeWorkerID maps task IDs to worker shards. Keeping this tiny and
|
||||
// deterministic makes it easy to test ordering assumptions.
|
||||
func getMythicTreeWorkerID(taskID int, workerCount int) int {
|
||||
if workerCount <= 1 {
|
||||
return 0
|
||||
}
|
||||
workerID := taskID % workerCount
|
||||
if workerID < 0 {
|
||||
return 0
|
||||
}
|
||||
return workerID
|
||||
}
|
||||
|
||||
// listenForMythicTreeWorker serializes MythicTree writes for a shard and
|
||||
// dispatches to the existing handlers so their request-specific semantics stay
|
||||
// unchanged.
|
||||
func listenForMythicTreeWorker(workerID int, input <-chan mythicTreeIngestMessage) {
|
||||
for msg := range input {
|
||||
switch msg.Kind {
|
||||
case mythicTreeIngestFileBrowser:
|
||||
if err := HandleAgentMessagePostResponseFileBrowser(msg.Task, msg.FileBrowser, msg.APITokenID); err != nil {
|
||||
logging.LogError(err, "Failed to handle file browser MythicTree post response", "worker_id", workerID, "task_id", msg.Task.ID)
|
||||
}
|
||||
case mythicTreeIngestFileBrowserFlush:
|
||||
if err := HandleAgentMessagePostResponseFileBrowser(msg.Task, nil, msg.APITokenID); err != nil {
|
||||
logging.LogError(err, "Failed to flush file browser MythicTree post response", "worker_id", workerID, "task_id", msg.Task.ID)
|
||||
}
|
||||
case mythicTreeIngestProcess:
|
||||
if err := HandleAgentMessagePostResponseProcesses(msg.Task, msg.Processes, msg.APITokenID); err != nil {
|
||||
logging.LogError(err, "Failed to handle process MythicTree post response", "worker_id", workerID, "task_id", msg.Task.ID)
|
||||
}
|
||||
case mythicTreeIngestCustomBrowser:
|
||||
handleAgentMessagePostResponseCustomBrowser(msg.Task, msg.CustomBrowser)
|
||||
default:
|
||||
logging.LogError(nil, "Unknown MythicTree ingest message kind", "worker_id", workerID, "task_id", msg.Task.ID, "kind", msg.Kind)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
package rabbitmq
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestGetMythicTreeWorkerID(t *testing.T) {
|
||||
if workerID := getMythicTreeWorkerID(10, 8); workerID != 2 {
|
||||
t.Fatalf("expected task 10 to shard to worker 2, got %d", workerID)
|
||||
}
|
||||
if workerID := getMythicTreeWorkerID(-1, 8); workerID != 0 {
|
||||
t.Fatalf("expected negative task id to shard to worker 0, got %d", workerID)
|
||||
}
|
||||
if workerID := getMythicTreeWorkerID(10, 0); workerID != 0 {
|
||||
t.Fatalf("expected zero workers to safely shard to worker 0, got %d", workerID)
|
||||
}
|
||||
}
|
||||
@@ -72,6 +72,8 @@ type callbackPortUsage struct {
|
||||
bytesSentToAgent atomic.Int64
|
||||
lastFlushedBytesReceivedFromAgent atomic.Int64
|
||||
lastFlushedBytesSentToAgent atomic.Int64
|
||||
pendingMessagesToAgent atomic.Int64
|
||||
pendingInteractiveMessagesToAgent atomic.Int64
|
||||
acceptedConnections *[]*acceptedConnection
|
||||
messagesToAgent chan proxyToAgentMessage
|
||||
interactiveMessagesToAgent chan agentMessagePostResponseInteractive
|
||||
@@ -352,6 +354,76 @@ func cloneCallbackPortUsages(ports []*callbackPortUsage) []*callbackPortUsage {
|
||||
return append([]*callbackPortUsage(nil), ports...)
|
||||
}
|
||||
|
||||
// queueProxyDataForAgent records that a SOCKS/RPFWD message is pending before
|
||||
// placing it on the buffered channel. GetProxyData uses the counter as a cheap
|
||||
// fast path so frequent agent polls do not allocate and drain empty queues.
|
||||
func (p *callbackPortUsage) queueProxyDataForAgent(message proxyToAgentMessage) bool {
|
||||
p.pendingMessagesToAgent.Add(1)
|
||||
select {
|
||||
case p.messagesToAgent <- message:
|
||||
return true
|
||||
default:
|
||||
p.pendingMessagesToAgent.Add(-1)
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// queueInteractiveDataForAgent mirrors queueProxyDataForAgent for interactive
|
||||
// terminal data. Interactive task lookup remains separate because it can create
|
||||
// outbound data even when the channel itself is empty.
|
||||
func (p *callbackPortUsage) queueInteractiveDataForAgent(message agentMessagePostResponseInteractive) bool {
|
||||
p.pendingInteractiveMessagesToAgent.Add(1)
|
||||
select {
|
||||
case p.interactiveMessagesToAgent <- message:
|
||||
return true
|
||||
default:
|
||||
p.pendingInteractiveMessagesToAgent.Add(-1)
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
func (p *callbackPortUsage) hasPendingProxyDataForAgent() bool {
|
||||
return p.pendingMessagesToAgent.Load() > 0 || len(p.messagesToAgent) > 0
|
||||
}
|
||||
|
||||
func (p *callbackPortUsage) hasPendingInteractiveDataForAgent() bool {
|
||||
return p.pendingInteractiveMessagesToAgent.Load() > 0 || len(p.interactiveMessagesToAgent) > 0
|
||||
}
|
||||
|
||||
func (p *callbackPortUsage) hasPendingDataForAgent() bool {
|
||||
switch p.PortType {
|
||||
case CALLBACK_PORT_TYPE_INTERACTIVE:
|
||||
return p.hasPendingInteractiveDataForAgent()
|
||||
case CALLBACK_PORT_TYPE_SOCKS:
|
||||
fallthrough
|
||||
case CALLBACK_PORT_TYPE_RPORTFWD:
|
||||
return p.hasPendingProxyDataForAgent()
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
func callbackPortUsagesHavePendingData(ports []*callbackPortUsage) bool {
|
||||
for _, port := range ports {
|
||||
if port.hasPendingDataForAgent() {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func decrementPositiveAtomicCounter(counter *atomic.Int64) {
|
||||
for {
|
||||
current := counter.Load()
|
||||
if current <= 0 {
|
||||
return
|
||||
}
|
||||
if counter.CompareAndSwap(current, current-1) {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func clampCallbackPortByteCount(value int64) int64 {
|
||||
if value < 0 {
|
||||
return 0
|
||||
@@ -488,6 +560,43 @@ func (c *callbackPortsInUse) getPortsForCallbackByType(callbackID int, portTypes
|
||||
return portsByType
|
||||
}
|
||||
|
||||
// getPortsWithPendingDataForCallbackByType returns only ports that have data
|
||||
// buffered for the agent. The common no-data poll avoids cloning every active
|
||||
// port slice and avoids calling the drain methods that allocate response slices.
|
||||
func (c *callbackPortsInUse) getPortsWithPendingDataForCallbackByType(callbackID int, portTypes []CallbackPortType) map[CallbackPortType][]*callbackPortUsage {
|
||||
var portsByType map[CallbackPortType][]*callbackPortUsage
|
||||
appendPendingPort := func(portType CallbackPortType, port *callbackPortUsage) {
|
||||
if portsByType == nil {
|
||||
portsByType = make(map[CallbackPortType][]*callbackPortUsage, len(portTypes))
|
||||
}
|
||||
portsByType[portType] = append(portsByType[portType], port)
|
||||
}
|
||||
c.RLock()
|
||||
defer c.RUnlock()
|
||||
if c.portsByCallbackIDAndType == nil {
|
||||
for _, port := range c.ports {
|
||||
if port.CallbackID != callbackID || !port.hasPendingDataForAgent() {
|
||||
continue
|
||||
}
|
||||
for _, portType := range portTypes {
|
||||
if port.PortType == portType {
|
||||
appendPendingPort(portType, port)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
return portsByType
|
||||
}
|
||||
for _, portType := range portTypes {
|
||||
for _, port := range c.portsByCallbackIDAndType[callbackID][portType] {
|
||||
if port.hasPendingDataForAgent() {
|
||||
appendPendingPort(portType, port)
|
||||
}
|
||||
}
|
||||
}
|
||||
return portsByType
|
||||
}
|
||||
|
||||
func (c *callbackPortsInUse) findPortForRemoval(callbackId int, portType CallbackPortType, localPort int, operationId int, remoteIP string, remotePort int,
|
||||
username string, password string) *callbackPortUsage {
|
||||
c.RLock()
|
||||
@@ -654,7 +763,7 @@ func (c *callbackPortsInUse) GetDataForCallbackIdAllTypes(callbackId int) (callb
|
||||
|
||||
func (c *callbackPortsInUse) getDataForCallbackIdPortTypes(callbackId int, portTypes []CallbackPortType) (callbackProxyData, error) {
|
||||
proxyData := callbackProxyData{}
|
||||
portsByType := c.getPortsForCallbackByType(callbackId, portTypes)
|
||||
portsByType := c.getPortsWithPendingDataForCallbackByType(callbackId, portTypes)
|
||||
for _, portType := range portTypes {
|
||||
ports := portsByType[portType]
|
||||
switch portType {
|
||||
@@ -662,7 +771,7 @@ func (c *callbackPortsInUse) getDataForCallbackIdPortTypes(callbackId int, portT
|
||||
for _, port := range ports {
|
||||
proxyData.Interactive = append(proxyData.Interactive, port.GetData().([]agentMessagePostResponseInteractive)...)
|
||||
}
|
||||
if len(ports) == 0 {
|
||||
if len(ports) == 0 && submittedTasksAwaitingFetching.hasInteractiveTasksForCallbackId(callbackId) {
|
||||
newInteractiveData, err := handleAgentMessageGetInteractiveTasking(callbackId)
|
||||
if err != nil {
|
||||
logging.LogError(err, "Failed to fetch interactive tasks")
|
||||
@@ -730,6 +839,60 @@ func (c *callbackPortsInUse) GetOtherCallbackIds(callbackId int) []int {
|
||||
}
|
||||
return callbackIds
|
||||
}
|
||||
|
||||
// GetOtherCallbackIdsWithPendingData returns other callbacks that have proxy
|
||||
// data worth routing through delegates. This keeps the delegate path from doing
|
||||
// BFS work for idle proxy ports while still preserving interactive tasking,
|
||||
// which is delivered through this same proxy/delegate response path.
|
||||
func (c *callbackPortsInUse) GetOtherCallbackIdsWithPendingData(callbackId int) []int {
|
||||
var candidateCallbackIds []int
|
||||
var pendingCallbackIds map[int]bool
|
||||
markPendingCallbackId := func(currentCallbackID int) {
|
||||
if pendingCallbackIds == nil {
|
||||
pendingCallbackIds = make(map[int]bool)
|
||||
}
|
||||
pendingCallbackIds[currentCallbackID] = true
|
||||
}
|
||||
|
||||
c.RLock()
|
||||
if c.portsByCallbackID == nil {
|
||||
callbackPortsByID := make(map[int][]*callbackPortUsage)
|
||||
for _, port := range c.ports {
|
||||
if port.CallbackID == callbackId {
|
||||
continue
|
||||
}
|
||||
if len(callbackPortsByID[port.CallbackID]) == 0 {
|
||||
candidateCallbackIds = append(candidateCallbackIds, port.CallbackID)
|
||||
}
|
||||
callbackPortsByID[port.CallbackID] = append(callbackPortsByID[port.CallbackID], port)
|
||||
}
|
||||
for _, currentCallbackID := range candidateCallbackIds {
|
||||
if callbackPortUsagesHavePendingData(callbackPortsByID[currentCallbackID]) {
|
||||
markPendingCallbackId(currentCallbackID)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
for _, currentCallbackID := range c.callbacksWithPorts {
|
||||
if currentCallbackID == callbackId {
|
||||
continue
|
||||
}
|
||||
candidateCallbackIds = append(candidateCallbackIds, currentCallbackID)
|
||||
if callbackPortUsagesHavePendingData(c.portsByCallbackID[currentCallbackID]) {
|
||||
markPendingCallbackId(currentCallbackID)
|
||||
}
|
||||
}
|
||||
}
|
||||
c.RUnlock()
|
||||
|
||||
callbackIdsWithInteractiveTasks := submittedTasksAwaitingFetching.getCallbackIdsWithInteractiveTasks(candidateCallbackIds)
|
||||
callbackIds := make([]int, 0, len(pendingCallbackIds)+len(callbackIdsWithInteractiveTasks))
|
||||
for _, currentCallbackID := range candidateCallbackIds {
|
||||
if pendingCallbackIds[currentCallbackID] || callbackIdsWithInteractiveTasks[currentCallbackID] {
|
||||
callbackIds = append(callbackIds, currentCallbackID)
|
||||
}
|
||||
}
|
||||
return callbackIds
|
||||
}
|
||||
func (c *callbackPortsInUse) GetDataForCallbackId(callbackId int, portType string) (interface{}, error) {
|
||||
return c.GetDataForCallbackIdPortType(callbackId, portType)
|
||||
}
|
||||
@@ -989,10 +1152,14 @@ func (p *callbackPortUsage) GetData() interface{} {
|
||||
return nil
|
||||
}
|
||||
func (p *callbackPortUsage) GetProxyData() []proxyToAgentMessage {
|
||||
if !p.hasPendingProxyDataForAgent() {
|
||||
return nil
|
||||
}
|
||||
messagesToSendToAgent := make([]proxyToAgentMessage, len(p.messagesToAgent))
|
||||
for i := 0; i < len(messagesToSendToAgent); i++ {
|
||||
select {
|
||||
case messagesToSendToAgent[i] = <-p.messagesToAgent:
|
||||
decrementPositiveAtomicCounter(&p.pendingMessagesToAgent)
|
||||
//logging.LogDebug("Agent picking up msg from Mythic", "serverID", messagesToSendToAgent[i].ServerID, "exit", messagesToSendToAgent[i].IsExit)
|
||||
default:
|
||||
//logging.LogDebug("returning set of messages to agent from Mythic", "msgs", messagesToSendToAgent)
|
||||
@@ -1003,10 +1170,14 @@ func (p *callbackPortUsage) GetProxyData() []proxyToAgentMessage {
|
||||
return messagesToSendToAgent
|
||||
}
|
||||
func (p *callbackPortUsage) GetInteractiveData() []agentMessagePostResponseInteractive {
|
||||
if !p.hasPendingInteractiveDataForAgent() && !submittedTasksAwaitingFetching.hasInteractiveTasksForCallbackId(p.CallbackID) {
|
||||
return nil
|
||||
}
|
||||
messagesToSendToAgent := make([]agentMessagePostResponseInteractive, len(p.interactiveMessagesToAgent))
|
||||
for i := 0; i < len(messagesToSendToAgent); i++ {
|
||||
select {
|
||||
case messagesToSendToAgent[i] = <-p.interactiveMessagesToAgent:
|
||||
decrementPositiveAtomicCounter(&p.pendingInteractiveMessagesToAgent)
|
||||
//logging.LogDebug("Got message from Mythic to agent", "TaskID", messagesToSendToAgent[i].TaskUUID)
|
||||
default:
|
||||
//logging.LogDebug("returning set of messages to agent from Mythic", "msgs", messagesToSendToAgent)
|
||||
@@ -1099,9 +1270,9 @@ func (p *callbackPortUsage) manageConnections() {
|
||||
ServerID: removeCon.ServerID,
|
||||
Port: p.LocalPort,
|
||||
},
|
||||
MessagesToAgent: p.messagesToAgent,
|
||||
ProxyType: p.PortType,
|
||||
CallbackID: p.CallbackID,
|
||||
CallbackPort: p,
|
||||
ProxyType: p.PortType,
|
||||
CallbackID: p.CallbackID,
|
||||
}:
|
||||
default:
|
||||
}
|
||||
@@ -1130,9 +1301,9 @@ func (p *callbackPortUsage) manageConnections() {
|
||||
ServerID: newMsg.ServerID,
|
||||
Port: p.LocalPort,
|
||||
},
|
||||
MessagesToAgent: p.messagesToAgent,
|
||||
ProxyType: p.PortType,
|
||||
CallbackID: p.CallbackID,
|
||||
CallbackPort: p,
|
||||
ProxyType: p.PortType,
|
||||
CallbackID: p.CallbackID,
|
||||
}
|
||||
|
||||
*/
|
||||
@@ -1162,9 +1333,9 @@ func (p *callbackPortUsage) manageConnections() {
|
||||
ServerID: newMsg.ServerID,
|
||||
Port: p.LocalPort,
|
||||
},
|
||||
MessagesToAgent: p.messagesToAgent,
|
||||
CallbackID: p.CallbackID,
|
||||
ProxyType: p.PortType,
|
||||
CallbackPort: p,
|
||||
CallbackID: p.CallbackID,
|
||||
ProxyType: p.PortType,
|
||||
}
|
||||
go SendAllOperationsMessage(fmt.Sprintf("Failed to connect to %s:%d for new rpfwd message", p.RemoteIP, p.RemotePort),
|
||||
p.OperationID, "rpfwd", database.MESSAGE_LEVEL_INFO, true)
|
||||
@@ -1756,9 +1927,9 @@ func (p *callbackPortUsage) handleSocksConnections() {
|
||||
ServerID: newUDPConnection.ServerID,
|
||||
Port: p.LocalPort,
|
||||
},
|
||||
MessagesToAgent: p.messagesToAgent,
|
||||
CallbackID: p.CallbackID,
|
||||
ProxyType: p.PortType,
|
||||
CallbackPort: p,
|
||||
CallbackID: p.CallbackID,
|
||||
ProxyType: p.PortType,
|
||||
}
|
||||
p.addBytesSentToAgent(int64(newUDPConnectionBufLength))
|
||||
go func(remoteAddr net.Addr) {
|
||||
@@ -1800,9 +1971,9 @@ func (p *callbackPortUsage) handleSocksConnections() {
|
||||
ServerID: newUDPConnection.ServerID,
|
||||
Port: p.LocalPort,
|
||||
},
|
||||
MessagesToAgent: p.messagesToAgent,
|
||||
CallbackID: p.CallbackID,
|
||||
ProxyType: p.PortType,
|
||||
CallbackPort: p,
|
||||
CallbackID: p.CallbackID,
|
||||
ProxyType: p.PortType,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1821,9 +1992,9 @@ func (p *callbackPortUsage) handleSocksConnections() {
|
||||
ServerID: newConnection.ServerID,
|
||||
Port: p.LocalPort,
|
||||
},
|
||||
MessagesToAgent: p.messagesToAgent,
|
||||
CallbackID: p.CallbackID,
|
||||
ProxyType: p.PortType,
|
||||
CallbackPort: p,
|
||||
CallbackID: p.CallbackID,
|
||||
ProxyType: p.PortType,
|
||||
}
|
||||
p.addBytesSentToAgent(int64(length))
|
||||
}
|
||||
@@ -1897,9 +2068,9 @@ func (p *callbackPortUsage) handleRpfwdConnections(newConnection *acceptedConnec
|
||||
ServerID: newConnection.ServerID,
|
||||
Port: p.LocalPort,
|
||||
},
|
||||
MessagesToAgent: p.messagesToAgent,
|
||||
ProxyType: p.PortType,
|
||||
CallbackID: p.CallbackID,
|
||||
CallbackPort: p,
|
||||
ProxyType: p.PortType,
|
||||
CallbackID: p.CallbackID,
|
||||
}
|
||||
p.addBytesSentToAgent(int64(length))
|
||||
//fmt.Printf("Message sent to p.messagesToAgent channel for chan %d\n", newConnection.ServerID)
|
||||
@@ -2001,9 +2172,9 @@ func (p *callbackPortUsage) handleInteractiveConnections() {
|
||||
TaskUUID: taskUUID,
|
||||
MessageType: 0,
|
||||
},
|
||||
InteractiveMessagesToAgent: p.interactiveMessagesToAgent,
|
||||
CallbackID: p.CallbackID,
|
||||
ProxyType: p.PortType,
|
||||
CallbackPort: p,
|
||||
CallbackID: p.CallbackID,
|
||||
ProxyType: p.PortType,
|
||||
}
|
||||
p.addBytesSentToAgent(int64(length))
|
||||
//fmt.Printf("Message sent to p.messagesToAgent channel for chan %d\n", newConnection.ServerID)
|
||||
|
||||
@@ -33,6 +33,37 @@ func removeTestCallbackPort(registry *callbackPortsInUse, port *callbackPortUsag
|
||||
registry.removePortLocked(port)
|
||||
}
|
||||
|
||||
func replaceSubmittedTasksForProxyTest(t *testing.T, tasks []submittedTask) {
|
||||
t.Helper()
|
||||
|
||||
submittedTasksAwaitingFetching.Lock()
|
||||
oldTasks := submittedTasksAwaitingFetching.Tasks
|
||||
oldTasksByCallbackID := submittedTasksAwaitingFetching.tasksByCallbackID
|
||||
oldInteractiveTasksByCallbackID := submittedTasksAwaitingFetching.interactiveTasksByCallbackID
|
||||
oldCallbacksWithTasks := submittedTasksAwaitingFetching.callbacksWithTasks
|
||||
oldCallbacksWithInteractiveTasks := submittedTasksAwaitingFetching.callbacksWithInteractiveTasks
|
||||
oldTaskByID := submittedTasksAwaitingFetching.taskByID
|
||||
|
||||
taskArray := make([]submittedTask, 0, len(tasks))
|
||||
submittedTasksAwaitingFetching.Tasks = &taskArray
|
||||
submittedTasksAwaitingFetching.resetTaskIndexesLocked()
|
||||
for _, task := range tasks {
|
||||
submittedTasksAwaitingFetching.addTaskLocked(task)
|
||||
}
|
||||
submittedTasksAwaitingFetching.Unlock()
|
||||
|
||||
t.Cleanup(func() {
|
||||
submittedTasksAwaitingFetching.Lock()
|
||||
submittedTasksAwaitingFetching.Tasks = oldTasks
|
||||
submittedTasksAwaitingFetching.tasksByCallbackID = oldTasksByCallbackID
|
||||
submittedTasksAwaitingFetching.interactiveTasksByCallbackID = oldInteractiveTasksByCallbackID
|
||||
submittedTasksAwaitingFetching.callbacksWithTasks = oldCallbacksWithTasks
|
||||
submittedTasksAwaitingFetching.callbacksWithInteractiveTasks = oldCallbacksWithInteractiveTasks
|
||||
submittedTasksAwaitingFetching.taskByID = oldTaskByID
|
||||
submittedTasksAwaitingFetching.Unlock()
|
||||
})
|
||||
}
|
||||
|
||||
func TestCallbackPortsInUseIndexesCallbacksAndPortTypes(t *testing.T) {
|
||||
registry := callbackPortsInUse{}
|
||||
socksPort := newTestCallbackPort(10, CALLBACK_PORT_TYPE_SOCKS, 7001, 1)
|
||||
@@ -67,9 +98,15 @@ func TestCallbackPortsInUseCombinedDataFetchUsesIndexedPorts(t *testing.T) {
|
||||
addTestCallbackPort(®istry, rpfwdPort)
|
||||
addTestCallbackPort(®istry, interactivePort)
|
||||
|
||||
socksPort.messagesToAgent <- proxyToAgentMessage{ServerID: 1, Message: []byte("socks")}
|
||||
rpfwdPort.messagesToAgent <- proxyToAgentMessage{ServerID: 2, Message: []byte("rpfwd")}
|
||||
interactivePort.interactiveMessagesToAgent <- agentMessagePostResponseInteractive{TaskUUID: "interactive"}
|
||||
if !socksPort.queueProxyDataForAgent(proxyToAgentMessage{ServerID: 1, Message: []byte("socks")}) {
|
||||
t.Fatal("failed to queue socks message")
|
||||
}
|
||||
if !rpfwdPort.queueProxyDataForAgent(proxyToAgentMessage{ServerID: 2, Message: []byte("rpfwd")}) {
|
||||
t.Fatal("failed to queue rpfwd message")
|
||||
}
|
||||
if !interactivePort.queueInteractiveDataForAgent(agentMessagePostResponseInteractive{TaskUUID: "interactive"}) {
|
||||
t.Fatal("failed to queue interactive message")
|
||||
}
|
||||
|
||||
proxyData, err := registry.GetDataForCallbackIdAllTypes(10)
|
||||
if err != nil {
|
||||
@@ -94,6 +131,101 @@ func TestCallbackPortsInUseCombinedDataFetchUsesIndexedPorts(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestCallbackPortsInUsePendingDelegateCallbacksSkipIdlePorts(t *testing.T) {
|
||||
replaceSubmittedTasksForProxyTest(t, nil)
|
||||
registry := callbackPortsInUse{}
|
||||
currentPort := newTestCallbackPort(10, CALLBACK_PORT_TYPE_SOCKS, 7001, 1)
|
||||
idlePort := newTestCallbackPort(20, CALLBACK_PORT_TYPE_SOCKS, 7002, 2)
|
||||
pendingPort := newTestCallbackPort(30, CALLBACK_PORT_TYPE_RPORTFWD, 7003, 3)
|
||||
addTestCallbackPort(®istry, currentPort)
|
||||
addTestCallbackPort(®istry, idlePort)
|
||||
addTestCallbackPort(®istry, pendingPort)
|
||||
|
||||
if callbackIDs := registry.GetOtherCallbackIdsWithPendingData(10); len(callbackIDs) != 0 {
|
||||
t.Fatalf("expected no delegate proxy callbacks before data is queued, got %#v", callbackIDs)
|
||||
}
|
||||
if !pendingPort.queueProxyDataForAgent(proxyToAgentMessage{ServerID: 1, Message: []byte("rpfwd")}) {
|
||||
t.Fatal("failed to queue rpfwd message")
|
||||
}
|
||||
|
||||
if callbackIDs := registry.GetOtherCallbackIdsWithPendingData(10); !slices.Equal(callbackIDs, []int{30}) {
|
||||
t.Fatalf("expected only callback 30 to have pending delegate proxy data, got %#v", callbackIDs)
|
||||
}
|
||||
|
||||
_ = pendingPort.GetProxyData()
|
||||
if callbackIDs := registry.GetOtherCallbackIdsWithPendingData(10); len(callbackIDs) != 0 {
|
||||
t.Fatalf("expected no delegate proxy callbacks after data is drained, got %#v", callbackIDs)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCallbackPortsInUsePendingDelegateCallbacksIncludeInteractiveTasks(t *testing.T) {
|
||||
replaceSubmittedTasksForProxyTest(t, []submittedTask{
|
||||
{TaskID: 100, CallbackID: 30, IsInteractiveTask: true},
|
||||
{TaskID: 101, CallbackID: 40, IsInteractiveTask: true},
|
||||
})
|
||||
registry := callbackPortsInUse{}
|
||||
currentPort := newTestCallbackPort(10, CALLBACK_PORT_TYPE_SOCKS, 7001, 1)
|
||||
idlePort := newTestCallbackPort(20, CALLBACK_PORT_TYPE_SOCKS, 7002, 2)
|
||||
interactiveTaskPort := newTestCallbackPort(30, CALLBACK_PORT_TYPE_SOCKS, 7003, 3)
|
||||
addTestCallbackPort(®istry, currentPort)
|
||||
addTestCallbackPort(®istry, idlePort)
|
||||
addTestCallbackPort(®istry, interactiveTaskPort)
|
||||
|
||||
if callbackIDs := registry.GetOtherCallbackIdsWithPendingData(10); !slices.Equal(callbackIDs, []int{30}) {
|
||||
t.Fatalf("expected only callback 30 to be eligible from interactive tasking, got %#v", callbackIDs)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCallbackPortUsagePendingDataTracksQueuedMessages(t *testing.T) {
|
||||
port := newTestCallbackPort(10, CALLBACK_PORT_TYPE_SOCKS, 7001, 1)
|
||||
|
||||
if port.hasPendingProxyDataForAgent() {
|
||||
t.Fatal("expected no pending data before queueing")
|
||||
}
|
||||
if !port.queueProxyDataForAgent(proxyToAgentMessage{ServerID: 1, Message: []byte("socks")}) {
|
||||
t.Fatal("failed to queue proxy message")
|
||||
}
|
||||
if got := port.pendingMessagesToAgent.Load(); got != 1 {
|
||||
t.Fatalf("expected one pending proxy message, got %d", got)
|
||||
}
|
||||
|
||||
messages := port.GetProxyData()
|
||||
if len(messages) != 1 || string(messages[0].Message) != "socks" {
|
||||
t.Fatalf("expected queued proxy message to drain, got %#v", messages)
|
||||
}
|
||||
if got := port.pendingMessagesToAgent.Load(); got != 0 {
|
||||
t.Fatalf("expected pending proxy count to drain to zero, got %d", got)
|
||||
}
|
||||
if port.hasPendingProxyDataForAgent() {
|
||||
t.Fatal("expected no pending proxy data after drain")
|
||||
}
|
||||
}
|
||||
|
||||
func TestCallbackPortUsagePendingDataTracksInteractiveMessages(t *testing.T) {
|
||||
port := newTestCallbackPort(10, CALLBACK_PORT_TYPE_INTERACTIVE, 7001, 1)
|
||||
|
||||
if port.hasPendingInteractiveDataForAgent() {
|
||||
t.Fatal("expected no pending interactive data before queueing")
|
||||
}
|
||||
if !port.queueInteractiveDataForAgent(agentMessagePostResponseInteractive{TaskUUID: "interactive"}) {
|
||||
t.Fatal("failed to queue interactive message")
|
||||
}
|
||||
if got := port.pendingInteractiveMessagesToAgent.Load(); got != 1 {
|
||||
t.Fatalf("expected one pending interactive message, got %d", got)
|
||||
}
|
||||
|
||||
messages := port.GetInteractiveData()
|
||||
if len(messages) != 1 || messages[0].TaskUUID != "interactive" {
|
||||
t.Fatalf("expected queued interactive message to drain, got %#v", messages)
|
||||
}
|
||||
if got := port.pendingInteractiveMessagesToAgent.Load(); got != 0 {
|
||||
t.Fatalf("expected pending interactive count to drain to zero, got %d", got)
|
||||
}
|
||||
if port.hasPendingInteractiveDataForAgent() {
|
||||
t.Fatal("expected no pending interactive data after drain")
|
||||
}
|
||||
}
|
||||
|
||||
func TestCallbackPortUsageByteCountersAccumulateWithoutChannels(t *testing.T) {
|
||||
port := newTestCallbackPort(10, CALLBACK_PORT_TYPE_SOCKS, 7001, 1)
|
||||
port.initializeByteCounters(10, 20)
|
||||
|
||||
Reference in New Issue
Block a user