mirror of
https://github.com/its-a-feature/Mythic
synced 2026-06-08 14:55:38 +00:00
c2 profile path modernization
This commit is contained in:
@@ -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>
|
||||
|
||||
@@ -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};
|
||||
|
||||
Reference in New Issue
Block a user