updated eventing and installed services tables

This commit is contained in:
its-a-feature
2026-05-08 08:49:14 -05:00
parent 5873fb05fa
commit f15b7a6b56
13 changed files with 527 additions and 160 deletions
@@ -150,7 +150,7 @@ export const ContextMenu = ({openContextMenu, dropdownAnchorRef, contextMenuOpti
<DropdownMenuItem
key={option.name}
disabled={option.disabled}
className={option.danger ? "mythic-menu-item-hover-danger" : undefined}
className={option.className || (option.danger ? "mythic-menu-item-hover-danger" : undefined)}
onClick={(event) => handleMenuItemClick(event, option.click)}
>
{option.icon}{option.name}
@@ -164,7 +164,7 @@ export const ContextMenu = ({openContextMenu, dropdownAnchorRef, contextMenuOpti
<DropdownMenuItem
key={menuOption.name}
disabled={menuOption.disabled}
className={menuOption.danger ? "mythic-menu-item-hover-danger" : undefined}
className={menuOption.className || (menuOption.danger ? "mythic-menu-item-hover-danger" : undefined)}
onClick={(event) => handleMenuItemClick(event, menuOption.click)}
>
{menuOption.icon}{menuOption.name}
@@ -296,7 +296,7 @@ export function EventFeed({}){
const resolveViewableErrors = useCallback( () => {
snackActions.info("Resolving Errors...");
const resolveIds = operationeventlog.reduce( (prev, cur) => {
if(cur.level === "warning" && !cur.resolved){
if(cur.warning && !cur.resolved){
return [...prev, cur.id];
}else{
return [...prev];
@@ -1,17 +1,17 @@
import React, {} from 'react';
import {EventStepInstanceRenderDialog, GetStatusSymbol} from "./EventStepRender";
import {EventingStatusChip, EventStepInstanceRenderDialog} from "./EventStepRender";
import {toLocalTime} from "../../utilities/Time";
import {MythicStyledTooltip} from "../../MythicComponents/MythicStyledTooltip";
import {MythicConfirmDialog} from '../../MythicComponents/MythicConfirmDialog';
import { gql, useMutation } from '@apollo/client';
import CalendarMonthTwoToneIcon from '@mui/icons-material/CalendarMonthTwoTone';
import AccessAlarmTwoToneIcon from '@mui/icons-material/AccessAlarmTwoTone';
import CancelTwoToneIcon from '@mui/icons-material/CancelTwoTone';
import ReplayIcon from '@mui/icons-material/Replay';
import ArrowDropDownIcon from '@mui/icons-material/ArrowDropDown';
import IconButton from '@mui/material/IconButton';
import ClickAwayListener from '@mui/material/ClickAwayListener';
import Moment from 'react-moment';
import moment from 'moment';
import CastConnectedTwoToneIcon from '@mui/icons-material/CastConnectedTwoTone';
import {snackActions} from "../../utilities/Snackbar";
import {MythicDialog} from "../../MythicComponents/MythicDialog";
import OpenInNewTwoToneIcon from '@mui/icons-material/OpenInNewTwoTone';
@@ -27,6 +27,7 @@ import {
gridValuePassesFilter,
isGridColumnFilterActive
} from "../../MythicComponents/MythicResizableGrid/GridColumnFilterDialog";
import {Dropdown, DropdownMenuItem} from "../../MythicComponents/MythicNestedMenus";
const cancelEventGroupInstanceMutation = gql(`
mutation cancelEventGroupInstanceMutation($eventgroupinstance_id: Int!){
@@ -83,6 +84,29 @@ export const adjustOutput = (e, newTime) => {
return newTime;
}
const EventingGridCell = ({children, className = "", rowData}) => (
<div className={`mythic-eventing-instance-cell ${className}`.trim()} data-selected={rowData?.selected ? "true" : undefined}>
{children}
</div>
);
const eventingInstanceMenuIconStyle = {fontSize: "1rem", marginRight: "8px"};
const EventingInstanceIdCell = ({onOpenMenu, rowData}) => (
<EventingGridCell className="mythic-eventing-instance-id-cell" rowData={rowData}>
<span className="mythic-eventing-instance-id">{rowData.id}</span>
<IconButton
aria-haspopup="menu"
className="mythic-table-row-icon-action mythic-table-row-icon-action-hover-info mythic-eventing-instance-id-menu-button"
onClick={(event) => {
event.preventDefault();
event.stopPropagation();
onOpenMenu({event, rowData});
}}
size="small"
>
<ArrowDropDownIcon fontSize="small" />
</IconButton>
</EventingGridCell>
);
function EventGroupInstancesTableMaterialReactTablePreMemo({eventgroups, me, setSelectedInstance, selectedInstanceID}){
const callbackTableGridRef = React.useRef();
@@ -91,6 +115,8 @@ function EventGroupInstancesTableMaterialReactTablePreMemo({eventgroups, me, set
const [openRetryDialog, setOpenRetryDialog] = React.useState(false);
const [openRunAgainDialog, setOpenRunAgainDialog] = React.useState(false);
const [openEventStepRender, setOpenEventStepRender] = React.useState(false);
const [openInstanceDropdown, setOpenInstanceDropdown] = React.useState(false);
const instanceDropdownRef = React.useRef({options: [], anchor: null});
const selectedLocalInstanceID = React.useRef(0);
const selectedLocalEventGroup = React.useRef({});
const foundQueryEvent = React.useRef(false);
@@ -162,6 +188,90 @@ function EventGroupInstancesTableMaterialReactTablePreMemo({eventgroups, me, set
copyStringToClipboard(path);
snackActions.success("copied shareable link to clipboard");
}
const getInstanceMenuOptions = (row) => {
if(!row?.id){
return [];
}
const controlOptions = [];
if(row.end_timestamp === null){
controlOptions.push({
name: "Cancel workflow",
type: "item",
icon: <CancelTwoToneIcon style={eventingInstanceMenuIconStyle} />,
className: "mythic-menu-item-hover-warning",
click: ({event}) => {
event?.preventDefault();
event?.stopPropagation();
onOpenCancelDialog({id: row.id});
},
});
}else if(row.status === "error" || row.status === "cancelled"){
controlOptions.push({
name: "Retry failed / cancelled steps",
type: "item",
icon: <ReplayIcon style={eventingInstanceMenuIconStyle} />,
className: "mythic-menu-item-hover-warning",
click: ({event}) => {
event?.preventDefault();
event?.stopPropagation();
onOpenRetryDialog({id: row.id});
},
});
}else if(row.status === "success"){
controlOptions.push({
name: "Run again",
type: "item",
icon: <ReplayIcon style={eventingInstanceMenuIconStyle} />,
className: "mythic-menu-item-hover-success",
click: ({event}) => {
event?.preventDefault();
event?.stopPropagation();
onOpenRunAgainDialog({id: row.id});
},
});
}
return [
{
name: "Open graph in modal",
type: "item",
icon: <OpenInNewTwoToneIcon style={eventingInstanceMenuIconStyle} />,
className: "mythic-menu-item-hover-info",
click: ({event}) => {
event?.preventDefault();
event?.stopPropagation();
openViewInstanceLargeDialog(row);
},
},
{
name: "Copy shareable link",
type: "item",
icon: <IosShareIcon style={eventingInstanceMenuIconStyle} />,
className: "mythic-menu-item-hover-info",
click: ({event}) => {
event?.preventDefault();
event?.stopPropagation();
onSaveToClipboard(row);
},
},
...controlOptions,
];
}
const openInstanceMenu = ({event, rowData}) => {
instanceDropdownRef.current = {
anchor: event?.currentTarget || event?.target,
options: getInstanceMenuOptions(rowData),
};
setOpenInstanceDropdown(true);
}
const closeInstanceMenu = () => {
setOpenInstanceDropdown(false);
}
const handleInstanceMenuItemClick = (event, clickOption) => {
event.preventDefault();
event.stopPropagation();
clickOption({event});
setOpenInstanceDropdown(false);
}
const [sortData, setSortData] = React.useState({"sortKey": null, "sortDirection": null, "sortType": null});
const [filterOptions, setFilterOptions] = React.useState({});
React.useEffect( () => {
@@ -181,13 +291,12 @@ function EventGroupInstancesTableMaterialReactTablePreMemo({eventgroups, me, set
const columns = React.useMemo(
() =>
[
{key: "id", type: "number", name: "ID", width: 60, enableHiding: false, disableSort: false, disableFilter: true},
{key: "status", type: 'string', name: "Status", width: 150, disableSort: true, enableHiding: false},
{key: "id", type: "number", name: "ID", width: 92, enableHiding: false, disableSort: false, disableFilter: true},
{key: "status", type: 'string', name: "Status", width: 140, disableSort: false, enableHiding: false},
{key: "eventgroup", type: 'string', name: "Event Group", inMetadata: true, fillWidth: true, disableSort: false, enableHiding: false},
{key: "trigger", type: 'string', name: "Trigger", fillWidth: true, disableSort: false, enableHiding: false},
{key: "time", type: 'date', name: "Time", width: 300, disableSort: true,},
{key: "operator", type: 'string', name: "Operator", inMetadata: true, fillWidth: true, disableSort: true,},
{key: "cancel", type: 'string', name: "Action", width: 70, disableSort: true, disableFilter: true},
]?.reduce( (prev, cur) => {
if(isGridColumnFilterActive(filterOptions[cur.key])){
return [...prev, {...cur, filtered: true}];
@@ -285,50 +394,24 @@ function EventGroupInstancesTableMaterialReactTablePreMemo({eventgroups, me, set
if(filterRow(row)){
return [...prev];
}
const selectedRow = {...row, selected: row.id === selectedInstanceID};
return [...prev, columns.map( c => {
switch(c.name){
case "ID":
return <CallbacksTableStringCell rowData={row} cellData={row.id} />
return <EventingInstanceIdCell rowData={selectedRow} onOpenMenu={openInstanceMenu} />
case "Status":
return (
<div className="mythic-table-row-actions mythic-table-row-actions-nowrap">
<GetStatusSymbol data={row} />
{
selectedInstanceID === 0 ?
(
<MythicStyledTooltip title={"View Graph Above"} >
<IconButton className="mythic-table-row-icon-action mythic-table-row-icon-action-hover-info" size="small" onClick={() => {setSelectedInstance(row.id);}} >
<CastConnectedTwoToneIcon fontSize="small" />
</IconButton>
</MythicStyledTooltip>
) :
(
<MythicStyledTooltip title={"Stop viewing graph"} >
<IconButton className="mythic-table-row-icon-action mythic-table-row-icon-action-hover-danger" size="small" onClick={() => {setSelectedInstance(0);}} >
<CancelTwoToneIcon fontSize="small" />
</IconButton>
</MythicStyledTooltip>
)
}
<MythicStyledTooltip title={"Open Graph in Modal"}>
<IconButton className="mythic-table-row-icon-action mythic-table-row-icon-action-hover-info" size="small" onClick={() => {openViewInstanceLargeDialog(row)}}>
<OpenInNewTwoToneIcon fontSize="small" />
</IconButton>
</MythicStyledTooltip>
<MythicStyledTooltip title={"Copy shareable link to workflow"}>
<IconButton className="mythic-table-row-icon-action mythic-table-row-icon-action-hover-info" size="small" onClick={() => onSaveToClipboard(row)}>
<IosShareIcon fontSize="small" />
</IconButton>
</MythicStyledTooltip>
</div>
<EventingGridCell className="mythic-eventing-instance-status-cell" rowData={selectedRow}>
<EventingStatusChip data={row} />
</EventingGridCell>
)
case "Event Group":
return <CallbacksTableStringCell rowData={row} cellData={row.eventgroup.name} />
return <CallbacksTableStringCell rowData={selectedRow} cellData={row.eventgroup.name} />
case "Trigger":
return <CallbacksTableStringCell rowData={row} cellData={row.trigger} />
return <CallbacksTableStringCell rowData={selectedRow} cellData={row.trigger} />
case "Time":
return (
<div className="mythic-eventing-instances-time-cell">
<EventingGridCell className="mythic-eventing-instances-time-cell" rowData={selectedRow}>
<div className="mythic-eventing-instances-time-line">
<CalendarMonthTwoToneIcon fontSize="small" />
{toLocalTime(row?.created_at, me?.user?.view_utc_time)}
@@ -355,42 +438,14 @@ function EventGroupInstancesTableMaterialReactTablePreMemo({eventgroups, me, set
</Moment>
}
</div>
</div>
</EventingGridCell>
)
case "Operator":
return <CallbacksTableStringCell rowData={row} cellData={row?.operator?.username} />
case "Action":
return (
<div className="mythic-table-row-actions mythic-table-row-actions-nowrap">
{row.end_timestamp === null ? (
<MythicStyledTooltip title={"Cancel Eventing"} >
<IconButton onClick={() => {onOpenCancelDialog({id: row.id});}}
className="mythic-table-row-icon-action mythic-table-row-icon-action-hover-warning" size="small">
<CancelTwoToneIcon fontSize="small" />
</IconButton>
</MythicStyledTooltip>
) : row.status === "error" || row.status === "cancelled" ? (
<MythicStyledTooltip title={"Retry Failed / Canceled Steps"} >
<IconButton onClick={() => {onOpenRetryDialog({id: row.id});}}
className="mythic-table-row-icon-action mythic-table-row-icon-action-hover-warning" size="small">
<ReplayIcon fontSize="small" />
</IconButton>
</MythicStyledTooltip>
) : row.status === "success" ? (
<MythicStyledTooltip title={"Run Again"} >
<IconButton onClick={() => {onOpenRunAgainDialog({id: row.id});}}
className="mythic-table-row-icon-action mythic-table-row-icon-action-hover-success" size="small">
<ReplayIcon fontSize="small" />
</IconButton>
</MythicStyledTooltip>
) : null}
</div>
)
return <CallbacksTableStringCell rowData={selectedRow} cellData={row?.operator?.username} />
}
})]
}, []);
}, [sortData, filterOptions, selectedInstanceID, eventgroups]);
}, [sortData, filterOptions, selectedInstanceID, eventgroups, columns]);
const sortColumn = columns.findIndex((column) => column.key === sortData.sortKey);
const [openContextMenu, setOpenContextMenu] = React.useState(false);
const [selectedColumn, setSelectedColumn] = React.useState({});
@@ -412,9 +467,21 @@ function EventGroupInstancesTableMaterialReactTablePreMemo({eventgroups, me, set
},];
const onRowDoubleClick = () => {
}
const onRowClick = ({event, rowDataStatic}) => {
if(event.target?.closest?.("button") || event.target?.closest?.("a")){
return;
}
if(rowDataStatic?.id === selectedInstanceID){
setSelectedInstance(0);
return;
}
if(rowDataStatic?.id){
setSelectedInstance(rowDataStatic.id);
}
}
const onRowContextClick = ({rowDataStatic}) => {
return [];
return getInstanceMenuOptions(rowDataStatic);
}
return (
<div className="mythic-eventing-instances-grid">
@@ -428,6 +495,7 @@ function EventGroupInstancesTableMaterialReactTablePreMemo({eventgroups, me, set
rowHeight={eventingRowHeight}
onClickHeader={onClickHeader}
onDoubleClickRow={onRowDoubleClick}
onRowClick={onRowClick}
contextMenuOptions={contextMenuOptions}
onRowContextMenuClick={onRowContextClick}
/>
@@ -446,6 +514,28 @@ function EventGroupInstancesTableMaterialReactTablePreMemo({eventgroups, me, set
}
/>
}
{openInstanceDropdown &&
<ClickAwayListener onClickAway={closeInstanceMenu} mouseEvent={"onMouseDown"}>
<Dropdown
isOpen={instanceDropdownRef.current.anchor}
onOpen={setOpenInstanceDropdown}
externallyOpen={openInstanceDropdown}
minWidth={250}
menu={
instanceDropdownRef.current.options.map((option, index) => (
<DropdownMenuItem
key={"eventing-instance-action-" + index}
className={option.className}
disabled={option.disabled}
onClick={(event) => handleInstanceMenuItemClick(event, option.click)}
>
{option.icon}{option.name}
</DropdownMenuItem>
))
}
/>
</ClickAwayListener>
}
{openDeleteDialog &&
<MythicConfirmDialog onClose={() => {
setOpenDeleteDialog(false);
@@ -89,6 +89,7 @@ subscription GetEventStepInstances($eventgroupinstance_id: Int!) {
order
action
}
eventgroupinstance_id
created_at
updated_at
end_timestamp
@@ -109,7 +110,6 @@ query getEventStepInstanceDetails($eventstepinstance_id: Int!){
created_at
order
end_timestamp
action
action_data
stdout
stderr
@@ -399,7 +399,7 @@ const getStatusClass = (status) => {
}
const hasEventingStatus = (status) => status !== undefined && status !== null && status !== "";
const getEventingStatusClass = (status) => hasEventingStatus(status) ? getStatusClass(status) : "configured";
const EventingStatusChip = ({data}) => {
export const EventingStatusChip = ({data}) => {
const hasStatus = hasEventingStatus(data?.status);
return (
<span className={`mythic-eventing-status-chip mythic-eventing-status-chip-${getEventingStatusClass(data?.status)}`.trim()}>
@@ -429,11 +429,46 @@ const stringifyEventingValue = (value) => {
}
}
const EventingCodeBlock = ({value, emptyText="No data"}) => {
const theme = useTheme();
const displayValue = stringifyEventingValue(value);
const formattedValue = React.useMemo(() => {
if(displayValue === ""){
return "";
}
try{
return JSON.stringify(JSON.parse(displayValue), null, 2);
}catch(error){
return JSON.stringify(displayValue, null, 2);
}
}, [displayValue]);
const lineCount = Math.max(3, Math.min(18, formattedValue.split("\n").length));
return (
<pre className={`mythic-eventing-code-block ${displayValue === "" ? "mythic-eventing-code-block-empty" : ""}`.trim()}>
{displayValue === "" ? emptyText : displayValue}
</pre>
<div className={`mythic-eventing-code-block ${displayValue === "" ? "mythic-eventing-code-block-empty" : ""}`.trim()}>
{displayValue === "" ? (
emptyText
) : (
<AceEditor
mode="json"
theme={theme.palette.mode === "dark" ? "monokai" : "xcode"}
fontSize={13}
showGutter={true}
highlightActiveLine={false}
readOnly={true}
showPrintMargin={false}
value={formattedValue}
width="100%"
minLines={lineCount}
maxLines={18}
wrapEnabled={true}
setOptions={{
showLineNumbers: true,
tabSize: 2,
useWorker: false,
}}
editorProps={{$blockScrolling: true}}
/>
)}
</div>
)
}
const EventingDetailSection = ({title, subtitle, count, actions, children, className = "", collapsible = false, defaultExpanded = false}) => {
@@ -862,6 +897,7 @@ function EventStepInstanceRender({selectedEventGroupInstance}) {
const [graphData, setGraphData] = React.useState({nodes: [], edges: [], groups: []});
const {fitView} = useReactFlow()
const updateNodeInternals = useUpdateNodeInternals();
const selectedEventGroupInstanceRef = React.useRef(selectedEventGroupInstance);
const [retryFromEventStep] = useMutation(retryFromEventStepMutation, {
onCompleted: (data) => {
if(data.eventingTriggerRetryFromStep.status === "success"){
@@ -883,21 +919,33 @@ function EventStepInstanceRender({selectedEventGroupInstance}) {
[]
);
const viewportRef = React.useRef(null);
React.useEffect(() => {
selectedEventGroupInstanceRef.current = selectedEventGroupInstance;
setSteps([]);
setNodes([]);
setEdges([]);
setGraphData({nodes: [], edges: [], groups: []});
setOpenContextMenu(false);
}, [selectedEventGroupInstance]);
useSubscription(sub_eventstepinstance, {
variables: {eventgroupinstance_id: selectedEventGroupInstance},
fetchPolicy: "no-cache",
onData: ({data}) => {
const newSteps = data.data.eventstepinstance_stream.reduce( (prev, cur) => {
let indx = prev.findIndex( ({id}) => id === cur.id);
if(indx > -1){
let updatingPrev = [...prev];
updatingPrev[indx] = cur;
return [...updatingPrev];
}
return [...prev, cur];
}, [...steps]);
newSteps.sort((a,b) => (a.id > b.id) ? -1 : ((b.id > a.id) ? 1 : 0));
setSteps(newSteps);
const activeEventGroupInstance = selectedEventGroupInstanceRef.current;
setSteps((previousSteps) => {
const incomingSteps = data.data.eventstepinstance_stream.filter((cur) => `${cur.eventgroupinstance_id}` === `${activeEventGroupInstance}`);
const newSteps = incomingSteps.reduce( (prev, cur) => {
let indx = prev.findIndex( ({id}) => id === cur.id);
if(indx > -1){
let updatingPrev = [...prev];
updatingPrev[indx] = cur;
return [...updatingPrev];
}
return [...prev, cur];
}, [...previousSteps]);
newSteps.sort((a,b) => (a.id > b.id) ? -1 : ((b.id > a.id) ? 1 : 0));
return newSteps;
});
}
});
const contextMenu = React.useMemo(() => {return [
@@ -1079,6 +1127,9 @@ function EventStepInstanceRender({selectedEventGroupInstance}) {
}
fitView();
});
} else {
setNodes([]);
setEdges([]);
}
})();
}, [graphData]);
@@ -33,8 +33,7 @@ import TableRow from '@mui/material/TableRow';
import MythicTableCell from "../../MythicComponents/MythicTableCell";
import {MythicStyledTooltip} from "../../MythicComponents/MythicStyledTooltip";
import {MythicAgentSVGIcon} from "../../MythicComponents/MythicAgentSVGIcon";
import {MythicStatusChip} from "../../MythicComponents/MythicStatusChip";
import CheckCircleOutlineIcon from '@mui/icons-material/CheckCircleOutline';
import {C2ProfileStatusSummary} from "./InstalledServiceStatus";
const toggleDeleteStatus = gql`
@@ -148,8 +147,6 @@ export function C2ProfilesRow({service, showDeleted}) {
if(service.deleted && !showDeleted){
return null;
}
const containerStatus = !service.container_running ? "error" : service.is_p2p ? "success" : "neutral";
const containerIcon = service.container_running && !service.is_p2p ? <CheckCircleOutlineIcon /> : undefined;
return (
<>
<TableRow hover>
@@ -178,7 +175,10 @@ export function C2ProfilesRow({service, showDeleted}) {
</MythicTableCell>
<MythicTableCell>
{service.name}
<div className="mythic-installed-service-identity">
<span className="mythic-installed-service-name">{service.name}</span>
<C2ProfileStatusSummary service={service} />
</div>
</MythicTableCell>
<MythicTableCell>{service.is_p2p ? "P2P" : "Egress"}</MythicTableCell>
<MythicTableCell>
@@ -197,29 +197,6 @@ export function C2ProfilesRow({service, showDeleted}) {
<b>Description: </b>{service.description}
</Typography>
</MythicTableCell>
<MythicTableCell>
<Typography variant="body2" component="p" >
<b>Container Status: </b>
</Typography>
<MythicStatusChip
label={service.container_running ? "Online" : "Offline"}
icon={containerIcon}
status={containerStatus}
showIcon
/>
{!service.is_p2p &&
<React.Fragment>
<Typography variant="body2" component="p" >
<b>C2 Server Status: </b>
</Typography>
<MythicStatusChip
label={!service.container_running ? "Unavailable" : service.running ? "Accepting Connections" : "Not Accepting Connections"}
status={!service.container_running ? "neutral" : service.running ? "success" : "error"}
showIcon={service.container_running}
/>
</React.Fragment>
}
</MythicTableCell>
<MythicTableCell>
{service.container_running ? (
service.running ?
@@ -20,7 +20,7 @@ import {MythicDialog} from "../../MythicComponents/MythicDialog";
import AttachFileIcon from '@mui/icons-material/AttachFile';
import {ConsumingServicesGetIDPMetadataDialog} from "./ConsumingServicesGetIDPMetadataDialog";
import {C2ProfileListFilesDialog} from "./C2ProfileListFilesDialog";
import {MythicStatusChip} from "../../MythicComponents/MythicStatusChip";
import {InstalledServiceContainerStatus} from "./InstalledServiceStatus";
const testWebhookMutation = gql`
mutation testWebhookWorks($service_type: String!){
@@ -166,10 +166,7 @@ export const ConsumingServicesTableRow = ({service, showDeleted}) => {
</MythicStyledTooltip>
);
const renderContainerStatus = (w) => (
<MythicStatusChip
label={w.container_running ? "Online" : "Offline"}
status={w.container_running ? "success" : "error"}
/>
<InstalledServiceContainerStatus isOnline={w.container_running} />
);
const renderFileButton = (w) => (
<MythicStyledTooltip title={w.container_running ? "View Files" : "Unable to view files since container is offline"}>
@@ -196,13 +193,13 @@ export const ConsumingServicesTableRow = ({service, showDeleted}) => {
<Typography variant={"h5"}>
{w.name}
</Typography>
{renderContainerStatus(w)}
<Typography variant={"body"}>
<b>Type: </b>{w.type}
</Typography>
<Typography variant={"body2"}>
<b>Description: </b>{w.description}
</Typography>
{renderContainerStatus(w)}
</MythicTableCell>
<MythicTableCell>
{renderFileButton(w)}
@@ -237,13 +234,13 @@ export const ConsumingServicesTableRow = ({service, showDeleted}) => {
<Typography variant={"h5"}>
{w.name}
</Typography>
{renderContainerStatus(w)}
<Typography variant={"body"}>
<b>Type: </b>{w.type}
</Typography>
<Typography variant={"body2"}>
<b>Description: </b>{w.description}
</Typography>
{renderContainerStatus(w)}
</MythicTableCell>
<MythicTableCell>
{renderFileButton(w)}
@@ -278,13 +275,13 @@ export const ConsumingServicesTableRow = ({service, showDeleted}) => {
<Typography variant={"h5"}>
{w.name}
</Typography>
{renderContainerStatus(w)}
<Typography variant={"body"}>
<b>Type: </b>{w.type}
</Typography>
<Typography variant={"body2"}>
<b>Description: </b>{w.description}
</Typography>
{renderContainerStatus(w)}
</MythicTableCell>
<MythicTableCell>
{renderFileButton(w)}
@@ -321,13 +318,13 @@ export const ConsumingServicesTableRow = ({service, showDeleted}) => {
<Typography variant={"h5"}>
{w.name}
</Typography>
{renderContainerStatus(w)}
<Typography variant={"body"}>
<b>Type: </b>{w.type}
</Typography>
<Typography variant={"body2"}>
<b>Description: </b>{w.description}
</Typography>
{renderContainerStatus(w)}
</MythicTableCell>
<MythicTableCell>
{renderFileButton(w)}
@@ -18,7 +18,7 @@ import Table from '@mui/material/Table';
import TableBody from '@mui/material/TableBody';
import TableContainer from '@mui/material/TableContainer';
import TableHead from '@mui/material/TableHead';
import {MythicStatusChip} from "../../MythicComponents/MythicStatusChip";
import {InstalledServiceContainerStatus} from "./InstalledServiceStatus";
const toggleDeleteStatus = gql`
mutation toggleCustomBrowserDeleteStatus($custombrowser_id: Int!, $deleted: Boolean!){
@@ -69,9 +69,9 @@ export function CustomBrowserRow({service, showDeleted}) {
<FontAwesomeIcon icon={faFolderOpen} style={{width: "60px", height: "60px"}} />
</MythicTableCell>
<MythicTableCell>
{service.name}
<div style={{marginTop: "0.35rem"}}>
<MythicStatusChip label={service.container_running ? "Online" : "Offline"} status={service.container_running ? "success" : "error"} />
<div className="mythic-installed-service-identity">
<span className="mythic-installed-service-name">{service.name}</span>
<InstalledServiceContainerStatus isOnline={service.container_running} />
</div>
</MythicTableCell>
<MythicTableCell>
@@ -0,0 +1,67 @@
import React from 'react';
const getServiceStatusTone = (isOnline, fallbackTone = "success") => isOnline ? fallbackTone : "error";
const InstalledServiceStatusSummary = ({label, tone, details = []}) => (
<div className={`mythic-service-status-summary mythic-service-status-summary-${tone}`.trim()}>
<div className="mythic-service-status-primary">
<span className="mythic-service-status-dot" />
<span className="mythic-service-status-primary-label">{label}</span>
</div>
{details.length > 0 &&
<div className="mythic-service-status-details">
{details.map((detail) => (
<span
className={`mythic-service-status-detail mythic-service-status-detail-${detail.tone || "neutral"}`.trim()}
key={`${detail.label}-${detail.value}`}
>
<span className="mythic-service-status-mini-dot" />
<span className="mythic-service-status-detail-label">{detail.label}</span>
<span className="mythic-service-status-detail-value">{detail.value}</span>
</span>
))}
</div>
}
</div>
);
export const InstalledServiceContainerStatus = ({isOnline}) => (
<InstalledServiceStatusSummary
label={isOnline ? "Container online" : "Container offline"}
tone={isOnline ? "success" : "error"}
/>
);
const getC2ProfileStatusSummary = (service) => {
if(!service.container_running){
return {label: "Container offline", tone: "error"};
}
if(service.is_p2p){
return {label: "Container online", tone: "success"};
}
if(service.running){
return {label: "Accepting connections", tone: "success"};
}
return {label: "Server stopped", tone: "warning"};
}
const getC2ProfileStatusDetails = (service) => {
if(service.is_p2p){
return [];
}
if(!service.container_running){
return [{label: "Server", value: "unavailable", tone: "neutral"}];
}
return [{label: "Container", value: "online", tone: getServiceStatusTone(service.container_running, "neutral")}];
}
export const C2ProfileStatusSummary = ({service}) => {
const summary = getC2ProfileStatusSummary(service);
return (
<InstalledServiceStatusSummary
label={summary.label}
tone={summary.tone}
details={getC2ProfileStatusDetails(service)}
/>
)
}
@@ -18,7 +18,7 @@ import {PayloadTypeCommandDialog} from "./PayloadTypeCommandsDialog";
import {MythicAgentSVGIcon} from "../../MythicComponents/MythicAgentSVGIcon";
import {C2ProfileListFilesDialog} from "./C2ProfileListFilesDialog";
import AttachFileIcon from '@mui/icons-material/AttachFile';
import {MythicStatusChip} from "../../MythicComponents/MythicStatusChip";
import {InstalledServiceContainerStatus} from "./InstalledServiceStatus";
const toggleDeleteStatus = gql`
mutation togglePayloadTypeDeleteStatus($payloadtype_id: Int!, $deleted: Boolean!){
@@ -84,7 +84,10 @@ export function PayloadTypeRow({service, showDeleted}){
<MythicAgentSVGIcon payload_type={service.name} style={{width: "80px", padding: "5px", objectFit: "unset"}} />
</MythicTableCell>
<MythicTableCell>
{service.name}
<div className="mythic-installed-service-identity">
<span className="mythic-installed-service-name">{service.name}</span>
<InstalledServiceContainerStatus isOnline={service.container_running} />
</div>
</MythicTableCell>
<MythicTableCell>
{service.wrapper ? "Wrapper" : service.agent_type === "agent" ? "Agent" : service.agent_type === "service" ? "3rd Party Service" : "Command Augmentation"}
@@ -110,9 +113,6 @@ export function PayloadTypeRow({service, showDeleted}){
<b>Description: </b>{service.note}
</Typography>
</MythicTableCell>
<MythicTableCell>
<MythicStatusChip label={service.container_running ? "Online" : "Offline"} status={service.container_running ? "success" : "error"} />
</MythicTableCell>
<MythicTableCell>
<div className="mythic-table-row-actions">
<MythicStyledTooltip title={"Documentation"}>
@@ -477,7 +477,6 @@ A few examples are github.com/MythicAgents/bloodhound and github.com/MythicAgent
<MythicTableCell>Service</MythicTableCell>
<MythicTableCell style={{width: "4rem"}}>Type</MythicTableCell>
<MythicTableCell>Metadata</MythicTableCell>
<MythicTableCell>Status</MythicTableCell>
<MythicTableCell style={{width: "12rem"}}>Actions</MythicTableCell>
</TableRow>
</TableHead>
@@ -15,7 +15,7 @@ import MythicTableCell from "../../MythicComponents/MythicTableCell";
import {MythicStyledTooltip} from "../../MythicComponents/MythicStyledTooltip";
import {MythicDialog} from "../../MythicComponents/MythicDialog";
import {C2ProfileListFilesDialog} from "./C2ProfileListFilesDialog";
import {MythicStatusChip} from "../../MythicComponents/MythicStatusChip";
import {InstalledServiceContainerStatus} from "./InstalledServiceStatus";
const toggleDeleteStatus = gql`
mutation toggleC2ProfileDeleteStatus($translationcontainer_id: Int!, $deleted: Boolean!){
@@ -66,7 +66,10 @@ export function TranslationContainerRow({service, showDeleted}) {
<FontAwesomeIcon icon={faLanguage} style={{width: "80px", height: "80px"}} />
</MythicTableCell>
<MythicTableCell>
{service.name}
<div className="mythic-installed-service-identity">
<span className="mythic-installed-service-name">{service.name}</span>
<InstalledServiceContainerStatus isOnline={service.container_running} />
</div>
</MythicTableCell>
<MythicTableCell>
Translation
@@ -82,9 +85,6 @@ export function TranslationContainerRow({service, showDeleted}) {
<b>Description: </b>{service.description}
</Typography>
</MythicTableCell>
<MythicTableCell>
<MythicStatusChip label={service.container_running ? "Online" : "Offline"} status={service.container_running ? "success" : "error"} />
</MythicTableCell>
<MythicTableCell>
<div className="mythic-table-row-actions">
<MythicStyledTooltip title={"Documentation"}>
+191 -15
View File
@@ -6119,6 +6119,7 @@ tspan {
line-height: 1.25;
}
.mythic-eventing-detail {
container-type: inline-size;
display: flex;
flex-direction: column;
gap: 0.5rem;
@@ -6144,12 +6145,13 @@ tspan {
border-radius: ${(props) => props.theme.shape.borderRadius}px;
box-shadow: ${(props) => props.theme.palette.mode === "dark" ? "inset 0 1px 0 rgba(255,255,255,0.055)" : "inset 0 1px 0 rgba(255,255,255,0.78)"};
color: ${(props) => props.theme.palette.text.primary};
container-type: inline-size;
display: grid;
flex: 0 0 auto;
gap: 0.75rem;
grid-template-columns: minmax(10.5rem, 0.55fr) minmax(16rem, 0.82fr) minmax(31rem, 2.2fr);
grid-template-columns: minmax(9.5rem, 0.55fr) minmax(13rem, 0.82fr) minmax(0, 2.2fr);
min-width: 0;
overflow: visible;
overflow: hidden;
padding: 0.72rem 0.78rem;
width: 100%;
}
@@ -6198,14 +6200,18 @@ tspan {
.mythic-eventing-workflow-overview-header-actions {
align-items: center;
display: flex;
flex: 0 0 auto;
flex: 1 1 auto;
flex-wrap: wrap;
gap: 0.4rem;
justify-content: flex-end;
max-width: 100%;
min-width: min(100%, 13.5rem);
}
.mythic-eventing-workflow-overview-header-actions .MuiButton-root.mythic-table-row-action {
flex: 0 1 auto;
min-width: 6.5rem;
max-width: 100%;
white-space: normal;
}
.mythic-eventing-workflow-overview-section {
display: flex;
@@ -6294,10 +6300,16 @@ tspan {
gap: 0.7rem;
}
.mythic-eventing-workflow-button-row .MuiButton-root.mythic-table-row-action {
flex: 0 1 auto;
gap: 0.25rem;
justify-content: flex-start;
line-height: 1.2;
max-width: 100%;
min-width: auto;
padding-left: 0.55rem;
padding-right: 0.65rem;
text-align: left;
white-space: normal;
}
.mythic-eventing-workflow-button-row .MuiButton-startIcon {
margin-left: 0;
@@ -6336,22 +6348,30 @@ tspan {
background-color: ${(props) => alpha(props.theme.palette.warning.main, props.theme.palette.mode === "dark" ? 0.28 : 0.18)} !important;
border-color: ${(props) => alpha(props.theme.palette.warning.main, props.theme.palette.mode === "dark" ? 0.68 : 0.5)} !important;
}
@media (max-width: 1180px) {
@container (max-width: 58rem) {
.mythic-eventing-workflow-overview {
grid-template-columns: 1fr;
grid-template-columns: repeat(2, minmax(0, 1fr));
}
.mythic-eventing-workflow-overview-header {
align-items: stretch;
flex-direction: column;
grid-column: 1 / -1;
}
.mythic-eventing-workflow-overview-header-actions {
justify-content: flex-start;
}
.mythic-eventing-workflow-overview-section + .mythic-eventing-workflow-overview-section {
border-left: 0;
border-top: 1px solid ${(props) => props.theme.table?.borderSoft || props.theme.borderColor};
padding-left: 0;
padding-top: 0.65rem;
border-left: 1px solid ${(props) => props.theme.table?.borderSoft || props.theme.borderColor};
border-top: 0;
padding-left: 0.75rem;
padding-top: 0;
}
.mythic-eventing-workflow-overview-actions {
border-left: 0 !important;
border-top: 1px solid ${(props) => props.theme.table?.borderSoft || props.theme.borderColor} !important;
grid-column: 1 / -1;
padding-left: 0 !important;
padding-top: 0.65rem !important;
}
}
.mythic-eventing-instances-grid {
@@ -6364,7 +6384,37 @@ tspan {
position: relative;
width: 100%;
}
.mythic-eventing-instance-cell {
align-items: center;
display: flex;
min-width: 0;
width: 100%;
}
.mythic-eventing-instance-id-cell {
gap: 0.28rem;
justify-content: space-between;
}
.mythic-eventing-instance-id {
color: ${(props) => props.theme.palette.text.primary};
font-weight: 850;
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.MuiIconButton-root.mythic-eventing-instance-id-menu-button {
flex: 0 0 auto;
}
.mythic-eventing-instance-status-cell .mythic-eventing-status-chip {
max-width: 100%;
}
.mythic-eventing-instance-status-cell .mythic-eventing-status-chip span {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.mythic-eventing-instances-time-cell {
align-items: flex-start;
display: flex;
flex-direction: column;
gap: 0.12rem;
@@ -6876,16 +6926,23 @@ tspan {
.mythic-eventing-code-block {
background-color: ${(props) => props.theme.palette.mode === "dark" ? "rgba(0,0,0,0.24)" : "rgba(255,255,255,0.72)"};
color: ${(props) => props.theme.palette.text.primary};
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
font-size: 0.72rem;
line-height: 1.42;
margin: 0;
max-height: 18rem;
min-height: 3.4rem;
overflow: auto;
padding: 0.62rem;
white-space: pre-wrap;
word-break: break-word;
padding: 0;
}
.mythic-eventing-code-block .ace_editor {
background-color: transparent !important;
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace !important;
}
.mythic-eventing-code-block .ace_gutter {
background-color: ${(props) => props.theme.palette.mode === "dark" ? "rgba(255,255,255,0.04)" : "rgba(0,0,0,0.035)"} !important;
border-right: 1px solid ${(props) => props.theme.table?.borderSoft || props.theme.borderColor};
color: ${(props) => props.theme.palette.text.secondary} !important;
}
.mythic-eventing-code-block .ace_scroller {
background-color: transparent !important;
}
.mythic-eventing-code-block-empty {
align-items: center;
@@ -6894,6 +6951,7 @@ tspan {
font-family: ${(props) => props.theme.typography.fontFamily};
font-size: 0.74rem;
font-weight: 700;
padding: 0.62rem;
}
.mythic-eventing-detail-table-wrap {
max-height: 18rem;
@@ -6931,6 +6989,106 @@ tspan {
.mythic-service-actions {
gap: 0.55rem;
}
.mythic-installed-service-identity {
align-items: flex-start;
display: flex;
flex-direction: column;
gap: 0.35rem;
min-width: 0;
}
.mythic-installed-service-name {
color: ${(props) => props.theme.palette.text.primary};
font-weight: 850;
line-height: 1.25;
overflow-wrap: anywhere;
}
.mythic-service-status-summary {
align-items: flex-start;
display: flex;
flex-direction: column;
gap: 0.32rem;
margin-top: 0.28rem;
min-width: 0;
}
.mythic-installed-service-identity .mythic-service-status-summary {
margin-top: 0;
}
.mythic-service-status-primary {
align-items: center;
color: ${(props) => props.theme.palette.text.primary};
display: flex;
font-size: 0.84rem;
font-weight: 800;
gap: 0.38rem;
line-height: 1.25;
min-width: 0;
}
.mythic-service-status-primary-label {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.mythic-service-status-dot,
.mythic-service-status-mini-dot {
border-radius: 999px;
flex: 0 0 auto;
}
.mythic-service-status-dot {
height: 0.55rem;
width: 0.55rem;
}
.mythic-service-status-mini-dot {
height: 0.38rem;
width: 0.38rem;
}
.mythic-service-status-summary-success .mythic-service-status-dot {
background-color: ${(props) => props.theme.palette.success.main};
box-shadow: 0 0 0 3px ${(props) => alpha(props.theme.palette.success.main, props.theme.palette.mode === "dark" ? 0.14 : 0.1)};
}
.mythic-service-status-summary-warning .mythic-service-status-dot {
background-color: ${(props) => props.theme.palette.warning.main};
box-shadow: 0 0 0 3px ${(props) => alpha(props.theme.palette.warning.main, props.theme.palette.mode === "dark" ? 0.16 : 0.12)};
}
.mythic-service-status-summary-error .mythic-service-status-dot {
background-color: ${(props) => props.theme.palette.error.main};
box-shadow: 0 0 0 3px ${(props) => alpha(props.theme.palette.error.main, props.theme.palette.mode === "dark" ? 0.16 : 0.1)};
}
.mythic-service-status-details {
align-items: center;
color: ${(props) => props.theme.palette.text.secondary};
display: flex;
flex-wrap: wrap;
font-size: 0.73rem;
font-weight: 650;
gap: 0.35rem 0.55rem;
line-height: 1.3;
min-width: 0;
}
.mythic-service-status-detail {
align-items: center;
display: inline-flex;
gap: 0.22rem;
min-width: 0;
}
.mythic-service-status-detail-label {
color: ${(props) => props.theme.palette.text.secondary};
}
.mythic-service-status-detail-value {
color: ${(props) => props.theme.palette.text.primary};
font-weight: 750;
}
.mythic-service-status-detail-neutral .mythic-service-status-mini-dot {
background-color: ${(props) => alpha(props.theme.palette.text.secondary, 0.55)};
}
.mythic-service-status-detail-success .mythic-service-status-mini-dot {
background-color: ${(props) => props.theme.palette.success.main};
}
.mythic-service-status-detail-warning .mythic-service-status-mini-dot {
background-color: ${(props) => props.theme.palette.warning.main};
}
.mythic-service-status-detail-error .mythic-service-status-mini-dot {
background-color: ${(props) => props.theme.palette.error.main};
}
.mythic-split-action-group {
box-shadow: none !important;
display: inline-flex;
@@ -7044,9 +7202,27 @@ tspan {
border-color: ${(props) => alpha(props.theme.palette.error.main, props.theme.palette.mode === "dark" ? 0.56 : 0.38)} !important;
color: ${(props) => props.theme.palette.error.main} !important;
}
.mythic-menu-item-hover-info .MuiSvgIcon-root,
.mythic-menu-item-hover-success .MuiSvgIcon-root,
.mythic-menu-item-hover-warning .MuiSvgIcon-root,
.mythic-menu-item-hover-danger .MuiSvgIcon-root {
color: inherit !important;
}
.mythic-menu-item-hover-info:hover,
.mythic-menu-item-hover-info.Mui-focusVisible {
background-color: ${(props) => alpha(props.theme.palette.info.main, props.theme.palette.mode === "dark" ? 0.18 : 0.1)} !important;
color: ${(props) => props.theme.palette.info.main} !important;
}
.mythic-menu-item-hover-success:hover,
.mythic-menu-item-hover-success.Mui-focusVisible {
background-color: ${(props) => alpha(props.theme.palette.success.main, props.theme.palette.mode === "dark" ? 0.18 : 0.1)} !important;
color: ${(props) => props.theme.palette.success.main} !important;
}
.mythic-menu-item-hover-warning:hover,
.mythic-menu-item-hover-warning.Mui-focusVisible {
background-color: ${(props) => alpha(props.theme.palette.warning.main, props.theme.palette.mode === "dark" ? 0.2 : 0.12)} !important;
color: ${(props) => props.theme.palette.warning.main} !important;
}
.mythic-menu-item-hover-danger:hover,
.mythic-menu-item-hover-danger.Mui-focusVisible {
background-color: ${(props) => props.theme.palette.error.main + "16"} !important;
+12 -2
View File
@@ -351,8 +351,18 @@ func CreateTask(createTaskInput CreateTaskInput) CreateTaskResponse {
}
}
if task.Command.ID == 0 {
logging.LogError(nil, "failed to find command matching payload type")
response.Error = "failed to find command matching payload type"
if createTaskInput.PayloadType != nil && *createTaskInput.PayloadType != "" {
logging.LogError(nil, "explicit payload type provided", "payload type", *createTaskInput.PayloadType, "no loaded commands match that payload type and name")
response.Error = fmt.Sprintf("This %s callback has no commands loaded that match the name (%s) and payload type (%s) provided.",
callback.Payload.Payloadtype.Name,
createTaskInput.CommandName,
*createTaskInput.PayloadType)
} else {
logging.LogError(nil, "failed to find command for callback's payload type", "command", createTaskInput.CommandName, "payload type", callback.Payload.Payloadtype.Name)
response.Error = fmt.Sprintf("This %s callback has no commands loaded that match the name %s",
callback.Payload.Payloadtype.Name,
createTaskInput.CommandName)
}
return response
}
task.CommandID.Int64 = int64(task.Command.ID)