updated builder and eventing pages

This commit is contained in:
its-a-feature
2026-05-05 17:07:12 -05:00
parent 7dadfc8048
commit 57bd0adabb
31 changed files with 2352 additions and 2452 deletions
-39
View File
@@ -21,7 +21,6 @@
"@mui/material": "^7.0.2",
"@mui/utils": "^7.0.2",
"@mui/x-charts": "^8.1.0",
"@mui/x-data-grid": "^8.1.0",
"@mui/x-date-pickers": "^8.1.0",
"@xterm/addon-fit": "^0.11.0",
"@xterm/xterm": "^6.0.0",
@@ -3845,44 +3844,6 @@
"robust-predicates": "^3.0.2"
}
},
"node_modules/@mui/x-data-grid": {
"version": "8.1.0",
"resolved": "https://registry.npmjs.org/@mui/x-data-grid/-/x-data-grid-8.1.0.tgz",
"integrity": "sha512-jt64FTMRKg9xUOUQkHY+VDbgpLWGssgvTti9n2BNbA4wuOl9r0C44OzYsPMXR0BxgUoWWREUBaeA7x5uwaArnQ==",
"license": "MIT",
"dependencies": {
"@babel/runtime": "^7.27.0",
"@mui/utils": "^7.0.2",
"@mui/x-internals": "8.0.0",
"clsx": "^2.1.1",
"prop-types": "^15.8.1",
"reselect": "^5.1.1",
"use-sync-external-store": "^1.5.0"
},
"engines": {
"node": ">=14.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/mui-org"
},
"peerDependencies": {
"@emotion/react": "^11.9.0",
"@emotion/styled": "^11.8.1",
"@mui/material": "^5.15.14 || ^6.0.0 || ^7.0.0",
"@mui/system": "^5.15.14 || ^6.0.0 || ^7.0.0",
"react": "^17.0.0 || ^18.0.0 || ^19.0.0",
"react-dom": "^17.0.0 || ^18.0.0 || ^19.0.0"
},
"peerDependenciesMeta": {
"@emotion/react": {
"optional": true
},
"@emotion/styled": {
"optional": true
}
}
},
"node_modules/@mui/x-date-pickers": {
"version": "8.1.0",
"resolved": "https://registry.npmjs.org/@mui/x-date-pickers/-/x-date-pickers-8.1.0.tgz",
-1
View File
@@ -26,7 +26,6 @@
"@mui/material": "^7.0.2",
"@mui/utils": "^7.0.2",
"@mui/x-charts": "^8.1.0",
"@mui/x-data-grid": "^8.1.0",
"@mui/x-date-pickers": "^8.1.0",
"@xterm/addon-fit": "^0.11.0",
"@xterm/xterm": "^6.0.0",
-84
View File
@@ -586,90 +586,6 @@ const getModernThemeAdditions = (themeMode, preferences = operatorSettingDefault
},
},
},
MuiDataGrid: {
styleOverrides: {
root: {
backgroundColor: backgroundPaper,
border: `1px solid ${borderColor}`,
borderRadius: 6,
color: textPrimary,
fontSize: "0.86rem",
"--DataGrid-rowBorderColor": tableBorderSoft,
"--DataGrid-containerBackground": tableHeaderColor,
overflow: "hidden",
"& .MuiDataGrid-main": {
backgroundColor: backgroundPaper,
},
"& .MuiDataGrid-virtualScroller": {
backgroundColor: backgroundPaper,
},
"& .MuiDataGrid-columnHeaders": {
backgroundColor: tableHeaderColor,
borderBottom: `1px solid ${borderColor}`,
},
"& .MuiDataGrid-columnHeader": {
backgroundColor: tableHeaderColor,
color: textPrimary,
fontWeight: 700,
},
"& .MuiDataGrid-columnHeaderTitle": {
fontSize: "0.76rem",
fontWeight: 700,
letterSpacing: 0,
textTransform: "uppercase",
},
"& .MuiDataGrid-columnSeparator": {
color: tableBorderSoft,
},
"& .MuiDataGrid-cell": {
borderBottom: `1px solid ${tableBorderSoft}`,
outline: "none",
fontVariantNumeric: "tabular-nums",
},
"& .MuiDataGrid-cell:focus, & .MuiDataGrid-cell:focus-within, & .MuiDataGrid-columnHeader:focus, & .MuiDataGrid-columnHeader:focus-within": {
outline: "none",
},
"& .MuiDataGrid-row:nth-of-type(even):not(.Mui-selected)": {
backgroundColor: tableRowStripeColor,
},
"& .MuiDataGrid-row:hover": {
backgroundColor: tableRowHoverColor,
},
"& .MuiDataGrid-row.Mui-selected": {
backgroundColor: tableSelectedColor,
"&:hover": {
backgroundColor: tableSelectedColor,
},
},
"& .MuiDataGrid-footerContainer": {
borderTop: `1px solid ${borderColor}`,
minHeight: 34,
color: textSecondary,
},
"& .MuiDataGrid-toolbarContainer": {
borderBottom: `1px solid ${tableBorderSoft}`,
minHeight: 34,
},
"& .MuiDataGrid-overlay": {
backgroundColor: backgroundPaper,
color: textSecondary,
},
"& .MuiTablePagination-root, & .MuiTablePagination-selectLabel, & .MuiTablePagination-displayedRows": {
color: textSecondary,
fontSize: "0.78rem",
},
"& .MuiSvgIcon-root, & .MuiIconButton-root": {
color: textSecondary,
},
"& .MuiCheckbox-root": {
color: textSecondary,
"&.Mui-checked, &.MuiCheckbox-indeterminate": {
color: primary,
},
},
},
},
},
},
};
};
@@ -1,68 +0,0 @@
import React from 'react';
import Box from '@mui/material/Box';
import { DataGrid } from '@mui/x-data-grid';
import {MythicLoadingState, MythicSearchEmptyState} from "./MythicStateDisplay";
const DefaultNoRowsOverlay = () => (
<MythicSearchEmptyState
compact
description="No rows match the current data or filters."
minHeight={160}
/>
);
const DefaultLoadingOverlay = () => (
<MythicLoadingState
compact
title="Loading rows"
description="Fetching the latest table data."
minHeight={160}
/>
);
export const MythicDataGrid = ({
containerSx = {},
sx = {},
slots = {},
density = "compact",
autoPageSize = true,
pageSizeOptions = [10, 25, 50, 100],
columnHeaderHeight = 34,
rowHeight = 34,
...props
}) => {
return (
<Box
sx={{
display: "flex",
flexDirection: "column",
width: "100%",
height: "100%",
minHeight: 0,
overflow: "hidden",
...containerSx,
}}
>
<DataGrid
density={density}
autoPageSize={autoPageSize}
pageSizeOptions={pageSizeOptions}
columnHeaderHeight={columnHeaderHeight}
rowHeight={rowHeight}
hideFooterSelectedRowCount
slots={{
noRowsOverlay: DefaultNoRowsOverlay,
loadingOverlay: DefaultLoadingOverlay,
...slots,
}}
sx={{
flexGrow: 1,
minHeight: 0,
width: "100%",
...sx,
}}
{...props}
/>
</Box>
);
}
@@ -2,6 +2,7 @@ import React from 'react';
import Box from '@mui/material/Box';
import Button from '@mui/material/Button';
import CircularProgress from '@mui/material/CircularProgress';
import Skeleton from '@mui/material/Skeleton';
import TableCell from '@mui/material/TableCell';
import TableRow from '@mui/material/TableRow';
import Typography from '@mui/material/Typography';
@@ -124,3 +125,68 @@ export function MythicTableEmptyState({colSpan, ...props}) {
</TableRow>
);
}
export function MythicTableSearchEmptyState({colSpan, ...props}) {
return (
<TableRow>
<TableCell colSpan={colSpan} sx={{borderBottom: 0, p: 0}}>
<MythicSearchEmptyState {...props} />
</TableCell>
</TableRow>
);
}
export function MythicTableErrorState({colSpan, ...props}) {
return (
<TableRow>
<TableCell colSpan={colSpan} sx={{borderBottom: 0, p: 0}}>
<MythicErrorState {...props} />
</TableCell>
</TableRow>
);
}
export function MythicTableSkeletonRows({colSpan, columns = 4, rows = 4}) {
const skeletonColumns = Math.max(1, columns);
return (
<>
{[...Array(rows).keys()].map((rowIndex) => (
<TableRow key={`mythic-table-skeleton-${rowIndex}`}>
<TableCell colSpan={colSpan} sx={{px: 1.25, py: 0.75}}>
<Box sx={{
display: "grid",
gap: 1,
gridTemplateColumns: `repeat(${skeletonColumns}, minmax(0, 1fr))`,
}}>
{[...Array(skeletonColumns).keys()].map((columnIndex) => (
<Skeleton
animation="wave"
height={22}
key={`mythic-table-skeleton-${rowIndex}-${columnIndex}`}
sx={{borderRadius: 1}}
variant="rounded"
width={columnIndex === skeletonColumns - 1 ? "72%" : "100%"}
/>
))}
</Box>
</TableCell>
</TableRow>
))}
</>
);
}
export function MythicTableLoadingState({colSpan, columns, rows = 4, showSkeleton = true, ...props}) {
return (
<>
<TableRow>
<TableCell colSpan={colSpan} sx={{borderBottom: showSkeleton ? undefined : 0, p: 0}}>
<MythicLoadingState {...props} />
</TableCell>
</TableRow>
{showSkeleton &&
<MythicTableSkeletonRows colSpan={colSpan} columns={columns} rows={rows} />
}
</>
);
}
@@ -9,8 +9,10 @@ export const MythicTablePagination = ({
fetchLimit,
onChange,
page,
id,
color = "primary",
label = "Total Results",
summary,
className = "",
containerStyle = {},
paginationStyle = {},
@@ -20,7 +22,7 @@ export const MythicTablePagination = ({
const controlledPage = page === undefined ? {} : {page};
return (
<Box className={`mythic-table-footer ${className}`.trim()} style={containerStyle}>
<Box className={`mythic-table-footer ${className}`.trim()} id={id} style={containerStyle}>
<Pagination
count={pageCount}
variant="outlined"
@@ -34,9 +36,84 @@ export const MythicTablePagination = ({
{...controlledPage}
{...paginationProps}
/>
{totalCount !== undefined && (
{summary !== undefined && summary !== null ? (
<Typography className="mythic-table-total">{summary}</Typography>
) : totalCount !== undefined && (
<Typography className="mythic-table-total">{label}: {totalCount}</Typography>
)}
</Box>
);
};
export const useMythicClientPagination = ({
items = [],
resetKey = "",
rowsPerPage = 25,
}) => {
const safeItems = React.useMemo(() => {
return Array.isArray(items) ? items : [];
}, [items]);
const [page, setPage] = React.useState(1);
const totalCount = safeItems.length;
const pageCount = Math.max(1, Math.ceil(totalCount / rowsPerPage));
const safePage = Math.min(page, pageCount);
const pageStart = (safePage - 1) * rowsPerPage;
const pageData = React.useMemo(() => {
return safeItems.slice(pageStart, pageStart + rowsPerPage);
}, [pageStart, rowsPerPage, safeItems]);
const pageEnd = totalCount === 0 ? 0 : Math.min(pageStart + rowsPerPage, totalCount);
const pageStartDisplay = totalCount === 0 ? 0 : pageStart + 1;
React.useEffect(() => {
setPage(1);
}, [resetKey, rowsPerPage, totalCount]);
React.useEffect(() => {
if(page > pageCount){
setPage(pageCount);
}
}, [page, pageCount]);
const onChangePage = React.useCallback((event, nextPage) => {
setPage(nextPage);
}, []);
return {
onChangePage,
page: safePage,
pageCount,
pageData,
pageEnd,
pageStart,
pageStartDisplay,
rangeLabel: `${pageStartDisplay}-${pageEnd} of ${totalCount}`,
rowsPerPage,
showPagination: totalCount > rowsPerPage,
totalCount,
};
};
export const MythicClientSideTablePagination = ({
id,
pagination,
selectedCount,
selectedLabel = "selected",
...paginationProps
}) => {
if(!pagination?.showPagination){
return null;
}
const summary = selectedCount === undefined || selectedCount === null ?
pagination.rangeLabel :
`${pagination.rangeLabel}, ${selectedCount} ${selectedLabel}`;
return (
<MythicTablePagination
count={pagination.pageCount}
id={id}
onChange={pagination.onChangePage}
page={pagination.page}
summary={summary}
{...paginationProps}
/>
);
};
@@ -3,8 +3,8 @@ import { gql, useMutation, useSubscription } from '@apollo/client';
import {BrowserScriptsTable} from './BrowserScriptsTable';
import {snackActions} from '../../utilities/Snackbar';
import { Backdrop } from '@mui/material';
import {CircularProgress} from '@mui/material';
import {MythicPageBody} from "../../MythicComponents/MythicPageBody";
import {MythicLoadingState} from "../../MythicComponents/MythicStateDisplay";
const SUB_BrowserScripts = gql`
@@ -138,7 +138,7 @@ export function BrowserScripts({me}){
return (
<MythicPageBody>
<Backdrop open={backdropOpen} style={{zIndex: 2, position: "absolute"}} invisible={false}>
<CircularProgress color="inherit" />
<MythicLoadingState compact title="Loading browser scripts" description="Fetching scripts for this operator." sx={{color: "inherit"}} />
</Backdrop>
<BrowserScriptsTable
browserscripts={browserScripts} operation_id={me?.user?.current_operation_id || 0} onToggleActive={onToggleActive}
@@ -1,4 +1,4 @@
import React, {useEffect} from 'react';
import React from 'react';
import Button from '@mui/material/Button';
import DialogActions from '@mui/material/DialogActions';
import DialogTitle from '@mui/material/DialogTitle';
@@ -6,9 +6,8 @@ import {useQuery, gql } from '@apollo/client';
import {useMutation} from '@apollo/client';
import {hideCallbacksMutation} from './CallbackMutations';
import {snackActions} from "../../utilities/Snackbar";
import {CallbacksTableLastCheckinCell, CallbacksTablePayloadTypeCell, CallbacksTableIPCell} from "./CallbacksTableRow";
import {MythicDataGrid} from "../../MythicComponents/MythicDataGrid";
import DialogContentText from '@mui/material/DialogContentText';
import {CallbacksTabsSelectTable} from "./CallbacksTabsSelectTable";
const callbacksAndFeaturesQuery = gql`
@@ -35,106 +34,6 @@ query callbacksAndFeatures{
}
}`;
const columns = [
{ field: 'display_id', headerName: 'ID', width: 70, type: 'number', },
{
field: 'host',
headerName: 'Host',
flex: 0.5,
},
{
field: 'user',
headerName: 'User',
flex: 0.5,
},
{
field: 'pid',
headerName: 'PID',
type: 'number',
width: 70,
},
{
field: 'description',
headerName: 'Description',
flex: 1,
},
{
field: 'ip',
headerName: 'IP',
width: 100,
renderCell: (params) => <CallbacksTableIPCell rowData={params.row} cellData={params.row.ip} />,
sortable: false,
valueGetter: (value, row) => {
try{
return JSON.parse(row.ip)[0];
}catch(error){
return row.ip;
}
}
},
{
field: "last_checkin",
headerName: "Checkin",
width: 100,
valueGetter: (value, row) => new Date(row.last_checkin),
renderCell: (params) =>
<CallbacksTableLastCheckinCell rowData={params.row} />,
},
{
field: "payload.payloadtype.name",
headerName: "Agent",
flex: 0.5,
valueGetter: (value, row) => row.payload.payloadtype.name,
renderCell: (params) => <CallbacksTablePayloadTypeCell rowData={params.row} />
},
{
field: "mythictree_groups_string",
headerName: "Groups",
flex: 0.5,
}
];
const CustomSelectTable = ({initialData, selectedData, sortModel}) => {
const [data, setData] = React.useState([]);
const [rowSelectionModel, setRowSelectionModel] = React.useState({
type: 'include',
ids: new Set([]),
});
React.useEffect( () => {
selectedData.current = data.reduce( (prev, cur) => {
if(rowSelectionModel.ids.has(cur.id)){return [...prev, cur]}
return [...prev];
}, []);
}, [data, rowSelectionModel]);
React.useEffect( () => {
setData(initialData.map(c => {
return {...c};
}));
}, [initialData]);
return (
<div style={{height: "calc(80vh)", minHeight: 0}}>
<MythicDataGrid
rows={data}
columns={columns}
initialState={{
pagination: {
paginationModel: {
},
},
sorting: {
sortModel: [sortModel],
},
}}
autoPageSize
checkboxSelection
onRowSelectionModelChange={(newRowSelectionModel) => {
setRowSelectionModel(newRowSelectionModel);
}}
rowSelectionModel={rowSelectionModel}
/>
</div>
)
}
export function CallbacksTabsHideMultipleDialog({onClose}) {
const selectedData = React.useRef([]);
@@ -150,7 +49,7 @@ export function CallbacksTabsHideMultipleDialog({onClose}) {
onClose();
}
});
useQuery(callbacksAndFeaturesQuery,{
const {loading} = useQuery(callbacksAndFeaturesQuery,{
fetchPolicy: "no-cache",
onCompleted: (data) => {
const callbackData = data.callback.map( c => {
@@ -178,9 +77,12 @@ export function CallbacksTabsHideMultipleDialog({onClose}) {
<DialogContentText style={{textAlign: "center"}}>
<b>Note: </b> Last checkin times are based on when this window opened and won't refresh.
</DialogContentText>
<CustomSelectTable initialData={initialData}
selectedData={selectedData}
sortModel={{ field: 'last_checkin', sort: 'asc' }}
<CallbacksTabsSelectTable initialData={initialData}
loading={loading}
selectedData={selectedData}
sortModel={{ field: 'last_checkin', sort: 'asc' }}
tableId="hide-multiple-callbacks-table"
tableLabel="Hide multiple callbacks"
/>
<DialogActions>
<Button onClick={onClose} variant="contained" color="primary">
@@ -197,7 +99,7 @@ export function CallbacksTabsSelectMultipleDialog({onClose, onSubmit}) {
const selectedData = React.useRef([]);
const [initialData, setInitialData] = React.useState([]);
useQuery(callbacksAndFeaturesQuery,{
const {loading} = useQuery(callbacksAndFeaturesQuery,{
fetchPolicy: "no-cache",
onCompleted: (data) => {
const callbackData = data.callback.map( c => {
@@ -223,9 +125,12 @@ export function CallbacksTabsSelectMultipleDialog({onClose, onSubmit}) {
<DialogContentText style={{textAlign: "center"}}>
<b>Note: </b> Last checkin times are based on when this window opened and won't refresh.
</DialogContentText>
<CustomSelectTable initialData={initialData}
selectedData={selectedData}
sortModel={{ field: 'display_id', sort: 'desc' }}
<CallbacksTabsSelectTable initialData={initialData}
loading={loading}
selectedData={selectedData}
sortModel={{ field: 'display_id', sort: 'desc' }}
tableId="select-multiple-callbacks-table"
tableLabel="Select multiple callbacks"
/>
<DialogActions>
<Button onClick={onClose} variant="contained" color="primary">
@@ -1,11 +1,10 @@
import React, {useContext} from 'react';
import React from 'react';
import Button from '@mui/material/Button';
import DialogActions from '@mui/material/DialogActions';
import DialogTitle from '@mui/material/DialogTitle';
import {useQuery, gql } from '@apollo/client';
import {CallbacksTableLastCheckinCell, CallbacksTablePayloadTypeCell, CallbacksTableIPCell} from "./CallbacksTableRow";
import {MythicDataGrid} from "../../MythicComponents/MythicDataGrid";
import DialogContentText from '@mui/material/DialogContentText';
import {CallbacksTabsSelectTable} from "./CallbacksTabsSelectTable";
const callbacksAndFeaturesQuery = gql`
@@ -32,96 +31,9 @@ query callbacksAndFeatures{
}
}`;
const columns = [
{ field: 'display_id', headerName: 'ID', width: 70, type: 'number', },
{
field: 'host',
headerName: 'Host',
flex: 0.5,
},
{
field: 'user',
headerName: 'User',
flex: 0.5,
},
{
field: 'pid',
headerName: 'PID',
type: 'number',
width: 70,
},
{
field: 'description',
headerName: 'Description',
flex: 1,
},
{
field: 'ip',
headerName: 'IP',
width: 100,
renderCell: (params) => <CallbacksTableIPCell rowData={params.row} cellData={params.row.ip} />,
sortable: false,
valueGetter: (value, row) => {
try{
return JSON.parse(row.ip)[0];
}catch(error){
return row.ip;
}
}
},
{
field: "last_checkin",
headerName: "Checkin",
width: 100,
valueGetter: (value, row) => new Date(row.last_checkin),
renderCell: (params) =>
<CallbacksTableLastCheckinCell rowData={params.row} />,
},
{
field: "payload.payloadtype.name",
headerName: "Agent",
flex: 0.5,
valueGetter: (value, row) => row.payload.payloadtype.name,
renderCell: (params) => <CallbacksTablePayloadTypeCell rowData={params.row} />
},
{
field: "mythictree_groups_string",
headerName: "Groups",
flex: 0.5,
}
];
const CustomSelectTable = ({initialData, selectedData, onRowClick}) => {
const [data, setData] = React.useState([]);
React.useEffect( () => {
setData(initialData.map(c => {
return {...c};
}));
}, [initialData]);
return (
<div style={{height: "calc(80vh)", minHeight: 0}}>
<MythicDataGrid
rows={data}
columns={columns}
initialState={{
pagination: {
paginationModel: {
},
},
sorting: {
sortModel: [{ field: 'display_id', sort: 'desc' }],
},
}}
autoPageSize
onRowClick={onRowClick}
/>
</div>
)
}
export function CallbacksTabsOpenMultipleDialog({onClose, tabType, onOpenTabs}) {
const selectedData = React.useRef([]);
const [initialData, setInitialData] = React.useState([]);
useQuery(callbacksAndFeaturesQuery,{
const {loading} = useQuery(callbacksAndFeaturesQuery,{
fetchPolicy: "no-cache",
onCompleted: (data) => {
const callbackData = data.callback.map( c => {
@@ -133,7 +45,7 @@ export function CallbacksTabsOpenMultipleDialog({onClose, tabType, onOpenTabs})
}
});
const onRowClick = (rowData) => {
onOpenTabs([rowData.row]);
onOpenTabs([rowData]);
}
@@ -143,9 +55,13 @@ export function CallbacksTabsOpenMultipleDialog({onClose, tabType, onOpenTabs})
<DialogContentText style={{textAlign: "center"}}>
<b>Note: </b> Last checkin times are based on when this window opened and won't refresh.
</DialogContentText>
<CustomSelectTable initialData={initialData}
selectedData={selectedData}
onRowClick={onRowClick}
<CallbacksTabsSelectTable initialData={initialData}
loading={loading}
onRowClick={onRowClick}
selectable={false}
sortModel={{ field: 'display_id', sort: 'desc' }}
tableId={`open-multiple-${tabType}-callbacks-table`}
tableLabel={`Open ${tabType} callback`}
/>
<DialogActions>
<Button onClick={onClose} variant="contained" color="primary">
@@ -0,0 +1,284 @@
import React from 'react';
import Checkbox from '@mui/material/Checkbox';
import Table from '@mui/material/Table';
import TableBody from '@mui/material/TableBody';
import TableContainer from '@mui/material/TableContainer';
import TableHead from '@mui/material/TableHead';
import TableRow from '@mui/material/TableRow';
import TableSortLabel from '@mui/material/TableSortLabel';
import MythicStyledTableCell from "../../MythicComponents/MythicTableCell";
import {MythicTableEmptyState, MythicTableLoadingState} from "../../MythicComponents/MythicStateDisplay";
import {CallbacksTableIPCell, CallbacksTableLastCheckinCell, CallbacksTablePayloadTypeCell} from "./CallbacksTableRow";
import {MythicClientSideTablePagination, useMythicClientPagination} from "../../MythicComponents/MythicTablePagination";
const getPrimaryIP = (row) => {
try{
return JSON.parse(row.ip)[0] || "";
}catch(error){
return row.ip || "";
}
}
const compareValues = (left, right) => {
if(left === right){
return 0;
}
if(left === undefined || left === null){
return -1;
}
if(right === undefined || right === null){
return 1;
}
if(left instanceof Date && right instanceof Date){
return left.getTime() - right.getTime();
}
if(typeof left === "number" && typeof right === "number"){
return left - right;
}
return String(left).localeCompare(String(right), undefined, {numeric: true, sensitivity: "base"});
}
const getColumns = ({includeAgent}) => {
const columns = [
{field: "display_id", headerName: "ID", width: "4.5rem", sortValue: (row) => row.display_id},
{field: "host", headerName: "Host", sortValue: (row) => row.host},
{field: "user", headerName: "User", sortValue: (row) => row.user},
{field: "pid", headerName: "PID", width: "4.5rem", sortValue: (row) => row.pid},
{field: "description", headerName: "Description", sortValue: (row) => row.description},
{
field: "ip",
headerName: "IP",
width: "8rem",
sortValue: getPrimaryIP,
render: (row) => <CallbacksTableIPCell rowData={row} cellData={row.ip} />,
},
{
field: "last_checkin",
headerName: "Checkin",
width: "8rem",
sortValue: (row) => new Date(row.last_checkin),
render: (row) => <CallbacksTableLastCheckinCell rowData={row} />,
},
];
if(includeAgent){
columns.push({
field: "payload.payloadtype.name",
headerName: "Agent",
sortValue: (row) => row.payload?.payloadtype?.name || "",
render: (row) => <CallbacksTablePayloadTypeCell rowData={row} />,
});
}
columns.push({field: "mythictree_groups_string", headerName: "Groups", sortValue: (row) => row.mythictree_groups_string});
return columns;
}
export const CallbacksTabsSelectTable = ({
includeAgent = true,
initialData,
loading = false,
onRowClick,
selectable = true,
selectedData,
sortModel = {field: "display_id", sort: "desc"},
tableId = "callbacks-select-table",
tableLabel = "Callback selection",
rowsPerPage = 25,
}) => {
const [data, setData] = React.useState([]);
const [orderBy, setOrderBy] = React.useState(sortModel.field);
const [order, setOrder] = React.useState(sortModel.sort || "asc");
const [selectedIds, setSelectedIds] = React.useState(new Set());
const columns = React.useMemo(() => getColumns({includeAgent}), [includeAgent]);
React.useEffect(() => {
setOrderBy(sortModel.field);
setOrder(sortModel.sort || "asc");
}, [sortModel.field, sortModel.sort]);
React.useEffect(() => {
setData(initialData.map((c) => ({...c})));
setSelectedIds(new Set());
}, [initialData]);
React.useEffect(() => {
if(selectable && selectedData){
selectedData.current = data.filter((row) => selectedIds.has(row.id));
}
}, [data, selectable, selectedData, selectedIds]);
const sortedData = React.useMemo(() => {
const sortColumn = columns.find((column) => column.field === orderBy);
if(!sortColumn){
return data;
}
const direction = order === "desc" ? -1 : 1;
return [...data].sort((left, right) => direction * compareValues(sortColumn.sortValue(left), sortColumn.sortValue(right)));
}, [columns, data, order, orderBy]);
const selectedCount = selectedIds.size;
const allSelected = selectable && sortedData.length > 0 && selectedCount === sortedData.length;
const partiallySelected = selectable && selectedCount > 0 && selectedCount < sortedData.length;
const pagination = useMythicClientPagination({
items: sortedData,
resetKey: `${orderBy}:${order}`,
rowsPerPage,
});
const handleSort = (field) => {
if(orderBy === field){
setOrder((current) => current === "asc" ? "desc" : "asc");
}else{
setOrderBy(field);
setOrder("asc");
}
}
const handleSelectAll = (event) => {
if(event.target.checked){
setSelectedIds(new Set(sortedData.map((row) => row.id)));
}else{
setSelectedIds(new Set());
}
}
const toggleSelectedRow = (row) => {
setSelectedIds((current) => {
const next = new Set(current);
if(next.has(row.id)){
next.delete(row.id);
}else{
next.add(row.id);
}
return next;
});
}
const handleSelectRow = (event, row) => {
event.stopPropagation();
toggleSelectedRow(row);
}
const handleRowClick = (row) => {
if(selectable){
toggleSelectedRow(row);
}else if(onRowClick){
onRowClick(row);
}
}
const handleRowKeyDown = (event, row) => {
if(!selectable && !onRowClick){
return;
}
if(event.key === "Enter" || event.key === " "){
event.preventDefault();
handleRowClick(row);
}
}
return (
<>
<TableContainer
aria-label={`${tableLabel} scroll area`}
className="mythicElement mythic-dialog-table-wrap mythic-fixed-row-table-wrap mythic-callback-select-table-wrap"
data-testid={`${tableId}-scroll-region`}
id={`${tableId}-scroll-region`}
role="region"
style={{height: "min(60vh, 42rem)", minHeight: "20rem", overflowY: "auto"}}
>
<Table aria-label={tableLabel} data-testid={tableId} id={tableId} stickyHeader size="small" style={{height: "auto"}}>
<TableHead>
<TableRow>
{selectable &&
<MythicStyledTableCell padding="checkbox">
<Checkbox
checked={allSelected}
color="primary"
indeterminate={partiallySelected}
inputProps={{"aria-label": `Select all callbacks in ${tableLabel}`}}
onChange={handleSelectAll}
size="small"
/>
</MythicStyledTableCell>
}
{columns.map((column) => (
<MythicStyledTableCell key={column.field} sortDirection={orderBy === column.field ? order : false} style={{width: column.width}}>
<TableSortLabel
active={orderBy === column.field}
direction={orderBy === column.field ? order : "asc"}
onClick={() => handleSort(column.field)}
>
{column.headerName}
</TableSortLabel>
</MythicStyledTableCell>
))}
</TableRow>
</TableHead>
<TableBody id={`${tableId}-body`}>
{loading ? (
<MythicTableLoadingState
colSpan={columns.length + (selectable ? 1 : 0)}
columns={columns.length + (selectable ? 1 : 0)}
compact
rows={4}
title="Loading callbacks"
description="Fetching callback data."
minHeight={100}
/>
) : sortedData.length === 0 ? (
<MythicTableEmptyState
colSpan={columns.length + (selectable ? 1 : 0)}
compact
title="No callbacks"
description="No active callbacks match this selection."
minHeight={180}
/>
) : (
pagination.pageData.map((row) => {
const selected = selectedIds.has(row.id);
return (
<TableRow
aria-label={`Callback ${row.display_id} ${row.user || ""}@${row.host || ""}`}
className={selected ? "selectedCallback" : ""}
hover
id={`${tableId}-row-${row.id}`}
key={row.id}
onClick={() => handleRowClick(row)}
onKeyDown={(event) => handleRowKeyDown(event, row)}
tabIndex={selectable || onRowClick ? 0 : undefined}
selected={selected}
style={{cursor: selectable || onRowClick ? "pointer" : undefined}}
>
{selectable &&
<MythicStyledTableCell padding="checkbox">
<Checkbox
checked={selected}
color="primary"
inputProps={{"aria-label": `Select callback ${row.display_id}`}}
onClick={(event) => handleSelectRow(event, row)}
size="small"
/>
</MythicStyledTableCell>
}
{columns.map((column) => (
<MythicStyledTableCell key={column.field}>
{column.render ? column.render(row) : row[column.field]}
</MythicStyledTableCell>
))}
</TableRow>
)
})
)}
</TableBody>
</Table>
</TableContainer>
{!loading &&
<MythicClientSideTablePagination
id={`${tableId}-pagination`}
pagination={pagination}
selectedCount={selectable ? selectedCount : undefined}
/>
}
</>
)
}
@@ -1,4 +1,4 @@
import React, {useEffect} from 'react';
import React from 'react';
import Button from '@mui/material/Button';
import DialogActions from '@mui/material/DialogActions';
import DialogTitle from '@mui/material/DialogTitle';
@@ -6,11 +6,10 @@ import Grid from '@mui/material/Grid';
import {useQuery, gql } from '@apollo/client';
import {TaskFromUIButton} from './TaskFromUIButton';
import {CallbacksTabsTaskingInput} from "./CallbacksTabsTaskingInput";
import {CallbacksTableIPCell, CallbacksTableLastCheckinCell} from "./CallbacksTableRow";
import {MythicDataGrid} from "../../MythicComponents/MythicDataGrid";
import { validate as uuidValidate } from 'uuid';
import {snackActions} from "../../utilities/Snackbar";
import DialogContentText from '@mui/material/DialogContentText';
import {CallbacksTabsSelectTable} from "./CallbacksTabsSelectTable";
const callbacksAndFeaturesQuery = gql`
@@ -36,99 +35,6 @@ query callbacksAndFeatures($payloadtype_id: Int!) {
}
}`;
const columns = [
{ field: 'display_id', headerName: 'ID', width: 80, type: 'number', },
{
field: 'host',
headerName: 'Host',
flex: 0.5,
},
{
field: 'user',
headerName: 'User',
flex: 0.5,
},
{
field: 'pid',
headerName: 'PID',
type: 'number',
width: 80,
},
{
field: 'description',
headerName: 'Description',
flex: 1,
},
{
field: 'ip',
headerName: 'IP',
width: 100,
renderCell: (params) => <CallbacksTableIPCell rowData={params.row} cellData={params.row.ip} />,
sortable: false,
valueGetter: (value, row) => {
try{
return JSON.parse(row.ip)[0];
}catch(error){
return row.ip;
}
}
},
{
field: "last_checkin",
headerName: "Checkin",
width: 100,
valueGetter: (value, row) => new Date(row.last_checkin),
renderCell: (params) =>
<CallbacksTableLastCheckinCell rowData={params.row} />,
},
{
field: "mythictree_groups_string",
headerName: "Groups",
flex: 0.5,
}
];
const CustomSelectTable = ({initialData, selectedData}) => {
const [data, setData] = React.useState([]);
const [rowSelectionModel, setRowSelectionModel] = React.useState({
type: 'include',
ids: new Set([]),
});
React.useEffect( () => {
selectedData.current = data.reduce( (prev, cur) => {
if(rowSelectionModel.ids.has(cur.id)){return [...prev, cur]}
return [...prev];
}, []);
}, [data, rowSelectionModel]);
React.useEffect( () => {
setData(initialData.map(c => {
return {...c};
}));
}, [initialData]);
return (
<div style={{height: "calc(80vh)", minHeight: 0}}>
<MythicDataGrid
rows={data}
columns={columns}
initialState={{
pagination: {
paginationModel: {
},
},
sorting: {
sortModel: [{ field: 'display_id', sort: 'desc' }],
},
}}
autoPageSize
checkboxSelection
onRowSelectionModelChange={(newRowSelectionModel) => {
setRowSelectionModel(newRowSelectionModel);
}}
rowSelectionModel={rowSelectionModel}
/>
</div>
)
}
export function CallbacksTabsTaskMultipleDialog({onClose, callback}) {
const mountedRef = React.useRef(true);
const [selectedToken, setSelectedToken] = React.useState({});
@@ -137,7 +43,7 @@ export function CallbacksTabsTaskMultipleDialog({onClose, callback}) {
const finalTaskedParameters = React.useRef(null);
const [initialData, setInitialData] = React.useState([]);
const selectedData = React.useRef([]);
useQuery(callbacksAndFeaturesQuery, {variables: {payloadtype_id: callback.payload.payloadtype.id},
const {loading} = useQuery(callbacksAndFeaturesQuery, {variables: {payloadtype_id: callback.payload.payloadtype.id},
fetchPolicy: "no-cache",
onCompleted: (data) => {
setInitialData(data.callback.map( c => {
@@ -294,8 +200,15 @@ export function CallbacksTabsTaskMultipleDialog({onClose, callback}) {
<DialogContentText style={{textAlign: "center"}}>
<b>Note: </b> Last checkin times are based on when this window opened and won't refresh.
</DialogContentText>
<CustomSelectTable initialData={initialData}
selectedData={selectedData} />
<CallbacksTabsSelectTable
includeAgent={false}
initialData={initialData}
loading={loading}
selectedData={selectedData}
sortModel={{ field: 'display_id', sort: 'desc' }}
tableId="task-multiple-callbacks-table"
tableLabel={`Task multiple ${callback.payload.payloadtype.name} callbacks`}
/>
<Grid size={12}>
<CallbacksTabsTaskingInput filterTasks={false} onSubmitFilter={()=>{}} onSubmitCommandLine={onSubmitCommandLine}
changeSelectedToken={changeSelectedToken}
@@ -1,54 +1,36 @@
import React from 'react';
import Table from '@mui/material/Table';
import TableBody from '@mui/material/TableBody';
import TableCell from '@mui/material/TableCell';
import TableContainer from '@mui/material/TableContainer';
import TableHead from '@mui/material/TableHead';
import TableRow from '@mui/material/TableRow';
import Typography from '@mui/material/Typography';
import { CreatePayloadParameter } from './CreatePayloadParameter';
import {GetGroupedParameters} from "./Step1SelectOS";
import Paper from '@mui/material/Paper';
import {useTheme} from '@mui/material/styles';
export function CreatePayloadBuildParametersTable(props){
const theme = useTheme();
const buildParameters = GetGroupedParameters({
buildParameters: props.buildParameters,
os: props.os,
c2_name: props.c2_name,
});
return (
<div style={{height: "100%", overflowY: "auto", width: "100%"}}>
<div className="mythic-create-parameter-scroll">
{buildParameters.map(b => (
b.parameters.length > 0 &&
<span key={b?.name || 'undefined'}>
<section className="mythic-create-parameter-group" key={b?.name || 'undefined'}>
{b.name !== '' && b.name !== undefined &&
<Paper elevation={5} style={{backgroundColor: theme.pageHeader.main, color: theme.pageHeaderText.main,marginBottom: "5px", }} variant={"elevation"}>
<Typography variant="h6" style={{textAlign: "left", display: "inline-block", marginLeft: "20px", color: theme.pageHeaderColor}}>
{b.name}
</Typography>
</Paper>
<div className="mythic-create-parameter-group-header">{b.name}</div>
}
<TableContainer className="mythicElement">
<Table stickyHeader={true} size="small" style={{"tableLayout": "fixed", "maxWidth": "calc(100vw)", "overflow": "scroll"}}>
<TableHead>
<TableRow>
<TableCell style={{width: "30%"}}>Build Parameter</TableCell>
<TableCell>Value</TableCell>
</TableRow>
</TableHead>
<TableBody>
{b.parameters.map( (op) => (
<CreatePayloadParameter selected_os={props.os} key={"buildparamtablerow" + op.id}
payload_type={props.payload_type}
instance_name={props.instance_name} onChange={props.onChange} {...op} />
))}
</TableBody>
</Table>
</TableContainer>
</span>
<div className="mythic-create-parameter-list">
{b.parameters.map( (op) => (
<CreatePayloadParameter
displayMode="card"
selected_os={props.os}
key={"buildparamtablerow" + op.id}
payload_type={props.payload_type}
instance_name={props.instance_name}
onChange={props.onChange}
{...op}
/>
))}
</div>
</section>
))}
</div>
@@ -1,11 +1,5 @@
import React from 'react';
import Table from '@mui/material/Table';
import TableBody from '@mui/material/TableBody';
import TableContainer from '@mui/material/TableContainer';
import TableHead from '@mui/material/TableHead';
import TableRow from '@mui/material/TableRow';
import { CreatePayloadParameter } from './CreatePayloadParameter';
import MythicStyledTableCell from "../../MythicComponents/MythicTableCell";
export function CreatePayloadC2ProfileParametersTable(props){
@@ -14,20 +8,16 @@ export function CreatePayloadC2ProfileParametersTable(props){
props.onChange(props.name, paramName, value, error);
}
return (
<TableContainer className="mythicElement">
<Table size="small" style={{"tableLayout": "fixed", "maxWidth": "calc(100vw)", "overflow": "auto"}}>
<TableHead>
<TableRow>
<MythicStyledTableCell style={{width: "20%"}}>Parameter</MythicStyledTableCell>
<MythicStyledTableCell>Value</MythicStyledTableCell>
</TableRow>
</TableHead>
<TableBody>
{props.c2profileparameters.map( (op) => (
<CreatePayloadParameter key={"c2paramtablerow" + op.id} returnAllDictValues={props.returnAllDictValues} onChange={onChange} {...op} />
))}
</TableBody>
</Table>
</TableContainer>
<div className="mythic-create-parameter-list">
{props.c2profileparameters.map( (op) => (
<CreatePayloadParameter
displayMode="card"
key={"c2paramtablerow" + op.id}
returnAllDictValues={props.returnAllDictValues}
onChange={onChange}
{...op}
/>
))}
</div>
);
}
@@ -48,7 +48,7 @@ function isTrue(value){
}
export function CreatePayloadParameter({onChange, parameter_type, default_value, name, required, verifier_regex, id,
description, initialValue, choices, trackedValue, instance_name,
payload_type, selected_os, dynamic_query_function}){
payload_type, selected_os, dynamic_query_function, displayMode = "table"}){
const theme = useTheme();
const [value, setValue] = React.useState("");
const [valueNum, setValueNum] = React.useState(0);
@@ -487,11 +487,11 @@ export function CreatePayloadParameter({onChange, parameter_type, default_value,
)
case "ChooseOne":
return (
<div style={{position: "relative", display: "flex", alignItems: "center", overflow: "hidden"}}>
<div className="mythic-create-inline-control">
<Backdrop open={backdropOpen} style={{zIndex: 2, position: "absolute"}} invisible={false}>
<CircularProgress color="inherit" />
</Backdrop>
<FormControl style={{width: "100%"}}>
<FormControl>
{ChoiceOptions.length === 0 &&
<InputLabel>{"No Options Available"}</InputLabel>
}
@@ -521,8 +521,8 @@ export function CreatePayloadParameter({onChange, parameter_type, default_value,
<Backdrop open={backdropOpen} style={{zIndex: 2, position: "absolute"}} invisible={false}>
<CircularProgress color="inherit" />
</Backdrop>
<div style={{width: "100%", display: "flex", alignItems: "center"}}>
<FormControl style={{width: "20%"}}>
<div className="mythic-create-inline-control">
<FormControl style={{flex: "0 1 12rem"}}>
<Select
multiple={false}
disabled={chooseOneCustomValue !== ""}
@@ -537,7 +537,7 @@ export function CreatePayloadParameter({onChange, parameter_type, default_value,
}
</Select>
</FormControl>
OR
<span className="mythic-create-choice-divider">OR</span>
<MythicTextField name={name} requiredValue={required} placeholder={"Custom Value"} value={chooseOneCustomValue} multiline={true} maxRows={5}
onChange={onChangeTextChooseOneCustom} display="inline-block"
/>
@@ -554,11 +554,11 @@ export function CreatePayloadParameter({onChange, parameter_type, default_value,
)
case "ChooseMultiple":
return (
<div style={{position: "relative", display: "flex", alignItems: "center", overflow: "hidden"}}>
<div className="mythic-create-inline-control">
<Backdrop open={backdropOpen} style={{zIndex: 2, position: "absolute"}} invisible={false}>
<CircularProgress color="inherit" />
</Backdrop>
<FormControl style={{width: "100%"}}>
<FormControl>
<InputLabel id={name + "select"} style={{paddingTop: "15px"}}>{"Select Multiple"}</InputLabel>
{ChoiceOptions.length === 0 &&
<InputLabel>{"No Options Available"}</InputLabel>
@@ -586,7 +586,7 @@ export function CreatePayloadParameter({onChange, parameter_type, default_value,
);
case "Array":
return (
<TableContainer className="mythicElement">
<TableContainer className="mythicElement mythic-create-array-table">
<Table size="small" style={{tableLayout: "fixed", maxWidth: "100%", "overflow": "auto"}}>
<TableBody>
{arrayValue.map( (a, i) => (
@@ -616,7 +616,7 @@ export function CreatePayloadParameter({onChange, parameter_type, default_value,
);
case "TypedArray":
return (
<TableContainer className="mythicElement">
<TableContainer className="mythicElement mythic-create-array-table">
<Table size="small" style={{tableLayout: "fixed", maxWidth: "100%", "overflow": "auto"}}>
<TableBody>
{typedArrayValue.map( (a, i) => (
@@ -662,15 +662,15 @@ export function CreatePayloadParameter({onChange, parameter_type, default_value,
return (
<React.Fragment>
{dictValue.map( (opt, i) => (
<div key={"dictval" + i}>
<IconButton className="mythic-table-row-icon-action mythic-table-row-icon-action-hover-danger" style={{width: "5%"}} onClick={(e) => {removeDictEntry(i)}} size="small"><DeleteIcon fontSize="small" /> </IconButton>
<Input style={{width:"20%"}} startAdornment={<Button disabled>Key</Button>} size="small" value={opt.name} onChange={(e) => onChangeDictKey(e, i)}></Input>
<Input style={{width:"75%"}} startAdornment={<Button disabled>value</Button>} size="small" value={opt.value} onChange={(e) => onChangeDictVal(e, i)}></Input>
<div className="mythic-create-dictionary-row" key={"dictval" + i}>
<IconButton className="mythic-table-row-icon-action mythic-table-row-icon-action-hover-danger" onClick={(e) => {removeDictEntry(i)}} size="small"><DeleteIcon fontSize="small" /> </IconButton>
<Input startAdornment={<Button disabled>Key</Button>} size="small" value={opt.name} onChange={(e) => onChangeDictKey(e, i)}></Input>
<Input startAdornment={<Button disabled>Value</Button>} size="small" value={opt.value} onChange={(e) => onChangeDictVal(e, i)}></Input>
</div>
)
)}
{dictSelectOptions.length > 0 ? (
<div>
<div className="mythic-create-dictionary-add">
<IconButton className="mythic-table-row-icon-action mythic-table-row-icon-action-hover-success" size="small" onClick={addDictValEntry}> <AddCircleIcon fontSize="small" /> </IconButton>
<Select size="small" value={dictSelectOptionsChoice} onChange={(e) => setDictSelectOptionsChoice(e.target.value)}>
{dictSelectOptions.map( (selectOpt, i) => (
@@ -796,7 +796,33 @@ export function CreatePayloadParameter({onChange, parameter_type, default_value,
return true;
}
}
const isModified = modifiedValue();
if(displayMode === "card"){
return (
<div className={`mythic-create-parameter-card ${isModified ? "mythic-create-parameter-card-modified" : ""}`.trim()} key={"buildparam" + id}>
<div className="mythic-create-parameter-copy">
<div className="mythic-create-parameter-title-row">
<Typography component="div" className="mythic-create-parameter-title">
{name}
</Typography>
</div>
<div className="mythic-create-parameter-chips">
<span className="mythic-create-parameter-chip">{parameter_type}</span>
{required && <span className="mythic-create-parameter-chip mythic-create-parameter-chip-required">Required</span>}
{isModified && <span className="mythic-create-parameter-chip mythic-create-parameter-chip-modified">Modified</span>}
</div>
{description &&
<Typography component="div" className="mythic-create-parameter-description">
{description}
</Typography>
}
</div>
<div className="mythic-create-parameter-control">
{getParameterObject()}
</div>
</div>
)
}
return (
<TableRow key={"buildparam" + id} hover>
<MythicStyledTableCell>
@@ -808,7 +834,7 @@ export function CreatePayloadParameter({onChange, parameter_type, default_value,
{description}
</Typography>
{modifiedValue() ? (
{isModified ? (
<Typography color="warning.main">Modified</Typography>
) : null}
</MythicStyledTooltip>
@@ -13,10 +13,9 @@ import AddCircleIcon from '@mui/icons-material/AddCircle';
import {getDefaultChoices, getDefaultValueForType, getSavedToType} from "./Step2SelectPayloadType";
import {CreatePayloadBuildParametersTable} from "./CreatePayloadBuildParametersTable";
import {ParseForDisplay} from "../Payloads/DetailedPayloadTable";
import Paper from '@mui/material/Paper';
import {useTheme} from '@mui/material/styles';
import {getModifiedC2Params} from "./Step4C2Profiles";
import { Backdrop, CircularProgress } from '@mui/material';
import { Backdrop } from '@mui/material';
import {MythicLoadingState} from "../../MythicComponents/MythicStateDisplay";
const GET_Payload_Types = gql`
@@ -414,31 +413,23 @@ export function Step1SelectOS(props){
payloadConfigRef.current = payload
}
return (
<div style={{
height: "100%",
display: "flex",
flexDirection: "column"
}}>
{/* Content area that can grow */}
<div style={{
flexGrow: 1,
overflow: "hidden",
display: "flex",
flexDirection: "column",
minHeight: 0 // Important for flex shrinking
}}>
{/* Top section - fixed height */}
<div style={{
display: "flex",
flexShrink: 0 // Don't shrink this section
}}>
<div style={{width: "100%", margin: "5px", border: "1px solid grey", borderRadius: "5px", padding: "10px"}}>
<Typography variant={"p"} style={{fontWeight: 600}}>
1. Select Operating System
</Typography>
<div className="mythic-create-flow-shell">
<div className="mythic-create-flow-content">
<div className="mythic-create-selection-grid">
<section className="mythic-create-section">
<div className="mythic-create-section-header">
<div>
<Typography component="div" className="mythic-create-section-title">
Select operating system
</Typography>
<Typography component="div" className="mythic-create-section-description">
Filter payload types and C2 profiles by the target platform.
</Typography>
</div>
</div>
<Select
className="mythic-create-select"
value={os}
style={{width: "100%"}}
disabled={!props.first}
onChange={onChangeOS}
>
@@ -448,85 +439,87 @@ export function Step1SelectOS(props){
))
}
</Select>
<Typography style={{fontWeight: 600}}>
Compatible Payload Types
</Typography>
{payloadtypesPerOS[os]?.join(", ")}
<Typography style={{fontWeight: 600}}>
Compatible C2 Profiles
</Typography>
{C2PerOS[os]?.join(", ")}
</div>
<div style={{width: "100%", margin: "5px", border: "1px solid grey", borderRadius: "5px", padding: "10px"}}>
<div style={{width: "100%", display: "flex", alignItems: "flex-start", marginBottom: "10px", flexDirection: "column"}}>
<Typography style={{fontWeight: 600}} variant={"p"}>
2. Select Payload Type
</Typography>
<Select
style={{width: "100%"}}
disabled={!props.first}
value={selectedPayloadType}
onChange={evt => setSelectedPayloadType(evt.target.value)}
>
{
payloadtypesPerOS[os]?.map((opt) => (
<MenuItem key={"step1" + opt} value={opt}>{opt}</MenuItem>
))
}
</Select>
<div className="mythic-create-meta-list">
<div>
<span className="mythic-create-meta-label">Compatible payload types</span>
<div className="mythic-create-meta-value">{payloadtypesPerOS[os]?.join(", ")}</div>
</div>
<div>
<span className="mythic-create-meta-label">Compatible C2 profiles</span>
<div className="mythic-create-meta-value">{C2PerOS[os]?.join(", ")}</div>
</div>
</div>
<div style={{display: "flex"}}>
<MythicAgentSVGIcon payload_type={selectedPayloadType} style={{width: "80px", padding: "5px", objectFit: "unset"}} />
<Typography variant="body2" component="p" style={{whiteSpace: "pre-wrap"}}>
<b>Version: </b>{payloadtypeData[selectedPayloadType]?.semver}<br/>
<b>Description: </b>{payloadtypeData[selectedPayloadType]?.note}<br/>
<b>C2: </b>{payloadtypeData[selectedPayloadType]?.payloadtypec2profiles.map(c => c.c2profile.name).join(", ")}
</Typography>
</section>
<section className="mythic-create-section">
<div className="mythic-create-section-header">
<div>
<Typography component="div" className="mythic-create-section-title">
Select payload type
</Typography>
<Typography component="div" className="mythic-create-section-description">
Choose the agent family to configure for this build.
</Typography>
</div>
</div>
</div>
<Select
className="mythic-create-select"
disabled={!props.first}
value={selectedPayloadType}
onChange={evt => setSelectedPayloadType(evt.target.value)}
>
{
payloadtypesPerOS[os]?.map((opt) => (
<MenuItem key={"step1" + opt} value={opt}>{opt}</MenuItem>
))
}
</Select>
<div className="mythic-create-agent-summary">
<div className="mythic-create-agent-icon">
<MythicAgentSVGIcon payload_type={selectedPayloadType} style={{width: "100%", height: "100%", objectFit: "contain"}} />
</div>
<div className="mythic-create-meta-list">
<div>
<span className="mythic-create-meta-label">Version</span>
<div className="mythic-create-meta-value">{payloadtypeData[selectedPayloadType]?.semver || "Unknown"}</div>
</div>
<div>
<span className="mythic-create-meta-label">Description</span>
<div className="mythic-create-meta-value">{payloadtypeData[selectedPayloadType]?.note || "No description available."}</div>
</div>
<div>
<span className="mythic-create-meta-label">C2</span>
<div className="mythic-create-meta-value">{payloadtypeData[selectedPayloadType]?.payloadtypec2profiles.map(c => c.c2profile.name).join(", ") || "None"}</div>
</div>
</div>
</div>
</section>
</div>
{/* Bottom section - scrollable table area */}
<div style={{
margin: "5px",
border: props.first ? "1px solid grey" : '',
borderRadius: "5px",
padding: "10px 5px 5px 10px",
display: "flex",
flexDirection: "column",
flexGrow: 1,
minHeight: 0, // Important for flex shrinking
overflow: "hidden"
}}>
{props.first &&
<div style={{flexGrow: 1, overflowY: "auto", position: "relative"}}>
<section className="mythic-create-section mythic-create-section-fill">
{props.first ? (
<div style={{display: "flex", flexDirection: "column", flexGrow: 1, minHeight: 0, overflow: "hidden", position: "relative"}}>
{openBackdrop &&
<Backdrop open={openBackdrop} onClick={()=>{setOpenBackdrop(false);}} style={{zIndex: 2000, position: "absolute"}}>
<CircularProgress color="inherit" disableShrink />
<Backdrop open={openBackdrop} style={{zIndex: 2000, position: "absolute"}}>
<MythicLoadingState compact title="Loading payloads" description="Fetching compatible payloads." sx={{color: "inherit"}} />
</Backdrop>
}
<StartFromExistingPayloadOrStartFresh first={props.first}
last={props.last}
canceled={canceled}
onSelectedPayload={onSelectedPayload}
payloadOptions={payloadOptions}
onStartFresh={onStartFresh}
/>
{!openBackdrop &&
<StartFromExistingPayloadOrStartFresh first={props.first}
last={props.last}
canceled={canceled}
onSelectedPayload={onSelectedPayload}
payloadOptions={payloadOptions}
onStartFresh={onStartFresh}
/>
}
</div>
}
{!props.first &&
) : (
<ConfigureBuildParameters os={os} selectedPayloadType={selectedPayloadType}
prevData={props.prevData}
onUpdatePayloadConfig={onUpdatePayloadConfig} />
}
</div>
)}
</section>
</div>
{/* Navigation buttons - always at bottom */}
<div style={{flexShrink: 0}}>
<div className="mythic-create-flow-footer">
<CreatePayloadNavigationButtons
first={props.first}
last={props.last}
@@ -544,27 +537,32 @@ export const StartFromExistingPayloadOrStartFresh = (
) => {
return (
<>
{/* Header section - fixed */}
<div style={{flexShrink: 0}}>
<Typography variant={"p"} style={{fontWeight: 600}}>
3. Continue from Existing Payload or
<div className="mythic-create-section-header">
<div>
<Typography component="div" className="mythic-create-section-title">
Continue from existing payload
</Typography>
<Typography component="div" className="mythic-create-section-description">
Select a compatible payload to reuse its configuration, or start with defaults.
</Typography>
</div>
<div className="mythic-create-section-actions">
<Button
className="mythic-table-row-action mythic-table-row-action-hover-success"
size="small"
variant="contained"
style={{marginLeft: "10px", marginBottom: "5px"}}
onClick={onStartFresh}
startIcon={<AddCircleIcon fontSize="small" />} >
Start Fresh
</Button>
</Typography>
</div>
</div>
{/* Scrollable table container */}
<div style={{
flexGrow: 1,
overflow: "auto",
display: "flex",
flexDirection: "column",
overflow: "hidden",
minHeight: 0 // Important for flex shrinking
}}>
<PayloadSelect
@@ -675,51 +673,27 @@ export const ConfigureBuildParameters = (
}, [selectedPayloadType, prevData?.payload]);
return (
<>
{/* Header section - fixed */}
<div style={{flexShrink: 0}}>
<Typography variant={"p"} style={{fontWeight: 600}}>
3.5. Configure Payload Build Parameters
</Typography>
<div className="mythic-create-section-header">
<div>
<Typography component="div" className="mythic-create-section-title">
Configure payload build parameters
</Typography>
<Typography component="div" className="mythic-create-section-description">
Review defaults, required fields, and any values changed from the payload type baseline.
</Typography>
</div>
</div>
<div style={{
flexGrow: 1,
overflow: "auto",
display: "flex",
flexDirection: "row",
minHeight: 0 // Important for flex shrinking
}}>
<div style={{
width: "30%",
margin: "5px",
border: "1px solid grey",
borderRadius: "5px",
padding: "10px",
display: "flex",
flexDirection: "column",
flexGrow: 1,
minHeight: 0, // Important for flex shrinking
overflow: "auto"
}}>
<Typography textAlign="center" variant={"h7"} style={{fontWeight: 600, width: "100%"}}>
<div className="mythic-create-builder-split">
<section className="mythic-create-section mythic-create-section-scroll">
<Typography component="div" className="mythic-create-section-title" style={{textAlign: "center"}}>
Configuration Summary
</Typography>
<ConfigurationSummary buildParameters={payloadTypeParameters} os={os} />
</div>
<div style={{
width: "100%",
margin: "5px",
border: "1px solid grey",
borderRadius: "5px",
padding: "0px",
display: "flex",
flexDirection: "column",
flexGrow: 1,
minHeight: 0, // Important for flex shrinking
overflow: "hidden"
}}>
</section>
<section className="mythic-create-section mythic-create-section-scroll">
<CreatePayloadBuildParametersTable onChange={onChange} buildParameters={payloadTypeParameters} os={os}
payload_type={selectedPayloadType}/>
</div>
</section>
</div>
</>
)
@@ -867,28 +841,25 @@ export const GetGroupedParameters = ({buildParameters, os, c2_name}) => {
return groupedData;
}
export const ConfigurationSummary = ({buildParameters, os, c2_name}) => {
const theme = useTheme();
const [groupedParameters, setGroupedParameters] = React.useState([]);
React.useEffect( () => {
// grouped should be array of groupName
setGroupedParameters(GetGroupedParameters({buildParameters, os, c2_name}));
}, [buildParameters, c2_name]);
return (
groupedParameters?.map((b,i) => (
<div key={b.name} >
groupedParameters?.map((b) => (
<div className="mythic-create-summary-group" key={b.name || "default-configuration-group"} >
{b.name !== '' && b.name !== undefined && b.parameters.length > 0 &&
<Paper elevation={5} style={{backgroundColor: theme.pageHeader.main, color: theme.pageHeaderText.main,marginBottom: "5px", }} variant={"elevation"}>
<Typography variant="h6" style={{textAlign: "left", display: "inline-block", marginLeft: "20px", color: theme.pageHeaderColor}}>
{b.name}
</Typography>
</Paper>
<div className="mythic-create-summary-group-header">
{b.name}
</div>
}
{b?.parameters?.map( (p,i) => (
<div key={p.name} style={{marginLeft: b.name === '' ? '' : "20px"}} className={i%2 > 0 ? 'alternateRow' : ''}>
<Typography style={{fontWeight: 600}} variant={"body2"}>
{b?.parameters?.map( (p) => (
<div className="mythic-create-summary-row" key={p.name}>
<Typography component="div" className="mythic-create-summary-name">
{p.name}
</Typography>
<div style={{marginLeft: "20px", marginTop: "5px", marginBottom: "5px", whiteSpace: "pre"}}>
<div className="mythic-create-summary-value">
<ParseForDisplay cmd={p} />
</div>
</div>
@@ -141,144 +141,116 @@ export function Step3SelectCommands(props){
const updateSelectedCommands = (commands) => {
selectedCommands.current = commands;
}
const hasHoveredCommand = hoveredCommand?.cmd !== undefined;
const commandReason = hoveredCommand?.reason || "This command can be included or removed as needed";
return (
<div style={{
height: "100%",
display: "flex",
flexDirection: "column",
}}>
{/* Content area that can grow */}
<div style={{
flexGrow: 1,
overflow: "hidden",
display: "flex",
flexDirection: "row",
minHeight: 0 // Important for flex shrinking
}}>
{/* Left section - fixed height */}
<div style={{
padding: "5px",
display: "flex",
width: "60%",
flexDirection: "column",
minHeight: 0,
overflow: "hidden" // Don't shrink this section
}}>
<div style={{width: "100%", marginBottom: "5px", border: "1px solid grey", borderRadius: "5px", padding: "10px", display: "flex", flexShrink: 0}}>
<MythicAgentSVGIcon payload_type={props.buildOptions.payload_type} style={{width: "80px", padding: "5px", objectFit: "unset"}} />
<Typography variant="body2" component="p" style={{whiteSpace: "pre-wrap"}}>
<b>Description: </b>{props.buildOptions.description}
</Typography>
</div>
<div style={{width: "100%",
//border: "1px solid grey",
flexDirection: "column",
borderRadius: "5px",
//padding: "10px",
flexGrow: 1, minHeight: 0, display: "flex"}}>
<Typography variant={"p"} style={{fontWeight: 600}}>
{"1. Select Commands to Include in the Payload"}
</Typography>
<div style={{flexGrow: 1, minHeight: 0, overflow: "hidden", display: "flex"}}>
<CommandTransferSelect commands={commandOptions}
payload_type={props.buildOptions["payload_type"]}
first={props.first} last={props.last}
updateSelectedCommands={updateSelectedCommands}
setHoveredCommand={setHoveredCommand}/>
<div className="mythic-create-flow-shell">
<div className="mythic-create-flow-content">
<div className="mythic-create-builder-split" style={{gridTemplateColumns: "minmax(0, 0.6fr) minmax(18rem, 0.4fr)"}}>
<section className="mythic-create-section mythic-create-section-fill mythic-create-section-plain">
<div className="mythic-create-subsection" style={{flex: "0 0 auto"}}>
<div className="mythic-create-agent-summary">
<div className="mythic-create-agent-icon">
<MythicAgentSVGIcon payload_type={props.buildOptions.payload_type} style={{width: "100%", height: "100%", objectFit: "contain"}} />
</div>
<div className="mythic-create-meta-list">
<div>
<span className="mythic-create-meta-label">Selected payload type</span>
<div className="mythic-create-meta-value">{props.buildOptions.payload_type}</div>
</div>
<div>
<span className="mythic-create-meta-label">Description</span>
<div className="mythic-create-meta-value">{props.buildOptions.description}</div>
</div>
</div>
</div>
</div>
<div className="mythic-create-subsection mythic-create-subsection-fill">
<div className="mythic-create-section-header">
<div>
<Typography component="div" className="mythic-create-section-title">
Select commands
</Typography>
<Typography component="div" className="mythic-create-section-description">
Move commands into the payload and review hover details before continuing.
</Typography>
</div>
</div>
<div style={{flexGrow: 1, minHeight: 0, overflow: "hidden", display: "flex"}}>
<CommandTransferSelect commands={commandOptions}
payload_type={props.buildOptions["payload_type"]}
first={props.first} last={props.last}
updateSelectedCommands={updateSelectedCommands}
setHoveredCommand={setHoveredCommand}/>
</div>
</div>
</div>
</div>
</section>
{/* Bottom section - scrollable table area */}
<div style={{
width: "40%",
padding: "5px",
borderRadius: "5px",
display: "flex",
flexDirection: "column",
flexGrow: 1,
minHeight: 0, // Important for flex shrinking
overflow: "hidden"
}}>
<div style={{width: "100%", flexGrow: 1, marginBottom: "5px", border: "1px solid grey", borderRadius: "5px", padding: "10px",
height: "40%"}}>
<Typography variant={"p"} style={{fontWeight: 600}}>
{"Hovered Command Details"}
</Typography><br/>
{hoveredCommand["cmd"] !== undefined &&
<>
{hoveredCommand["cmd"]}
<StyledDivider classes={{root: classes.divider}}/>
<Typography style={{}}>
<b>Description: </b>
<section className="mythic-create-section mythic-create-section-fill mythic-create-section-plain">
<div className="mythic-create-subsection">
<Typography component="div" className="mythic-create-section-title">
Hovered command details
</Typography>
{hasHoveredCommand ? (
<div className="mythic-create-meta-list">
<div>
<span className="mythic-create-meta-label">Command</span>
<div className="mythic-create-meta-value">{hoveredCommand.cmd}</div>
</div>
<div>
<span className="mythic-create-meta-label">Description</span>
<div className="mythic-create-meta-value">{hoveredCommand.description}</div>
</div>
</div>
) : (
<Typography component="div" className="mythic-create-section-description">
Hover over a command to preview its description and behavior.
</Typography>
)}
</div>
<div className="mythic-create-subsection mythic-create-subsection-fill">
<Typography component="div" className="mythic-create-section-title">
Command behavior
</Typography>
<div className="mythic-create-subsection-scroll">
{hasHoveredCommand ? (
<div className="mythic-create-meta-list">
<div>
<span className="mythic-create-meta-label">{hoveredCommand.disabled ? "Cannot be moved" : "Information"}</span>
<div className="mythic-create-meta-value">{commandReason}</div>
</div>
<div>
<span className="mythic-create-meta-label">Command line help</span>
<div className="mythic-create-meta-value">{hoveredCommand.help_cmd}</div>
</div>
<div>
<span className="mythic-create-meta-label">Needs admin permissions</span>
<div className="mythic-create-meta-value">{hoveredCommand.needs_admin ? "True" : "False"}</div>
</div>
<div>
<span className="mythic-create-meta-label">Supported UI features</span>
<div className="mythic-create-meta-value">{hoveredCommand?.supported_ui_features?.join(", ") || "None"}</div>
</div>
{hoveredCommand?.attributes?.dependencies && hoveredCommand?.attributes?.dependencies.length > 0 &&
<div>
<span className="mythic-create-meta-label">Dependencies</span>
<div className="mythic-create-meta-value">{hoveredCommand?.attributes?.dependencies.join(", ")}</div>
</div>}
{hoveredCommand?.attributes?.alias !== undefined &&
<div>
<span className="mythic-create-meta-label">Alias</span>
<div className="mythic-create-meta-value">{hoveredCommand?.attributes?.alias ? "True":"False"}</div>
</div>}
</div>
) : (
<Typography component="div" className="mythic-create-section-description">
Command metadata appears here after hovering over an available or selected command.
</Typography>
<Typography style={{marginLeft: "20px"}}>
{hoveredCommand["description"]}
</Typography><br/>
</>
}
</div>
<div style={{width: "100%", flexGrow: 1, marginBottom: "5px", border: "1px solid grey", borderRadius: "5px", padding: "10px",
height: "20%", overflow: "scroll"}}>
{hoveredCommand["reason"] !== "" ? (
<>
<Typography style={{}}>
<b>{hoveredCommand["disabled"] ? ("Cannot be moved: ") : ("Information: ")} </b>
</Typography>
<Typography style={{marginLeft: "20px"}}>
{hoveredCommand["reason"]}
</Typography>
</>
) : (
<>
<Typography style={{}}>
<b>{"Information: "} </b>
</Typography>
<Typography style={{marginLeft: "20px"}}>
{"This command can be included or removed as needed"}
</Typography>
</>
)}
<Typography style={{}}>
<b>Command line Help: </b>
</Typography>
<Typography style={{marginLeft: "20px"}}>
{hoveredCommand["help_cmd"]}
</Typography>
<Typography style={{}}>
<b>Needs Admin Permissions: </b>
</Typography>
<Typography style={{marginLeft: "20px"}}>
{hoveredCommand["needs_admin"] ? "True" : "False"}
</Typography>
<Typography style={{}}>
<b>Supported UI Features: </b>
</Typography>
<Typography style={{marginLeft: "20px"}}>
{hoveredCommand?.supported_ui_features?.join(", ")}
</Typography>
{hoveredCommand?.attributes?.dependencies && hoveredCommand?.attributes?.dependencies.length > 0 &&
<>
<Typography style={{}}>
<b>Dependencies: </b>
</Typography>
<Typography style={{marginLeft: "20px"}}>
{hoveredCommand?.attributes?.dependencies.join(", ")}
</Typography>
</>}
{hoveredCommand?.attributes?.alias !== undefined &&
<>
<Typography style={{}}>
<b>Alias: </b>
</Typography>
<Typography style={{marginLeft: "20px"}}>
{hoveredCommand?.attributes?.alias ? "True":"False"}
</Typography>
</>}
</div>
)}
</div>
</div>
</section>
</div>
</div>
{openConfirmDialog &&
@@ -288,8 +260,7 @@ export function Step3SelectCommands(props){
acceptText="Accept"
onSubmit={acceptConfirm} />
}
{/* Navigation buttons - always at bottom */}
<div style={{flexShrink: 0}}>
<div className="mythic-create-flow-footer">
<CreatePayloadNavigationButtons
first={props.first}
last={props.last}
@@ -374,44 +374,39 @@ export function Step4C2Profiles(props){
setDisabledC2Add(false);
}, [includedC2Profiles, selectedC2]);
return (
<div style={{
height: "100%",
display: "flex",
flexDirection: "column"
}}>
{/* Content area that can grow */}
<div style={{
flexGrow: 1,
overflow: "hidden",
display: "flex",
flexDirection: "column",
minHeight: 0 // Important for flex shrinking
}}>
{/* Top section - fixed height */}
<div style={{
display: "flex",
flexShrink: 0 // Don't shrink this section
}}>
<div style={{width: "40%", margin: "5px", border: "1px solid grey", borderRadius: "5px", padding: "10px", display: "flex"}}>
<MythicAgentSVGIcon payload_type={props.buildOptions.payload_type} style={{width: "80px", padding: "5px", objectFit: "unset"}} />
<div>
<Typography variant={"p"} style={{}}>
<b>OS: </b>{props.buildOptions.os}
</Typography><br/>
<Typography variant="body2" component="p" style={{whiteSpace: "pre-wrap"}}>
<b>Description: </b>{props.buildOptions.description}
</Typography>
<div className="mythic-create-flow-shell">
<div className="mythic-create-flow-content">
<div className="mythic-create-selection-grid">
<section className="mythic-create-section">
<div className="mythic-create-agent-summary">
<div className="mythic-create-agent-icon">
<MythicAgentSVGIcon payload_type={props.buildOptions.payload_type} style={{width: "100%", height: "100%", objectFit: "contain"}} />
</div>
<div className="mythic-create-meta-list">
<div>
<span className="mythic-create-meta-label">Operating system</span>
<div className="mythic-create-meta-value">{props.buildOptions.os}</div>
</div>
<div>
<span className="mythic-create-meta-label">Description</span>
<div className="mythic-create-meta-value">{props.buildOptions.description}</div>
</div>
</div>
</div>
</section>
<section className="mythic-create-section">
<div className="mythic-create-section-header">
<div>
<Typography component="div" className="mythic-create-section-title">
Select C2 profiles
</Typography>
<Typography component="div" className="mythic-create-section-description">
Add one or more egress or peer profiles to configure for this payload.
</Typography>
</div>
</div>
</div>
<div style={{width: "60%",
margin: "5px",
border: "1px solid grey",
borderRadius: "5px", padding: "10px"}}>
<Typography variant={"p"} style={{fontWeight: 600}}>
1. Select C2 Profiles to Include
</Typography>
<Select
style={{width: "100%", marginBottom: "5px", marginTop: "5px", display: "flex"}}
className="mythic-create-select"
value={selectedC2}
onChange={onChangeSelectedC2}
>
@@ -429,79 +424,43 @@ export function Step4C2Profiles(props){
className="mythic-table-row-action mythic-table-row-action-hover-success"
size="small"
variant="contained"
style={{marginLeft: "10px", marginBottom: "5px"}}
onClick={addC2}
disabled={disabledC2Add}
startIcon={<AddCircleIcon fontSize="small" />} >
Include Profile
</Button>
</div>
</section>
</div>
{/* Bottom section - scrollable table area */}
<div style={{
margin: "5px",
//border: "1px solid grey",
borderRadius: "5px",
//padding: "10px",
display: "flex",
flexDirection: "column",
flexGrow: 1,
minHeight: 0, // Important for flex shrinking
overflow: "hidden"
}}>
{/* Header section - fixed */}
<div style={{flexShrink: 0}}>
<Typography variant={"p"} style={{fontWeight: 600}}>
2. Include and Configure C2 Profiles
</Typography>
<section className="mythic-create-section mythic-create-section-fill">
<div className="mythic-create-section-header">
<div>
<Typography component="div" className="mythic-create-section-title">
Include and configure C2 profiles
</Typography>
<Typography component="div" className="mythic-create-section-description">
Review the active C2 profiles and adjust their build-time parameters.
</Typography>
</div>
</div>
<div style={{
flexGrow: 1,
overflow: "auto",
display: "flex",
flexDirection: "row",
minHeight: 0 // Important for flex shrinking
}}>
<div style={{
width: "30%",
margin: "5px",
border: "1px solid grey",
borderRadius: "5px",
padding: "5px",
display: "flex",
flexDirection: "column",
flexGrow: 1,
minHeight: 0, // Important for flex shrinking
overflow: "auto"
}}>
<Typography textAlign="center" variant={"h7"} style={{fontWeight: 600, width: "100%"}}>
<div className="mythic-create-builder-split">
<section className="mythic-create-section mythic-create-section-scroll">
<Typography component="div" className="mythic-create-section-title" style={{textAlign: "center"}}>
Configuration Summary
</Typography>
{includedC2Profiles.map( (c, index) => (
<ConfigurationSummary key={c.name + index} buildParameters={c.c2profileparameters}
os={props.buildOptions.os} c2_name={c.name} />
))}
</div>
<div style={{
width: "100%",
margin: "5px",
border: "1px solid grey",
borderRadius: "5px",
padding: "0px",
display: "flex",
flexDirection: "column",
flexGrow: 1,
minHeight: 0, // Important for flex shrinking
overflow: "hidden"
}}>
</section>
<section className="mythic-create-section mythic-create-section-scroll">
<C2ProfileTabs includedC2Profiles={includedC2Profiles} os={props.buildOptions.os}
onCloseTab={removeC2} onChange={updateC2Parameter}
onChangeCreatedInstanceName={onChangeCreatedInstanceName}
/>
</div>
</section>
</div>
</div>
</section>
</div>
{openConfirmDialog &&
<MythicConfirmDialog open={openConfirmDialog}
@@ -510,8 +469,7 @@ export function Step4C2Profiles(props){
acceptText="Accept"
onSubmit={acceptConfirm} />
}
{/* Navigation buttons - always at bottom */}
<div style={{flexShrink: 0}}>
<div className="mythic-create-flow-footer">
<CreatePayloadNavigationButtons
first={props.first}
last={props.last}
@@ -673,7 +631,8 @@ const C2ProfileTabs = ({includedC2Profiles, onChange, os, onCloseTab, onChangeCr
}} >
{c.c2profileparametersinstances.length > 0 &&
<Select
style={{width: "100%", marginBottom: "5px"}}
className="mythic-create-select"
style={{marginBottom: "0.65rem"}}
value={c.selected_instance}
onChange={evt => onChangeCreatedInstanceName(evt, index, c)}
>
@@ -202,78 +202,55 @@ export function Step5Build(props){
}
return (
<div style={{
height: "100%",
display: "flex",
flexDirection: "column"
}}>
{/* Content area that can grow */}
<div style={{
flexGrow: 1,
overflow: "hidden",
display: "flex",
flexDirection: "column",
minHeight: 0 // Important for flex shrinking
}}>
{/* Top section - fixed height */}
<div style={{
display: "flex",
flexShrink: 0 // Don't shrink this section
}}>
<div style={{width: "100%", margin: "5px", border: "1px solid grey", borderRadius: "5px", padding: "10px", display: "flex"}}>
<MythicAgentSVGIcon payload_type={props.buildOptions[1].payload_type} style={{width: "80px", padding: "5px", objectFit: "unset"}} />
<div>
<Typography variant={"p"} style={{}}>
<b>OS: </b>{props.buildOptions[1].os}
<div className="mythic-create-flow-shell">
<div className="mythic-create-flow-content">
<div className="mythic-create-selection-grid">
<section className="mythic-create-section">
<div className="mythic-create-agent-summary">
<div className="mythic-create-agent-icon">
<MythicAgentSVGIcon payload_type={props.buildOptions[1].payload_type} style={{width: "100%", height: "100%", objectFit: "contain"}} />
</div>
<div className="mythic-create-meta-list">
<div>
<span className="mythic-create-meta-label">Operating system</span>
<div className="mythic-create-meta-value">{props.buildOptions[1].os}</div>
</div>
<div>
<span className="mythic-create-meta-label">Description</span>
<div className="mythic-create-meta-value">{props.buildOptions[1].description}</div>
</div>
<MythicStyledTooltip title={"Edit OS / Payload Type"}>
<IconButton className="mythic-table-row-icon-action mythic-table-row-icon-action-hover-info" size="small" onClick={() => props.moveToStep(0)}>
<DriveFileRenameOutlineIcon />
</IconButton>
</MythicStyledTooltip>
</Typography><br/>
<Typography variant="body2" component="p" style={{whiteSpace: "pre-wrap"}}>
<b>Description: </b>{props.buildOptions[1].description}
</Typography>
</div>
</div>
</div>
<div style={{width: "100%", margin: "5px", border: "1px solid grey", borderRadius: "5px", padding: "10px"}}>
<Typography variant={"p"} style={{fontWeight: 600}}>
1. Provide Payload Name and Description
</Typography>
<div style={{width: "100%", display: "flex", alignItems: "flex-start", marginBottom: "10px", flexDirection: "column"}}>
</section>
<section className="mythic-create-section">
<div className="mythic-create-section-header">
<div>
<Typography component="div" className="mythic-create-section-title">
Payload name and description
</Typography>
<Typography component="div" className="mythic-create-section-description">
These values are used for the generated file and operator-facing description.
</Typography>
</div>
</div>
<div className="mythic-form">
<MythicTextField onEnter={finished} autoFocus={true} required={false} placeholder={"Filename"}
value={filename} multiline={false} onChange={onChangeFilename} display="inline-block"/>
<MythicTextField onEnter={finished} required={false} placeholder={"description"} value={description}
multiline={false} onChange={onChangeDescription} display="inline-block"/>
</div>
</div>
</section>
</div>
{/* Bottom section - scrollable table area */}
<div style={{
margin: "5px",
// border: "1px solid grey",
borderRadius: "5px",
//padding: "10px 5px 5px 10px",
display: "flex",
flexGrow: 1,
minHeight: 0, // Important for flex shrinking
overflow: "hidden"
}}>
<div style={{
width: "30%",
margin: "5px",
border: "1px solid grey",
borderRadius: "5px",
padding: "5px",
display: "flex",
flexDirection: "column",
flexGrow: 1,
minHeight: 0, // Important for flex shrinking
overflow: "auto"
}}>
<Typography textAlign="center" variant={"h7"} style={{fontWeight: 600, width: "100%"}}>
2. Build Parameter Configuration
<div className="mythic-create-builder-split mythic-create-builder-split-three">
<section className="mythic-create-section mythic-create-section-scroll">
<Typography component="div" className="mythic-create-section-title" style={{textAlign: "center"}}>
Build parameter configuration
<MythicStyledTooltip title={"Edit Build Parameters"}>
<IconButton className="mythic-table-row-icon-action mythic-table-row-icon-action-hover-info" size="small" onClick={() => props.moveToStep(1)}>
<DriveFileRenameOutlineIcon />
@@ -281,21 +258,10 @@ export function Step5Build(props){
</MythicStyledTooltip>
</Typography>
<ConfigurationSummary buildParameters={props.buildOptions[1].parameters} os={props.buildOptions[1].os} />
</div>
<div style={{
width: "30%",
margin: "5px",
border: "1px solid grey",
borderRadius: "5px",
padding: "5px",
display: "flex",
flexDirection: "column",
flexGrow: 1,
minHeight: 0, // Important for flex shrinking
overflow: "auto"
}}>
<Typography textAlign="center" variant={"h7"} style={{fontWeight: 600, width: "100%"}}>
3. Command Selection
</section>
<section className="mythic-create-section mythic-create-section-scroll">
<Typography component="div" className="mythic-create-section-title" style={{textAlign: "center"}}>
Command selection
<MythicStyledTooltip title={"Edit Commands"}>
<IconButton className="mythic-table-row-icon-action mythic-table-row-icon-action-hover-info" size="small" onClick={() => props.moveToStep(2)}>
<DriveFileRenameOutlineIcon />
@@ -313,21 +279,10 @@ export function Step5Build(props){
</div>
))}
</div>
<div style={{
width: "40%",
margin: "5px",
border: "1px solid grey",
borderRadius: "5px",
padding: "5px",
display: "flex",
flexDirection: "column",
flexGrow: 1,
minHeight: 0, // Important for flex shrinking
overflow: "auto"
}}>
<Typography textAlign="center" variant={"h7"} style={{fontWeight: 600, width: "100%"}}>
4. C2 Configuration
</section>
<section className="mythic-create-section mythic-create-section-scroll">
<Typography component="div" className="mythic-create-section-title" style={{textAlign: "center"}}>
C2 configuration
<MythicStyledTooltip title={"Edit C2 Parameters"}>
<IconButton className="mythic-table-row-icon-action mythic-table-row-icon-action-hover-info" size="small" onClick={() => props.moveToStep(3)}>
<DriveFileRenameOutlineIcon />
@@ -338,12 +293,11 @@ export function Step5Build(props){
<ConfigurationSummary key={c.name + index} buildParameters={c.c2profileparameters}
os={props.buildOptions[1].os} c2_name={c.name} />
))}
</div>
</section>
</div>
</div>
{/* Navigation buttons - always at bottom */}
<div style={{flexShrink: 0}}>
<div className="mythic-create-flow-footer">
<CreatePayloadNavigationButtons
first={props.first}
last={props.last}
@@ -1,30 +1,14 @@
import React from 'react';
import Table from '@mui/material/Table';
import TableBody from '@mui/material/TableBody';
import TableCell from '@mui/material/TableCell';
import TableContainer from '@mui/material/TableContainer';
import TableHead from '@mui/material/TableHead';
import TableRow from '@mui/material/TableRow';
import {CreatePayloadParameter} from '../CreatePayload/CreatePayloadParameter';
export function CreatePayloadBuildParametersTable(props){
return (
<TableContainer className="mythicElement">
<Table size="small" style={{"tableLayout": "fixed", "maxWidth": "calc(100vw)", "overflow": "scroll"}}>
<TableHead>
<TableRow>
<TableCell style={{width: "30%"}}>Build Parameter</TableCell>
<TableCell>Value</TableCell>
</TableRow>
</TableHead>
<TableBody>
{props.buildParameters.map( (op) => (
<CreatePayloadParameter key={"buildparamtablerow" + op.id} onChange={props.onChange} {...op} />
))}
</TableBody>
</Table>
</TableContainer>
<div className="mythic-create-parameter-list">
{props.buildParameters.map( (op) => (
<CreatePayloadParameter displayMode="card" key={"buildparamtablerow" + op.id} onChange={props.onChange} {...op} />
))}
</div>
);
}
@@ -7,8 +7,9 @@ import Typography from '@mui/material/Typography';
import {snackActions} from '../../utilities/Snackbar';
import {useMythicLazyQuery} from "../../utilities/useMythicLazyQuery";
import {getDefaultChoices, getDefaultValueForType, getSavedToType} from "../CreatePayload/Step2SelectPayloadType";
import { Backdrop, CircularProgress } from '@mui/material';
import { Backdrop } from '@mui/material';
import {MythicAgentSVGIcon} from "../../MythicComponents/MythicAgentSVGIcon";
import {MythicLoadingState} from "../../MythicComponents/MythicStateDisplay";
import {
ConfigureBuildParameters,
GetPayloads,
@@ -220,31 +221,23 @@ export function Step1SelectOS(props){
payloadConfigRef.current = payload
}
return (
<div style={{
height: "100%",
display: "flex",
flexDirection: "column"
}}>
{/* Content area that can grow */}
<div style={{
flexGrow: 1,
overflow: "hidden",
display: "flex",
flexDirection: "column",
minHeight: 0 // Important for flex shrinking
}}>
{/* Top section - fixed height */}
<div style={{
display: "flex",
flexShrink: 0 // Don't shrink this section
}}>
<div style={{width: "100%", margin: "5px", border: "1px solid grey", borderRadius: "5px", padding: "10px"}}>
<Typography variant={"p"} style={{fontWeight: 600}}>
1. Select Operating System
</Typography>
<div className="mythic-create-flow-shell">
<div className="mythic-create-flow-content">
<div className="mythic-create-selection-grid">
<section className="mythic-create-section">
<div className="mythic-create-section-header">
<div>
<Typography component="div" className="mythic-create-section-title">
Select operating system
</Typography>
<Typography component="div" className="mythic-create-section-description">
Filter wrapper types by the target platform.
</Typography>
</div>
</div>
<Select
className="mythic-create-select"
value={os}
style={{width: "100%"}}
disabled={!props.first}
onChange={onChangeOS}
>
@@ -254,57 +247,57 @@ export function Step1SelectOS(props){
))
}
</Select>
<Typography style={{fontWeight: 600}}>
Compatible Payload Types
</Typography>
{payloadtypesPerOS[os]?.join(", ")}
</div>
<div style={{width: "100%", margin: "5px", border: "1px solid grey", borderRadius: "5px", padding: "10px"}}>
<div style={{width: "100%", display: "flex", alignItems: "flex-start", marginBottom: "10px", flexDirection: "column"}}>
<Typography style={{fontWeight: 600}} variant={"p"}>
2. Select Payload Type
</Typography>
<Select
style={{width: "100%"}}
disabled={!props.first}
value={selectedPayloadType}
onChange={evt => setSelectedPayloadType(evt.target.value)}
>
{
payloadtypesPerOS[os]?.map((opt) => (
<MenuItem key={"step1" + opt} value={opt}>{opt}</MenuItem>
))
}
</Select>
<div>
<span className="mythic-create-meta-label">Compatible wrapper types</span>
<div className="mythic-create-meta-value">{payloadtypesPerOS[os]?.join(", ")}</div>
</div>
<div style={{display: "flex"}}>
<MythicAgentSVGIcon payload_type={selectedPayloadType} style={{width: "80px", padding: "5px", objectFit: "unset"}} />
<Typography variant="body2" component="p" style={{whiteSpace: "pre-wrap"}}>
<b>Version: </b>{payloadtypeData[selectedPayloadType]?.semver}<br/>
<b>Description: </b>{payloadtypeData[selectedPayloadType]?.note}<br/>
</Typography>
</section>
<section className="mythic-create-section">
<div className="mythic-create-section-header">
<div>
<Typography component="div" className="mythic-create-section-title">
Select wrapper type
</Typography>
<Typography component="div" className="mythic-create-section-description">
Choose the wrapper family to configure for this build.
</Typography>
</div>
</div>
</div>
<Select
className="mythic-create-select"
disabled={!props.first}
value={selectedPayloadType}
onChange={evt => setSelectedPayloadType(evt.target.value)}
>
{
payloadtypesPerOS[os]?.map((opt) => (
<MenuItem key={"step1" + opt} value={opt}>{opt}</MenuItem>
))
}
</Select>
<div className="mythic-create-agent-summary">
<div className="mythic-create-agent-icon">
<MythicAgentSVGIcon payload_type={selectedPayloadType} style={{width: "100%", height: "100%", objectFit: "contain"}} />
</div>
<div className="mythic-create-meta-list">
<div>
<span className="mythic-create-meta-label">Version</span>
<div className="mythic-create-meta-value">{payloadtypeData[selectedPayloadType]?.semver || "Unknown"}</div>
</div>
<div>
<span className="mythic-create-meta-label">Description</span>
<div className="mythic-create-meta-value">{payloadtypeData[selectedPayloadType]?.note || "No description available."}</div>
</div>
</div>
</div>
</section>
</div>
{/* Bottom section - scrollable table area */}
<div style={{
margin: "5px",
border: props.first ? "1px solid grey" : '',
borderRadius: "5px",
padding: "10px 5px 5px 10px",
display: "flex",
flexDirection: "column",
flexGrow: 1,
minHeight: 0, // Important for flex shrinking
overflow: "hidden"
}}>
{props.first &&
<div style={{flexGrow: 1, overflowY: "auto", position: "relative"}}>
<section className="mythic-create-section mythic-create-section-fill">
{props.first ? (
<div style={{display: "flex", flexDirection: "column", flexGrow: 1, minHeight: 0, overflow: "hidden", position: "relative"}}>
{openBackdrop &&
<Backdrop open={openBackdrop} onClick={()=>{setOpenBackdrop(false);}} style={{zIndex: 2000, position: "absolute"}}>
<CircularProgress color="inherit" disableShrink />
<MythicLoadingState compact title="Loading payloads" description="Fetching compatible payloads." sx={{color: "inherit"}} />
</Backdrop>
}
<StartFromExistingPayloadOrStartFresh first={props.first}
@@ -315,18 +308,15 @@ export function Step1SelectOS(props){
onStartFresh={onStartFresh}
/>
</div>
}
{!props.first &&
) : (
<ConfigureBuildParameters os={os} selectedPayloadType={selectedPayloadType}
prevData={props.prevData}
onUpdatePayloadConfig={onUpdatePayloadConfig} />
}
</div>
)}
</section>
</div>
{/* Navigation buttons - always at bottom */}
<div style={{flexShrink: 0}}>
<div className="mythic-create-flow-footer">
<CreatePayloadNavigationButtons
first={props.first}
last={props.last}
@@ -1,5 +1,4 @@
import React from 'react';
import { styled } from '@mui/material/styles';
import {useQuery, gql} from '@apollo/client';
import { CreatePayloadNavigationButtons} from './CreatePayloadNavigationButtons';
import Typography from '@mui/material/Typography';
@@ -7,7 +6,6 @@ import Button from '@mui/material/Button';
import { snackActions } from '../../utilities/Snackbar';
import Table from '@mui/material/Table';
import TableBody from '@mui/material/TableBody';
import TableCell from '@mui/material/TableCell';
import TableContainer from '@mui/material/TableContainer';
import TableHead from '@mui/material/TableHead';
import TableRow from '@mui/material/TableRow';
@@ -20,41 +18,14 @@ import InfoIconOutline from '@mui/icons-material/InfoOutlined';
import IconButton from '@mui/material/IconButton';
import {b64DecodeUnicode} from '../Callbacks/ResponseDisplay';
import {MythicAgentSVGIcon} from "../../MythicComponents/MythicAgentSVGIcon";
import { Backdrop, CircularProgress } from '@mui/material';
import { Backdrop } from '@mui/material';
import {PayloadsTableRowBuildStatus} from "../Payloads/PayloadsTableRowBuildStatus";
import {PayloadsTableRowBuildProgress} from "../Payloads/PayloadsTableRowBuildProgress";
import {MythicLoadingState, MythicTableEmptyState} from "../../MythicComponents/MythicStateDisplay";
import MythicStyledTableCell from "../../MythicComponents/MythicTableCell";
import {MythicClientSideTablePagination, useMythicClientPagination} from "../../MythicComponents/MythicTablePagination";
const PREFIX = 'Step3SelectPayload';
const classes = {
root: `${PREFIX}-root`,
paper: `${PREFIX}-paper`,
button: `${PREFIX}-button`
};
const Root = styled('div')((
{
theme
}
) => ({
[`& .${classes.root}`]: {
margin: 'auto',
width: "100%"
},
[`& .${classes.paper}`]: {
width: 200,
height: 230,
overflow: 'auto',
},
[`& .${classes.button}`]: {
margin: theme.spacing(0.5, 0),
}
}));
const GET_Payload_Types = gql`
query getWrappablePayloads($payloadType: Int!) {
payloadtype_by_pk(id: $payloadType) {
@@ -107,6 +78,7 @@ export function Step3SelectPayload(props){
setOpenBackdrop(false);
}else{
snackActions.warning("No supported payload for that wrapper");
setOpenBackdrop(false);
}
}
@@ -122,71 +94,66 @@ export function Step3SelectPayload(props){
props.canceled();
}
return (
<div style={{
height: "100%",
display: "flex",
flexDirection: "column"
}}>
{/* Content area that can grow */}
<div style={{
flexGrow: 1,
overflow: "hidden",
display: "flex",
flexDirection: "column",
minHeight: 0 // Important for flex shrinking
}}>
{/* Top section - fixed height */}
<div style={{
display: "flex",
flexShrink: 0 // Don't shrink this section
}}>
<div style={{display: "flex", width: "100%", margin: "5px", border: "1px solid grey", borderRadius: "5px", padding: "10px"}}>
<MythicAgentSVGIcon payload_type={props.buildOptions.payload_type} style={{width: "80px", padding: "5px", objectFit: "unset"}} />
<div className="mythic-create-flow-shell">
<div className="mythic-create-flow-content">
<div className="mythic-create-selection-grid">
<section className="mythic-create-section">
<div className="mythic-create-agent-summary">
<div className="mythic-create-agent-icon">
<MythicAgentSVGIcon payload_type={props.buildOptions.payload_type} style={{width: "100%", height: "100%", objectFit: "contain"}} />
</div>
<div className="mythic-create-meta-list">
<div>
<span className="mythic-create-meta-label">Operating system</span>
<div className="mythic-create-meta-value">{props.buildOptions.os}</div>
</div>
<div>
<span className="mythic-create-meta-label">Description</span>
<div className="mythic-create-meta-value">{props.buildOptions.description}</div>
</div>
</div>
</div>
</section>
<section className="mythic-create-section">
<div className="mythic-create-section-header">
<div>
<Typography component="div" className="mythic-create-section-title">
Select payload to include
</Typography>
<Typography component="div" className="mythic-create-section-description">
Pick the existing payload that this wrapper should embed.
</Typography>
</div>
</div>
</section>
</div>
<section className="mythic-create-section mythic-create-section-fill">
<div className="mythic-create-section-header">
<div>
<Typography variant={"p"} style={{}}>
<b>OS: </b>{props.buildOptions.os}
</Typography><br/>
<Typography variant="body2" component="p" style={{whiteSpace: "pre-wrap"}}>
<b>Description: </b>{props.buildOptions.description}
<Typography component="div" className="mythic-create-section-title">
Compatible payloads
</Typography>
<Typography component="div" className="mythic-create-section-description">
Only payloads matching the selected wrapper type and operating system are shown.
</Typography>
</div>
</div>
<div style={{width: "100%", margin: "5px", border: "1px solid grey", borderRadius: "5px", padding: "10px"}}>
<div style={{width: "100%", display: "flex", alignItems: "flex-start", marginBottom: "10px", flexDirection: "column"}}>
</div>
</div>
</div>
{/* Bottom section - scrollable table area */}
<div style={{
margin: "5px",
border: props.first ? "1px solid grey" : '',
borderRadius: "5px",
padding: "10px 5px 5px 10px",
display: "flex",
flexDirection: "column",
flexGrow: 1,
minHeight: 0, // Important for flex shrinking
overflow: "hidden"
}}>
<Typography variant={"p"} style={{fontWeight: 600}}>
1. Select Payload to Include
</Typography>
<div style={{flexGrow: 1, overflowY: "auto", position: "relative"}}>
<div style={{display: "flex", flexDirection: "column", flexGrow: 1, minHeight: 0, overflow: "hidden", position: "relative"}}>
{openBackdrop &&
<Backdrop open={openBackdrop} onClick={()=>{setOpenBackdrop(false);}} style={{zIndex: 2000, position: "absolute"}}>
<CircularProgress color="inherit" disableShrink />
<Backdrop open={openBackdrop} style={{zIndex: 2000, position: "absolute"}}>
<MythicLoadingState compact title="Loading payloads" description="Fetching compatible payloads." sx={{color: "inherit"}} />
</Backdrop>
}
<PayloadSelect payloadOptions={payloadOptions} first={props.first} last={props.last}
canceled={canceled} finished={finished}/>
{!openBackdrop &&
<PayloadSelect payloadOptions={payloadOptions} first={props.first} last={props.last}
canceled={canceled} finished={finished}/>
}
</div>
</div>
</section>
</div>
{/* Navigation buttons - always at bottom */}
<div style={{flexShrink: 0}}>
<div className="mythic-create-flow-footer">
<CreatePayloadNavigationButtons disableNext first={props.first} last={props.last}
canceled={props.canceled} finished={finished}/>
<br/><br/>
@@ -196,34 +163,61 @@ export function Step3SelectPayload(props){
}
export function PayloadSelect(props) {
const payloadOptions = React.useMemo(() => {
return Array.isArray(props.payloadOptions) ? props.payloadOptions : [];
}, [props.payloadOptions]);
const resetKey = React.useMemo(() => {
return payloadOptions.map((payload) => payload.id).join(",");
}, [payloadOptions]);
const pagination = useMythicClientPagination({
items: payloadOptions,
resetKey,
rowsPerPage: 25,
});
const finished = (payload) => {
props.finished(payload);
}
return (
<TableContainer className="mythicElement" style={{height: "100%", overflow: "auto"}}>
<Table stickyHeader size="small" style={{tableLayout:"fixed", maxWidth: "100%",}}>
<TableHead>
<TableRow>
<TableCell style={{width: "6rem"}}> Select</TableCell>
<TableCell style={{width: "15rem"}}>Timestamp</TableCell>
<TableCell>File</TableCell>
<TableCell>Status</TableCell>
<TableCell>Description</TableCell>
<TableCell style={{width: "5rem"}}>Details</TableCell>
</TableRow>
</TableHead>
<TableBody>
<div style={{display: "flex", flexDirection: "column", height: "100%", minHeight: 0}}>
<TableContainer
className="mythicElement mythic-fixed-row-table-wrap"
style={{flex: "1 1 auto", minHeight: 0, overflowY: "auto"}}
>
<Table stickyHeader size="small" style={{tableLayout:"fixed", height: "auto", maxWidth: "100%"}}>
<TableHead>
<TableRow>
<MythicStyledTableCell style={{width: "6rem"}}>Select</MythicStyledTableCell>
<MythicStyledTableCell style={{width: "15rem"}}>Timestamp</MythicStyledTableCell>
<MythicStyledTableCell>File</MythicStyledTableCell>
<MythicStyledTableCell>Status</MythicStyledTableCell>
<MythicStyledTableCell>Description</MythicStyledTableCell>
<MythicStyledTableCell style={{width: "5rem"}}>Details</MythicStyledTableCell>
</TableRow>
</TableHead>
<TableBody>
{props.payloadOptions?.map( (op) => (
<PayloadsTableRow
onSelected={finished}
key={"payload" + op.id}
payload={op}
/>
))}
</TableBody>
</Table>
</TableContainer>
{payloadOptions.length === 0 ? (
<MythicTableEmptyState
colSpan={6}
compact
title="No compatible payloads"
description="Start fresh or adjust the selected payload type and operating system."
minHeight={180}
/>
) : (
pagination.pageData.map( (op) => (
<PayloadsTableRow
onSelected={finished}
key={"payload" + op.id}
payload={op}
/>
))
)}
</TableBody>
</Table>
</TableContainer>
<MythicClientSideTablePagination id="create-payload-select-pagination" pagination={pagination} />
</div>
);
}
@@ -236,33 +230,35 @@ export function PayloadsTableRow(props){
}
return (
<React.Fragment>
<TableRow key={"payload" + props.payload.uuid} hover>
<TableCell>
<Button className="mythic-table-row-action mythic-table-row-action-hover-success" size="small" onClick={onSelected} variant="contained">Select</Button>
</TableCell>
<TableCell>{toLocalTime(props.payload.creation_time, me.user.view_utc_time)}</TableCell>
<TableCell>{b64DecodeUnicode(props.payload.filemetum.filename_text)}</TableCell>
<TableCell>
<TableRow key={"payload" + props.payload.uuid} hover onClick={onSelected} style={{cursor: "pointer"}}>
<MythicStyledTableCell>
<Button className="mythic-table-row-action mythic-table-row-action-hover-info" size="small" onClick={(event) => {event.stopPropagation(); onSelected();}} variant="contained">Select</Button>
</MythicStyledTableCell>
<MythicStyledTableCell>{toLocalTime(props.payload.creation_time, me.user.view_utc_time)}</MythicStyledTableCell>
<MythicStyledTableCell>{b64DecodeUnicode(props.payload.filemetum.filename_text)}</MythicStyledTableCell>
<MythicStyledTableCell>
<div className="mythic-payload-progress-cell">
<PayloadsTableRowBuildStatus {...props.payload} />
<PayloadsTableRowBuildProgress {...props.payload} />
</div>
</TableCell>
<TableCell>{props.payload.description}</TableCell>
<TableCell>
<IconButton className="mythic-table-row-icon-action mythic-table-row-icon-action-hover-info" size="small" onClick={() => setOpenDetailedView(true)}>
</MythicStyledTableCell>
<MythicStyledTableCell>{props.payload.description}</MythicStyledTableCell>
<MythicStyledTableCell>
<IconButton className="mythic-table-row-icon-action mythic-table-row-icon-action-hover-info" size="small" onClick={(event) => {event.stopPropagation(); setOpenDetailedView(true);}}>
<InfoIconOutline fontSize="small" />
</IconButton>
</TableCell>
</MythicStyledTableCell>
</TableRow>
<TableRow>
{openDetailedView ? (
<MythicDialog fullWidth={true} maxWidth="md" open={openDetailedView} me={me}
onClose={()=>{setOpenDetailedView(false);}}
innerDialog={<DetailedPayloadTable {...props.payload} me={me} payload_id={props.payload.id} onClose={()=>{setOpenDetailedView(false);}} />}
/>
<TableRow style={{display: "none"}}>
<MythicStyledTableCell colSpan={6}>
<MythicDialog fullWidth={true} maxWidth="md" open={openDetailedView} me={me}
onClose={()=>{setOpenDetailedView(false);}}
innerDialog={<DetailedPayloadTable {...props.payload} me={me} payload_id={props.payload.id} onClose={()=>{setOpenDetailedView(false);}} />}
/>
</MythicStyledTableCell>
</TableRow>
) : null }
</TableRow>
</React.Fragment>
)
}
@@ -158,78 +158,55 @@ export function Step5Build(props){
}
return (
<div style={{
height: "100%",
display: "flex",
flexDirection: "column"
}}>
{/* Content area that can grow */}
<div style={{
flexGrow: 1,
overflow: "hidden",
display: "flex",
flexDirection: "column",
minHeight: 0 // Important for flex shrinking
}}>
{/* Top section - fixed height */}
<div style={{
display: "flex",
flexShrink: 0 // Don't shrink this section
}}>
<div style={{width: "100%", margin: "5px", border: "1px solid grey", borderRadius: "5px", padding: "10px", display: "flex"}}>
<MythicAgentSVGIcon payload_type={props.buildOptions[1].payload_type} style={{width: "80px", padding: "5px", objectFit: "unset"}} />
<div>
<Typography variant={"p"} style={{}}>
<b>OS: </b>{props.buildOptions[1].os}
<div className="mythic-create-flow-shell">
<div className="mythic-create-flow-content">
<div className="mythic-create-selection-grid">
<section className="mythic-create-section">
<div className="mythic-create-agent-summary">
<div className="mythic-create-agent-icon">
<MythicAgentSVGIcon payload_type={props.buildOptions[1].payload_type} style={{width: "100%", height: "100%", objectFit: "contain"}} />
</div>
<div className="mythic-create-meta-list">
<div>
<span className="mythic-create-meta-label">Operating system</span>
<div className="mythic-create-meta-value">{props.buildOptions[1].os}</div>
</div>
<div>
<span className="mythic-create-meta-label">Description</span>
<div className="mythic-create-meta-value">{props.buildOptions[1].description}</div>
</div>
<MythicStyledTooltip title={"Edit OS / Payload Type"}>
<IconButton className="mythic-table-row-icon-action mythic-table-row-icon-action-hover-info" size="small" onClick={() => props.moveToStep(0)}>
<DriveFileRenameOutlineIcon />
</IconButton>
</MythicStyledTooltip>
</Typography><br/>
<Typography variant="body2" component="p" style={{whiteSpace: "pre-wrap"}}>
<b>Description: </b>{props.buildOptions[1].description}
</Typography>
</div>
</div>
</div>
<div style={{width: "100%", margin: "5px", border: "1px solid grey", borderRadius: "5px", padding: "10px"}}>
<Typography variant={"p"} style={{fontWeight: 600}}>
1. Provide Payload Name and Description
</Typography>
<div style={{width: "100%", display: "flex", alignItems: "flex-start", marginBottom: "10px", flexDirection: "column"}}>
</section>
<section className="mythic-create-section">
<div className="mythic-create-section-header">
<div>
<Typography component="div" className="mythic-create-section-title">
Wrapper name and description
</Typography>
<Typography component="div" className="mythic-create-section-description">
These values are used for the wrapper file and operator-facing description.
</Typography>
</div>
</div>
<div className="mythic-form">
<MythicTextField onEnter={finished} autoFocus={true} required={false} placeholder={"Filename"}
value={filename} multiline={false} onChange={onChangeFilename} display="inline-block"/>
<MythicTextField onEnter={finished} required={false} placeholder={"description"} value={description}
multiline={false} onChange={onChangeDescription} display="inline-block"/>
</div>
</div>
</section>
</div>
{/* Bottom section - scrollable table area */}
<div style={{
margin: "5px",
// border: "1px solid grey",
borderRadius: "5px",
//padding: "10px 5px 5px 10px",
display: "flex",
flexGrow: 1,
minHeight: 0, // Important for flex shrinking
overflow: "hidden"
}}>
<div style={{
width: "30%",
margin: "5px",
border: "1px solid grey",
borderRadius: "5px",
padding: "5px",
display: "flex",
flexDirection: "column",
flexGrow: 1,
minHeight: 0, // Important for flex shrinking
overflow: "auto"
}}>
<Typography textAlign="center" variant={"h7"} style={{fontWeight: 600, width: "100%"}}>
2. Build Parameter Configuration
<div className="mythic-create-builder-split">
<section className="mythic-create-section mythic-create-section-scroll">
<Typography component="div" className="mythic-create-section-title" style={{textAlign: "center"}}>
Build parameter configuration
<MythicStyledTooltip title={"Edit Build Parameters"}>
<IconButton className="mythic-table-row-icon-action mythic-table-row-icon-action-hover-info" size="small" onClick={() => props.moveToStep(1)}>
<DriveFileRenameOutlineIcon />
@@ -237,21 +214,10 @@ export function Step5Build(props){
</MythicStyledTooltip>
</Typography>
<ConfigurationSummary buildParameters={props.buildOptions[1].parameters} os={props.buildOptions[1].os} />
</div>
<div style={{
width: "70%",
margin: "5px",
border: "1px solid grey",
borderRadius: "5px",
padding: "5px",
display: "flex",
flexDirection: "column",
flexGrow: 1,
minHeight: 0, // Important for flex shrinking
overflow: "auto"
}}>
<Typography textAlign="center" variant={"h7"} style={{fontWeight: 600, width: "100%"}}>
3. Embedded Payload Configuration
</section>
<section className="mythic-create-section mythic-create-section-scroll">
<Typography component="div" className="mythic-create-section-title" style={{textAlign: "center"}}>
Embedded payload configuration
<MythicStyledTooltip title={"Edit Selected Payload"}>
<IconButton className="mythic-table-row-icon-action mythic-table-row-icon-action-hover-info" size="small" onClick={() => props.moveToStep(2)}>
<DriveFileRenameOutlineIcon />
@@ -274,13 +240,11 @@ export function Step5Build(props){
}}
/>
</div>
</div>
</section>
</div>
</div>
{/* Navigation buttons - always at bottom */}
<div style={{flexShrink: 0}}>
<div className="mythic-create-flow-footer">
<CreatePayloadNavigationButtons
first={props.first}
last={props.last}
File diff suppressed because it is too large Load Diff
@@ -17,6 +17,7 @@ import CheckCircleTwoToneIcon from '@mui/icons-material/CheckCircleTwoTone';
import CancelTwoToneIcon from '@mui/icons-material/CancelTwoTone';
import {snackActions} from "../../utilities/Snackbar";
import {MythicStatusChip} from "../../MythicComponents/MythicStatusChip";
import {MythicTableEmptyState} from "../../MythicComponents/MythicStateDisplay";
const updateApprovalStatusMutation = gql`
mutation updateApprovalStatus($eventgroupapproval_id: Int!, $approved: Boolean!) {
@@ -76,54 +77,63 @@ export function EventGroupTableRunAsDialog({eventgroupapprovals, me, onClose, se
</TableRow>
</TableHead>
<TableBody>
{eventgroupapprovals.map( e => (
<TableRow key={e.id}>
<TableCell>{e.operator.username}</TableCell>
<TableCell>
{e.approved ? (
<div className="mythic-table-row-actions">
<MythicStatusChip label="Approved" status="success" icon={<CheckCircleTwoToneIcon />} />
<Button className="mythic-table-row-action mythic-table-row-action-hover-warning" disabled={e.operator.id !== me?.user?.id} variant={"contained"}
startIcon={<CancelTwoToneIcon fontSize="small" />}
onClick={() => onApprovalClick({id: e.id, approved: false})}>
Deny
</Button>
</div>
{eventgroupapprovals.length === 0 ? (
<MythicTableEmptyState
colSpan={3}
compact
title="No approvals needed"
description="This workflow does not have pending operator approvals."
/>
) : (
eventgroupapprovals.map( e => (
<TableRow key={e.id}>
<TableCell>{e.operator.username}</TableCell>
<TableCell>
{e.approved ? (
<div className="mythic-table-row-actions">
<MythicStatusChip label="Approved" status="success" icon={<CheckCircleTwoToneIcon />} />
<Button className="mythic-table-row-action mythic-table-row-action-hover-warning" disabled={e.operator.id !== me?.user?.id} variant={"contained"}
startIcon={<CancelTwoToneIcon fontSize="small" />}
onClick={() => onApprovalClick({id: e.id, approved: false})}>
Deny
</Button>
</div>
) : e.created_at === e.updated_at ? (
<div className="mythic-table-row-actions">
<Button className="mythic-table-row-action mythic-table-row-action-hover-success" disabled={e.operator.id !== me?.user?.id}
variant={"contained"}
startIcon={<CheckCircleTwoToneIcon fontSize="small" />}
onClick={() => onApprovalClick({id: e.id, approved: true})}>
Approve
</Button>
<Button className="mythic-table-row-action mythic-table-row-action-hover-warning" disabled={e.operator.id !== me?.user?.id} variant={"contained"}
startIcon={<CancelTwoToneIcon fontSize="small" />}
onClick={() => onApprovalClick({id: e.id, approved: false})}>
Deny
</Button>
</div>
) : (
<div className="mythic-table-row-actions">
<Button className="mythic-table-row-action mythic-table-row-action-hover-success" variant={"contained"}
disabled={e.operator.id !== me?.user?.id}
startIcon={<CheckCircleTwoToneIcon fontSize="small" />}
onClick={() => onApprovalClick({id: e.id, approved: true})}>
Approve
</Button>
<MythicStatusChip label="Denied" status="warning" icon={<CancelTwoToneIcon />} />
) : e.created_at === e.updated_at ? (
<div className="mythic-table-row-actions">
<Button className="mythic-table-row-action mythic-table-row-action-hover-success" disabled={e.operator.id !== me?.user?.id}
variant={"contained"}
startIcon={<CheckCircleTwoToneIcon fontSize="small" />}
onClick={() => onApprovalClick({id: e.id, approved: true})}>
Approve
</Button>
<Button className="mythic-table-row-action mythic-table-row-action-hover-warning" disabled={e.operator.id !== me?.user?.id} variant={"contained"}
startIcon={<CancelTwoToneIcon fontSize="small" />}
onClick={() => onApprovalClick({id: e.id, approved: false})}>
Deny
</Button>
</div>
) : (
<div className="mythic-table-row-actions">
<Button className="mythic-table-row-action mythic-table-row-action-hover-success" variant={"contained"}
disabled={e.operator.id !== me?.user?.id}
startIcon={<CheckCircleTwoToneIcon fontSize="small" />}
onClick={() => onApprovalClick({id: e.id, approved: true})}>
Approve
</Button>
<MythicStatusChip label="Denied" status="warning" icon={<CancelTwoToneIcon />} />
</div>
)}
</TableCell>
<TableCell>
{e.created_at !== e.updated_at ? (
toLocalTime(e?.updated_at, me?.user?.view_utc_time)
) : null}
</TableCell>
</TableRow>
))}
</div>
)}
</TableCell>
<TableCell>
{e.created_at !== e.updated_at ? (
toLocalTime(e?.updated_at, me?.user?.view_utc_time)
) : null}
</TableCell>
</TableRow>
))
)}
</TableBody>
</Table>
</TableContainer>
@@ -24,6 +24,7 @@ import DeleteIcon from '@mui/icons-material/Delete';
import {IconButton, Link, Typography} from '@mui/material';
import {MythicConfirmDialog} from "../../MythicComponents/MythicConfirmDialog";
import CloudUploadIcon from '@mui/icons-material/CloudUpload';
import {MythicTableEmptyState} from "../../MythicComponents/MythicStateDisplay";
export function EventFileManageDialog({onClose, selectedEventGroup}) {
@@ -60,9 +61,18 @@ export function EventFileManageDialog({onClose, selectedEventGroup}) {
<TableContainer className="mythicElement">
<Table>
<TableBody>
{selectedEventGroup.filemeta.map( e => (
<EventFileManageDialogTableRow key={e.id} eventFile={e} />
))}
{selectedEventGroup.filemeta.length === 0 ? (
<MythicTableEmptyState
colSpan={1}
compact
title="No workflow files"
description="Uploaded files for this workflow will appear here."
/>
) : (
selectedEventGroup.filemeta.map( e => (
<EventFileManageDialogTableRow key={e.id} eventFile={e} />
))
)}
</TableBody>
</Table>
</TableContainer>
@@ -13,6 +13,7 @@ import TableRow from '@mui/material/TableRow';
import MythicStyledTableCell from "../../MythicComponents/MythicTableCell";
import {Typography} from '@mui/material';
import {useTheme} from '@mui/material/styles';
import {MythicTableEmptyState} from "../../MythicComponents/MythicStateDisplay";
export function EventGroupConsumingContainersDialog({onClose, selectedEventGroup}) {
@@ -34,9 +35,18 @@ export function EventGroupConsumingContainersDialog({onClose, selectedEventGroup
</TableRow>
</TableHead>
<TableBody>
{selectedEventGroup.eventgroupconsumingcontainers.map( e => (
<EventGroupConsumingContainersDialogTableRow key={e.id} container={e} />
))}
{selectedEventGroup.eventgroupconsumingcontainers.length === 0 ? (
<MythicTableEmptyState
colSpan={4}
compact
title="No container requirements"
description="This workflow does not declare required eventing containers."
/>
) : (
selectedEventGroup.eventgroupconsumingcontainers.map( e => (
<EventGroupConsumingContainersDialogTableRow key={e.id} container={e} />
))
)}
</TableBody>
</Table>
</TableContainer>
@@ -58,6 +58,7 @@ import DeleteIcon from '@mui/icons-material/Delete';
import {getStringSize} from "../Callbacks/ResponseDisplayTable";
import {MythicPageHeader} from "../../MythicComponents/MythicPageHeader";
import {MythicEmptyState, MythicErrorState, MythicLoadingState} from "../../MythicComponents/MythicStateDisplay";
import {MythicClientSideTablePagination, useMythicClientPagination} from "../../MythicComponents/MythicTablePagination";
const getEventSteps = gql`
@@ -1492,7 +1493,12 @@ function EventStepDetailDialog({selectedEventStep, onClose}) {
)
}
function EventDetailsCallbacksTable({callbacks, includeContext = false}){
const callbackCount = callbacks?.length || 0;
const callbackRows = callbacks || [];
const callbackCount = callbackRows.length;
const pagination = useMythicClientPagination({
items: callbackRows,
resetKey: includeContext ? "with-context" : "without-context",
});
return (
<EventingDetailSection collapsible title="Callbacks generated" count={callbackCount}>
{callbackCount === 0 ? (
@@ -1501,43 +1507,50 @@ function EventDetailsCallbacksTable({callbacks, includeContext = false}){
description="This workflow has not produced any callbacks yet."
/>
) : (
<TableContainer className="mythicElement mythic-eventing-detail-table-wrap">
<Table>
<TableHead>
<TableRow>
<MythicStyledTableCell>Callback</MythicStyledTableCell>
{includeContext &&
<MythicStyledTableCell>Context</MythicStyledTableCell>
}
</TableRow>
</TableHead>
<TableBody>
{callbacks.map(trackedData => (
<TableRow key={"callbacks" + trackedData.id} hover>
<MythicStyledTableCell>
<Link className="mythic-eventing-resource-link" color="textPrimary" underline="always" target="_blank"
href={"/new/callbacks/" + trackedData.display_id}>{trackedData.display_id}</Link>
</MythicStyledTableCell>
<>
<TableContainer className="mythicElement mythic-eventing-detail-table-wrap mythic-fixed-row-table-wrap">
<Table style={{height: "auto"}}>
<TableHead>
<TableRow>
<MythicStyledTableCell>Callback</MythicStyledTableCell>
{includeContext &&
<MythicStyledTableCell>
<span className="mythic-eventing-resource-secondary">
{trackedData.user || "unknown"} @ {trackedData.host || "unknown"}
{trackedData.pid ? ` (${trackedData.pid})` : ""}
{trackedData.process_name ? ` - ${trackedData.process_name}` : ""}
</span>
Context
</MythicStyledTableCell>
}
</TableRow>
))}
</TableBody>
</Table>
</TableContainer>
</TableHead>
<TableBody>
{pagination.pageData.map(trackedData => (
<TableRow key={"callbacks" + trackedData.id} hover>
<MythicStyledTableCell>
<Link className="mythic-eventing-resource-link" color="textPrimary" underline="always" target="_blank"
href={"/new/callbacks/" + trackedData.display_id}>{trackedData.display_id}</Link>
</MythicStyledTableCell>
{includeContext &&
<MythicStyledTableCell>
<span className="mythic-eventing-resource-secondary">
{trackedData.user || "unknown"} @ {trackedData.host || "unknown"}
{trackedData.pid ? ` (${trackedData.pid})` : ""}
{trackedData.process_name ? ` - ${trackedData.process_name}` : ""}
</span>
</MythicStyledTableCell>
}
</TableRow>
))}
</TableBody>
</Table>
</TableContainer>
<MythicClientSideTablePagination pagination={pagination} />
</>
)}
</EventingDetailSection>
)
}
function EventDetailsAPITokensTable({tokens}){
const tokenCount = tokens?.length || 0;
const tokenRows = tokens || [];
const tokenCount = tokenRows.length;
const pagination = useMythicClientPagination({items: tokenRows});
return (
<EventingDetailSection collapsible title="API tokens generated" count={tokenCount}>
{tokenCount === 0 ? (
@@ -1546,36 +1559,41 @@ function EventDetailsAPITokensTable({tokens}){
description="This workflow has not created any API tokens."
/>
) : (
<TableContainer className="mythicElement mythic-eventing-detail-table-wrap">
<Table>
<TableHead>
<TableRow>
<TableCell style={{width: "2rem"}}></TableCell>
<TableCell style={{width: "5rem"}}>Active</TableCell>
<TableCell style={{width: "12rem"}}>Created By</TableCell>
<TableCell style={{width: "7rem"}}>Token</TableCell>
<TableCell style={{width: "9rem"}}>Type</TableCell>
<TableCell>Name</TableCell>
<TableCell></TableCell>
</TableRow>
</TableHead>
<TableBody>
{tokens.map(trackedData => (
<APITokenRow key={"apitoken" + trackedData.id} {...trackedData}
onToggleActive={() => {}}
onDeleteAPIToken={() =>{}}
>
</APITokenRow>
))}
</TableBody>
</Table>
</TableContainer>
<>
<TableContainer className="mythicElement mythic-eventing-detail-table-wrap mythic-fixed-row-table-wrap">
<Table style={{height: "auto"}}>
<TableHead>
<TableRow>
<TableCell style={{width: "2rem"}}></TableCell>
<TableCell style={{width: "5rem"}}>Active</TableCell>
<TableCell style={{width: "12rem"}}>Created By</TableCell>
<TableCell style={{width: "7rem"}}>Token</TableCell>
<TableCell style={{width: "9rem"}}>Type</TableCell>
<TableCell>Name</TableCell>
<TableCell></TableCell>
</TableRow>
</TableHead>
<TableBody>
{pagination.pageData.map(trackedData => (
<APITokenRow key={"apitoken" + trackedData.id} {...trackedData}
onToggleActive={() => {}}
onDeleteAPIToken={() =>{}}
>
</APITokenRow>
))}
</TableBody>
</Table>
</TableContainer>
<MythicClientSideTablePagination pagination={pagination} />
</>
)}
</EventingDetailSection>
)
}
function EventDetailsPayloadsTable({payloads, deletePayload}){
const payloadCount = payloads?.length || 0;
const payloadRows = payloads || [];
const payloadCount = payloadRows.length;
const pagination = useMythicClientPagination({items: payloadRows});
const [openDetailedView, setOpenDetailedView] = React.useState(false);
const [openDelete, setOpenDeleteDialog] = React.useState(false);
const selectedPayloadRef = React.useRef({});
@@ -1598,9 +1616,9 @@ function EventDetailsPayloadsTable({payloads, deletePayload}){
const onDownloadBulkPayloads = () => {
snackActions.info("Zipping up files...");
let fileIds = [];
for(let i = 0; i < payloads.length; i++){
if(payloads[i].build_phase === "success" && !payloads[i].deleted){
fileIds.push(payloads[i].filemetum.agent_file_id);
for(let i = 0; i < payloadRows.length; i++){
if(payloadRows[i].build_phase === "success" && !payloadRows[i].deleted){
fileIds.push(payloadRows[i].filemetum.agent_file_id);
}
}
downloadBulk({variables:{files: fileIds}})
@@ -1647,8 +1665,9 @@ function EventDetailsPayloadsTable({payloads, deletePayload}){
description="This workflow has not generated any payloads."
/>
) : (
<TableContainer className="mythicElement mythic-eventing-detail-table-wrap">
<Table>
<>
<TableContainer className="mythicElement mythic-eventing-detail-table-wrap mythic-fixed-row-table-wrap">
<Table style={{height: "auto"}}>
<TableHead>
<TableRow>
<MythicStyledTableCell style={{width: "2rem"}}></MythicStyledTableCell>
@@ -1659,7 +1678,7 @@ function EventDetailsPayloadsTable({payloads, deletePayload}){
</TableRow>
</TableHead>
<TableBody>
{payloads.map(trackedData => (
{pagination.pageData.map(trackedData => (
<TableRow key={"payloads" + trackedData.id} hover>
<MythicStyledTableCell style={{width: "2rem"}}>
{!trackedData.deleted &&
@@ -1689,13 +1708,17 @@ function EventDetailsPayloadsTable({payloads, deletePayload}){
</TableBody>
</Table>
</TableContainer>
<MythicClientSideTablePagination pagination={pagination} />
</>
)}
</EventingDetailSection>
</>
)
}
function EventDetailsTaskTable({tasks}){
const taskCount = tasks?.length || 0;
const taskRows = tasks || [];
const taskCount = taskRows.length;
const pagination = useMythicClientPagination({items: taskRows});
return (
<EventingDetailSection collapsible title="Tasks issued" count={taskCount}>
{taskCount === 0 ? (
@@ -1704,51 +1727,56 @@ function EventDetailsTaskTable({tasks}){
description="This workflow has not issued any tasks."
/>
) : (
<TableContainer className="mythicElement mythic-eventing-detail-table-wrap">
<Table>
<TableHead>
<TableRow>
<MythicStyledTableCell style={{width: "4rem"}}>Type</MythicStyledTableCell>
<MythicStyledTableCell>Callback</MythicStyledTableCell>
<MythicStyledTableCell>Task</MythicStyledTableCell>
<MythicStyledTableCell>Command</MythicStyledTableCell>
<MythicStyledTableCell>Operator</MythicStyledTableCell>
</TableRow>
</TableHead>
<TableBody>
{tasks.map(trackedData => (
<TableRow key={"tasks" + trackedData.id} hover>
<MythicStyledTableCell style={{width: "4rem"}}>
{trackedData.callback?.payload?.payloadtype?.name &&
<MythicAgentSVGIcon payload_type={trackedData.callback.payload.payloadtype.name}
style={{height: "40px"}} />
}
</MythicStyledTableCell>
<MythicStyledTableCell>
<Link className="mythic-eventing-resource-link" color="textPrimary" underline="always" target="_blank"
href={"/new/callbacks/" + trackedData.callback.display_id}>{trackedData.callback.display_id}</Link>
</MythicStyledTableCell>
<MythicStyledTableCell>
<Link className="mythic-eventing-resource-link" color={"textPrimary"} underline={"always"} target={"_blank"}
href={"/new/task/" + trackedData.display_id}>{trackedData.display_id}</Link>
</MythicStyledTableCell>
<MythicStyledTableCell>
<span className="mythic-eventing-resource-command">{trackedData.command_name}</span> {trackedData.display_params}
</MythicStyledTableCell>
<MythicStyledTableCell>
{trackedData.operator.username}
</MythicStyledTableCell>
<>
<TableContainer className="mythicElement mythic-eventing-detail-table-wrap mythic-fixed-row-table-wrap">
<Table style={{height: "auto"}}>
<TableHead>
<TableRow>
<MythicStyledTableCell style={{width: "4rem"}}>Type</MythicStyledTableCell>
<MythicStyledTableCell>Callback</MythicStyledTableCell>
<MythicStyledTableCell>Task</MythicStyledTableCell>
<MythicStyledTableCell>Command</MythicStyledTableCell>
<MythicStyledTableCell>Operator</MythicStyledTableCell>
</TableRow>
))}
</TableBody>
</Table>
</TableContainer>
</TableHead>
<TableBody>
{pagination.pageData.map(trackedData => (
<TableRow key={"tasks" + trackedData.id} hover>
<MythicStyledTableCell style={{width: "4rem"}}>
{trackedData.callback?.payload?.payloadtype?.name &&
<MythicAgentSVGIcon payload_type={trackedData.callback.payload.payloadtype.name}
style={{height: "40px"}} />
}
</MythicStyledTableCell>
<MythicStyledTableCell>
<Link className="mythic-eventing-resource-link" color="textPrimary" underline="always" target="_blank"
href={"/new/callbacks/" + trackedData.callback.display_id}>{trackedData.callback.display_id}</Link>
</MythicStyledTableCell>
<MythicStyledTableCell>
<Link className="mythic-eventing-resource-link" color={"textPrimary"} underline={"always"} target={"_blank"}
href={"/new/task/" + trackedData.display_id}>{trackedData.display_id}</Link>
</MythicStyledTableCell>
<MythicStyledTableCell>
<span className="mythic-eventing-resource-command">{trackedData.command_name}</span> {trackedData.display_params}
</MythicStyledTableCell>
<MythicStyledTableCell>
{trackedData.operator.username}
</MythicStyledTableCell>
</TableRow>
))}
</TableBody>
</Table>
</TableContainer>
<MythicClientSideTablePagination pagination={pagination} />
</>
)}
</EventingDetailSection>
)
}
function EventDetailsFilesTable({files}){
const fileCount = files?.length || 0;
const fileRows = files || [];
const fileCount = fileRows.length;
const pagination = useMythicClientPagination({items: fileRows});
const [downloadBulk] = useMutation(downloadBulkQuery, {
onCompleted: (data) => {
snackActions.dismiss();
@@ -1768,9 +1796,9 @@ function EventDetailsFilesTable({files}){
const onDownloadBulkPayloads = () => {
snackActions.info("Zipping up files...");
let fileIds = [];
for(let i = 0; i < files.length; i++){
if( !files[i].deleted){
fileIds.push(files[i].agent_file_id);
for(let i = 0; i < fileRows.length; i++){
if( !fileRows[i].deleted){
fileIds.push(fileRows[i].agent_file_id);
}
}
downloadBulk({variables:{files: fileIds}})
@@ -1795,45 +1823,48 @@ function EventDetailsFilesTable({files}){
description="This workflow has not tracked any files."
/>
) : (
<TableContainer className="mythicElement mythic-eventing-detail-table-wrap">
<Table>
<TableHead>
<TableRow>
<MythicStyledTableCell>File name</MythicStyledTableCell>
<MythicStyledTableCell>Comment</MythicStyledTableCell>
<MythicStyledTableCell>Size</MythicStyledTableCell>
</TableRow>
</TableHead>
<TableBody>
{files.map(trackedData => (
<TableRow key={"filemeta" + trackedData.id} hover>
<MythicStyledTableCell>
{trackedData.deleted ? (
<Typography variant="body2" style={{wordBreak: "break-all"}}>
{b64DecodeUnicode(trackedData.full_remote_path_text) === "" ?
b64DecodeUnicode(trackedData.filename_text) :
b64DecodeUnicode(trackedData.full_remote_path_text)}
</Typography>
) : (
<Link className="mythic-eventing-resource-link" color="textPrimary" underline="always" href={"/direct/download/" + trackedData.agent_file_id}>
{b64DecodeUnicode(trackedData.full_remote_path_text) === "" ?
b64DecodeUnicode(trackedData.filename_text) :
b64DecodeUnicode(trackedData.full_remote_path_text)}
</Link>
)
}
</MythicStyledTableCell>
<MythicStyledTableCell>
{trackedData.comment}
</MythicStyledTableCell>
<MythicStyledTableCell>
{getStringSize({cellData: {"plaintext": trackedData.size}})}
</MythicStyledTableCell>
<>
<TableContainer className="mythicElement mythic-eventing-detail-table-wrap mythic-fixed-row-table-wrap">
<Table style={{height: "auto"}}>
<TableHead>
<TableRow>
<MythicStyledTableCell>File name</MythicStyledTableCell>
<MythicStyledTableCell>Comment</MythicStyledTableCell>
<MythicStyledTableCell>Size</MythicStyledTableCell>
</TableRow>
))}
</TableBody>
</Table>
</TableContainer>
</TableHead>
<TableBody>
{pagination.pageData.map(trackedData => (
<TableRow key={"filemeta" + trackedData.id} hover>
<MythicStyledTableCell>
{trackedData.deleted ? (
<Typography variant="body2" style={{wordBreak: "break-all"}}>
{b64DecodeUnicode(trackedData.full_remote_path_text) === "" ?
b64DecodeUnicode(trackedData.filename_text) :
b64DecodeUnicode(trackedData.full_remote_path_text)}
</Typography>
) : (
<Link className="mythic-eventing-resource-link" color="textPrimary" underline="always" href={"/direct/download/" + trackedData.agent_file_id}>
{b64DecodeUnicode(trackedData.full_remote_path_text) === "" ?
b64DecodeUnicode(trackedData.filename_text) :
b64DecodeUnicode(trackedData.full_remote_path_text)}
</Link>
)
}
</MythicStyledTableCell>
<MythicStyledTableCell>
{trackedData.comment}
</MythicStyledTableCell>
<MythicStyledTableCell>
{getStringSize({cellData: {"plaintext": trackedData.size}})}
</MythicStyledTableCell>
</TableRow>
))}
</TableBody>
</Table>
</TableContainer>
<MythicClientSideTablePagination pagination={pagination} />
</>
)}
</EventingDetailSection>
)
@@ -252,7 +252,9 @@ function CommandBlockListTableRow(props){
<TableRow hover>
<TableCell>
<IconButton className="mythic-table-row-icon-action mythic-table-row-icon-action-hover-danger" size="small" onClick={()=>{setOpenDeleteDialog(true);}}><DeleteIcon fontSize="small" /></IconButton>
<MythicConfirmDialog onClose={() => {setOpenDeleteDialog(false);}} onSubmit={onAcceptDelete} open={openDelete}/>
{openDelete &&
<MythicConfirmDialog onClose={() => {setOpenDeleteDialog(false);}} onSubmit={onAcceptDelete} open={openDelete}/>
}
</TableCell>
<TableCell>
<Button className="mythic-table-row-action" size="small" onClick={()=>{setOpenUpdateDialog(true);}} startIcon={<EditIcon/>} variant="outlined">Edit</Button>
@@ -4,6 +4,7 @@ import TableRow from '@mui/material/TableRow';
import DeleteIcon from '@mui/icons-material/Delete';
import Switch from '@mui/material/Switch';
import MythicStyledTableCell from "../../MythicComponents/MythicTableCell";
import {toLocalTime} from "../../utilities/Time";
export function APITokenRow(props){
return (
@@ -26,7 +27,16 @@ export function APITokenRow(props){
name="active"
/>
</MythicStyledTableCell>
<MythicStyledTableCell>{props.created_by_operator?.username}</MythicStyledTableCell>
<MythicStyledTableCell>
<Typography>
{props.created_by_operator?.username}
</Typography>
{props.creation_time &&
<Typography color="textSecondary" style={{fontSize: "0.75rem"}}>
Created at: {toLocalTime(props.creation_time, props.me?.user?.view_utc_time)}
</Typography>
}
</MythicStyledTableCell>
<MythicStyledTableCell>
{(props.scopes || []).join(", ")}
</MythicStyledTableCell>
@@ -1,5 +1,9 @@
import React from 'react';
import {Button, DialogContent, DialogTitle, Link, TextField} from '@mui/material';
import {Button, DialogContent, DialogTitle, TextField} from '@mui/material';
import Table from '@mui/material/Table';
import TableBody from '@mui/material/TableBody';
import TableContainer from '@mui/material/TableContainer';
import TableHead from '@mui/material/TableHead';
import TableRow from '@mui/material/TableRow';
import Switch from '@mui/material/Switch';
import Box from '@mui/material/Box';
@@ -33,7 +37,10 @@ import {gql, useMutation} from '@apollo/client';
import ContentCopyIcon from '@mui/icons-material/ContentCopy';
import {copyStringToClipboard} from "../../utilities/Clipboard";
import {useMythicLazyQuery} from "../../utilities/useMythicLazyQuery";
import {MythicDataGrid} from "../../MythicComponents/MythicDataGrid";
import {MythicTableEmptyState, MythicTableErrorState, MythicTableLoadingState} from "../../MythicComponents/MythicStateDisplay";
import {MythicClientSideTablePagination, useMythicClientPagination} from "../../MythicComponents/MythicTablePagination";
import {APITokenRow} from "./SettingsOperatorAPITokenRow";
import {MythicSectionHeader} from "../../MythicComponents/MythicPageHeader";
import {
MythicDialogBody,
MythicDialogButton,
@@ -118,12 +125,10 @@ export function SettingsOperatorTableRow(props){
const [openSecretsConfig, setOpenSecretsConfig] = React.useState(false);
const [showDeleted, setShowDeleted] = React.useState(false);
const [apiTokens, setAPITokens] = React.useState([]);
const queryAPITokensSuccess = (data) => {
setAPITokens(data.apitokens);
}
const queryAPITokens = useMythicLazyQuery(GetAPITokens, {
fetchPolicy: "no-cache"
});
const [apiTokensLoading, setAPITokensLoading] = React.useState(false);
const [apiTokensError, setAPITokensError] = React.useState("");
const apiTokenQueryOptions = React.useMemo(() => ({fetchPolicy: "no-cache"}), []);
const queryAPITokens = useMythicLazyQuery(GetAPITokens, apiTokenQueryOptions);
const [createAPIToken] = useMutation(createAPITokenMutation, {
onCompleted: (data) => {
if(data.createAPIToken.status === "success"){
@@ -218,11 +223,30 @@ export function SettingsOperatorTableRow(props){
toggleAPITokenActive({variables:{id, active}})
}
React.useEffect( () => {
let active = true;
if(open){
setAPITokensLoading(true);
setAPITokensError("");
queryAPITokens({variables: {operator_id: props.id}})
.then(({data}) => queryAPITokensSuccess(data)).catch(({data}) => console.log(data));
.then(({data}) => {
if(active){
setAPITokens(data.apitokens);
}
}).catch((error) => {
console.log(error);
if(active){
setAPITokensError("Unable to load API tokens for this operator.");
}
}).finally(() => {
if(active){
setAPITokensLoading(false);
}
});
}
}, [open]);
return () => {
active = false;
}
}, [open, props.id, queryAPITokens]);
return (
<React.Fragment>
<TableRow key={props.id}>
@@ -366,11 +390,11 @@ export function SettingsOperatorTableRow(props){
<MythicStyledTableCell style={{ paddingBottom: 0, paddingTop: 0 }} colSpan={10}>
<Collapse in={open} timeout="auto" unmountOnExit>
<Box margin={1}>
<div className="mythic-detail-section-header">
<Typography component="div" className="mythic-detail-section-title">
API Tokens
</Typography>
<div className="mythic-detail-section-actions">
<MythicSectionHeader
dense
title="API Tokens"
actions={
<>
{showDeleted ? (
<MythicStyledTooltip title={"Hide API Tokens"}>
<IconButton className="mythic-dialog-title-action" size="small" onClick={() => setShowDeleted(!showDeleted)}><VisibilityIcon fontSize="small" /></IconButton>
@@ -390,8 +414,9 @@ export function SettingsOperatorTableRow(props){
>
API Token
</Button>
</div>
</div>
</>
}
/>
{openNewAPIToken &&
<MythicDialog open={openNewAPIToken}
fullWidth={true}
@@ -411,7 +436,15 @@ export function SettingsOperatorTableRow(props){
/>}
/>
}
<APITokens me={me} apiTokens={apiTokens} onDeleteAPIToken={onDeleteAPIToken} onToggleActive={onToggleActive} showDeleted={showDeleted} />
<APITokens
error={apiTokensError}
loading={apiTokensLoading}
me={me}
apiTokens={apiTokens}
onDeleteAPIToken={onDeleteAPIToken}
onToggleActive={onToggleActive}
showDeleted={showDeleted}
/>
</Box>
</Collapse>
</MythicStyledTableCell>
@@ -420,98 +453,6 @@ export function SettingsOperatorTableRow(props){
</React.Fragment>
)
}
const columns = [
{ field: 'deleted',
headerName: 'Delete',
width: 60,
renderCell: (params) => (
params.row.deleted ? null : (
<IconButton className="mythic-table-row-icon-action mythic-table-row-icon-action-hover-danger" size="small" onClick={() => {params.row.onDeleteAPIToken(params.row.id)}}>
<DeleteIcon fontSize="small" />
</IconButton>
)
),
valueGetter: (value, row) => row.deleted
},
{
field: 'active',
headerName: 'Active',
width: 80,
valueGetter: (value, row) => row.active,
renderCell: (params) => (
<Switch
color={ "info"}
disabled={params.row.deleted}
checked={params.row.active}
onChange={() => {params.row.onToggleActive(params.row.id, !params.row.active)}}
inputProps={{ 'aria-label': 'info checkbox' }}
name="active"
/>
)
},
{
field: 'created_by',
headerName: 'Created By',
flex: 1,
valueGetter: (value, row) => row.created_by_operator?.username,
renderCell: (params) => (
<>
<Typography>
{params.row?.created_by_operator?.username}
</Typography>
<Typography style={{fontSize: params.row?.theme?.typography.pxToRem(12),}}>
Created at: {toLocalTime(params.row?.creation_time, params.row.me?.user?.view_utc_time )}
</Typography>
</>
)
},
{
field: 'scopes',
headerName: 'Scopes',
flex: 1,
valueGetter: (value, row) => (row.scopes || []).join(", "),
renderCell: (params) => (
<Typography>
{(params.row.scopes || []).join(", ")}
</Typography>
)
},
{
field: 'token_type',
headerName: 'Type',
width: 150,
},
{
field: 'name',
headerName: 'Name',
flex: 1,
},
{
field: 'eventstepinstance',
headerName: 'Eventing Usage',
flex: 1,
renderCell: (params) => (
params.row.eventstepinstance &&
<>
<Typography>
{params.row.eventstepinstance?.eventgroupinstance?.eventgroup?.name}{" / "}
<Link target={"_blank"} color="textPrimary" underline="always"
href={'/new/eventing?eventgroup=' +
params.row?.eventstepinstance?.eventgroupinstance?.eventgroup?.id +
"&eventgroupinstance=" + params.row?.eventstepinstance?.eventgroupinstance?.id
}>
{ params.row.eventstepinstance?.eventstep?.name}{" (" + params.row?.eventstepinstance?.eventgroupinstance?.id + ")"}
</Link>
</Typography>
</>
),
sortable: false,
valueGetter: (value, row) => {
return row.eventstepinstance?.eventgroupinstance?.eventgroup?.name
}
},
];
const APITokenValueDialog = ({tokenValue, onClose}) => {
const onCopyTokenValue = () => {
let success = copyStringToClipboard(tokenValue);
@@ -549,36 +490,76 @@ const APITokenValueDialog = ({tokenValue, onClose}) => {
</>
);
}
const APITokens = ({apiTokens, onDeleteAPIToken, onToggleActive, showDeleted, me}) => {
const theme = useTheme();
const [data, setData] = React.useState([]);
React.useEffect( () => {
setData(apiTokens.reduce( (prev, c) => {
if(showDeleted || (!showDeleted && !c.deleted)){
return [...prev, {...c, onDeleteAPIToken: onDeleteAPIToken, onToggleActive: onToggleActive,
me: me, theme: theme
}];
}
return [...prev];
}, []));
const APITokens = ({apiTokens, error, loading, onDeleteAPIToken, onToggleActive, showDeleted, me}) => {
const data = React.useMemo(() => {
return apiTokens
.filter((token) => showDeleted || !token.deleted)
.sort((left, right) => right.id - left.id);
}, [apiTokens, showDeleted]);
return (
<div style={{display: "flex", flexDirection: "column", width: "100%", height: "calc(30vh)", minHeight: 0}}>
<MythicDataGrid
rows={data}
columns={columns}
initialState={{
pagination: {
paginationModel: {
},
},
sorting: {
sortModel: [{ field: 'id', sort: 'desc' }],
},
}}
autoPageSize
/>
</div>
const pagination = useMythicClientPagination({
items: data,
resetKey: showDeleted ? "show-deleted" : "hide-deleted",
});
return (
<>
<TableContainer className="mythicElement mythic-dialog-table-wrap mythic-fixed-row-table-wrap" style={{height: "calc(30vh)", minHeight: "12rem", overflowY: "auto"}}>
<Table stickyHeader size="small" style={{height: "auto"}}>
<TableHead>
<TableRow>
<MythicStyledTableCell style={{width: "3rem"}} />
<MythicStyledTableCell style={{width: "5rem"}}>Active</MythicStyledTableCell>
<MythicStyledTableCell>Created By</MythicStyledTableCell>
<MythicStyledTableCell>Scopes</MythicStyledTableCell>
<MythicStyledTableCell style={{width: "9rem"}}>Type</MythicStyledTableCell>
<MythicStyledTableCell>Name</MythicStyledTableCell>
<MythicStyledTableCell>Eventing Usage</MythicStyledTableCell>
</TableRow>
</TableHead>
<TableBody>
{loading ? (
<MythicTableLoadingState
colSpan={7}
columns={7}
compact
minHeight={86}
rows={3}
title="Loading API tokens"
description="Fetching tokens for this operator."
/>
) : error ? (
<MythicTableErrorState
colSpan={7}
compact
minHeight={150}
title="Unable to load API tokens"
description={error}
/>
) : data.length === 0 ? (
<MythicTableEmptyState
colSpan={7}
compact
minHeight={160}
title="No API tokens"
description={showDeleted ? "This operator does not have any API tokens yet." : "This operator does not have any active API tokens."}
/>
) : (
pagination.pageData.map((token) => (
<APITokenRow
key={"api-token" + token.id}
{...token}
me={me}
onDeleteAPIToken={onDeleteAPIToken}
onToggleActive={onToggleActive}
/>
))
)}
</TableBody>
</Table>
</TableContainer>
{!loading && !error &&
<MythicClientSideTablePagination pagination={pagination} />
}
</>
)
}
+513 -89
View File
@@ -374,6 +374,12 @@ tspan {
display: flex;
flex-grow: 1;
}
.MuiTableContainer-root.mythic-fixed-row-table-wrap {
display: block !important;
}
.MuiTableContainer-root.mythic-fixed-row-table-wrap > .MuiTable-root {
height: auto !important;
}
// gets the footer of the table
.MuiPaper-root > .MuiBox-root > * {
margin: 0;
@@ -387,9 +393,6 @@ tspan {
background-color: ${(props) => props.theme.table?.selectedHierarchy || props.theme.selectedCallbackHierarchyColor + "CC"};
}
.MuiDataGrid-row.Mui-selected {
background-color: ${(props) => props.theme.table?.selected || props.theme.selectedCallbackColor + "CC"} !important;
}
.roundedBottomCorners {
border-bottom-left-radius: 4px;
border-bottom-right-radius: 4px;
@@ -1087,6 +1090,12 @@ tspan {
.mythic-form .MuiTextField-root {
margin: 0 !important;
}
.mythic-form-field-control .MuiFormControl-root:has(> .MuiInputLabel-root),
.mythic-form-field-control .MuiTextField-root:has(> .MuiInputLabel-root),
.mythic-form .MuiFormControl-root:has(> .MuiInputLabel-root),
.mythic-form .MuiTextField-root:has(> .MuiInputLabel-root) {
margin-top: 0.45rem !important;
}
.mythic-form-field-control .MuiInputBase-root,
.mythic-form .MuiInputBase-root {
min-height: 38px;
@@ -1115,6 +1124,357 @@ tspan {
.mythic-form-switch-control {
flex: 0 0 auto;
}
.mythic-create-flow-shell {
display: flex;
flex-direction: column;
gap: 0.75rem;
height: 100%;
min-height: 0;
}
.mythic-create-flow-content {
display: flex;
flex: 1 1 auto;
flex-direction: column;
gap: 0.75rem;
min-height: 0;
overflow: hidden;
}
.mythic-create-flow-footer {
flex: 0 0 auto;
}
.mythic-create-selection-grid {
display: grid;
flex: 0 0 auto;
gap: 0.75rem;
grid-template-columns: repeat(auto-fit, minmax(min(100%, 22rem), 1fr));
min-width: 0;
}
.mythic-create-builder-split {
display: grid;
flex: 1 1 auto;
gap: 0.75rem;
grid-template-columns: minmax(15rem, 0.34fr) minmax(0, 1fr);
min-height: 0;
overflow: hidden;
}
.mythic-create-builder-split-three {
grid-template-columns: minmax(14rem, 0.3fr) minmax(14rem, 0.3fr) minmax(0, 0.4fr);
}
.mythic-create-section {
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;
display: flex;
flex-direction: column;
gap: 0.65rem;
min-height: 0;
min-width: 0;
overflow: hidden;
padding: 0.75rem;
}
.mythic-create-section-fill {
flex: 1 1 auto;
}
.mythic-create-section-plain {
background-color: transparent;
border: 0;
padding: 0;
}
.mythic-create-section-scroll {
overflow: auto;
}
.mythic-create-section-header {
align-items: flex-start;
display: flex;
gap: 0.75rem;
justify-content: space-between;
min-width: 0;
}
.mythic-create-section-title {
color: ${(props) => props.theme.palette.text.primary};
font-size: 0.88rem;
font-weight: 800;
line-height: 1.25;
}
.mythic-create-section-description {
color: ${(props) => props.theme.palette.text.secondary};
font-size: 0.76rem;
line-height: 1.35;
margin-top: 0.12rem;
}
.mythic-create-section-actions {
align-items: center;
display: flex;
flex: 0 0 auto;
flex-wrap: wrap;
gap: 0.4rem;
}
.mythic-create-section .MuiFormControl-root,
.mythic-create-section .MuiTextField-root {
margin: 0 !important;
}
.mythic-create-section .MuiFormControl-root:has(> .MuiInputLabel-root),
.mythic-create-section .MuiTextField-root:has(> .MuiInputLabel-root) {
margin-top: 0.45rem !important;
}
.mythic-create-select {
width: 100%;
}
.mythic-create-agent-summary {
align-items: flex-start;
display: flex;
gap: 0.75rem;
min-width: 0;
}
.mythic-create-subsection {
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;
gap: 0.55rem;
min-height: 0;
min-width: 0;
padding: 0.65rem;
}
.mythic-create-subsection-fill {
flex: 1 1 auto;
overflow: hidden;
}
.mythic-create-subsection-scroll {
flex: 1 1 auto;
overflow: auto;
}
.mythic-create-agent-icon {
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;
display: flex;
flex: 0 0 4.75rem;
height: 4.75rem;
justify-content: center;
padding: 0.35rem;
}
.mythic-create-agent-icon svg,
.mythic-create-agent-icon img {
max-height: 100%;
max-width: 100%;
}
.mythic-create-meta-list {
display: flex;
flex-direction: column;
gap: 0.45rem;
min-width: 0;
}
.mythic-create-meta-label {
color: ${(props) => props.theme.palette.text.secondary};
display: block;
font-size: 0.72rem;
font-weight: 750;
line-height: 1.2;
}
.mythic-create-meta-value {
color: ${(props) => props.theme.palette.text.primary};
font-size: 0.82rem;
line-height: 1.35;
overflow-wrap: anywhere;
}
.mythic-create-parameter-scroll {
display: flex;
flex-direction: column;
gap: 0.75rem;
height: 100%;
min-height: 0;
overflow: auto;
width: 100%;
}
.mythic-create-parameter-group {
display: flex;
flex-direction: column;
gap: 0.55rem;
min-width: 0;
}
.mythic-create-parameter-group-header {
background-image: ${(props) => {
const headerTextColor = props.theme.pageHeaderText?.main || props.theme.palette.text.primary;
return `linear-gradient(90deg, ${alpha(props.theme.palette.primary.main, props.theme.palette.mode === "dark" ? 0.18 : 0.1)} 0%, ${alpha(headerTextColor, props.theme.palette.mode === "dark" ? 0.055 : 0.04)} 100%)`;
}};
border: 1px solid ${(props) => props.theme.table?.borderSoft || props.theme.borderColor};
border-radius: ${(props) => props.theme.shape.borderRadius}px;
color: ${(props) => props.theme.pageHeaderText?.main || props.theme.palette.text.primary};
font-size: 0.82rem;
font-weight: 800;
padding: 0.45rem 0.65rem;
}
.mythic-create-summary-group {
display: flex;
flex-direction: column;
gap: 0.35rem;
min-width: 0;
}
.mythic-create-summary-group-header {
background-image: ${(props) => {
const headerTextColor = props.theme.pageHeaderText?.main || props.theme.palette.text.primary;
return `linear-gradient(90deg, ${alpha(props.theme.palette.primary.main, props.theme.palette.mode === "dark" ? 0.18 : 0.1)} 0%, ${alpha(headerTextColor, props.theme.palette.mode === "dark" ? 0.055 : 0.04)} 100%)`;
}};
border: 1px solid ${(props) => props.theme.table?.borderSoft || props.theme.borderColor};
border-radius: ${(props) => props.theme.shape.borderRadius}px;
color: ${(props) => props.theme.palette.text.primary};
font-size: 0.82rem;
font-weight: 800;
line-height: 1.25;
padding: 0.45rem 0.65rem;
}
.mythic-create-summary-row {
border-bottom: 1px solid ${(props) => props.theme.table?.borderSoft || props.theme.borderColor};
padding: 0.45rem 0.1rem 0.5rem;
}
.mythic-create-summary-row:last-child {
border-bottom: 0;
}
.mythic-create-summary-name {
color: ${(props) => props.theme.palette.text.primary};
font-size: 0.78rem;
font-weight: 800;
line-height: 1.25;
}
.mythic-create-summary-value {
color: ${(props) => props.theme.palette.text.secondary};
font-size: 0.76rem;
line-height: 1.35;
margin-top: 0.18rem;
overflow-wrap: anywhere;
padding-left: 0.65rem;
white-space: pre-wrap;
}
.mythic-create-parameter-list {
display: flex;
flex-direction: column;
gap: 0.55rem;
min-width: 0;
}
.mythic-create-parameter-card {
background-color: ${(props) => props.theme.palette.background.paper};
border: 1px solid ${(props) => props.theme.table?.borderSoft || props.theme.borderColor};
border-left: 4px solid transparent;
border-radius: ${(props) => props.theme.shape.borderRadius}px;
display: grid;
gap: 0.75rem;
grid-template-columns: minmax(13rem, 0.32fr) minmax(0, 1fr);
min-width: 0;
padding: 0.7rem 0.75rem;
}
.mythic-create-parameter-card-modified {
border-left-color: ${(props) => props.theme.palette.warning.main};
}
.mythic-create-parameter-copy {
min-width: 0;
}
.mythic-create-parameter-title-row {
align-items: flex-start;
display: flex;
flex-wrap: wrap;
gap: 0.45rem;
justify-content: space-between;
min-width: 0;
}
.mythic-create-parameter-title {
color: ${(props) => props.theme.palette.text.primary};
font-size: 0.84rem;
font-weight: 800;
line-height: 1.25;
min-width: 0;
}
.mythic-create-parameter-description {
color: ${(props) => props.theme.palette.text.secondary};
font-size: 0.75rem;
line-height: 1.38;
margin-top: 0.24rem;
}
.mythic-create-parameter-chips {
align-items: center;
display: flex;
flex-wrap: wrap;
gap: 0.3rem;
margin-top: 0.35rem;
}
.mythic-create-parameter-chip {
background-color: ${(props) => props.theme.palette.mode === "dark" ? "rgba(255,255,255,0.055)" : "rgba(0,0,0,0.035)"};
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};
font-size: 0.68rem;
font-weight: 800;
line-height: 1;
padding: 0.22rem 0.38rem;
}
.mythic-create-parameter-chip-required {
border-color: ${(props) => alpha(props.theme.palette.error.main, props.theme.palette.mode === "dark" ? 0.5 : 0.35)};
color: ${(props) => props.theme.palette.error.main};
}
.mythic-create-parameter-chip-modified {
border-color: ${(props) => alpha(props.theme.palette.warning.main, props.theme.palette.mode === "dark" ? 0.56 : 0.4)};
color: ${(props) => props.theme.palette.warning.main};
}
.mythic-create-parameter-control {
min-width: 0;
position: relative;
}
.mythic-create-parameter-control .MuiFormControl-root,
.mythic-create-parameter-control .MuiTextField-root,
.mythic-create-parameter-control .MythicTextField-root {
margin: 0 !important;
width: 100%;
}
.mythic-create-parameter-control .MuiFormControl-root:has(> .MuiInputLabel-root),
.mythic-create-parameter-control .MuiTextField-root:has(> .MuiInputLabel-root) {
margin-top: 0.45rem !important;
}
.mythic-create-inline-control {
align-items: center;
display: flex;
gap: 0.5rem;
min-width: 0;
position: relative;
width: 100%;
}
.mythic-create-inline-control > .MuiFormControl-root {
flex: 1 1 auto;
min-width: 0;
}
.mythic-create-choice-divider {
color: ${(props) => props.theme.palette.text.secondary};
flex: 0 0 auto;
font-size: 0.7rem;
font-weight: 800;
}
.mythic-create-array-table.MuiTableContainer-root {
border-color: ${(props) => props.theme.table?.borderSoft || props.theme.borderColor};
}
.mythic-create-array-table .MuiTableCell-root {
padding: 0.35rem 0.45rem !important;
}
.mythic-create-dictionary-row {
align-items: center;
display: grid;
gap: 0.5rem;
grid-template-columns: 2rem minmax(8rem, 0.28fr) minmax(0, 1fr);
margin-bottom: 0.45rem;
min-width: 0;
}
.mythic-create-dictionary-add {
align-items: center;
display: flex;
gap: 0.5rem;
}
@media (max-width: 900px) {
.mythic-create-builder-split,
.mythic-create-builder-split-three,
.mythic-create-parameter-card {
grid-template-columns: 1fr;
}
}
.mythic-dashboard-content {
display: flex;
flex-direction: column;
@@ -1440,6 +1800,10 @@ tspan {
.mythic-dialog-body .MuiTextField-root {
margin: 0 !important;
}
.mythic-dialog-body .MuiFormControl-root:has(> .MuiInputLabel-root),
.mythic-dialog-body .MuiTextField-root:has(> .MuiInputLabel-root) {
margin-top: 0.45rem !important;
}
.mythic-table-bulk-actions {
align-items: center;
border-bottom: 1px solid ${(props) => props.theme.table?.borderSoft || props.theme.borderColor};
@@ -1902,6 +2266,10 @@ tspan {
min-height: 0;
min-width: 0;
}
.mythic-eventing-wizard .MuiFormControl-root:has(> .MuiInputLabel-root),
.mythic-eventing-wizard .MuiTextField-root:has(> .MuiInputLabel-root) {
margin-top: 0.45rem !important;
}
.mythic-eventing-wizard-header {
border-bottom: 1px solid ${(props) => props.theme.table?.borderSoft || props.theme.borderColor};
padding: 0.75rem 1rem;
@@ -2093,43 +2461,14 @@ tspan {
min-height: 1.65rem;
padding: 0 0.55rem;
}
.mythic-eventing-metadata-trigger-table {
border-collapse: separate !important;
border-spacing: 0 0.4rem !important;
table-layout: fixed;
width: 100%;
.mythic-eventing-trigger-parameter-list {
display: flex;
flex-direction: column;
gap: 0.55rem;
min-width: 0;
}
.mythic-eventing-metadata-trigger-table > .MuiTableHead-root {
display: none;
}
.mythic-eventing-metadata-trigger-table > .MuiTableBody-root > .MuiTableRow-root > .MuiTableCell-root {
border-bottom: 1px solid ${(props) => props.theme.table?.borderSoft || props.theme.borderColor} !important;
border-top: 1px solid ${(props) => props.theme.table?.borderSoft || props.theme.borderColor} !important;
padding: 0.5rem 0.6rem !important;
}
.mythic-eventing-metadata-trigger-table > .MuiTableBody-root > .MuiTableRow-root > .MuiTableCell-root:first-of-type {
border-left: 1px solid ${(props) => props.theme.table?.borderSoft || props.theme.borderColor} !important;
border-radius: ${(props) => props.theme.shape.borderRadius}px 0 0 ${(props) => props.theme.shape.borderRadius}px;
color: ${(props) => props.theme.palette.text.secondary};
font-size: 0.72rem;
font-weight: 850;
vertical-align: top;
width: 38%;
}
.mythic-eventing-metadata-trigger-table > .MuiTableBody-root > .MuiTableRow-root > .MuiTableCell-root:last-of-type {
border-radius: 0 ${(props) => props.theme.shape.borderRadius}px ${(props) => props.theme.shape.borderRadius}px 0;
border-right: 1px solid ${(props) => props.theme.table?.borderSoft || props.theme.borderColor} !important;
width: 62%;
}
.mythic-eventing-metadata-trigger-table .MuiTable-root:not(.mythic-eventing-metadata-trigger-table) .MuiTableCell-root {
vertical-align: middle;
}
.mythic-eventing-metadata-trigger-table .MuiTable-root:not(.mythic-eventing-metadata-trigger-table) .MuiTableCell-root:first-of-type {
padding-right: 0.4rem !important;
width: 2.5rem !important;
}
.mythic-eventing-metadata-trigger-table .MuiTable-root:not(.mythic-eventing-metadata-trigger-table) .MuiTableCell-root:first-of-type + .MuiTableCell-root {
padding-left: 0.4rem !important;
.mythic-eventing-trigger-parameter-list .mythic-create-parameter-card {
grid-template-columns: minmax(14rem, 0.36fr) minmax(0, 1fr);
}
.mythic-eventing-choice-row {
align-items: flex-start;
@@ -2137,6 +2476,15 @@ tspan {
gap: 0.5rem;
grid-template-columns: minmax(0, 1fr) auto minmax(0, 1fr);
}
.mythic-eventing-choice-row > .MuiFormControl-root,
.mythic-eventing-choice-row > .mythic-eventing-choice-custom {
margin-top: 0.45rem;
}
.mythic-eventing-choice-row > .MuiFormControl-root .MuiTextField-root,
.mythic-eventing-choice-row > .mythic-eventing-choice-custom .MuiFormControl-root,
.mythic-eventing-choice-row > .mythic-eventing-choice-custom .MuiTextField-root {
margin-top: 0 !important;
}
.mythic-eventing-choice-separator {
align-items: center;
align-self: center;
@@ -2406,7 +2754,7 @@ tspan {
overflow: hidden;
}
.mythic-eventing-step-config-card-modern {
background-color: ${(props) => props.theme.palette.background.paper};
background-color: ${(props) => props.theme.surfaces?.muted || props.theme.palette.background.default};
}
.mythic-eventing-step-config-summary {
align-items: flex-start;
@@ -2490,14 +2838,18 @@ tspan {
padding: 0.75rem;
}
.mythic-eventing-step-config-section {
background-color: ${(props) => props.theme.palette.mode === "dark" ? "rgba(255,255,255,0.018)" : "rgba(0,0,0,0.01)"};
background-color: ${(props) => props.theme.surfaces?.raised || 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 1px 2px ${(props) => alpha(props.theme.palette.common.black, props.theme.palette.mode === "dark" ? 0.18 : 0.06)};
min-width: 0;
overflow: hidden;
}
.mythic-eventing-step-config-section-header {
background-color: ${(props) => props.theme.palette.mode === "dark" ? "rgba(255,255,255,0.025)" : "rgba(0,0,0,0.012)"};
background-image: ${(props) => {
const headerTextColor = props.theme.pageHeaderText?.main || props.theme.palette.text.primary;
return `linear-gradient(90deg, ${alpha(props.theme.palette.primary.main, props.theme.palette.mode === "dark" ? 0.12 : 0.07)} 0%, ${alpha(headerTextColor, props.theme.palette.mode === "dark" ? 0.045 : 0.035)} 100%)`;
}};
border-bottom: 1px solid ${(props) => props.theme.table?.borderSoft || props.theme.borderColor};
padding: 0.62rem 0.7rem 0.55rem;
}
@@ -2517,13 +2869,18 @@ tspan {
.mythic-eventing-step-config-section-body {
padding: 0.7rem;
}
.mythic-eventing-step-section-grid,
.mythic-eventing-step-field-grid {
display: grid;
gap: 0.75rem;
grid-template-columns: repeat(2, minmax(0, 1fr));
min-width: 0;
}
.mythic-eventing-step-section-stack {
display: flex;
flex-direction: column;
gap: 0.75rem;
min-width: 0;
}
.mythic-eventing-step-config-section-wide {
width: 100%;
}
@@ -2581,7 +2938,7 @@ tspan {
}
.mythic-eventing-step-list-item {
align-items: flex-start;
background-color: ${(props) => props.theme.palette.mode === "dark" ? "rgba(255,255,255,0.02)" : "rgba(0,0,0,0.012)"};
background-color: ${(props) => props.theme.palette.mode === "dark" ? "rgba(255,255,255,0.026)" : "rgba(0,0,0,0.018)"};
border: 1px solid ${(props) => props.theme.table?.borderSoft || props.theme.borderColor};
border-radius: ${(props) => props.theme.shape.borderRadius}px;
display: flex;
@@ -2590,32 +2947,59 @@ tspan {
padding: 0.5rem;
width: 100%;
}
.mythic-eventing-step-list-item-editable {
display: block;
}
.mythic-eventing-step-list-item > .MuiIconButton-root {
flex: 0 0 auto;
margin-top: 0.15rem;
margin-top: 0.45rem;
}
.mythic-eventing-step-list-content {
flex: 1 1 auto;
min-width: 0;
}
.mythic-eventing-step-input-grid {
align-items: center;
display: grid;
gap: 0.5rem;
grid-template-columns: minmax(9rem, 0.75fr) minmax(10rem, 0.8fr) minmax(14rem, 1.45fr);
grid-template-columns: 2rem minmax(9rem, 0.75fr) minmax(10rem, 0.8fr) minmax(14rem, 1.45fr);
min-width: 0;
}
.mythic-eventing-step-output-grid {
align-items: center;
display: grid;
gap: 0.5rem;
grid-template-columns: minmax(9rem, 0.75fr) minmax(14rem, 1.5fr);
grid-template-columns: 2rem minmax(9rem, 0.75fr) minmax(14rem, 1.5fr);
min-width: 0;
}
.mythic-eventing-step-list-item-editable .mythic-eventing-step-row-action {
align-self: center;
justify-self: center;
margin-top: 0.45rem !important;
}
.mythic-eventing-step-input-grid > :not(.mythic-eventing-step-row-action):not(.mythic-eventing-step-helper-text),
.mythic-eventing-step-output-grid > :not(.mythic-eventing-step-row-action):not(.mythic-eventing-step-helper-text) {
margin-top: 0.45rem;
}
.mythic-eventing-step-input-grid > :not(.mythic-eventing-step-row-action):not(.mythic-eventing-step-helper-text) .MuiFormControl-root,
.mythic-eventing-step-input-grid > :not(.mythic-eventing-step-row-action):not(.mythic-eventing-step-helper-text) .MuiTextField-root,
.mythic-eventing-step-output-grid > :not(.mythic-eventing-step-row-action):not(.mythic-eventing-step-helper-text) .MuiFormControl-root,
.mythic-eventing-step-output-grid > :not(.mythic-eventing-step-row-action):not(.mythic-eventing-step-helper-text) .MuiTextField-root {
margin-top: 0 !important;
}
.mythic-eventing-step-list-item-editable .mythic-eventing-choice-row {
align-items: center;
}
.mythic-eventing-step-list-item-editable .mythic-eventing-choice-separator {
align-self: center;
}
.mythic-eventing-step-helper-text {
color: ${(props) => props.theme.palette.text.secondary};
font-size: 0.68rem;
font-weight: 600;
grid-column: 2 / -1;
line-height: 1.35;
margin-top: 0.4rem;
margin-top: -0.05rem;
}
.mythic-eventing-step-empty-inline {
align-items: center;
@@ -2632,7 +3016,7 @@ tspan {
.mythic-eventing-step-action-data {
min-width: 0;
}
.mythic-eventing-step-action-data > .MuiTypography-root {
.mythic-eventing-step-help-text {
background-color: ${(props) => props.theme.palette.info.main + "10"};
border: 1px solid ${(props) => props.theme.palette.info.main + "2f"};
border-radius: ${(props) => props.theme.shape.borderRadius}px;
@@ -2643,47 +3027,76 @@ tspan {
margin: 0 0 0.65rem;
padding: 0.55rem 0.65rem;
}
.mythic-eventing-step-action-data > .MuiTable-root {
border-collapse: separate !important;
border-spacing: 0 0.5rem !important;
table-layout: fixed;
width: 100%;
.mythic-eventing-action-data-list {
display: flex;
flex-direction: column;
gap: 0.55rem;
min-width: 0;
}
.mythic-eventing-step-action-data > .MuiTable-root > .MuiTableHead-root {
display: none;
.mythic-eventing-action-data-card {
background-color: ${(props) => props.theme.palette.background.paper};
border: 1px solid ${(props) => props.theme.table?.borderSoft || props.theme.borderColor};
border-left: 4px solid transparent;
border-radius: ${(props) => props.theme.shape.borderRadius}px;
display: grid;
gap: 0.75rem;
grid-template-columns: minmax(13rem, 0.34fr) minmax(0, 1fr);
min-width: 0;
padding: 0.7rem 0.75rem;
}
.mythic-eventing-step-action-data > .MuiTable-root > .MuiTableBody-root > .MuiTableRow-root {
background-color: transparent;
.mythic-eventing-action-data-copy {
min-width: 0;
}
.mythic-eventing-step-action-data > .MuiTable-root > .MuiTableBody-root > .MuiTableRow-root > .MuiTableCell-root {
background-color: ${(props) => props.theme.palette.mode === "dark" ? "rgba(255,255,255,0.018)" : "rgba(0,0,0,0.01)"};
border-bottom: 1px solid ${(props) => props.theme.table?.borderSoft || props.theme.borderColor} !important;
border-top: 1px solid ${(props) => props.theme.table?.borderSoft || props.theme.borderColor} !important;
padding: 0.62rem 0.7rem !important;
vertical-align: top;
.mythic-eventing-action-data-title-row {
align-items: center;
display: flex;
flex-wrap: wrap;
gap: 0.4rem;
}
.mythic-eventing-step-action-data > .MuiTable-root > .MuiTableBody-root > .MuiTableRow-root > .MuiTableCell-root:first-of-type {
border-left: 1px solid ${(props) => props.theme.table?.borderSoft || props.theme.borderColor} !important;
border-radius: ${(props) => props.theme.shape.borderRadius}px 0 0 ${(props) => props.theme.shape.borderRadius}px;
width: 34%;
}
.mythic-eventing-step-action-data > .MuiTable-root > .MuiTableBody-root > .MuiTableRow-root > .MuiTableCell-root:last-of-type {
border-radius: 0 ${(props) => props.theme.shape.borderRadius}px ${(props) => props.theme.shape.borderRadius}px 0;
border-right: 1px solid ${(props) => props.theme.table?.borderSoft || props.theme.borderColor} !important;
width: 66%;
}
.mythic-eventing-step-action-data > .MuiTable-root > .MuiTableBody-root > .MuiTableRow-root > .MuiTableCell-root:first-of-type .MuiTypography-root:first-of-type {
.mythic-eventing-action-data-title {
color: ${(props) => props.theme.palette.text.primary};
font-size: 0.76rem;
font-size: 0.78rem;
font-weight: 850 !important;
line-height: 1.25;
}
.mythic-eventing-step-action-data > .MuiTable-root > .MuiTableBody-root > .MuiTableRow-root > .MuiTableCell-root:first-of-type .MuiTypography-root:not(:first-of-type) {
.mythic-eventing-action-data-chip {
background-color: ${(props) => props.theme.palette.warning.main + "16"};
border: 1px solid ${(props) => props.theme.palette.warning.main + "45"};
border-radius: ${(props) => props.theme.shape.borderRadius}px;
color: ${(props) => props.theme.palette.warning.main};
display: inline-flex;
font-size: 0.64rem;
font-weight: 850;
line-height: 1;
min-height: 1.2rem;
padding: 0 0.35rem;
}
.mythic-eventing-action-data-description {
color: ${(props) => props.theme.palette.text.secondary};
font-size: 0.7rem;
font-weight: 600;
line-height: 1.35;
margin-top: 0.2rem;
margin-top: 0.28rem;
}
.mythic-eventing-action-data-control {
min-width: 0;
}
.mythic-eventing-action-data-control .MuiFormControl-root,
.mythic-eventing-action-data-control .MuiTextField-root {
width: 100%;
}
.mythic-eventing-action-array-list {
display: flex;
flex-direction: column;
gap: 0.45rem;
min-width: 0;
}
.mythic-eventing-action-array-row {
align-items: center;
display: grid;
gap: 0.5rem;
grid-template-columns: 2rem minmax(0, 1fr);
min-width: 0;
}
.mythic-eventing-step-config-card .mythic-eventing-wizard-table {
border-spacing: 0 !important;
@@ -2697,11 +3110,14 @@ tspan {
border-right: 0 !important;
border-top: 0 !important;
}
@media (max-width: 1200px) {
.mythic-eventing-step-input-grid {
grid-template-columns: 1fr;
}
}
@media (max-width: 1200px) {
.mythic-eventing-step-input-grid {
grid-template-columns: 2rem minmax(0, 1fr);
}
.mythic-eventing-step-input-grid > :not(.mythic-eventing-step-row-action):not(.mythic-eventing-step-helper-text) {
grid-column: 2;
}
}
@media (max-width: 900px) {
.mythic-eventing-step-config-summary,
.mythic-eventing-step-config-summary-actions {
@@ -2711,12 +3127,20 @@ tspan {
.mythic-eventing-step-switch-row {
width: 100%;
}
.mythic-eventing-step-section-grid,
.mythic-eventing-step-field-grid,
.mythic-eventing-step-output-grid {
grid-template-columns: 1fr;
}
}
.mythic-eventing-step-field-grid {
grid-template-columns: 1fr;
}
.mythic-eventing-step-output-grid {
grid-template-columns: 2rem minmax(0, 1fr);
}
.mythic-eventing-step-output-grid > :not(.mythic-eventing-step-row-action):not(.mythic-eventing-step-helper-text) {
grid-column: 2;
}
.mythic-eventing-action-data-card,
.mythic-eventing-trigger-parameter-list .mythic-create-parameter-card {
grid-template-columns: 1fr;
}
}
.mythic-eventing-wizard-empty {
color: ${(props) => props.theme.palette.text.secondary};
font-size: 0.82rem;