c2 profile path modernization

This commit is contained in:
its-a-feature
2026-05-10 21:33:34 -05:00
parent 0fdaf9992c
commit 2f67e80165
7 changed files with 1416 additions and 443 deletions
@@ -21,10 +21,11 @@ import { toPng, toSvg } from 'html-to-image';
import InsertPhotoIcon from '@mui/icons-material/InsertPhoto';
import CameraAltIcon from '@mui/icons-material/CameraAlt';
import SwapCallsIcon from '@mui/icons-material/SwapCalls';
import WifiIcon from '@mui/icons-material/Wifi';
import InsertLinkTwoToneIcon from '@mui/icons-material/InsertLinkTwoTone';
import {snackActions} from "../../utilities/Snackbar";
import {TaskLabelFlat, getLabelText} from './TaskDisplay';
import {MythicDialog, MythicViewJSONAsTableDialog} from "../../MythicComponents/MythicDialog";
import {MythicSelectFromListDialog} from "../../MythicComponents/MythicSelectFromListDialog";
import {ManuallyAddEdgeDialog} from "./ManuallyAddEdgeDialog";
import {TaskParametersDialog} from "./TaskParametersDialog";
import {addEdgeMutation, createTaskingMutation, hideCallbackMutation, removeEdgeMutation} from "./CallbackMutations";
@@ -52,6 +53,249 @@ const MenuProps = {
},
};
const getEdgeEndpointIds = (edge) => [
String(edge?.source?.id || ""),
String(edge?.destination?.id || ""),
];
const getReachableCallbackIds = (edges, callbackId) => {
const startId = String(callbackId || "");
if(startId.length === 0){return new Set()}
const reachable = new Set([startId]);
let foundMore = true;
while(foundMore){
foundMore = false;
for(let i = 0; i < edges.length; i++){
const edge = edges[i];
const [sourceId, destinationId] = getEdgeEndpointIds(edge);
if(reachable.has(sourceId) && !reachable.has(destinationId)){
reachable.add(destinationId);
foundMore = true;
}
if(reachable.has(destinationId) && !reachable.has(sourceId)){
reachable.add(sourceId);
foundMore = true;
}
}
}
return reachable;
};
const getC2RouteSummary = (callback, callbackgraphedges) => {
const edges = callbackgraphedges || [];
const callbackId = String(callback?.id || "");
const activeEdges = edges.filter((edge) => edge.end_timestamp === null);
const endedEdges = edges.filter((edge) => edge.end_timestamp !== null);
const activeReachableIds = getReachableCallbackIds(activeEdges, callbackId);
const historicalReachableIds = getReachableCallbackIds(edges, callbackId);
const directEgressRoutes = edges.filter((edge) => {
const [sourceId, destinationId] = getEdgeEndpointIds(edge);
return !edge.c2profile?.is_p2p && sourceId === callbackId && destinationId === callbackId;
});
const activeDirectRoutes = directEgressRoutes.filter((edge) => edge.end_timestamp === null);
const activeRoutedEgressRoutes = activeEdges.filter((edge) => {
if(edge.c2profile?.is_p2p){return false}
const [sourceId, destinationId] = getEdgeEndpointIds(edge);
return activeReachableIds.has(sourceId) || activeReachableIds.has(destinationId);
});
const historicalEgressRoutes = edges.filter((edge) => {
if(edge.c2profile?.is_p2p){return false}
const [sourceId, destinationId] = getEdgeEndpointIds(edge);
return historicalReachableIds.has(sourceId) || historicalReachableIds.has(destinationId);
});
const baseCounts = {
activeEdgeCount: activeEdges.length,
endedEdgeCount: endedEdges.length,
totalEdgeCount: edges.length,
egressEdgeCount: edges.filter((edge) => !edge.c2profile?.is_p2p).length,
p2pEdgeCount: edges.filter((edge) => edge.c2profile?.is_p2p).length,
};
if(activeDirectRoutes.length > 0){
return {
...baseCounts,
tone: "success",
icon: "direct",
label: "Direct route active",
description: "This callback has an active direct route to Mythic.",
};
}
if(directEgressRoutes.length > 0){
return {
...baseCounts,
tone: "error",
icon: "direct",
label: "Direct route ended",
description: "This callback has direct route history, but the direct route is no longer active.",
};
}
if(activeRoutedEgressRoutes.length > 0){
return {
...baseCounts,
tone: "success",
icon: "p2p",
label: "P2P route active",
description: "This callback can reach Mythic through active peer-to-peer links.",
};
}
if(historicalEgressRoutes.length > 0){
return {
...baseCounts,
tone: "warning",
icon: "p2p",
label: "Route history only",
description: "The graph includes historical C2 links, but no active route from this callback reaches Mythic.",
};
}
return {
...baseCounts,
tone: edges.length > 0 ? "warning" : "neutral",
icon: "p2p",
label: edges.length > 0 ? "No egress route" : "No route data",
description: edges.length > 0 ?
"Links exist for this callback group, but none currently reach an egress route to Mythic." :
"No C2 route data has been recorded for this callback yet.",
};
};
const getCallbackDisplay = (callback) => callback?.display_id ? `#${callback.display_id}` : "Mythic";
const getEdgeRouteParts = (edge) => {
const isDirect = edge?.source?.id === edge?.destination?.id && !edge?.c2profile?.is_p2p;
return {
source: getCallbackDisplay(edge?.source),
profile: edge?.c2profile?.name || "Unknown profile",
destination: isDirect ? "Mythic" : getCallbackDisplay(edge?.destination),
active: edge?.end_timestamp === null,
isP2P: edge?.c2profile?.is_p2p || false,
};
};
const C2ActionRoute = ({edge}) => {
const route = getEdgeRouteParts(edge);
return (
<div className="mythic-c2-action-route">
<span>{route.source}</span>
<span className="mythic-c2-action-route-profile">{route.profile}</span>
<span>{route.destination}</span>
</div>
);
};
const C2ManualRemoveEdgeDialog = ({options = [], onSubmit, onClose}) => {
const [selectedEdge, setSelectedEdge] = React.useState(options?.[0] || "");
const submit = () => {
if(selectedEdge === ""){return}
onSubmit(selectedEdge);
onClose();
};
return (
<>
<DialogTitle className="mythic-c2-action-title">
<Typography component="div" className="mythic-c2-action-title-text">
Remove Active Edge
</Typography>
<Typography component="div" className="mythic-c2-action-title-subtitle">
Select the active C2 route edge to close from this graph.
</Typography>
</DialogTitle>
<DialogContent dividers={true}>
<div className="mythic-c2-action-body">
{options.length === 0 ?
<div className="mythic-c2-action-empty">
No active edges are available to remove for this callback.
</div> :
<div className="mythic-c2-action-list">
{options.map((edge) => {
const route = getEdgeRouteParts(edge);
const selected = selectedEdge?.id === edge.id;
return (
<button
type="button"
key={edge.id}
className={`mythic-c2-action-card ${selected ? "mythic-c2-action-card-selected" : ""}`}
onClick={() => setSelectedEdge(edge)}
>
<div className="mythic-c2-action-card-main">
<C2ActionRoute edge={edge} />
<Typography component="div" className="mythic-c2-action-card-description">
{route.isP2P ? "Peer-to-peer link" : "Direct egress link"} from {route.source} through {route.profile}.
</Typography>
</div>
<span className={`mythic-c2-action-state ${route.active ? "mythic-c2-action-state-active" : "mythic-c2-action-state-ended"}`}>
{route.active ? "Active" : "Ended"}
</span>
</button>
);
})}
</div>
}
</div>
</DialogContent>
<MythicDialogFooter>
<MythicDialogButton onClick={onClose}>Close</MythicDialogButton>
<MythicDialogButton disabled={selectedEdge === ""} onClick={submit} intent="destructive">
Remove Edge
</MythicDialogButton>
</MythicDialogFooter>
</>
);
};
const C2SelectLinkCommandDialog = ({options = [], callback, onSubmit, onClose}) => {
const [selectedCommand, setSelectedCommand] = React.useState(options?.[0] || "");
const submit = () => {
if(selectedCommand === ""){return}
onSubmit(selectedCommand);
onClose();
};
return (
<>
<DialogTitle className="mythic-c2-action-title">
<Typography component="div" className="mythic-c2-action-title-text">
Select Link Command
</Typography>
<Typography component="div" className="mythic-c2-action-title-subtitle">
Choose the graph-link command to task callback {getCallbackDisplay(callback)}.
</Typography>
</DialogTitle>
<DialogContent dividers={true}>
<div className="mythic-c2-action-body">
{options.length === 0 ?
<div className="mythic-c2-action-empty">
No loaded commands support graph link tasking for this callback.
</div> :
<div className="mythic-c2-action-list">
{options.map((option) => {
const command = option.command;
const selected = selectedCommand?.command?.id === command.id;
return (
<button
type="button"
key={command.id}
className={`mythic-c2-action-card ${selected ? "mythic-c2-action-card-selected" : ""}`}
onClick={() => setSelectedCommand(option)}
>
<div className="mythic-c2-action-card-main">
<div className="mythic-c2-action-command-row">
<span className="mythic-c2-action-command-name">{command.cmd}</span>
{command.needs_admin &&
<span className="mythic-c2-action-state mythic-c2-action-state-warning">Admin</span>
}
</div>
<Typography component="div" className="mythic-c2-action-card-description">
{command.help_cmd || command.description || "No command help available."}
</Typography>
</div>
</button>
);
})}
</div>
}
</div>
</DialogContent>
<MythicDialogFooter>
<MythicDialogButton onClick={onClose}>Close</MythicDialogButton>
<MythicDialogButton disabled={selectedCommand === ""} onClick={submit} intent="primary">
Configure Task
</MythicDialogButton>
</MythicDialogFooter>
</>
);
};
function getStyles(name, selectedOptions, theme) {
return {
fontWeight:
@@ -191,13 +435,7 @@ export function C2PathDialog({callback, callbackgraphedges, onClose, onOpenTab})
}
const contextMenu = useMemo(() => {return [
{
title: 'Hide Callback',
onClick: function(node) {
hideCallback({variables: {callback_display_id: node.display_id}});
}
},
{
title: 'Interact',
title: 'Open Tasking',
onClick: function(node){
//console.log(node);
if(onOpenTab){
@@ -209,7 +447,14 @@ export function C2PathDialog({callback, callbackgraphedges, onClose, onOpenTab})
}
},
{
title: "Manually Remove Edge",
title: "Add P2P Edge",
onClick: function(node){
setAddEdgeSource(node);
setManuallyAddEdgeDialogOpen(true);
}
},
{
title: "Remove Active Edge",
onClick: function(node){
const opts = callbackgraphedges.reduce( (prev, e) => {
if(e.source.id === node.id || e.destination.id === node.id){
@@ -232,14 +477,7 @@ export function C2PathDialog({callback, callbackgraphedges, onClose, onOpenTab})
}
},
{
title: "Manually Add Edge",
onClick: function(node){
setAddEdgeSource(node);
setManuallyAddEdgeDialogOpen(true);
}
},
{
title: "Task Callback for Edge",
title: "Task Link Command",
onClick: function(node){
setLinkCommands([]);
setSelectedLinkCommand(null);
@@ -248,12 +486,28 @@ export function C2PathDialog({callback, callbackgraphedges, onClose, onOpenTab})
setSelectedCallback(node);
}
},
]}, [getLinkCommands, hideCallback, viewConfig, onOpenTab]);
{
title: 'Hide Callback',
onClick: function(node) {
hideCallback({variables: {callback_display_id: node.display_id}});
}
},
]}, [getLinkCommands, hideCallback, callbackgraphedges, onOpenTab]);
const activeEdgeCount = callbackgraphedges?.filter(edge => edge.end_timestamp === null).length || 0;
const totalEdgeCount = callbackgraphedges?.length || 0;
const egressEdgeCount = callbackgraphedges?.filter(edge => !edge.c2profile?.is_p2p).length || 0;
const p2pEdgeCount = callbackgraphedges?.filter(edge => edge.c2profile?.is_p2p).length || 0;
const routeSummary = useMemo(
() => getC2RouteSummary(callback, callbackgraphedges),
[callback, callbackgraphedges]
);
const RouteSummaryIcon = routeSummary.icon === "direct" ? WifiIcon : InsertLinkTwoToneIcon;
const callbackGraphNodeSignature = getCallbackGraphNodeSignature(callback);
const callbackGraphNodeRef = React.useRef(null);
const callbackGraphNodeSignatureRef = React.useRef("");
const providedCallbackNodesRef = React.useRef([]);
if(callbackGraphNodeSignatureRef.current !== callbackGraphNodeSignature){
callbackGraphNodeSignatureRef.current = callbackGraphNodeSignature;
callbackGraphNodeRef.current = getCallbackGraphNodeData(callback);
providedCallbackNodesRef.current = callbackGraphNodeRef.current ? [callbackGraphNodeRef.current] : [];
}
return (
<>
@@ -268,14 +522,51 @@ export function C2PathDialog({callback, callbackgraphedges, onClose, onOpenTab})
</Typography>
</div>
<div className="mythic-c2-path-summary">
<span className="mythic-c2-path-summary-chip">{activeEdgeCount} active</span>
<span className="mythic-c2-path-summary-chip">{totalEdgeCount} total</span>
<span className="mythic-c2-path-summary-chip">{egressEdgeCount} egress</span>
<span className="mythic-c2-path-summary-chip">{p2pEdgeCount} p2p</span>
<span className={`mythic-c2-path-summary-chip mythic-c2-path-summary-chip-${routeSummary.tone}`}>
<RouteSummaryIcon fontSize="inherit" />
{routeSummary.label}
</span>
<span className="mythic-c2-path-summary-chip">{routeSummary.activeEdgeCount} active</span>
<span className="mythic-c2-path-summary-chip">{routeSummary.endedEdgeCount} ended</span>
<span className="mythic-c2-path-summary-chip">{routeSummary.egressEdgeCount} egress</span>
<span className="mythic-c2-path-summary-chip">{routeSummary.p2pEdgeCount} p2p</span>
</div>
</div>
</DialogTitle>
<DialogContent dividers={true} className="mythic-c2-path-content">
<div className={`mythic-c2-path-route-panel mythic-c2-path-route-panel-${routeSummary.tone}`}>
<div className="mythic-c2-path-route-state">
<span className="mythic-c2-path-route-icon">
<RouteSummaryIcon fontSize="small" />
</span>
<div className="mythic-c2-path-route-copy">
<Typography component="div" className="mythic-c2-path-route-label">
{routeSummary.label}
</Typography>
<Typography component="div" className="mythic-c2-path-route-description">
{routeSummary.description}
</Typography>
</div>
</div>
<div className="mythic-c2-path-legend">
<span className="mythic-c2-path-legend-item">
<WifiIcon fontSize="inherit" />
Direct to Mythic
</span>
<span className="mythic-c2-path-legend-item">
<InsertLinkTwoToneIcon fontSize="inherit" />
P2P route
</span>
<span className="mythic-c2-path-legend-item">
<span className="mythic-c2-path-edge-swatch mythic-c2-path-edge-swatch-active" />
Active link
</span>
<span className="mythic-c2-path-legend-item">
<span className="mythic-c2-path-edge-swatch mythic-c2-path-edge-swatch-ended" />
Ended link
</span>
</div>
</div>
<div className="mythic-c2-path-toolbar">
<div className="mythic-c2-path-toolbar-copy">
<Typography component="div" className="mythic-c2-path-toolbar-title">
@@ -333,8 +624,11 @@ export function C2PathDialog({callback, callbackgraphedges, onClose, onOpenTab})
{manuallyRemoveEdgeDialogOpen &&
<MythicDialog fullWidth={true} maxWidth="sm" open={manuallyRemoveEdgeDialogOpen}
onClose={()=>{setManuallyRemoveEdgeDialogOpen(false);}}
innerDialog={<MythicSelectFromListDialog onClose={()=>{setManuallyRemoveEdgeDialogOpen(false);}} identifier="id" display="display"
onSubmit={onSubmitManuallyRemoveEdge} options={edgeOptions} title={"Manually Remove Edge"} action={"remove"} />}
innerDialog={<C2ManualRemoveEdgeDialog
onClose={()=>{setManuallyRemoveEdgeDialogOpen(false);}}
onSubmit={onSubmitManuallyRemoveEdge}
options={edgeOptions}
/>}
/>
}
{manuallyAddEdgeDialogOpen &&
@@ -353,18 +647,22 @@ export function C2PathDialog({callback, callbackgraphedges, onClose, onOpenTab})
{openSelectLinkCommandDialog &&
<MythicDialog fullWidth={true} maxWidth="sm" open={openSelectLinkCommandDialog}
onClose={()=>{setOpenSelectLinkCommandDialog(false);}}
innerDialog={<MythicSelectFromListDialog onClose={()=>{setOpenSelectLinkCommandDialog(false);}}
onSubmit={onSubmitSelectedLinkCommand} options={linkCommands} title={"Select Link Command"}
action={"select"} display={"display"} identifier={"display"}/>}
innerDialog={<C2SelectLinkCommandDialog
onClose={()=>{setOpenSelectLinkCommandDialog(false);}}
onSubmit={onSubmitSelectedLinkCommand}
options={linkCommands}
callback={selectedCallback}
/>}
/>
}
<div className="mythic-c2-path-canvas">
<div className="mythic-c2-path-canvas mythic-graph-canvas">
<DrawC2PathElementsFlowWithProvider
providedNodes={[callback]}
providedNodes={providedCallbackNodesRef.current}
edges={callbackgraphedges}
view_config={viewConfig}
theme={theme}
contextMenu={contextMenu}
focusedCallbackId={callback.id}
/>
</div>
</DialogContent>
@@ -422,8 +720,13 @@ function AgentNode({data}) {
filter: "grayscale(1)",
opacity: 0.5
} : {};
const nodeClasses = [
"mythic-c2-agent-node",
data?.isMythic ? "mythic-c2-agent-node-mythic" : "",
data?.isFocused ? "mythic-c2-agent-node-focused" : "",
].filter(Boolean).join(" ");
return (
<div style={{padding: 0, margin: 0, ...additionalStyles}}>
<div className={nodeClasses} style={{padding: 0, margin: 0, ...additionalStyles}}>
{
[...Array(data.sourceCount)].map((e, i) => (
<Handle type={"source"} id={`${i+1}`} key={`${i+1}`} style={data.sourceCount > 1 ? getOffset(i) : {}} isConnectable={false} position={sourcePosition} />
@@ -432,7 +735,7 @@ function AgentNode({data}) {
}
<ImageWithAuth alt={data.img} style={{margin: "auto"}} src={data.img} className={"circleImageNode"} />
<Handle type={"target"} position={targetPosition} isConnectable={false}/>
<Typography style={{textAlign: "center", margin: 0, padding: 0}} >{data.label}</Typography>
<Typography className="mythic-c2-agent-node-label" >{data.label}</Typography>
</div>
)
}
@@ -746,6 +1049,96 @@ const getGroupBy = (node, view_config) => {
return node[view_config.group_by];
}
}
const getCallbackGraphEndpoint = (node, view_config) => {
if(!node){return {parentId: ""}}
return {
id: node.id,
display_id: node.display_id,
parentId: getGroupBy(node, view_config),
}
}
const getCallbackGraphNodeData = (node) => {
if(!node){return null}
return {
active: node.active,
id: node.id,
display_id: node.display_id,
user: node.user,
host: node.host,
ip: node.ip,
domain: node.domain,
os: node.os,
process_name: node.process_name,
integrity_level: node.integrity_level,
payload: {
payloadtype: {
name: node.payload?.payloadtype?.name || "",
id: node.payload?.payloadtype?.id || 0,
},
},
}
}
const getCallbackGraphNodeSignature = (node) => {
if(!node){return ""}
return [
node.id,
node.active,
node.display_id,
node.user,
node.host,
node.ip,
node.domain,
node.os,
node.process_name,
node.integrity_level,
node.payload?.payloadtype?.name,
node.payload?.payloadtype?.id,
].map((value) => String(value ?? "")).join("::");
}
const getCallbackGraphEdgeSignature = (edge) => {
if(!edge){return ""}
return [
edge.id,
edge.end_timestamp,
edge.c2profile?.id,
edge.c2profile?.name,
edge.c2profile?.is_p2p,
edge.c2profile?.has_logo,
getCallbackGraphNodeSignature(edge.source),
getCallbackGraphNodeSignature(edge.destination),
].map((value) => String(value ?? "")).join("::");
}
const getGraphInputSignature = ({edges, providedNodes, view_config, filterOptions, focusedCallbackId, theme}) => {
const edgeSignature = (edges || [])
.map(getCallbackGraphEdgeSignature)
.sort()
.join("||");
const nodeSignature = (providedNodes || [])
.map(getCallbackGraphNodeSignature)
.sort()
.join("||");
const filterSignature = Object.entries(filterOptions || {})
.sort(([keyA], [keyB]) => keyA.localeCompare(keyB))
.map(([key, value]) => `${key}:${String(value ?? "")}`)
.join("||");
const viewSignature = [
view_config?.rankDir,
view_config?.packet_flow_view,
view_config?.include_disconnected,
view_config?.show_all_nodes,
view_config?.group_by,
(view_config?.label_components || []).join(","),
].map((value) => String(value ?? "")).join("::");
const themeSignature = [
theme.palette.mode,
theme.palette.success.main,
theme.palette.error.main,
theme.palette.secondary.main,
theme.palette.background.contrast,
theme.tableHover,
].map((value) => String(value ?? "")).join("::");
return [edgeSignature, nodeSignature, filterSignature, viewSignature, focusedCallbackId, themeSignature].join("##");
}
const shouldUseGroups = (view_config) => {
if(view_config["packet_flow_view"] && view_config.group_by !== "None"){
return true;
@@ -759,12 +1152,11 @@ export const DrawC2PathElementsFlowWithProvider = (props) => {
</ReactFlowProvider>
)
}
export const DrawC2PathElementsFlow = ({edges, panel, view_config, contextMenu, providedNodes, filterOptions}) =>{
export const DrawC2PathElementsFlow = ({edges, panel, view_config, contextMenu, providedNodes, filterOptions, focusedCallbackId}) =>{
const theme = useTheme();
const [graphData, setGraphData] = React.useState({nodes: [], edges: [], groups: []});
const [nodes, setNodes, onNodesChange] = useNodesState([]);
const selectedNodes = React.useRef([]);
const extraNodes = React.useRef(providedNodes);
const [edgeFlow, setEdgeFlow, onEdgesChange] = useEdgesState([]);
const [openContextMenu, setOpenContextMenu] = React.useState(false);
const [contextMenuCoord, setContextMenuCord] = React.useState({});
@@ -772,6 +1164,43 @@ export const DrawC2PathElementsFlow = ({edges, panel, view_config, contextMenu,
const contextMenuNode = React.useRef(null);
const {fitView} = useReactFlow()
const updateNodeInternals = useUpdateNodeInternals();
const graphInputSignature = React.useMemo(() => getGraphInputSignature({
edges,
providedNodes,
view_config,
filterOptions,
focusedCallbackId,
theme,
}), [
edges,
providedNodes,
view_config,
filterOptions,
focusedCallbackId,
theme,
]);
const latestGraphInputs = React.useRef({});
latestGraphInputs.current = {
edges,
providedNodes,
view_config,
filterOptions,
focusedCallbackId,
theme,
};
const getLocalContextMenuCoordinates = useCallback((event) => {
const bounds = viewportRef.current?.getBoundingClientRect();
if(!bounds){
return {
top: event.clientY,
left: event.clientX,
};
}
return {
top: event.clientY - bounds.top,
left: event.clientX - bounds.left,
};
}, []);
const onDownloadImageClickSvg = () => {
// we calculate a transform for the nodes so that all nodes are visible
// we then overwrite the transform of the `.react-flow__viewport` element
@@ -817,12 +1246,9 @@ export const DrawC2PathElementsFlow = ({edges, panel, view_config, contextMenu,
}
event.preventDefault();
contextMenuNode.current = {...node.data, id: node.data.callback_id};
setContextMenuCord({
top: event.clientY,
left: event.clientX,
});
setContextMenuCord(getLocalContextMenuCoordinates(event));
setOpenContextMenu(true);
}, [contextMenu])
}, [contextMenu, getLocalContextMenuCoordinates])
const onPaneClick = useCallback( () => {
setOpenContextMenu(false);
selectedNodes.current = [];
@@ -889,6 +1315,14 @@ export const DrawC2PathElementsFlow = ({edges, panel, view_config, contextMenu,
setNodes(updatedNodes);
}, [graphData, nodes, selectedNodes.current])
React.useEffect( () => {
const {
edges = [],
providedNodes = [],
view_config,
filterOptions,
focusedCallbackId,
theme,
} = latestGraphInputs.current;
let tempNodes = [{
id: "Mythic",
position: { x: 0, y: 0 },
@@ -927,7 +1361,7 @@ export const DrawC2PathElementsFlow = ({edges, panel, view_config, contextMenu,
data: {
has_logo: edge.c2profile.has_logo,
is_p2p: edge.c2profile.is_p2p,
source: {...edge.source, parentId: getGroupBy(edge.source, view_config)},
source: getCallbackGraphEndpoint(edge.source, view_config),
target: {parentId: "Mythic"},
end_timestamp: edge.end_timestamp,
}
@@ -967,6 +1401,7 @@ export const DrawC2PathElementsFlow = ({edges, panel, view_config, contextMenu,
label: getLabel(node, view_config["label_components"]),
img: "/static/" + node.payload.payloadtype.name + "_" + theme.palette.mode + ".svg",
isMythic: false,
isFocused: String(node.id) === String(focusedCallbackId),
callback_id: node.id,
display_id: node.display_id,
}
@@ -1020,7 +1455,7 @@ export const DrawC2PathElementsFlow = ({edges, panel, view_config, contextMenu,
let found = false;
let edgeID = `e${edge.source.id}-${edge.destination.id}-${edge.c2profile.name}`;
if(egress_flow){
if(edge.source.to_mythic){
if(edge.source_to_mythic){
edgeID = `e${edge.destination.id}-${edge.source.id}-${edge.c2profile.name}`;
for(let i = 0; i < tempEdges.length; i++){
if(tempEdges[i].id === edgeID ){
@@ -1045,8 +1480,8 @@ export const DrawC2PathElementsFlow = ({edges, panel, view_config, contextMenu,
end_timestamp: edge.end_timestamp,
has_logo: edge.c2profile.has_logo,
is_p2p: edge.c2profile.is_p2p,
source: {...edge.destination, parentId: getGroupBy(edge.destination, view_config)},
target: {...edge.source, parentId: getGroupBy(edge.source, view_config)},
source: getCallbackGraphEndpoint(edge.destination, view_config),
target: getCallbackGraphEndpoint(edge.source, view_config),
}
},
)
@@ -1082,8 +1517,8 @@ export const DrawC2PathElementsFlow = ({edges, panel, view_config, contextMenu,
end_timestamp: edge.end_timestamp,
has_logo: edge.c2profile.has_logo,
is_p2p: edge.c2profile.is_p2p,
source: {...edge.source, parentId: getGroupBy(edge.source, view_config)},
target: {...edge.target, parentId: getGroupBy(edge.target, view_config)},
source: getCallbackGraphEndpoint(edge.source, view_config),
target: getCallbackGraphEndpoint(edge.destination, view_config),
}
},
)
@@ -1121,8 +1556,8 @@ export const DrawC2PathElementsFlow = ({edges, panel, view_config, contextMenu,
end_timestamp: edge.end_timestamp,
has_logo: edge.c2profile.has_logo,
is_p2p: edge.c2profile.is_p2p,
source: {...edge.source, parentId: getGroupBy(edge.source, view_config)},
target: {...edge.target, parentId: getGroupBy(edge.destination, view_config)},
source: getCallbackGraphEndpoint(edge.source, view_config),
target: getCallbackGraphEndpoint(edge.destination, view_config),
}
},
)
@@ -1130,10 +1565,12 @@ export const DrawC2PathElementsFlow = ({edges, panel, view_config, contextMenu,
}
}
const hasEdge = (sourceId, destinationId, c2ProfileName) => {
const sourceKey = String(sourceId);
const destinationKey = String(destinationId);
for(let i = 0; i < tempEdges.length; i++){
if(tempEdges[i].source === sourceId &&
tempEdges[i].destination === destinationId &&
tempEdges[i].data.label === c2ProfileName){
if(tempEdges[i].source === sourceKey &&
tempEdges[i].target === destinationKey &&
tempEdges[i].label === c2ProfileName){
return true;
}
}
@@ -1150,10 +1587,12 @@ export const DrawC2PathElementsFlow = ({edges, panel, view_config, contextMenu,
return false;
}
const getEdge = (sourceId, destinationId, c2ProfileName) => {
const sourceKey = String(sourceId);
const destinationKey = String(destinationId);
for(let i = 0; i < tempEdges.length; i++){
if(tempEdges[i].source === sourceId &&
tempEdges[i].destination === destinationId &&
tempEdges[i].data.label === c2ProfileName){
if(tempEdges[i].source === sourceKey &&
tempEdges[i].target === destinationKey &&
tempEdges[i].label === c2ProfileName){
return tempEdges[i];
}
}
@@ -1179,74 +1618,77 @@ export const DrawC2PathElementsFlow = ({edges, panel, view_config, contextMenu,
// loop through until all edges have one side marked as "toward_mythic"
let tempNewEdges = edges.reduce((prev, cur) => {
if(filterRow(cur.source) || filterRow(cur.destination)){
return [...prev];
return prev;
}
return [...prev, cur];
prev.push({
edge: cur,
sourceToMythic: false,
destinationToMythic: false,
});
return prev;
}, []);
let edgesToUpdate = tempNewEdges.length;
if (edgesToUpdate === 0) {return []}
let edgesUpdated = 0;
let toMythicIds = new Set();
let loop_count = 0;
while(edgesUpdated < edgesToUpdate){
//console.log(edges, tempEdges, edgesToUpdate, edgesUpdated)
tempNewEdges = tempNewEdges.map( e => {
//console.log(e)
if(!e.source.to_mythic && !e.destination.to_mythic){
if(e.source.id === e.destination.id){
e.source.to_mythic = true;
e.destination.to_mythic = true;
toMythicIds.add(e.source.id);
edgesUpdated += 1;
} else if(toMythicIds.has(e.source.id)){
e.source.to_mythic = true;
e.destination.to_mythic = false;
edgesUpdated += 1;
} else if(toMythicIds.has(e.destination.id)){
e.destination.to_mythic = true;
e.source.to_mythic = false;
edgesUpdated += 1;
for(let loop_count = 0; loop_count <= 2 * edgesToUpdate; loop_count++){
let changed = false;
let complete = true;
for(let i = 0; i < tempNewEdges.length; i++){
const current = tempNewEdges[i];
const edge = current.edge;
if(!current.sourceToMythic && !current.destinationToMythic){
complete = false;
if(edge.source.id === edge.destination.id){
current.sourceToMythic = true;
current.destinationToMythic = true;
toMythicIds.add(edge.source.id);
changed = true;
} else if(toMythicIds.has(edge.source.id)){
current.sourceToMythic = true;
current.destinationToMythic = false;
changed = true;
} else if(toMythicIds.has(edge.destination.id)){
current.destinationToMythic = true;
current.sourceToMythic = false;
changed = true;
} else {
// check if either source/destination has any edges that identify
tempNewEdges.forEach( e2 => {
if(e2.source.id === e.source.id){
for(let j = 0; j < tempNewEdges.length; j++){
const candidate = tempNewEdges[j];
const candidateEdge = candidate.edge;
if(candidateEdge.source.id === edge.source.id){
// only look at edges that contain our source
if(e2.destination.to_mythic){
e.source.to_mythic = true;
edgesUpdated += 1;
if(candidate.destinationToMythic){
current.sourceToMythic = true;
changed = true;
break;
}
} else if(e2.destination.id === e.source.id){
if(e2.source.to_mythic){
e.source.to_mythic = true;
edgesUpdated += 1;
} else if(candidateEdge.destination.id === edge.source.id){
if(candidate.sourceToMythic){
current.sourceToMythic = true;
changed = true;
break;
}
}
})
}
}
} else {
edgesUpdated += 1;
}
//edgesUpdated += 1;
return e;
})
loop_count += 1;
if (loop_count > 2 * edgesToUpdate){
//console.log("aborting early", tempEdges, edgesUpdated)
edgesUpdated = edgesToUpdate;
}
if(complete || !changed){break}
}
return tempNewEdges
return tempNewEdges.map(({edge, sourceToMythic, destinationToMythic}) => ({
...edge,
source_to_mythic: sourceToMythic,
destination_to_mythic: destinationToMythic,
}))
}
if(extraNodes.current){
for(let i = 0; i < extraNodes.current.length; i++){
if(view_config["include_disconnected"]) {
if(filterRow(extraNodes.current[i])){
if(providedNodes){
for(let i = 0; i < providedNodes.length; i++){
if(view_config["show_all_nodes"]) {
if(filterRow(providedNodes[i])){
continue
}
add_node(extraNodes.current[i], view_config);
add_node(providedNodes[i], view_config);
}
}
}
@@ -1297,6 +1739,8 @@ export const DrawC2PathElementsFlow = ({edges, panel, view_config, contextMenu,
}
}
});
const visibleNodeCount = tempNodes.filter((node) => !node.hidden).length;
const shouldAnimateEdges = visibleNodeCount <= 20;
for(let i = 0; i < tempEdges.length; i++){
if(tempEdges[i].data.end_timestamp === null){
tempEdges[i].markerEnd = {
@@ -1304,7 +1748,8 @@ export const DrawC2PathElementsFlow = ({edges, panel, view_config, contextMenu,
}
tempEdges[i].style = {
stroke: theme.palette.success.main,
strokeWidth: 2,
strokeWidth: 2.5,
opacity: 0.95,
}
} else {
tempEdges[i].markerEnd = {
@@ -1313,6 +1758,7 @@ export const DrawC2PathElementsFlow = ({edges, panel, view_config, contextMenu,
tempEdges[i].style = {
stroke: theme.palette.error.main,
strokeWidth: 2,
opacity: 0.72,
}
}
tempEdges[i].markerEnd.type = "arrowclosed"
@@ -1325,7 +1771,7 @@ export const DrawC2PathElementsFlow = ({edges, panel, view_config, contextMenu,
}
tempEdges[i].labelShowBg = true
tempEdges[i].zIndex = 20;
tempEdges[i].animated = tempEdges[i].data.end_timestamp === null;
tempEdges[i].animated = shouldAnimateEdges && tempEdges[i].data.end_timestamp === null;
}
if(shouldUseGroups(view_config)){
// only add in edges from parents to parents/mythic if we're doing egress flow
@@ -1415,8 +1861,9 @@ export const DrawC2PathElementsFlow = ({edges, panel, view_config, contextMenu,
nodes: tempNodes,
edges: tempEdges
})
}, [edges, view_config, theme, filterOptions]);
}, [graphInputSignature]);
React.useEffect( () => {
let cancelled = false;
(async () => {
if(graphData.nodes.length > 0){
const {newNodes, newEdges} = await createLayout({
@@ -1425,9 +1872,11 @@ export const DrawC2PathElementsFlow = ({edges, panel, view_config, contextMenu,
initialEdges: graphData.edges,
alignment: view_config["rankDir"]
});
if(cancelled){return}
setNodes(newNodes);
setEdgeFlow(newEdges);
window.requestAnimationFrame(() => {
if(cancelled){return}
for(let i = 0; i < newNodes.length; i++){
updateNodeInternals(newNodes[i].id);
}
@@ -1435,12 +1884,17 @@ export const DrawC2PathElementsFlow = ({edges, panel, view_config, contextMenu,
});
}
})();
}, [graphData, view_config]);
return () => {
cancelled = true;
}
}, [graphData, view_config, setNodes, setEdgeFlow, updateNodeInternals, fitView]);
const onlyRenderVisibleGraphElements = nodes.length > 20;
const showMiniMap = nodes.length <= 250;
return (
<div style={{height: "100%", width: "100%"}} ref={viewportRef}>
<div className="mythic-graph-canvas mythic-c2-flow-canvas" style={{height: "100%", width: "100%", position: "relative"}} ref={viewportRef}>
<ReactFlow
fitView
onlyRenderVisibleElements={false}
onlyRenderVisibleElements={onlyRenderVisibleGraphElements}
panOnScrollSpeed={30}
maxZoom={100}
minZoom={0}
@@ -1455,7 +1909,7 @@ export const DrawC2PathElementsFlow = ({edges, panel, view_config, contextMenu,
onNodeClick={onNodeSelected}
>
<Panel position={"top-left"} >{panel}</Panel>
<Controls showInteractive={false} >
<Controls showInteractive={false} className="mythic-graph-controls">
<ControlButton onClick={onDownloadImageClickPng} title={"Download PNG"}>
<CameraAltIcon />
</ControlButton>
@@ -1463,12 +1917,14 @@ export const DrawC2PathElementsFlow = ({edges, panel, view_config, contextMenu,
<InsertPhotoIcon />
</ControlButton>
</Controls>
<MiniMap pannable={true} zoomable={true} />
{showMiniMap &&
<MiniMap pannable={true} zoomable={true} />
}
</ReactFlow>
{openContextMenu &&
<div style={{...contextMenuCoord, position: "fixed"}} className="context-menu">
<div style={{...contextMenuCoord, position: "absolute"}} className="context-menu mythic-graph-context-menu">
{contextMenu.map( (m) => (
<Button key={m.title} color={"info"} className="context-menu-button" onClick={() => {
<Button key={m.title} color={"info"} className="context-menu-button mythic-graph-context-menu-button" onClick={() => {
m.onClick(contextMenuNode.current);
setOpenContextMenu(false);
}}>{m.title}</Button>
@@ -2133,7 +2589,7 @@ export const DrawBrowserScriptElementsFlow = ({edges, panel, view_config, theme,
}
return (
<div style={{height: "100%", width: "100%", overflow: "hidden"}} ref={viewportRef}>
<div className="mythic-graph-canvas mythic-browser-script-graph-canvas" style={{height: "100%", width: "100%", overflow: "hidden"}} ref={viewportRef}>
<ReactFlow
fitView
onlyRenderVisibleElements={false}
@@ -2153,7 +2609,7 @@ export const DrawBrowserScriptElementsFlow = ({edges, panel, view_config, theme,
onEdgeClick={onEdgeSelected}
>
<Panel position={"top-left"} >{panel}</Panel>
<Controls showInteractive={false} style={{marginLeft: "40px"}} >
<Controls showInteractive={false} className="mythic-graph-controls mythic-browser-script-graph-controls" >
<ControlButton onClick={toggleViewConfig} title={"Toggle View"}>
<SwapCallsIcon />
</ControlButton>
@@ -2167,9 +2623,9 @@ export const DrawBrowserScriptElementsFlow = ({edges, panel, view_config, theme,
<MiniMap pannable={true} zoomable={true} />
</ReactFlow>
{openContextMenu &&
<div style={{...contextMenuCoord, position: "fixed"}} className="context-menu">
<div style={{...contextMenuCoord, position: "fixed"}} className="context-menu mythic-graph-context-menu">
{localContextMenu.map( (m) => (
<Button key={m?.key ? m.key : m.title} color={"info"} className="context-menu-button" onClick={() => {
<Button key={m?.key ? m.key : m.title} color={"info"} className="context-menu-button mythic-graph-context-menu-button" onClick={() => {
m.onClick(contextMenuNode.current);
setOpenContextMenu(false);
}}>{m.title}</Button>
@@ -1,10 +1,7 @@
import React, {useEffect, useState, useMemo, useContext} from 'react';
import {DrawC2PathElementsFlowWithProvider} from './C2PathDialog';
import {Button} from '@mui/material';
import ButtonGroup from '@mui/material/ButtonGroup';
import ArrowDropDownIcon from '@mui/icons-material/ArrowDropDown';
import MenuItem from '@mui/material/MenuItem';
import ClickAwayListener from '@mui/material/ClickAwayListener';
import {useMutation } from '@apollo/client';
import {hideCallbackMutation, removeEdgeMutation, addEdgeMutation} from './CallbackMutations';
import { MythicDialog } from '../../MythicComponents/MythicDialog';
@@ -19,8 +16,7 @@ import OutlinedInput from '@mui/material/OutlinedInput';
import InputLabel from '@mui/material/InputLabel';
import FormControl from '@mui/material/FormControl';
import Select from '@mui/material/Select';
import {CallbackGraphEdgesContext, CallbacksContext, OnOpenTabContext} from './CallbacksTop';
import {Dropdown, DropdownMenuItem} from "../../MythicComponents/MythicNestedMenus";
import {CallbackGraphEdgesContext, CallbacksContext} from './CallbacksTop';
import {GetMythicSetting} from "../../MythicComponents/MythicSavedUserSetting";
import {operatorSettingDefaults} from "../../../cache";
@@ -60,8 +56,6 @@ query loadedLinkCommandsQuery ($callback_id: Int!){
const GraphViewOptions = ({viewConfig, setViewConfig}) => {
const theme = useTheme();
const dropdownAnchorRef = React.useRef(null);
const [dropdownOpen, setDropdownOpen] = React.useState(false);
const [showConfiguration, setShowConfiguration] = React.useState(false);
const labelComponentOptions = ["display_id", "user", "host", "ip", "domain", "os", "process_name"];
const [selectedComponentOptions, setSelectedComponentOptions] = React.useState(["display_id", "user"]);
@@ -85,120 +79,104 @@ const GraphViewOptions = ({viewConfig, setViewConfig}) => {
useEffect( () => {
setViewConfig({...viewConfig, group_by: selectedGroupBy});
}, [selectedGroupBy])
const handleDropdownToggle = (evt) => {
evt.stopPropagation();
setDropdownOpen((prevOpen) => !prevOpen);
};
const handleMenuItemClick = (event, click) => {
click();
setDropdownOpen(false);
};
const options = [
{name: showConfiguration ? "Hide Grouping Options": "Show Grouping Options", click: () => {
setShowConfiguration(!showConfiguration);
}},
{name: viewConfig["include_disconnected"] ? 'Show Only Active Edges' : "Show All Edges", click: () => {
const view = {...viewConfig, include_disconnected: !viewConfig["include_disconnected"]};
setViewConfig(view);
}},
{name: viewConfig["show_all_nodes"] ? 'Hide inactive callbacks' : 'Show All Callbacks', click: () => {
const view = {...viewConfig, show_all_nodes: !viewConfig["show_all_nodes"]};
setViewConfig(view);
}},
{name: viewConfig["rankDir"] === "LR" ? 'Change Layout to Top-Bottom' : "Change Layout to Left-Right", click: () => {
if(viewConfig["rankDir"] === "LR"){
const view = {...viewConfig, rankDir: "TB"};
setViewConfig(view);
}else{
const view = {...viewConfig, rankDir: "LR"};
setViewConfig(view);
}
}},
{name: viewConfig["packet_flow_view"] ? "View Connection Directions" : "View Egress Routes" , click: () => {
const view = {...viewConfig, packet_flow_view: !viewConfig["packet_flow_view"]};
setViewConfig(view);
}},
];
const handleClose = (event) => {
if (dropdownAnchorRef.current && dropdownAnchorRef.current.contains(event.target)) {
return;
}
setDropdownOpen(false);
};
const toggleViewConfig = (field) => {
setViewConfig({...viewConfig, [field]: !viewConfig[field]});
}
const toggleRankDirection = () => {
setViewConfig({...viewConfig, rankDir: viewConfig["rankDir"] === "LR" ? "TB" : "LR"});
}
return (
<div >
<ButtonGroup variant="contained" ref={dropdownAnchorRef} aria-label="split button" style={{marginTop: "0px", marginLeft: "25px"}} color="primary">
<Button size="small" color="primary" aria-controls={dropdownOpen ? 'split-button-menu' : undefined}
aria-expanded={dropdownOpen ? 'true' : undefined}
aria-haspopup="menu"
onClick={handleDropdownToggle}>
Configure Graph View <ArrowDropDownIcon />
</Button>
</ButtonGroup>
<div className="mythic-callback-graph-options">
<Button
size="small"
className="mythic-callback-graph-options-toggle"
onClick={() => setShowConfiguration(!showConfiguration)}
>
{showConfiguration ? "Hide Graph Options" : "Graph Options"}
</Button>
{showConfiguration &&
<FormControl sx={{ width: 200, backgroundColor: theme.palette.background.paper}} >
<InputLabel id="demo-chip-label">Group Callbacks By</InputLabel>
<Select
labelId="demo-chip-label"
id="demo-chip"
value={selectedGroupBy}
onChange={handleGroupByChange}
input={<OutlinedInput id="select-chip" label="Group Callbacks By" />}
>
{groupByOptions.map((name) => (
<MenuItem
key={name}
value={name}
<div className="mythic-callback-graph-options-panel">
<div className="mythic-callback-graph-options-actions">
<Button
size="small"
className={`mythic-callback-graph-option-button ${!viewConfig["include_disconnected"] ? "mythic-callback-graph-option-button-active" : ""}`}
onClick={() => toggleViewConfig("include_disconnected")}
>
{viewConfig["include_disconnected"] ? "All Edges" : "Active Edges"}
</Button>
<Button
size="small"
className={`mythic-callback-graph-option-button ${viewConfig["show_all_nodes"] ? "mythic-callback-graph-option-button-warning" : ""}`}
onClick={() => toggleViewConfig("show_all_nodes")}
>
{viewConfig["show_all_nodes"] ? "All Callbacks" : "Connected Callbacks"}
</Button>
<Button
size="small"
className="mythic-callback-graph-option-button"
onClick={toggleRankDirection}
>
{viewConfig["rankDir"] === "LR" ? "Left to Right" : "Top to Bottom"}
</Button>
<Button
size="small"
className="mythic-callback-graph-option-button"
onClick={() => toggleViewConfig("packet_flow_view")}
>
{viewConfig["packet_flow_view"] ? "Egress Routes" : "Connection Directions"}
</Button>
</div>
<div className="mythic-callback-graph-options-fields">
<FormControl size="small" className="mythic-callback-graph-options-field">
<InputLabel id="callback-graph-group-label">Group By</InputLabel>
<Select
labelId="callback-graph-group-label"
id="callback-graph-group"
value={selectedGroupBy}
onChange={handleGroupByChange}
input={<OutlinedInput id="select-callback-graph-group" label="Group By" />}
>
{name}
</MenuItem>
))}
</Select>
</FormControl>
}
{showConfiguration &&
<FormControl sx={{minWidth: 300, backgroundColor: theme.palette.background.paper}}>
<InputLabel id="demo-multiple-chip-label">Display Properties per Callback</InputLabel>
<Select
labelId="demo-multiple-chip-label"
id="demo-multiple-chip"
multiple
value={selectedComponentOptions}
onChange={handleChange}
input={<OutlinedInput id="select-multiple-chip" label="Display Properties per Callback" />}
MenuProps={MenuProps}
>
{labelComponentOptions.map((name) => (
<MenuItem
key={name}
value={name}
style={getStyles(name, selectedComponentOptions, theme)}
{groupByOptions.map((name) => (
<MenuItem
key={name}
value={name}
>
{name}
</MenuItem>
))}
</Select>
</FormControl>
<FormControl size="small" className="mythic-callback-graph-options-field mythic-callback-graph-options-field-wide">
<InputLabel id="callback-graph-label-components-label">Callback Labels</InputLabel>
<Select
labelId="callback-graph-label-components-label"
id="callback-graph-label-components"
multiple
value={selectedComponentOptions}
onChange={handleChange}
input={<OutlinedInput id="select-callback-graph-label-components" label="Callback Labels" />}
MenuProps={MenuProps}
renderValue={(selected) => selected.join(", ")}
>
{name}
</MenuItem>
))}
</Select>
</FormControl>
}
<ClickAwayListener onClickAway={handleClose}>
<Dropdown
isOpen={dropdownAnchorRef.current}
onOpen={setDropdownOpen}
externallyOpen={dropdownOpen}
menu={
options.map((option, index) => (
<DropdownMenuItem
key={option.name}
disabled={option.disabled}
onClick={(event) => handleMenuItemClick(event, option.click)}
>
{option.name}
</DropdownMenuItem>
))
{labelComponentOptions.map((name) => (
<MenuItem
key={name}
value={name}
style={getStyles(name, selectedComponentOptions, theme)}
>
{name}
</MenuItem>
))}
</Select>
</FormControl>
</div>
{viewConfig["show_all_nodes"] &&
<div className="mythic-callback-graph-options-warning">
Showing all callbacks can be slow in large operations.
</div>
}
/>
</ClickAwayListener>
</div>
}
</div>
)
}
@@ -257,7 +235,7 @@ export function CallbacksGraph({onOpenTab}){
rankDir: "TB",
label_components: ["display_id", "user"],
packet_flow_view: true,
include_disconnected: true,
include_disconnected: false,
show_all_nodes: false,
group_by: "None"
});
@@ -314,19 +292,20 @@ export function CallbacksGraph({onOpenTab}){
}
const contextMenu = useMemo(() => {return [
{
title: 'Hide Callback',
onClick: function(node) {
hideCallback({variables: {callback_display_id: node.display_id}});
}
},
{
title: 'Interact',
title: 'Open Tasking',
onClick: function(node){
onOpenTab({tabType: "interact", tabID: node.callback_id + "interact", callbackID: node.callback_id});
}
},
{
title: "Manually Remove Edge",
title: "Add P2P Edge",
onClick: function(node){
setAddEdgeSource(node);
setManuallyAddEdgeDialogOpen(true);
}
},
{
title: "Remove Active Edge",
onClick: function(node){
const opts = callbackgraphedges.reduce( (prev, e) => {
if(e.source.id === node.id || e.destination.id === node.id){
@@ -348,15 +327,8 @@ export function CallbacksGraph({onOpenTab}){
setManuallyRemoveEdgeDialogOpen(true);
}
},
{
title: "Manually Add Edge",
onClick: function(node){
setAddEdgeSource(node);
setManuallyAddEdgeDialogOpen(true);
}
},
{
title: "Task Callback for Edge",
title: "Task Link Command",
onClick: function(node){
setLinkCommands([]);
setSelectedLinkCommand(null);
@@ -365,7 +337,13 @@ export function CallbacksGraph({onOpenTab}){
setSelectedCallback(node);
}
},
]}, [getLinkCommands, hideCallback, viewConfig, onOpenTab]);
{
title: 'Hide Callback',
onClick: function(node) {
hideCallback({variables: {callback_display_id: node.display_id}});
}
},
]}, [getLinkCommands, hideCallback, callbackgraphedges, onOpenTab]);
React.useEffect( () => {
try {
const storageItemOptions = GetMythicSetting({setting_name: "callbacks_table_filter_options", default_value: operatorSettingDefaults.callbacks_table_filters});
@@ -142,7 +142,6 @@ callback(where: {active: {_eq: true}}) {
active
id
display_id
operation_id
user
host
ip
@@ -150,34 +149,17 @@ callback(where: {active: {_eq: true}}) {
os
process_name
integrity_level
extra_info
tags {
tagtype {
name
color
id
}
id
}
payload {
payloadtype {
name
id
}
}
callbackc2profiles {
c2profile {
name
has_logo
is_p2p
}
}
}
source {
active
id
display_id
operation_id
user
host
ip
@@ -185,28 +167,12 @@ callback(where: {active: {_eq: true}}) {
os
process_name
integrity_level
extra_info
tags {
tagtype {
name
color
id
}
id
}
payload {
payloadtype {
name
id
}
}
callbackc2profiles {
c2profile {
name
has_logo
is_p2p
}
}
}
c2profile {
id
@@ -225,9 +191,7 @@ callbackgraphedge_stream(batch_size: 100, cursor: {initial_value: {updated_at: $
destination {
active
id
color
display_id
operation_id
user
host
ip
@@ -235,35 +199,17 @@ callbackgraphedge_stream(batch_size: 100, cursor: {initial_value: {updated_at: $
os
process_name
integrity_level
extra_info
tags {
tagtype {
name
color
id
}
id
}
payload {
payloadtype {
name
id
}
}
callbackc2profiles {
c2profile {
name
has_logo
is_p2p
}
}
}
source {
active
id
color
display_id
operation_id
user
host
ip
@@ -271,28 +217,12 @@ callbackgraphedge_stream(batch_size: 100, cursor: {initial_value: {updated_at: $
os
process_name
integrity_level
extra_info
tags {
tagtype {
name
color
id
}
id
}
payload {
payloadtype {
name
id
}
}
callbackc2profiles {
c2profile {
name
has_logo
is_p2p
}
}
}
c2profile {
id
@@ -1,40 +1,24 @@
import React, {useRef} from 'react';
import { styled } from '@mui/material/styles';
import Button from '@mui/material/Button';
import DialogActions from '@mui/material/DialogActions';
import React from 'react';
import Box from '@mui/material/Box';
import DialogContent from '@mui/material/DialogContent';
import DialogTitle from '@mui/material/DialogTitle';
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 OutlinedInput from '@mui/material/OutlinedInput';
import Typography from '@mui/material/Typography';
import {useQuery, gql } from '@apollo/client';
import LinearProgress from '@mui/material/LinearProgress';
import {snackActions} from "../../utilities/Snackbar";
const PREFIX = 'ManuallyAddEdgeDialog';
const classes = {
formControl: `${PREFIX}-formControl`,
selectEmpty: `${PREFIX}-selectEmpty`
};
const Root = styled('div')((
{
theme
}
) => ({
[`& .${classes.formControl}`]: {
margin: theme.spacing(1),
minWidth: 120,
width: "97%"
},
[`& .${classes.selectEmpty}`]: {
marginTop: theme.spacing(2),
}
}));
import {
MythicDialogBody,
MythicDialogButton,
MythicDialogFooter,
MythicDialogGrid,
MythicDialogSection,
MythicFormField
} from "../../MythicComponents/MythicDialogLayout";
const getP2PProfilesAndCallbacks = gql`
query getP2PProfilesAndCallbacks{
@@ -52,112 +36,201 @@ query getP2PProfilesAndCallbacks{
}
}
`;
const CallbackSummary = ({callback, label}) => (
<Box className="mythic-c2-edge-callback-summary">
{label &&
<Typography component="div" className="mythic-c2-edge-summary-label">
{label}
</Typography>
}
<Box className="mythic-c2-edge-summary-main">
<span className="mythic-c2-edge-callback-id">#{callback?.display_id}</span>
<span className="mythic-c2-edge-summary-description">
{callback?.description || "No description"}
</span>
</Box>
</Box>
);
const getDestinationOptions = (profiles, sourceId) => {
const destinations = new Map();
profiles.forEach((profile) => {
profile.callbackc2profiles.forEach((callbackProfile) => {
if(callbackProfile.callback.id === sourceId){return}
const key = callbackProfile.callback.id;
const current = destinations.get(key) || {
callback: callbackProfile.callback,
profiles: [],
};
current.profiles.push(profile);
destinations.set(key, current);
});
});
return Array.from(destinations.values()).sort((a, b) => b.callback.display_id - a.callback.display_id);
};
export function ManuallyAddEdgeDialog(props) {
const [callbackOptions, setCallbackOptions] = React.useState([]);
const [profileOptions, setProfileOptions] = React.useState([]);
const [selectedDestination, setSelectedDestination] = React.useState('');
const [selectedProfile, setSelectedProfile] = React.useState('');
const inputRefC2 = useRef(null);
const inputRefDestination = useRef(null);
const handleChangeProfile = (event) => {
setSelectedProfile(event.target.value);
if(event.target.value === ""){
setCallbackOptions([]);
setSelectedDestination("");
}else{
const cbopts = event.target.value["callbackc2profiles"].filter( (cb) => cb.callback.id !== props.source.id );
setCallbackOptions(cbopts);
if(cbopts.length > 0){
setSelectedDestination(cbopts[0]);
}
}
setSelectedProfile(event.target.value);
};
const handleChangeDestination = (event) => {
setSelectedDestination(event.target.value);
const destination = event.target.value;
setSelectedDestination(destination);
if(destination === ""){
setSelectedProfile("");
return;
}
const availableProfiles = destination.profiles || [];
const profileStillValid = availableProfiles.some((profile) => profile.id === selectedProfile?.id);
if(!profileStillValid){
setSelectedProfile(availableProfiles[0] || "");
}
};
const handleSubmit = () => {
if(selectedDestination === ""){
snackActions.error("Must select a valid destination");
return;
}
if(selectedProfile === ""){
snackActions.error("Must select a valid P2P C2 profile");
return;
}
props.onSubmit(props.source.display_id, selectedProfile, selectedDestination.callback);
props.onClose();
}
const { loading, error } = useQuery(getP2PProfilesAndCallbacks, {
onCompleted: data => {
setProfileOptions([...data.c2profile]);
if(data.c2profile.length > 0){
setSelectedProfile(data.c2profile[0]);
const cbopts = data.c2profile[0]["callbackc2profiles"].filter( (cb) => cb.callback.id !== props.source.id );
setCallbackOptions(cbopts);
if(cbopts.length > 0){
setSelectedDestination(cbopts[0]);
}
const profiles = [...data.c2profile];
const destinations = getDestinationOptions(profiles, props.source.id);
setProfileOptions(profiles);
setCallbackOptions(destinations);
if(destinations.length > 0){
setSelectedDestination(destinations[0]);
setSelectedProfile(destinations[0].profiles[0] || "");
}else{
setSelectedDestination("");
setSelectedProfile("");
}
},
fetchPolicy: "network-only"
});
const availableProfileOptions = selectedDestination === "" ? profileOptions : selectedDestination.profiles || [];
if (loading) {
return <LinearProgress style={{marginTop: "10px"}} />;
return (
<>
<DialogTitle className="mythic-c2-action-title">Add P2P Edge</DialogTitle>
<DialogContent dividers={true}>
<LinearProgress />
</DialogContent>
</>
);
}
if (error) {
console.error(error);
return <div>Error! {error.message}</div>;
return (
<>
<DialogTitle className="mythic-c2-action-title">Add P2P Edge</DialogTitle>
<DialogContent dividers={true}>
<MythicDialogBody>
<MythicDialogSection title="Unable to load edge options" description={error.message} />
</MythicDialogBody>
</DialogContent>
<MythicDialogFooter>
<MythicDialogButton onClick={props.onClose}>Close</MythicDialogButton>
</MythicDialogFooter>
</>
);
}
return (
<Root>
<DialogTitle >Manually Add Edge From Callback {props.source.display_id}</DialogTitle>
<>
<DialogTitle className="mythic-c2-action-title">
<div className="mythic-dialog-title-row">
<div>
<Typography component="div" className="mythic-c2-action-title-text">
Add P2P Edge
</Typography>
<Typography component="div" className="mythic-c2-action-title-subtitle">
Create a manual route from callback {props.source.display_id} through a peer-to-peer C2 profile.
</Typography>
</div>
</div>
</DialogTitle>
<DialogContent dividers={true}>
<React.Fragment>
Manually add an edge from Callback {props.source.display_id} to another callback via a P2P C2 Profile.<br/>
<FormControl className={classes.formControl}>
<InputLabel ref={inputRefC2}>Profile</InputLabel>
<Select
labelId="demo-dialog-select-label-profile"
id="demo-dialog-select"
value={selectedProfile}
onChange={handleChangeProfile}
style={{minWidth: "30%"}}
input={<Input />}
>
<MenuItem value="">
<em>None</em>
</MenuItem>
{profileOptions.map( (opt) => (
<MenuItem value={opt} key={"profile:" + opt.id}>{opt.name}</MenuItem>
) )}
</Select>
</FormControl><br/>
<FormControl className={classes.formControl}>
<InputLabel ref={inputRefDestination}>Destination</InputLabel>
<Select
labelId="demo-dialog-select-label-destination"
id="demo-dialog-select-destination"
value={selectedDestination}
onChange={handleChangeDestination}
input={<Input />}
>
<MenuItem value="">
<em>None</em>
</MenuItem>
{callbackOptions.map( (opt) => (
<MenuItem value={opt} key={"callback:" + opt.callback.id}>{opt.callback.display_id} ({opt.callback.description})</MenuItem>
) )}
</Select>
</FormControl>
</React.Fragment>
<MythicDialogBody>
<MythicDialogSection
title="Route"
description="Choose the destination callback first, then the P2P C2 profile for that route."
>
<MythicDialogGrid minWidth="15rem" className="mythic-c2-edge-endpoints-grid">
<MythicFormField label="Source callback" className="mythic-c2-edge-source-field">
<CallbackSummary callback={props.source} />
</MythicFormField>
<MythicFormField
label="Destination callback"
required
>
<FormControl fullWidth size="small">
<InputLabel id="manual-c2-edge-destination-label">Destination</InputLabel>
<Select
labelId="manual-c2-edge-destination-label"
id="manual-c2-edge-destination"
value={selectedDestination}
onChange={handleChangeDestination}
input={<OutlinedInput label="Destination" />}
>
<MenuItem value="">
<em>None</em>
</MenuItem>
{callbackOptions.map( (opt) => (
<MenuItem value={opt} key={"callback:" + opt.callback.id}>
#{opt.callback.display_id} {opt.callback.description ? `- ${opt.callback.description}` : ""}
</MenuItem>
) )}
</Select>
</FormControl>
</MythicFormField>
</MythicDialogGrid>
<Box className="mythic-c2-edge-profile-row">
<MythicFormField
label="P2P C2 profile"
description="Only profiles available for the selected destination are shown."
required
>
<FormControl fullWidth size="small">
<InputLabel id="manual-c2-edge-profile-label">Profile</InputLabel>
<Select
labelId="manual-c2-edge-profile-label"
id="manual-c2-edge-profile"
value={selectedProfile}
onChange={handleChangeProfile}
input={<OutlinedInput label="Profile" />}
>
<MenuItem value="">
<em>None</em>
</MenuItem>
{availableProfileOptions.map( (opt) => (
<MenuItem value={opt} key={"profile:" + opt.id}>
{opt.name}
</MenuItem>
) )}
</Select>
</FormControl>
</MythicFormField>
</Box>
</MythicDialogSection>
</MythicDialogBody>
</DialogContent>
<DialogActions>
<Button onClick={props.onClose} variant="contained" color="primary">
<MythicDialogFooter>
<MythicDialogButton onClick={props.onClose}>
Close
</Button>
<Button disabled={selectedDestination === ""} onClick={handleSubmit} variant="contained" color="success">
Add
</Button>
</DialogActions>
</Root>
</MythicDialogButton>
<MythicDialogButton disabled={selectedDestination === "" || selectedProfile === ""} onClick={handleSubmit} intent="primary">
Add Edge
</MythicDialogButton>
</MythicDialogFooter>
</>
);
}
@@ -78,12 +78,15 @@ export const ResponseDisplayGraph = ({graph, task, expand}) =>{
if(!showGraph){
return (
<>
<div style={{display: "flex", width: "100%", height: "100%", justifyContent: "center", flexDirection: "column", alignItems: "center"}}>
<Typography variant={"h4"} >
{`Graph Hidden by Default Due to Size: Nodes (${graph.nodes.length}), Edges (${graph.edges.length}) `}
<div className="mythic-graph-empty-state">
<Typography component="div" className="mythic-graph-empty-title">
Large graph hidden
</Typography>
<Button variant={"contained"} color={"error"} onClick={() => {setShowGraph(!showGraph)}}>
{"Show Graph"}
<Typography component="div" className="mythic-graph-empty-description">
{`This response contains ${graph.nodes.length} nodes and ${graph.edges.length} edges.`}
</Typography>
<Button className="mythic-graph-empty-action" variant={"contained"} onClick={() => {setShowGraph(!showGraph)}}>
Show Graph
</Button>
</div>
</>
@@ -92,9 +95,12 @@ export const ResponseDisplayGraph = ({graph, task, expand}) =>{
if(graph.nodes.length === 0){
return (
<>
<div style={{display: "flex", width: "100%", height: "100%", justifyContent: "center", flexDirection: "column", alignItems: "center"}}>
<Typography variant={"h4"} >
{`Empty Graph`}
<div className="mythic-graph-empty-state">
<Typography component="div" className="mythic-graph-empty-title">
Empty graph
</Typography>
<Typography component="div" className="mythic-graph-empty-description">
There are no nodes to render for this response.
</Typography>
</div>
</>
@@ -118,4 +124,4 @@ export const ResponseDisplayGraph = ({graph, task, expand}) =>{
/>
</div>
);
}
}
@@ -831,7 +831,7 @@ function EventStepRender({selectedEventGroup, useSuppliedData}) {
})();
}, [graphData]);
return (
<div className="mythic-eventing-flow-canvas" ref={viewportRef}>
<div className="mythic-eventing-flow-canvas mythic-graph-canvas" ref={viewportRef}>
<ReactFlow
fitView
onlyRenderVisibleElements={false}
@@ -850,13 +850,13 @@ function EventStepRender({selectedEventGroup, useSuppliedData}) {
{selectedEventGroup.id > 0 &&
<div className="mythic-eventing-flow-badge">Event group {selectedEventGroup.id}</div>
}
<Controls showInteractive={false} className="mythic-eventing-flow-controls">
<Controls showInteractive={false} className="mythic-eventing-flow-controls mythic-graph-controls">
</Controls>
</ReactFlow>
{openContextMenu &&
<div style={{...contextMenuCoord, position: "fixed"}} className="context-menu">
<div style={{...contextMenuCoord, position: "fixed"}} className="context-menu mythic-graph-context-menu">
{contextMenu.map( (m) => (
<Button key={m.title} className="context-menu-button mythic-table-row-action mythic-table-row-action-hover-info" variant="outlined" onClick={() => {
<Button key={m.title} className="context-menu-button mythic-graph-context-menu-button mythic-table-row-action mythic-table-row-action-hover-info" variant="outlined" onClick={() => {
m.onClick(contextMenuNode.current);
setOpenContextMenu(false);
}}>{m.title}</Button>
@@ -1134,7 +1134,7 @@ function EventStepInstanceRender({selectedEventGroupInstance}) {
})();
}, [graphData]);
return (
<div className="mythic-eventing-flow-canvas" ref={viewportRef}>
<div className="mythic-eventing-flow-canvas mythic-graph-canvas" ref={viewportRef}>
<ReactFlow
fitView
onlyRenderVisibleElements={false}
@@ -1151,13 +1151,13 @@ function EventStepInstanceRender({selectedEventGroupInstance}) {
onNodeContextMenu={onNodeContextMenu}
>
<div className="mythic-eventing-flow-badge">Instance {selectedEventGroupInstance}</div>
<Controls showInteractive={false} className="mythic-eventing-flow-controls">
<Controls showInteractive={false} className="mythic-eventing-flow-controls mythic-graph-controls">
</Controls>
</ReactFlow>
{openContextMenu &&
<div style={{...contextMenuCoord, position: "fixed"}} className="context-menu">
<div style={{...contextMenuCoord, position: "fixed"}} className="context-menu mythic-graph-context-menu">
{contextMenu.map( (m) => (
<Button key={m.title} className="context-menu-button mythic-table-row-action mythic-table-row-action-hover-info" variant="outlined" onClick={() => {
<Button key={m.title} className="context-menu-button mythic-graph-context-menu-button mythic-table-row-action mythic-table-row-action-hover-info" variant="outlined" onClick={() => {
m.onClick(contextMenuNode.current);
setOpenContextMenu(false);
}}>{m.title}</Button>
+530
View File
@@ -236,6 +236,191 @@ tspan {
background-color: ${(props) => props.theme.palette.background.paper};
color: unset;
}
.mythic-graph-context-menu.context-menu {
display: flex;
flex-direction: column;
gap: 0.2rem;
min-width: 11rem;
padding: 0.25rem;
z-index: 1400;
}
.mythic-graph-context-menu .mythic-graph-context-menu-button.MuiButton-root {
align-items: center;
background-color: transparent !important;
border: 1px solid transparent !important;
border-radius: ${(props) => props.theme.shape.borderRadius}px;
color: ${(props) => props.theme.palette.text.primary} !important;
display: flex;
font-size: 0.76rem;
font-weight: 750;
justify-content: flex-start;
min-height: 30px;
padding: 0.38rem 0.5rem;
text-align: left;
text-transform: none;
}
.mythic-graph-context-menu .mythic-graph-context-menu-button.MuiButton-root:hover {
background-color: ${(props) => alpha(props.theme.palette.info.main, props.theme.palette.mode === "dark" ? 0.18 : 0.08)} !important;
border-color: ${(props) => alpha(props.theme.palette.info.main, 0.36)} !important;
color: ${(props) => props.theme.palette.info.main} !important;
}
.mythic-graph-canvas .react-flow__controls {
background-color: ${(props) => props.theme.palette.background.paper};
border: 1px solid ${(props) => props.theme.table?.borderSoft || props.theme.borderColor};
border-radius: ${(props) => props.theme.shape.borderRadius}px;
box-shadow: 0 6px 18px ${(props) => alpha(props.theme.palette.common.black, props.theme.palette.mode === "dark" ? 0.22 : 0.08)} !important;
overflow: hidden;
}
.mythic-graph-canvas .react-flow__controls-button {
background-color: ${(props) => props.theme.palette.background.paper};
border-bottom-color: ${(props) => props.theme.table?.borderSoft || props.theme.borderColor};
color: ${(props) => props.theme.palette.text.secondary};
}
.mythic-graph-canvas .react-flow__controls-button:hover {
background-color: ${(props) => alpha(props.theme.palette.primary.main, props.theme.palette.mode === "dark" ? 0.16 : 0.08)};
color: ${(props) => props.theme.palette.primary.main};
}
.mythic-graph-canvas .react-flow__controls-button svg {
color: inherit;
}
.mythic-graph-canvas .react-flow__minimap {
background-color: ${(props) => props.theme.palette.background.paper};
border: 1px solid ${(props) => props.theme.table?.borderSoft || props.theme.borderColor};
border-radius: ${(props) => props.theme.shape.borderRadius}px;
box-shadow: 0 6px 18px ${(props) => alpha(props.theme.palette.common.black, props.theme.palette.mode === "dark" ? 0.22 : 0.08)};
overflow: hidden;
}
.mythic-graph-empty-state {
align-items: center;
background: ${(props) => getSubtleAccentGradient(props)};
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: flex;
flex-direction: column;
gap: 0.35rem;
height: 100%;
justify-content: center;
min-height: 12rem;
padding: 1.25rem;
text-align: center;
width: 100%;
}
.mythic-graph-empty-title.MuiTypography-root {
color: ${(props) => props.theme.palette.text.primary};
font-size: 1rem;
font-weight: 850;
}
.mythic-graph-empty-description.MuiTypography-root {
color: ${(props) => props.theme.palette.text.secondary};
font-size: 0.8rem;
}
.mythic-graph-empty-action.MuiButton-root {
background-color: ${(props) => props.theme.palette.mode === "dark" ? "rgba(255,255,255,0.06)" : "rgba(0,0,0,0.035)"} !important;
border: 1px solid ${(props) => props.theme.table?.borderSoft || props.theme.borderColor} !important;
border-radius: ${(props) => props.theme.shape.borderRadius}px;
box-shadow: none !important;
color: ${(props) => props.theme.palette.text.primary} !important;
font-size: 0.76rem;
font-weight: 750;
margin-top: 0.35rem;
min-height: 30px;
text-transform: none;
}
.mythic-graph-empty-action.MuiButton-root:hover {
background-color: ${(props) => alpha(props.theme.palette.info.main, props.theme.palette.mode === "dark" ? 0.18 : 0.08)} !important;
border-color: ${(props) => alpha(props.theme.palette.info.main, 0.36)} !important;
color: ${(props) => props.theme.palette.info.main} !important;
}
.mythic-callback-graph-options {
display: flex;
flex-direction: column;
gap: 0.45rem;
margin: 0.65rem;
max-width: min(44rem, calc(100vw - 3rem));
min-width: min(28rem, calc(100vw - 3rem));
}
.mythic-callback-graph-options-toggle.MuiButton-root,
.mythic-callback-graph-option-button.MuiButton-root {
background-color: ${(props) => props.theme.palette.background.paper} !important;
border: 1px solid ${(props) => props.theme.table?.borderSoft || props.theme.borderColor} !important;
border-radius: ${(props) => props.theme.shape.borderRadius}px;
box-shadow: 0 6px 18px ${(props) => alpha(props.theme.palette.common.black, props.theme.palette.mode === "dark" ? 0.22 : 0.08)} !important;
color: ${(props) => props.theme.palette.text.primary} !important;
font-size: 0.74rem;
font-weight: 800;
min-height: 30px;
text-transform: none;
}
.mythic-callback-graph-options-toggle.MuiButton-root {
align-self: flex-start;
}
.mythic-callback-graph-options-toggle.MuiButton-root:hover,
.mythic-callback-graph-option-button.MuiButton-root:hover {
background-color: ${(props) => alpha(props.theme.palette.primary.main, props.theme.palette.mode === "dark" ? 0.16 : 0.08)} !important;
border-color: ${(props) => alpha(props.theme.palette.primary.main, 0.38)} !important;
color: ${(props) => props.theme.palette.primary.main} !important;
}
.mythic-callback-graph-options-panel {
background-color: ${(props) => alpha(props.theme.palette.background.paper, props.theme.palette.mode === "dark" ? 0.96 : 0.98)};
border: 1px solid ${(props) => props.theme.table?.borderSoft || props.theme.borderColor};
border-radius: ${(props) => props.theme.shape.borderRadius}px;
box-shadow: 0 12px 32px ${(props) => alpha(props.theme.palette.common.black, props.theme.palette.mode === "dark" ? 0.32 : 0.12)};
display: flex;
flex-direction: column;
gap: 0.65rem;
min-width: 0;
padding: 0.65rem;
}
.mythic-callback-graph-options-actions {
display: flex;
flex-wrap: wrap;
gap: 0.4rem;
min-width: 0;
}
.mythic-callback-graph-option-button.MuiButton-root {
box-shadow: none !important;
}
.mythic-callback-graph-option-button-active.MuiButton-root {
background-color: ${(props) => alpha(props.theme.palette.success.main, props.theme.palette.mode === "dark" ? 0.18 : 0.09)} !important;
border-color: ${(props) => alpha(props.theme.palette.success.main, 0.38)} !important;
color: ${(props) => props.theme.palette.success.main} !important;
}
.mythic-callback-graph-option-button-warning.MuiButton-root {
background-color: ${(props) => alpha(props.theme.palette.warning.main, props.theme.palette.mode === "dark" ? 0.18 : 0.09)} !important;
border-color: ${(props) => alpha(props.theme.palette.warning.main, 0.38)} !important;
color: ${(props) => props.theme.palette.warning.main} !important;
}
.mythic-callback-graph-options-fields {
display: grid;
gap: 0.65rem;
grid-template-columns: minmax(12rem, 0.8fr) minmax(18rem, 1.4fr);
min-width: 0;
}
.mythic-callback-graph-options-field.MuiFormControl-root {
background-color: ${(props) => props.theme.palette.background.paper};
min-width: 0;
}
.mythic-callback-graph-options-field .MuiInputBase-root {
min-height: 38px;
}
.mythic-callback-graph-options-warning {
background-color: ${(props) => alpha(props.theme.palette.warning.main, props.theme.palette.mode === "dark" ? 0.16 : 0.08)};
border: 1px solid ${(props) => alpha(props.theme.palette.warning.main, 0.34)};
border-radius: ${(props) => props.theme.shape.borderRadius}px;
color: ${(props) => props.theme.palette.warning.main};
font-size: 0.74rem;
font-weight: 780;
padding: 0.45rem 0.55rem;
}
@media (max-width: 760px) {
.mythic-callback-graph-options {
min-width: min(20rem, calc(100vw - 2rem));
}
.mythic-callback-graph-options-fields {
grid-template-columns: minmax(0, 1fr);
}
}
.selectedTask {
background-color: ${(props) => props.theme.selectedCallbackColor + "DD"} !important;
@@ -2784,16 +2969,37 @@ tspan {
gap: 0.35rem;
}
.mythic-c2-path-summary-chip {
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;
font-size: 0.7rem;
font-weight: 800;
gap: 0.28rem;
line-height: 1;
padding: 0.32rem 0.45rem;
white-space: nowrap;
}
.mythic-c2-path-summary-chip svg {
font-size: 0.9rem;
}
.mythic-c2-path-summary-chip-success {
background-color: ${(props) => alpha(props.theme.palette.success.main, props.theme.palette.mode === "dark" ? 0.18 : 0.1)};
border-color: ${(props) => alpha(props.theme.palette.success.main, 0.42)};
color: ${(props) => props.theme.palette.success.main};
}
.mythic-c2-path-summary-chip-warning {
background-color: ${(props) => alpha(props.theme.palette.warning.main, props.theme.palette.mode === "dark" ? 0.2 : 0.12)};
border-color: ${(props) => alpha(props.theme.palette.warning.main, 0.46)};
color: ${(props) => props.theme.palette.warning.main};
}
.mythic-c2-path-summary-chip-error {
background-color: ${(props) => alpha(props.theme.palette.error.main, props.theme.palette.mode === "dark" ? 0.2 : 0.11)};
border-color: ${(props) => alpha(props.theme.palette.error.main, 0.48)};
color: ${(props) => props.theme.palette.error.main};
}
.mythic-c2-path-content.MuiDialogContent-root {
display: flex;
flex-direction: column;
@@ -2802,6 +3008,110 @@ tspan {
min-height: 28rem;
padding: 0.85rem;
}
.mythic-c2-path-route-panel {
align-items: center;
background: ${(props) => getSubtleAccentHorizontalGradient(props)};
border: 1px solid ${(props) => props.theme.table?.borderSoft || props.theme.borderColor};
border-radius: ${(props) => props.theme.shape.borderRadius}px;
display: flex;
flex-wrap: wrap;
gap: 0.75rem;
justify-content: space-between;
min-width: 0;
padding: 0.65rem 0.7rem;
}
.mythic-c2-path-route-panel-success {
background: ${(props) => `linear-gradient(90deg, ${alpha(props.theme.palette.success.main, props.theme.palette.mode === "dark" ? 0.16 : 0.08)} 0%, ${alpha(props.theme.palette.background.paper, 0)} 70%)`};
border-color: ${(props) => alpha(props.theme.palette.success.main, 0.28)};
}
.mythic-c2-path-route-panel-warning {
background: ${(props) => `linear-gradient(90deg, ${alpha(props.theme.palette.warning.main, props.theme.palette.mode === "dark" ? 0.18 : 0.09)} 0%, ${alpha(props.theme.palette.background.paper, 0)} 70%)`};
border-color: ${(props) => alpha(props.theme.palette.warning.main, 0.32)};
}
.mythic-c2-path-route-panel-error {
background: ${(props) => `linear-gradient(90deg, ${alpha(props.theme.palette.error.main, props.theme.palette.mode === "dark" ? 0.18 : 0.09)} 0%, ${alpha(props.theme.palette.background.paper, 0)} 70%)`};
border-color: ${(props) => alpha(props.theme.palette.error.main, 0.34)};
}
.mythic-c2-path-route-state {
align-items: center;
display: flex;
gap: 0.65rem;
min-width: min(100%, 18rem);
}
.mythic-c2-path-route-icon {
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;
color: ${(props) => props.theme.palette.text.secondary};
display: inline-flex;
flex: 0 0 auto;
height: 2rem;
justify-content: center;
width: 2rem;
}
.mythic-c2-path-route-panel-success .mythic-c2-path-route-icon {
color: ${(props) => props.theme.palette.success.main};
}
.mythic-c2-path-route-panel-warning .mythic-c2-path-route-icon {
color: ${(props) => props.theme.palette.warning.main};
}
.mythic-c2-path-route-panel-error .mythic-c2-path-route-icon {
color: ${(props) => props.theme.palette.error.main};
}
.mythic-c2-path-route-copy {
min-width: 0;
}
.mythic-c2-path-route-label.MuiTypography-root {
color: ${(props) => props.theme.palette.text.primary};
font-size: 0.84rem;
font-weight: 850;
line-height: 1.2;
}
.mythic-c2-path-route-description.MuiTypography-root {
color: ${(props) => props.theme.palette.text.secondary};
font-size: 0.74rem;
line-height: 1.35;
margin-top: 0.12rem;
}
.mythic-c2-path-legend {
align-items: center;
display: flex;
flex: 1 1 24rem;
flex-wrap: wrap;
gap: 0.45rem;
justify-content: flex-end;
min-width: min(100%, 18rem);
}
.mythic-c2-path-legend-item {
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;
font-size: 0.7rem;
font-weight: 750;
gap: 0.3rem;
line-height: 1;
padding: 0.32rem 0.45rem;
white-space: nowrap;
}
.mythic-c2-path-legend-item svg {
font-size: 0.9rem;
}
.mythic-c2-path-edge-swatch {
border-radius: 999px;
display: inline-block;
height: 0.18rem;
width: 1.25rem;
}
.mythic-c2-path-edge-swatch-active {
background-image: ${(props) => `repeating-linear-gradient(90deg, ${props.theme.palette.success.main} 0, ${props.theme.palette.success.main} 5px, transparent 5px, transparent 9px)`};
}
.mythic-c2-path-edge-swatch-ended {
background-color: ${(props) => props.theme.palette.error.main};
}
.mythic-c2-path-toolbar {
align-items: center;
background-color: ${(props) => props.theme.surfaces?.muted || props.theme.palette.background.paper};
@@ -2865,6 +3175,226 @@ tspan {
background-color: ${(props) => alpha(props.theme.palette.primary.main, props.theme.palette.mode === "dark" ? 0.16 : 0.08)};
color: ${(props) => props.theme.palette.primary.main};
}
.mythic-c2-agent-node {
align-items: center;
border-radius: ${(props) => props.theme.shape.borderRadius}px;
display: flex;
flex-direction: column;
height: 100%;
justify-content: center;
min-width: 0;
transition: background-color 120ms ease, box-shadow 120ms ease, outline-color 120ms ease;
width: 100%;
}
.mythic-c2-agent-node-focused {
background: ${(props) => `linear-gradient(180deg, ${alpha(props.theme.palette.primary.main, props.theme.palette.mode === "dark" ? 0.2 : 0.1)} 0%, ${alpha(props.theme.palette.background.paper, 0)} 100%)`};
box-shadow: 0 0 0 2px ${(props) => alpha(props.theme.palette.primary.main, 0.42)};
outline: 1px solid ${(props) => alpha(props.theme.palette.primary.main, 0.72)};
outline-offset: 2px;
}
.mythic-c2-agent-node-mythic {
opacity: 0.96;
}
.mythic-c2-agent-node-label.MuiTypography-root {
color: ${(props) => props.theme.palette.text.primary};
font-size: 0.72rem;
font-weight: 760;
line-height: 1.15;
margin: 0;
max-width: 100%;
overflow: hidden;
padding: 0;
text-align: center;
text-overflow: ellipsis;
white-space: nowrap;
}
.mythic-c2-action-title.MuiDialogTitle-root {
background-image: ${getSectionHeaderGradient} !important;
border-bottom: 1px solid ${(props) => props.theme.table?.borderSoft || props.theme.borderColor} !important;
color: ${(props) => props.theme.pageHeaderText?.main || props.theme.palette.text.primary};
cursor: move;
padding: 1rem 1.15rem !important;
position: relative;
}
.mythic-c2-action-title.MuiDialogTitle-root::before {
background-color: ${getSectionHeaderAccent};
bottom: 0;
box-shadow: 0 0 0 1px ${(props) => alpha(props.theme.pageHeaderText?.main || props.theme.palette.text.primary, 0.2)};
content: "";
left: 0;
position: absolute;
top: 0;
width: 6px;
}
.mythic-c2-action-title-text.MuiTypography-root {
color: ${(props) => props.theme.pageHeaderText?.main || props.theme.palette.text.primary};
font-size: 1rem;
font-weight: 850;
line-height: 1.2;
}
.mythic-c2-action-title-subtitle.MuiTypography-root {
color: ${(props) => alpha(props.theme.pageHeaderText?.main || props.theme.palette.text.secondary, 0.76)};
font-size: 0.78rem;
line-height: 1.35;
margin-top: 0.18rem;
}
.mythic-c2-action-body {
display: flex;
flex-direction: column;
gap: 0.65rem;
min-width: 0;
}
.mythic-c2-action-list {
display: flex;
flex-direction: column;
gap: 0.45rem;
}
.mythic-c2-action-card {
align-items: flex-start;
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;
color: ${(props) => props.theme.palette.text.primary};
cursor: pointer;
display: flex;
gap: 0.65rem;
justify-content: space-between;
min-width: 0;
padding: 0.65rem;
text-align: left;
width: 100%;
}
.mythic-c2-action-card:hover,
.mythic-c2-action-card-selected {
background-color: ${(props) => alpha(props.theme.palette.info.main, props.theme.palette.mode === "dark" ? 0.14 : 0.07)};
border-color: ${(props) => alpha(props.theme.palette.info.main, 0.42)};
}
.mythic-c2-action-card-main {
display: flex;
flex: 1 1 auto;
flex-direction: column;
gap: 0.25rem;
min-width: 0;
}
.mythic-c2-action-route {
align-items: center;
color: ${(props) => props.theme.palette.text.primary};
display: flex;
flex-wrap: wrap;
font-size: 0.78rem;
font-weight: 800;
gap: 0.35rem;
}
.mythic-c2-action-route-profile {
align-items: center;
background-color: ${(props) => alpha(props.theme.palette.primary.main, props.theme.palette.mode === "dark" ? 0.18 : 0.09)};
border: 1px solid ${(props) => alpha(props.theme.palette.primary.main, 0.34)};
border-radius: 999px;
color: ${(props) => props.theme.palette.primary.main};
display: inline-flex;
font-size: 0.7rem;
font-weight: 850;
line-height: 1;
padding: 0.22rem 0.42rem;
}
.mythic-c2-action-card-description.MuiTypography-root {
color: ${(props) => props.theme.palette.text.secondary};
font-size: 0.74rem;
line-height: 1.35;
}
.mythic-c2-action-state {
align-items: center;
border-radius: 999px;
display: inline-flex;
flex: 0 0 auto;
font-size: 0.68rem;
font-weight: 850;
line-height: 1;
padding: 0.24rem 0.42rem;
white-space: nowrap;
}
.mythic-c2-action-state-active {
background-color: ${(props) => alpha(props.theme.palette.success.main, props.theme.palette.mode === "dark" ? 0.18 : 0.1)};
border: 1px solid ${(props) => alpha(props.theme.palette.success.main, 0.4)};
color: ${(props) => props.theme.palette.success.main};
}
.mythic-c2-action-state-ended,
.mythic-c2-action-state-warning {
background-color: ${(props) => alpha(props.theme.palette.warning.main, props.theme.palette.mode === "dark" ? 0.18 : 0.1)};
border: 1px solid ${(props) => alpha(props.theme.palette.warning.main, 0.42)};
color: ${(props) => props.theme.palette.warning.main};
}
.mythic-c2-action-empty,
.mythic-c2-edge-empty-selection {
align-items: center;
background-color: ${(props) => props.theme.palette.mode === "dark" ? "rgba(255,255,255,0.04)" : "rgba(0,0,0,0.025)"};
border: 1px dashed ${(props) => props.theme.table?.borderSoft || props.theme.borderColor};
border-radius: ${(props) => props.theme.shape.borderRadius}px;
color: ${(props) => props.theme.palette.text.secondary};
display: flex;
font-size: 0.78rem;
min-height: 3rem;
padding: 0.65rem;
}
.mythic-c2-action-command-row {
align-items: center;
display: flex;
flex-wrap: wrap;
gap: 0.4rem;
}
.mythic-c2-action-command-name {
color: ${(props) => props.theme.palette.text.primary};
font-family: ${(props) => props.theme.typography.fontFamilyMono || "monospace"};
font-size: 0.82rem;
font-weight: 850;
}
.mythic-c2-edge-form-grid,
.mythic-c2-edge-profile-row {
margin-top: 0.75rem;
}
.mythic-c2-edge-source-field .mythic-c2-edge-callback-summary {
margin-top: 0.45rem;
}
.mythic-c2-edge-callback-summary {
align-items: flex-start;
background-color: ${(props) => props.theme.palette.background.paper};
border: 1px solid ${(props) => props.theme.table?.borderSoft || props.theme.borderColor};
border-radius: ${(props) => props.theme.shape.borderRadius}px;
display: flex;
flex-direction: column;
justify-content: center;
min-height: 38px;
padding: 0.55rem;
}
.mythic-c2-edge-summary-label.MuiTypography-root {
color: ${(props) => props.theme.palette.text.secondary};
font-size: 0.68rem;
font-weight: 850;
line-height: 1;
}
.mythic-c2-edge-summary-main {
align-items: center;
display: flex;
flex-wrap: wrap;
gap: 0.35rem;
min-width: 0;
}
.mythic-c2-edge-summary-label + .mythic-c2-edge-summary-main {
margin-top: 0.35rem;
}
.mythic-c2-edge-callback-id {
color: ${(props) => props.theme.palette.text.primary};
font-size: 0.84rem;
font-weight: 850;
}
.mythic-c2-edge-summary-description {
color: ${(props) => props.theme.palette.text.secondary};
font-size: 0.76rem;
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.mythic-callback-trigger-summary {
align-items: flex-start;
background-color: ${(props) => props.theme.surfaces?.muted || props.theme.palette.background.paper};