major updates of the search, eventing, and payloads pages

This commit is contained in:
its-a-feature
2026-05-05 09:23:50 -05:00
parent 78e7aeff0a
commit 43dfec70fe
135 changed files with 10685 additions and 5713 deletions
+155 -4
View File
@@ -238,6 +238,16 @@ const getModernThemeAdditions = (themeMode, preferences = operatorSettingDefault
},
},
},
MuiDialogContentText: {
styleOverrides: {
root: {
color: textSecondary,
fontSize: "0.88rem",
lineHeight: 1.45,
margin: "0 0 10px",
},
},
},
MuiDialogActions: {
styleOverrides: {
root: {
@@ -294,6 +304,28 @@ const getModernThemeAdditions = (themeMode, preferences = operatorSettingDefault
},
},
},
MuiFormControlLabel: {
styleOverrides: {
root: {
marginLeft: 0,
marginRight: 0,
},
label: {
color: textPrimary,
fontSize: "0.86rem",
},
},
},
MuiFormHelperText: {
styleOverrides: {
root: {
color: textSecondary,
fontSize: "0.74rem",
lineHeight: 1.35,
marginLeft: 0,
},
},
},
MuiTabs: {
styleOverrides: {
root: {
@@ -363,7 +395,10 @@ const getModernThemeAdditions = (themeMode, preferences = operatorSettingDefault
styleOverrides: {
root: {
backgroundColor: backgroundPaper,
border: `1px solid ${borderColor}`,
borderRadius: 6,
minHeight: 0,
overflow: "auto",
},
},
},
@@ -373,6 +408,16 @@ const getModernThemeAdditions = (themeMode, preferences = operatorSettingDefault
backgroundColor: backgroundPaper,
borderCollapse: "separate",
borderSpacing: 0,
width: "100%",
},
},
},
MuiTableHead: {
styleOverrides: {
root: {
"& .MuiTableRow-root": {
backgroundColor: tableHeaderColor,
},
},
},
},
@@ -381,10 +426,17 @@ const getModernThemeAdditions = (themeMode, preferences = operatorSettingDefault
root: {
borderBottom: `1px solid ${tableBorderSoft}`,
color: textPrimary,
fontVariantNumeric: "tabular-nums",
fontSize: "0.86rem",
lineHeight: 1.35,
padding: "6px 10px",
verticalAlign: "middle",
"&:first-of-type": {
paddingLeft: 12,
},
"&:last-of-type": {
paddingRight: 12,
},
},
head: {
backgroundColor: tableHeaderColor,
@@ -397,6 +449,11 @@ const getModernThemeAdditions = (themeMode, preferences = operatorSettingDefault
textTransform: "uppercase",
whiteSpace: "nowrap",
},
paddingCheckbox: {
paddingLeft: 6,
paddingRight: 6,
width: 36,
},
stickyHeader: {
backgroundColor: tableHeaderColor,
zIndex: 3,
@@ -406,6 +463,10 @@ const getModernThemeAdditions = (themeMode, preferences = operatorSettingDefault
MuiTableRow: {
styleOverrides: {
root: {
backgroundColor: backgroundPaper,
".MuiTableBody-root &:nth-of-type(even):not(.Mui-selected):not(.selectedCallback):not(.selectedCallbackHierarchy)": {
backgroundColor: tableRowStripeColor,
},
"&:last-child .MuiTableCell-root": {
borderBottom: 0,
},
@@ -421,6 +482,24 @@ const getModernThemeAdditions = (themeMode, preferences = operatorSettingDefault
},
},
},
MuiTablePagination: {
styleOverrides: {
root: {
color: textSecondary,
},
selectLabel: {
color: textSecondary,
fontSize: "0.78rem",
},
displayedRows: {
color: textSecondary,
fontSize: "0.78rem",
},
toolbar: {
minHeight: 34,
},
},
},
MuiTableSortLabel: {
styleOverrides: {
root: {
@@ -437,6 +516,76 @@ const getModernThemeAdditions = (themeMode, preferences = operatorSettingDefault
},
},
},
MuiPagination: {
styleOverrides: {
root: {
display: "flex",
},
ul: {
gap: 4,
},
},
},
MuiPaginationItem: {
styleOverrides: {
root: {
minWidth: 28,
height: 28,
borderRadius: 6,
color: textSecondary,
fontSize: "0.78rem",
fontWeight: 650,
"&.Mui-selected": {
backgroundColor: withAlpha(primary, isDark ? "44" : "1F"),
borderColor: primary,
color: textPrimary,
"&:hover": {
backgroundColor: withAlpha(primary, isDark ? "55" : "2B"),
},
},
"&:hover": {
backgroundColor: tableRowHoverColor,
color: textPrimary,
},
},
outlined: {
borderColor: tableBorderSoft,
},
icon: {
fontSize: "1rem",
},
},
},
MuiToggleButton: {
styleOverrides: {
root: {
borderColor: tableBorderSoft,
borderRadius: 6,
color: textSecondary,
fontSize: "0.78rem",
fontWeight: 650,
gap: 6,
minHeight: 32,
padding: "5px 9px",
textTransform: "none",
"&:hover": {
backgroundColor: tableRowHoverColor,
color: textPrimary,
},
"&.Mui-selected": {
backgroundColor: withAlpha(primary, isDark ? "44" : "1F"),
borderColor: primary,
color: textPrimary,
"&:hover": {
backgroundColor: withAlpha(primary, isDark ? "55" : "2B"),
},
},
},
sizeSmall: {
minHeight: 30,
},
},
},
MuiDataGrid: {
styleOverrides: {
root: {
@@ -457,7 +606,6 @@ const getModernThemeAdditions = (themeMode, preferences = operatorSettingDefault
"& .MuiDataGrid-columnHeaders": {
backgroundColor: tableHeaderColor,
borderBottom: `1px solid ${borderColor}`,
minHeight: 34,
},
"& .MuiDataGrid-columnHeader": {
backgroundColor: tableHeaderColor,
@@ -476,11 +624,12 @@ const getModernThemeAdditions = (themeMode, preferences = operatorSettingDefault
"& .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)": {
"& .MuiDataGrid-row:nth-of-type(even):not(.Mui-selected)": {
backgroundColor: tableRowStripeColor,
},
"& .MuiDataGrid-row:hover": {
@@ -612,7 +761,8 @@ export function App(props) {
preferences?.palette?.pageHeader?.light || operatorSettingDefaults.palette.pageHeader.light,
},
pageHeaderText: {
main: 'white',
main: themeMode === 'dark' ? preferences?.palette?.text?.dark || operatorSettingDefaults.palette.text.dark :
preferences?.palette?.text?.light || operatorSettingDefaults.palette.text.light,
},
topAppBarColor: themeMode === 'dark' ? (preferences?.palette?.navBarColor?.dark || operatorSettingDefaults.palette.navBarColor.dark) :
(preferences?.palette?.navBarColor?.light || operatorSettingDefaults.palette.navBarColor.light),
@@ -723,7 +873,8 @@ export function App(props) {
operatorSettingDefaults.palette.pageHeader.light,
},
pageHeaderText: {
main: 'white',
main: themeMode === 'dark' ? operatorSettingDefaults.palette.text.dark :
operatorSettingDefaults.palette.text.light,
},
topAppBarColor: themeMode === 'dark' ? operatorSettingDefaults.palette.navBarColor.dark :
operatorSettingDefaults.palette.navBarColor.light,
@@ -1,10 +1,9 @@
import React from 'react';
import Button from '@mui/material/Button';
import DialogActions from '@mui/material/DialogActions';
import DialogTitle from '@mui/material/DialogTitle';
import {MythicDialog} from './MythicDialog';
import DialogContentText from '@mui/material/DialogContentText';
import DialogContent from '@mui/material/DialogContent';
import {MythicDialogButton, MythicDialogFooter} from "./MythicDialogLayout";
export function MythicConfirmDialog(props) {
const handleSubmit = () => {
@@ -14,6 +13,10 @@ export function MythicConfirmDialog(props) {
}
props.onClose();
}
const acceptIntent = props.acceptColor === "success" ? "primary" :
props.acceptColor === "warning" ? "warning" :
props.acceptColor === "info" ? "info" :
"destructive";
return (
<MythicDialog fullWidth={false} maxWidth="sm" open={props.open} onClose={()=>{props.onClose()}} innerDialog={
@@ -26,14 +29,14 @@ export function MythicConfirmDialog(props) {
</DialogContentText>
</DialogContent>
)}
<DialogActions>
<Button onClick={props.onClose} variant="contained" color="primary">
<MythicDialogFooter>
<MythicDialogButton onClick={props.onClose}>
{props.cancelText ? (props.cancelText) : ("Cancel")}
</Button>
<Button onClick={handleSubmit} autoFocus variant="contained" color={props.acceptColor ? (props.acceptColor) : ("error")}>
</MythicDialogButton>
<MythicDialogButton onClick={handleSubmit} autoFocus intent={acceptIntent}>
{props.acceptText ? (props.acceptText) : ("Remove")}
</Button>
</DialogActions>
</MythicDialogButton>
</MythicDialogFooter>
</React.Fragment>
} />
);
@@ -1,8 +1,36 @@
import React from 'react';
import Box from '@mui/material/Box';
import { DataGrid } from '@mui/x-data-grid';
import {MythicLoadingState, MythicSearchEmptyState} from "./MythicStateDisplay";
export const MythicDataGrid = ({containerSx = {}, sx = {}, density = "compact", autoPageSize = true, pageSizeOptions = [10, 25, 50, 100], ...props}) => {
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={{
@@ -19,7 +47,14 @@ export const MythicDataGrid = ({containerSx = {}, sx = {}, density = "compact",
density={density}
autoPageSize={autoPageSize}
pageSizeOptions={pageSizeOptions}
columnHeaderHeight={columnHeaderHeight}
rowHeight={rowHeight}
hideFooterSelectedRowCount
slots={{
noRowsOverlay: DefaultNoRowsOverlay,
loadingOverlay: DefaultLoadingOverlay,
...slots,
}}
sx={{
flexGrow: 1,
minHeight: 0,
@@ -1,7 +1,5 @@
import React, {useEffect} from 'react';
import Dialog from '@mui/material/Dialog';
import Button from '@mui/material/Button';
import DialogActions from '@mui/material/DialogActions';
import DialogContent from '@mui/material/DialogContent';
import DialogTitle from '@mui/material/DialogTitle';
import Table from '@mui/material/Table';
@@ -23,9 +21,17 @@ import {IconButton} from '@mui/material';
import {MythicStyledTooltip} from "./MythicStyledTooltip";
import Draggable from 'react-draggable';
import {MythicDraggableDialogTitle} from "./MythicDraggableDialogTitle";
import {MythicDialogButton, MythicDialogFooter} from "./MythicDialogLayout";
let mythicDialogIdCounter = 0;
let mythicDialogStack = [];
export function MythicDialog(props) {
const theme = useTheme();
const dialogIdRef = React.useRef(null);
if(dialogIdRef.current === null){
dialogIdRef.current = mythicDialogIdCounter++;
}
const [draggedState, setDraggedState] = React.useState({
style: {},
paperStyle: {
@@ -41,6 +47,19 @@ export function MythicDialog(props) {
});
const nodeRef = React.useRef(null);
const descriptionElementRef = React.useRef(null);
React.useEffect(() => {
if(!props.open){
return undefined;
}
mythicDialogStack = mythicDialogStack.filter((id) => id !== dialogIdRef.current);
mythicDialogStack.push(dialogIdRef.current);
return () => {
mythicDialogStack = mythicDialogStack.filter((id) => id !== dialogIdRef.current);
};
}, [props.open]);
const isTopDialog = () => {
return mythicDialogStack[mythicDialogStack.length - 1] === dialogIdRef.current;
};
React.useEffect(() => {
if (props.open) {
const { current: descriptionElement } = descriptionElementRef;
@@ -50,20 +69,30 @@ export function MythicDialog(props) {
}
}, [props.open]);
const dialogOnClick = (e) => {
//e.preventDefault();
//e.stopPropagation();
if(e.target.classList.length > 0 && e.target.classList.contains("MuiDialog-container")){
props.onClose();
if(e.target.classList?.contains("MuiDialog-container")){
e.stopPropagation();
if(!isTopDialog()){
return;
}
if(draggedState.hideBackdrop){
props.onClose?.();
}
}
}
const dialogOnContextMenu = (e) => {
e.stopPropagation();
}
const handleOnClose = (event, reason) => {
if(reason === "backdropClick" || reason === "escapeKeyDown"){
event?.stopPropagation?.();
if(!isTopDialog()){
return;
}
}
if(reason === "backdropClick" && draggedState.hideBackdrop){
return;
}
props.onClose();
props.onClose?.();
}
const onStart = (e) => {
if(e){
@@ -110,7 +139,6 @@ export function MythicDialog(props) {
<Dialog
ref={nodeRef}
open={props.open}
onClick={dialogOnClick}
onClose={handleOnClose}
scroll="paper"
maxWidth={props.maxWidth}
@@ -194,18 +222,18 @@ export function MythicModifyStringDialog(props) {
/>
</DialogContent>
{ (props.onClose || props.onSubmit) &&
<DialogActions>
<MythicDialogFooter>
{props.onClose &&
<Button onClick={props.onClose} variant="contained" color="primary">
<MythicDialogButton onClick={props.onClose}>
Close
</Button>
</MythicDialogButton>
}
{props.onSubmit &&
<Button onClick={onCommitSubmit} variant="contained" color="success">
<MythicDialogButton intent="primary" onClick={onCommitSubmit}>
{props.onSubmitText ? props.onSubmitText : "Submit"}
</Button>
</MythicDialogButton>
}
</DialogActions>
</MythicDialogFooter>
}
</React.Fragment>
);
@@ -345,11 +373,11 @@ export function MythicViewJSONAsTableDialog(props) {
</TableBody>
</Table>
</TableContainer>
<DialogActions>
<Button onClick={props.onClose} variant="contained" color="primary">
<MythicDialogFooter>
<MythicDialogButton onClick={props.onClose}>
Close
</Button>
</DialogActions>
</MythicDialogButton>
</MythicDialogFooter>
</React.Fragment>
);
}
@@ -393,11 +421,11 @@ export function MythicViewObjectPropertiesAsTableDialog(props) {
</TableContainer>
</Paper>
</DialogContent>
<DialogActions>
<Button onClick={props.onClose} variant="contained" color="primary">
<MythicDialogFooter>
<MythicDialogButton onClick={props.onClose}>
Close
</Button>
</DialogActions>
</MythicDialogButton>
</MythicDialogFooter>
</React.Fragment>
);
}
@@ -0,0 +1,168 @@
import React from 'react';
import Box from '@mui/material/Box';
import Button from '@mui/material/Button';
import DialogActions from '@mui/material/DialogActions';
import Typography from '@mui/material/Typography';
const joinClasses = (...classes) => classes.filter(Boolean).join(" ");
export function MythicDialogBody({children, className, compact = false, ...props}) {
return (
<Box
className={joinClasses("mythic-dialog-body", compact && "mythic-dialog-body-compact", className)}
{...props}
>
{children}
</Box>
);
}
export function MythicDialogSection({title, description, actions, children, className, ...props}) {
return (
<Box component="section" className={joinClasses("mythic-dialog-section", className)} {...props}>
{(title || description || actions) &&
<Box className="mythic-dialog-section-header">
<Box sx={{minWidth: 0}}>
{title &&
<Typography component="h3" className="mythic-dialog-section-title">
{title}
</Typography>
}
{description &&
<Typography component="div" className="mythic-dialog-section-description">
{description}
</Typography>
}
</Box>
{actions &&
<Box className="mythic-dialog-section-actions">
{actions}
</Box>
}
</Box>
}
{children}
</Box>
);
}
export function MythicDialogGrid({children, className, minWidth = "16rem", sx = {}, ...props}) {
return (
<Box
className={joinClasses("mythic-dialog-grid", className)}
sx={{"--mythic-dialog-grid-min": minWidth, ...sx}}
{...props}
>
{children}
</Box>
);
}
export function MythicDialogChoiceRow({children, className, ...props}) {
return (
<Box className={joinClasses("mythic-dialog-choice-row", className)} {...props}>
{children}
</Box>
);
}
export function MythicDialogChoiceDivider({children = "OR", className, ...props}) {
return (
<Box component="span" className={joinClasses("mythic-dialog-choice-divider", className)} {...props}>
{children}
</Box>
);
}
export function MythicDialogFooter({children, className, ...props}) {
return (
<DialogActions className={joinClasses("mythic-dialog-actions", className)} {...props}>
{children}
</DialogActions>
);
}
export function MythicDialogButton({children, className, intent = "secondary", ...props}) {
return (
<Button
className={joinClasses("mythic-dialog-button", `mythic-dialog-button-${intent}`, className)}
size="small"
variant="contained"
{...props}
>
{children}
</Button>
);
}
export function MythicForm({children, className, ...props}) {
return (
<Box component="form" className={joinClasses("mythic-form", className)} {...props}>
{children}
</Box>
);
}
export function MythicFormGrid({children, className, minWidth = "16rem", sx = {}, ...props}) {
return (
<Box
className={joinClasses("mythic-form-grid", className)}
sx={{"--mythic-form-grid-min": minWidth, ...sx}}
{...props}
>
{children}
</Box>
);
}
export function MythicFormField({children, className, description, label, required = false, ...props}) {
return (
<Box className={joinClasses("mythic-form-field", className)} {...props}>
{(label || description) &&
<Box className="mythic-form-field-copy">
{label &&
<Typography component="label" className="mythic-form-field-label">
{label}{required && <Box component="span" className="mythic-form-field-required"> *</Box>}
</Typography>
}
{description &&
<Typography component="div" className="mythic-form-field-description">
{description}
</Typography>
}
</Box>
}
<Box className="mythic-form-field-control">
{children}
</Box>
</Box>
);
}
export function MythicFormNote({children, className, ...props}) {
return (
<Box className={joinClasses("mythic-form-note", className)} {...props}>
{children}
</Box>
);
}
export function MythicFormSwitchRow({control, label, description, className, ...props}) {
return (
<Box className={joinClasses("mythic-form-switch-row", className)} {...props}>
<Box sx={{minWidth: 0}}>
<Typography component="div" className="mythic-form-field-label">
{label}
</Typography>
{description &&
<Typography component="div" className="mythic-form-field-description">
{description}
</Typography>
}
</Box>
<Box className="mythic-form-switch-control">
{control}
</Box>
</Box>
);
}
@@ -1,42 +1,281 @@
import {useTheme} from '@mui/material/styles';
import Box from '@mui/material/Box';
import Chip from '@mui/material/Chip';
import Paper from '@mui/material/Paper';
import {Typography} from "@mui/material";
import Stack from '@mui/material/Stack';
import Typography from '@mui/material/Typography';
import {alpha, useTheme} from '@mui/material/styles';
export const MythicPageHeader = ({children, title, style={}, headerVariant="h6"}) => {
export const MythicPageHeader = ({
actions,
children,
className = "",
dense = false,
icon,
meta,
style = {},
subtitle,
sx = {},
title,
headerVariant = "h6",
}) => {
const theme = useTheme();
const headerTextColor = theme.palette.getContrastText(theme.pageHeader.main);
const headerTextColor = theme.pageHeaderText?.main || theme.palette.text.primary;
const supportingTextColor = alpha(headerTextColor, 0.78);
const hasActionContent = actions || children;
const actionSurface = alpha(headerTextColor, 0.1);
const actionSurfaceHover = alpha(headerTextColor, 0.16);
const actionSurfaceActive = alpha(headerTextColor, 0.23);
const actionBorder = alpha(headerTextColor, 0.24);
const actionBorderHover = alpha(headerTextColor, 0.42);
const getIntentHoverStyles = (paletteName) => {
const paletteColor = theme.palette[paletteName]?.main || headerTextColor;
return {
backgroundColor: `${alpha(paletteColor, theme.palette.mode === "dark" ? 0.28 : 0.16)} !important`,
borderColor: `${alpha(paletteColor, theme.palette.mode === "dark" ? 0.78 : 0.55)} !important`,
color: `${paletteColor} !important`,
};
};
return (
<Paper elevation={0}
style={{
backgroundColor: theme.pageHeader.main,
color: headerTextColor,
border: `1px solid ${theme.borderColor}`,
alignItems: "center",
display: "flex",
justifyContent: "space-between",
gap: "0.75rem",
minHeight: "44px",
marginBottom: "0rem",
padding: "0.5rem 0.75rem",
borderRadius: theme.shape.borderRadius,
...style
}}
>
<Typography variant={headerVariant} style={{
textAlign: "left",
display: "inline-block",
marginLeft: "0.25rem",
width: "100%",
fontWeight: 650,
letterSpacing: 0,
<Paper
className={`mythic-page-header${className ? ` ${className}` : ""}`}
elevation={0}
style={style}
sx={{
alignItems: {xs: "stretch", md: "center"},
backgroundColor: theme.pageHeader.main,
border: `1px solid ${alpha(headerTextColor, 0.18)}`,
borderRadius: `${theme.shape.borderRadius}px`,
boxShadow: `inset 0 1px 0 ${alpha(headerTextColor, 0.12)}`,
color: headerTextColor,
}}>
{title}
</Typography>
<div style={{display: "flex", marginRight: "0.25rem", alignItems: "center", gap: "0.35rem", color: headerTextColor}}>
{children}
</div>
display: "flex",
flex: "0 0 auto",
flexDirection: {xs: "column", md: "row"},
flexWrap: {xs: "nowrap", md: "wrap"},
gap: dense ? 0.75 : 1,
height: "auto",
justifyContent: "space-between",
maxHeight: "none",
minHeight: dense ? 44 : 58,
minWidth: 0,
overflow: "visible",
p: dense ? "0.5rem 0.75rem" : "0.7rem 0.85rem",
width: "100%",
"& .MuiButton-root, & .MuiToggleButton-root": {
alignItems: "center",
backgroundColor: `${actionSurface} !important`,
border: `1px solid ${actionBorder} !important`,
borderRadius: `${theme.shape.borderRadius}px`,
boxShadow: "none !important",
color: `${headerTextColor} !important`,
fontSize: "0.76rem",
fontWeight: 750,
gap: 0.35,
justifyContent: "center",
letterSpacing: 0,
lineHeight: 1.2,
minHeight: 32,
minWidth: {xs: "auto", sm: "7rem"},
px: 1.05,
textTransform: "none",
whiteSpace: "nowrap",
"&:hover": {
backgroundColor: `${actionSurfaceHover} !important`,
borderColor: `${actionBorderHover} !important`,
boxShadow: `inset 0 1px 0 ${alpha(headerTextColor, 0.12)}`,
},
},
"& .MuiButton-root.Mui-focusVisible, & .MuiToggleButton-root.Mui-focusVisible": {
outline: `2px solid ${alpha(headerTextColor, 0.38)}`,
outlineOffset: 2,
},
"& .MuiButton-root.Mui-disabled, & .MuiToggleButton-root.Mui-disabled": {
backgroundColor: `${alpha(headerTextColor, 0.04)} !important`,
borderColor: `${alpha(headerTextColor, 0.1)} !important`,
color: `${alpha(headerTextColor, 0.38)} !important`,
},
"& .MuiToggleButton-root.Mui-selected": {
backgroundColor: `${actionSurfaceActive} !important`,
borderColor: `${alpha(headerTextColor, 0.48)} !important`,
color: `${headerTextColor} !important`,
boxShadow: `inset 0 1px 0 ${alpha(headerTextColor, 0.16)}`,
"&:hover": {
backgroundColor: `${alpha(headerTextColor, 0.28)} !important`,
},
},
"& .MuiButton-startIcon, & .MuiButton-endIcon": {
color: "inherit",
marginLeft: 0,
marginRight: 0,
},
"& .MuiButton-startIcon": {
paddingRight: "0.25rem",
},
"& .MuiButton-endIcon": {
paddingLeft: "0.25rem",
},
"& .MuiIconButton-root": {
backgroundColor: `${actionSurface} !important`,
border: `1px solid ${actionBorder} !important`,
borderRadius: `${theme.shape.borderRadius}px`,
color: `${headerTextColor} !important`,
height: 32,
width: 32,
"&:hover": {
backgroundColor: `${actionSurfaceHover} !important`,
borderColor: `${actionBorderHover} !important`,
},
"&.Mui-disabled": {
backgroundColor: `${alpha(headerTextColor, 0.04)} !important`,
borderColor: `${alpha(headerTextColor, 0.1)} !important`,
color: `${alpha(headerTextColor, 0.38)} !important`,
},
},
"& .MuiButton-root.mythic-table-row-action-hover-info:hover, & .MuiToggleButton-root.mythic-table-row-action-hover-info:hover, & .MuiIconButton-root.mythic-table-row-icon-action-hover-info:hover": getIntentHoverStyles("info"),
"& .MuiButton-root.mythic-table-row-action-hover-success:hover, & .MuiToggleButton-root.mythic-table-row-action-hover-success:hover, & .MuiIconButton-root.mythic-table-row-icon-action-hover-success:hover": getIntentHoverStyles("success"),
"& .MuiButton-root.mythic-table-row-action-hover-warning:hover, & .MuiToggleButton-root.mythic-table-row-action-hover-warning:hover, & .MuiIconButton-root.mythic-table-row-icon-action-hover-warning:hover": getIntentHoverStyles("warning"),
"& .MuiButton-root.mythic-table-row-action-hover-danger:hover, & .MuiToggleButton-root.mythic-table-row-action-hover-danger:hover, & .MuiIconButton-root.mythic-table-row-icon-action-hover-danger:hover": getIntentHoverStyles("error"),
...sx,
}}
>
<Stack direction="row" sx={{gap: 1, minWidth: 0, flex: "1 1 24rem"}}>
{icon &&
<Box sx={{
alignItems: "center",
backgroundColor: alpha(headerTextColor, 0.1),
border: `1px solid ${alpha(headerTextColor, 0.16)}`,
borderRadius: `${theme.shape.borderRadius}px`,
display: "inline-flex",
flex: "0 0 auto",
height: dense ? 32 : 36,
justifyContent: "center",
mt: subtitle || meta ? 0.15 : 0,
width: dense ? 32 : 36,
}}>
{icon}
</Box>
}
<Box sx={{display: "flex", flexDirection: "column", gap: 0.25, minWidth: 0}}>
<Typography
variant={headerVariant}
sx={{
color: headerTextColor,
fontWeight: 750,
letterSpacing: 0,
lineHeight: 1.18,
minWidth: 0,
}}
>
{title}
</Typography>
{subtitle &&
<Typography
component="div"
sx={{
color: supportingTextColor,
fontSize: "0.78rem",
fontWeight: 600,
lineHeight: 1.35,
maxWidth: "64rem",
}}
>
{subtitle}
</Typography>
}
{meta &&
<Box sx={{alignItems: "center", display: "flex", flexWrap: "wrap", gap: 0.5, mt: 0.25}}>
{meta}
</Box>
}
</Box>
</Stack>
{hasActionContent &&
<Stack direction="row" sx={{
alignContent: "center",
alignItems: "center",
color: headerTextColor,
flex: "0 1 auto",
flexWrap: "wrap",
gap: 0.45,
justifyContent: {xs: "flex-start", md: "flex-end"},
maxWidth: "100%",
minWidth: 0,
}}>
{actions}
{children}
</Stack>
}
</Paper>
)
);
}
export const MythicPageHeaderChip = ({status, sx = {}, ...props}) => {
const theme = useTheme();
const headerTextColor = theme.pageHeaderText?.main || theme.palette.text.primary;
const statusColor = status ? theme.palette[status]?.main : null;
const chipColor = statusColor || alpha(headerTextColor, 0.88);
return (
<Chip
size="small"
variant="outlined"
{...props}
sx={{
backgroundColor: statusColor ? alpha(statusColor, theme.palette.mode === "dark" ? 0.22 : 0.13) : alpha(headerTextColor, 0.08),
borderColor: statusColor ? alpha(statusColor, theme.palette.mode === "dark" ? 0.55 : 0.38) : alpha(headerTextColor, 0.2),
color: chipColor,
fontSize: "0.72rem",
fontWeight: 750,
height: 22,
letterSpacing: 0,
"& .MuiChip-icon": {
color: statusColor ? chipColor : alpha(headerTextColor, 0.82),
},
...sx,
}}
/>
);
};
export const MythicSectionHeader = ({
actions,
dense = false,
subtitle,
sx = {},
title,
}) => {
const theme = useTheme();
const headerTextColor = theme.pageHeaderText?.main || theme.palette.text.primary;
const sectionAccentColor = theme.palette.primary.main;
const sectionOverlayStart = alpha(sectionAccentColor, theme.palette.mode === "dark" ? 0.28 : 0.18);
const sectionOverlayMiddle = alpha(sectionAccentColor, theme.palette.mode === "dark" ? 0.12 : 0.08);
const sectionOverlayEnd = alpha(headerTextColor, theme.palette.mode === "dark" ? 0.055 : 0.04);
return (
<MythicPageHeader
actions={actions}
className="mythic-section-header"
dense={dense}
subtitle={subtitle}
title={title}
sx={{
backgroundImage: `linear-gradient(90deg, ${sectionOverlayStart} 0%, ${sectionOverlayMiddle} 48%, ${sectionOverlayEnd} 100%)`,
borderColor: alpha(sectionAccentColor, theme.palette.mode === "dark" ? 0.55 : 0.38),
boxShadow: `inset 0 1px 0 ${alpha(headerTextColor, 0.22)}, 0 2px 6px ${alpha(theme.palette.common.black, theme.palette.mode === "dark" ? 0.28 : 0.12)}`,
mb: 0.5,
mt: dense ? 1 : 1.25,
overflow: "hidden",
pl: dense ? 1.45 : 1.6,
position: "relative",
"&::before": {
backgroundColor: sectionAccentColor,
bottom: 0,
boxShadow: `0 0 0 1px ${alpha(headerTextColor, 0.2)}`,
content: '""',
left: 0,
position: "absolute",
top: 0,
width: 6,
},
...sx,
}}
/>
);
};
@@ -126,6 +126,7 @@ export const ContextMenu = ({openContextMenu, dropdownAnchorRef, contextMenuOpti
<DropdownMenuItem
key={option.name}
disabled={option.disabled}
className={option.danger ? "mythic-menu-item-hover-danger" : undefined}
onClick={(event) => handleMenuItemClick(event, option.click)}
>
{option.icon}{option.name}
@@ -136,11 +137,12 @@ export const ContextMenu = ({openContextMenu, dropdownAnchorRef, contextMenuOpti
disabled={option.disabled}
menu={
option.menuItems.map((menuOption, indx) => (
<DropdownMenuItem
key={menuOption.name}
disabled={menuOption.disabled}
onClick={(event) => handleMenuItemClick(event, menuOption.click)}
>
<DropdownMenuItem
key={menuOption.name}
disabled={menuOption.disabled}
className={menuOption.danger ? "mythic-menu-item-hover-danger" : undefined}
onClick={(event) => handleMenuItemClick(event, menuOption.click)}
>
{menuOption.icon}{menuOption.name}
</DropdownMenuItem>
))
@@ -183,4 +185,4 @@ export default Cell;
</Grow>
)}
</Popper>
*/
*/
@@ -0,0 +1,126 @@
import React from 'react';
import Box from '@mui/material/Box';
import Button from '@mui/material/Button';
import CircularProgress from '@mui/material/CircularProgress';
import TableCell from '@mui/material/TableCell';
import TableRow from '@mui/material/TableRow';
import Typography from '@mui/material/Typography';
import {alpha, useTheme} from '@mui/material/styles';
import ErrorOutlineOutlinedIcon from '@mui/icons-material/ErrorOutlineOutlined';
import HourglassEmptyOutlinedIcon from '@mui/icons-material/HourglassEmptyOutlined';
import InboxOutlinedIcon from '@mui/icons-material/InboxOutlined';
import SearchOffOutlinedIcon from '@mui/icons-material/SearchOffOutlined';
const stateIcons = {
empty: <InboxOutlinedIcon />,
error: <ErrorOutlineOutlinedIcon />,
loading: <HourglassEmptyOutlinedIcon />,
search: <SearchOffOutlinedIcon />,
};
export function MythicStateDisplay({
action,
actionLabel,
compact = false,
description,
icon,
loading = false,
minHeight,
onAction,
severity = "empty",
sx = {},
title,
}) {
const theme = useTheme();
const defaultIcon = stateIcons[severity] || stateIcons.empty;
const iconNode = loading ? (
<CircularProgress size={compact ? 18 : 22} color="inherit" disableShrink />
) : React.cloneElement(icon || defaultIcon, {fontSize: compact ? "small" : "medium"});
const resolvedTitle = title || (loading ? "Loading" : "Nothing to show");
const resolvedMinHeight = minHeight || (compact ? 112 : 176);
return (
<Box sx={{
alignItems: "center",
color: theme.palette.text.secondary,
display: "flex",
justifyContent: "center",
minHeight: resolvedMinHeight,
p: compact ? 1.5 : 2.5,
width: "100%",
...sx,
}}>
<Box sx={{
alignItems: "center",
display: "flex",
flexDirection: "column",
gap: compact ? 0.75 : 1,
maxWidth: compact ? 360 : 460,
textAlign: "center",
}}>
<Box sx={{
alignItems: "center",
backgroundColor: theme.palette.mode === "dark" ? alpha(theme.palette.common.white, 0.055) : alpha(theme.palette.common.black, 0.035),
border: `1px solid ${theme.palette.mode === "dark" ? alpha(theme.palette.common.white, 0.09) : alpha(theme.palette.common.black, 0.08)}`,
borderRadius: "6px",
color: severity === "error" ? theme.palette.error.main : theme.palette.text.secondary,
display: "inline-flex",
height: compact ? 32 : 40,
justifyContent: "center",
width: compact ? 32 : 40,
}}>
{iconNode}
</Box>
<Typography component="div" sx={{
color: theme.palette.text.primary,
fontSize: compact ? "0.86rem" : "0.95rem",
fontWeight: 800,
lineHeight: 1.25,
}}>
{resolvedTitle}
</Typography>
{description &&
<Typography component="div" sx={{
color: theme.palette.text.secondary,
fontSize: compact ? "0.76rem" : "0.82rem",
lineHeight: 1.35,
}}>
{description}
</Typography>
}
{action || (actionLabel && onAction) ? (
action || (
<Button size="small" variant="outlined" onClick={onAction} sx={{mt: 0.5}}>
{actionLabel}
</Button>
)
) : null}
</Box>
</Box>
);
}
export function MythicEmptyState(props) {
return <MythicStateDisplay severity="empty" {...props} />;
}
export function MythicSearchEmptyState(props) {
return <MythicStateDisplay severity="search" title="No results" {...props} />;
}
export function MythicLoadingState(props) {
return <MythicStateDisplay severity="loading" loading title="Loading" {...props} />;
}
export function MythicErrorState(props) {
return <MythicStateDisplay severity="error" title="Something went wrong" {...props} />;
}
export function MythicTableEmptyState({colSpan, ...props}) {
return (
<TableRow>
<TableCell colSpan={colSpan} sx={{borderBottom: 0, p: 0}}>
<MythicEmptyState {...props} />
</TableCell>
</TableRow>
);
}
@@ -0,0 +1,111 @@
import React from 'react';
import Chip from '@mui/material/Chip';
import {alpha, useTheme} from '@mui/material/styles';
import CheckCircleOutlineIcon from '@mui/icons-material/CheckCircleOutline';
import ErrorOutlineIcon from '@mui/icons-material/ErrorOutline';
import WarningAmberIcon from '@mui/icons-material/WarningAmber';
import InfoOutlinedIcon from '@mui/icons-material/InfoOutlined';
import RadioButtonCheckedIcon from '@mui/icons-material/RadioButtonChecked';
import RadioButtonUncheckedIcon from '@mui/icons-material/RadioButtonUnchecked';
import DeleteOutlineIcon from '@mui/icons-material/DeleteOutline';
import LockOutlinedIcon from '@mui/icons-material/LockOutlined';
import TimelapseIcon from '@mui/icons-material/Timelapse';
import DoneAllIcon from '@mui/icons-material/DoneAll';
import BlockIcon from '@mui/icons-material/Block';
import HideSourceIcon from '@mui/icons-material/HideSource';
const statusConfig = {
success: {palette: "success", icon: <CheckCircleOutlineIcon />, label: "Success"},
error: {palette: "error", icon: <ErrorOutlineIcon />, label: "Error"},
warning: {palette: "warning", icon: <WarningAmberIcon />, label: "Warning"},
info: {palette: "info", icon: <InfoOutlinedIcon />, label: "Info"},
active: {palette: "success", icon: <RadioButtonCheckedIcon />, label: "Active"},
inactive: {palette: "error", icon: <RadioButtonUncheckedIcon />, label: "Inactive"},
deleted: {palette: "error", icon: <DeleteOutlineIcon />, label: "Deleted"},
locked: {palette: "warning", icon: <LockOutlinedIcon />, label: "Locked"},
building: {palette: "info", icon: <TimelapseIcon />, label: "Building"},
completed: {palette: "success", icon: <DoneAllIcon />, label: "Completed"},
blocked: {palette: "error", icon: <BlockIcon />, label: "Blocked"},
skipped: {palette: "secondary", icon: <HideSourceIcon />, label: "Skipped"},
neutral: {palette: "secondary", icon: <InfoOutlinedIcon />, label: "Status"},
};
export function getMythicStatusConfig(status) {
return statusConfig[status] || statusConfig.neutral;
}
export function getMythicStatusFromTaskStatus(status) {
const normalized = `${status || ""}`.toLowerCase();
if(normalized.includes("error") || normalized.includes("failed")){
return "error";
}
if(normalized.includes("warning")){
return "warning";
}
if(normalized.includes("success") || normalized.includes("completed")){
return "success";
}
if(normalized.includes("processing") || normalized.includes("building")){
return "building";
}
return "info";
}
export function MythicStatusChip({
label,
status = "neutral",
icon,
showIcon = true,
size = "small",
variant = "soft",
sx = {},
...props
}) {
const theme = useTheme();
const config = getMythicStatusConfig(status);
const paletteColor = theme.palette[config.palette]?.main || theme.palette.text.secondary;
const textColor = theme.palette.mode === "dark" ? theme.palette.text.primary : paletteColor;
const backgroundColor = variant === "outlined" ? "transparent" : alpha(paletteColor, theme.palette.mode === "dark" ? 0.18 : 0.11);
const borderColor = alpha(paletteColor, theme.palette.mode === "dark" ? 0.55 : 0.35);
const chipIcon = showIcon ? (icon || config.icon) : undefined;
return (
<Chip
label={label || config.label}
icon={chipIcon}
size={size}
variant="outlined"
sx={{
backgroundColor,
borderColor,
borderRadius: "5px",
color: textColor,
fontSize: "0.72rem",
fontWeight: 700,
height: size === "small" ? 24 : 28,
letterSpacing: 0,
maxWidth: "100%",
textDecoration: "none",
textTransform: "none",
"& .MuiChip-label": {
overflow: "hidden",
px: chipIcon ? 0.75 : 1,
textOverflow: "ellipsis",
whiteSpace: "nowrap",
},
"& .MuiChip-icon": {
color: paletteColor,
fontSize: size === "small" ? "0.95rem" : "1.05rem",
marginLeft: "6px",
marginRight: "-2px",
},
"&.MuiChip-clickable:hover": {
backgroundColor: alpha(paletteColor, theme.palette.mode === "dark" ? 0.28 : 0.18),
borderColor: paletteColor,
},
...sx,
}}
{...props}
/>
);
}
@@ -23,6 +23,7 @@ export function MythicTabPanel(props) {
display: value === index ? 'flex' : 'none',
flexDirection: 'column',
flexGrow: 1,
minHeight: 0,
width: '100%',
maxWidth: '100%',
overflowY: "auto",
@@ -0,0 +1,42 @@
import React from 'react';
import Box from '@mui/material/Box';
import Pagination from '@mui/material/Pagination';
import Typography from '@mui/material/Typography';
export const MythicTablePagination = ({
count,
totalCount,
fetchLimit,
onChange,
page,
color = "primary",
label = "Total Results",
className = "",
containerStyle = {},
paginationStyle = {},
...paginationProps
}) => {
const pageCount = count ?? Math.ceil((totalCount || 0) / (fetchLimit || 1));
const controlledPage = page === undefined ? {} : {page};
return (
<Box className={`mythic-table-footer ${className}`.trim()} style={containerStyle}>
<Pagination
count={pageCount}
variant="outlined"
color={color}
boundaryCount={1}
siblingCount={1}
onChange={onChange}
showFirstButton={true}
showLastButton={true}
style={paginationStyle}
{...controlledPage}
{...paginationProps}
/>
{totalCount !== undefined && (
<Typography className="mythic-table-total">{label}: {totalCount}</Typography>
)}
</Box>
);
};
@@ -0,0 +1,126 @@
import React from 'react';
import Box from '@mui/material/Box';
import Button from '@mui/material/Button';
import IconButton from '@mui/material/IconButton';
import MenuItem from '@mui/material/MenuItem';
import Select from '@mui/material/Select';
import ToggleButton from '@mui/material/ToggleButton';
import Tooltip from '@mui/material/Tooltip';
import SearchIcon from '@mui/icons-material/Search';
import MythicTextField from './MythicTextField';
export const MythicTableToolbar = ({children, className = "", style = {}}) => {
return (
<Box className={`mythic-table-toolbar ${className}`.trim()} style={style}>
{children}
</Box>
);
};
export const MythicTableToolbarGroup = ({children, grow = false, className = "", style = {}}) => {
return (
<Box className={`mythic-table-toolbar-group ${grow ? "mythic-table-toolbar-group-grow" : ""} ${className}`.trim()} style={style}>
{children}
</Box>
);
};
export const MythicToolbarSelect = ({children, className = "", style = {}, ...props}) => {
return (
<Select
className={`mythic-toolbar-select ${className}`.trim()}
size="small"
style={style}
{...props}
>
{children}
</Select>
);
};
export const MythicToolbarMenuItem = MenuItem;
export const MythicSearchField = ({
value,
onChange,
onEnter,
onSearch,
disabled = false,
placeholder = "Search...",
name = "Search",
showLabel = false,
autoFocus,
inputProps = {},
endAdornment = null,
}) => {
return (
<MythicTextField
disabled={disabled}
placeholder={placeholder}
value={value}
marginTop="0px"
marginBottom="0px"
showLabel={showLabel}
onChange={onChange}
onEnter={onEnter || onSearch}
name={name}
autoFocus={autoFocus}
InputProps={{
endAdornment: onSearch ? (
<React.Fragment>
{endAdornment}
<Tooltip title="Search">
<span>
<IconButton
className="mythic-toolbar-icon-button"
disabled={disabled}
onClick={onSearch}
size="small"
>
<SearchIcon color="info" fontSize="small" />
</IconButton>
</span>
</Tooltip>
</React.Fragment>
) : endAdornment,
style: {padding: 0},
...inputProps,
}}
/>
);
};
export const MythicToolbarButton = ({children, className = "", ...props}) => {
return (
<Button className={`mythic-toolbar-button ${className}`.trim()} size="small" {...props}>
{children}
</Button>
);
};
export const MythicToolbarToggle = ({
checked,
onClick,
label,
activeLabel,
inactiveLabel,
activeIcon,
inactiveIcon,
value = "toggle",
className = "",
...props
}) => {
return (
<ToggleButton
className={`mythic-toolbar-toggle ${className}`.trim()}
value={value}
selected={checked}
onClick={onClick}
size="small"
{...props}
>
{checked ? activeIcon : inactiveIcon}
<span>{checked ? (activeLabel || label) : (inactiveLabel || label)}</span>
</ToggleButton>
);
};
@@ -530,7 +530,7 @@ return (
) )}
</Select>
{selectedTag.id &&
<IconButton size="small" style={{float: "right"}} onClick={()=>{setOpenDeleteDialog(true);}} color="error" variant="contained"><DeleteIcon/></IconButton>
<IconButton className="mythic-table-row-icon-action mythic-table-row-icon-action-hover-danger" size="small" style={{float: "right"}} onClick={()=>{setOpenDeleteDialog(true);}}><DeleteIcon fontSize="small" /></IconButton>
}
{openDelete &&
<MythicConfirmDialog onClose={() => {setOpenDeleteDialog(false);}} onSubmit={onAcceptDelete} open={openDelete}/>
@@ -752,4 +752,4 @@ export const ViewEditTags = ({target_object, target_object_id}) => {
</React.Fragment>
)
}
}
@@ -9,8 +9,6 @@ import ListItem from '@mui/material/ListItem';
import ListItemIcon from '@mui/material/ListItemIcon';
import ListItemText from '@mui/material/ListItemText';
import Checkbox from '@mui/material/Checkbox';
import { CardContent } from '@mui/material';
import CardHeader from '@mui/material/CardHeader';
import Divider from '@mui/material/Divider';
const PREFIX = 'MythicTransferList';
@@ -61,10 +59,9 @@ const CustomListElement = ({value, onClick, itemKey}) => {
const CustomList = ({title, items, left, onClick, itemKey}) => {
return (
<>
<CardHeader title={title} />
<StyledDivider classes={{root: classes.divider}}/>
<CardContent style={{flexGrow: 1, height: "100%", width: "100%", overflowY: "auto", padding: 0}}>
<div className="mythic-transfer-list">
<div className="mythic-transfer-list-header">{title}</div>
<div className="mythic-transfer-list-body">
<List dense component="div" role="list" style={{padding:0, width: "100%"}}>
{items.map((value, index) => (
<div key={index}>
@@ -80,8 +77,8 @@ const CustomList = ({title, items, left, onClick, itemKey}) => {
))}
</List>
</CardContent>
</>
</div>
</div>
);
}
const CustomTransferList = ({leftTitle, rightTitle, initialData, parentLeftData, parentRightData, itemKey}) => {
@@ -148,7 +145,7 @@ const CustomTransferList = ({leftTitle, rightTitle, initialData, parentLeftData,
<div style={{paddingLeft: 0, flexGrow: 1, marginLeft: 0, marginRight: "10px", position: "relative", overflowY: "auto", display: "flex", flexDirection: "column" }}>
<CustomList title={leftTitle} left={true} items={data} onClick={handleToggle} itemKey={itemKey} />
</div>
<div style={{display: "flex", flexDirection: "column", justifyContent: "center"}}>
<div className="mythic-transfer-controls">
<StyledButton
variant="contained"
size="small"
@@ -253,4 +250,3 @@ export function MythicTransferListDialog(props) {
</>
);
}
+111 -70
View File
@@ -57,17 +57,11 @@ import AddCircleIcon from '@mui/icons-material/AddCircle';
import ManageSearchIcon from '@mui/icons-material/ManageSearch';
import MoreHorizIcon from '@mui/icons-material/MoreHoriz';
import Button from '@mui/material/Button';
import DialogActions from '@mui/material/DialogActions';
import DialogContent from '@mui/material/DialogContent';
import DialogTitle from '@mui/material/DialogTitle';
import TableRow from '@mui/material/TableRow';
import Table from '@mui/material/Table';
import TableBody from '@mui/material/TableBody';
import {TableContainer, TableHead, Paper} from '@mui/material';
import Select from '@mui/material/Select';
import Input from '@mui/material/Input';
import DeleteIcon from '@mui/icons-material/Delete';
import DragHandleIcon from '@mui/icons-material/DragHandle';
import MythicStyledTableCell from "./MythicComponents/MythicTableCell";
import {snackActions} from "./utilities/Snackbar";
import {Dropdown, DropdownMenuItem} from "./MythicComponents/MythicNestedMenus";
import ClickAwayListener from '@mui/material/ClickAwayListener';
@@ -80,6 +74,12 @@ import {reorder} from "./MythicComponents/MythicDraggableList";
import { useNavigate } from 'react-router-dom';
import LogoutIcon from '@mui/icons-material/Logout';
import TuneIcon from '@mui/icons-material/Tune';
import {
MythicDialogBody,
MythicDialogButton,
MythicDialogFooter,
MythicDialogSection,
} from "./MythicComponents/MythicDialogLayout";
const PREFIX = 'TopAppBarVertical';
@@ -97,13 +97,13 @@ const openedMixin = (theme) => ({
});
const closedMixin = (theme) => ({
overflowX: 'hidden',
width: `calc(${theme.spacing(4)} + 1px)`,
width: "60px",
borderRadius: "0 !important",
border: "0px !important",
background: `${theme.navigation.background} !important`,
backgroundColor: `${theme.navigation.backgroundColor} !important`,
[theme.breakpoints.up('sm')]: {
width: `calc(${theme.spacing(5)} + 1px)`,
width: "60px",
borderRadius: "0 !important",
border: "0px !important",
},
@@ -129,6 +129,9 @@ const Drawer = styled(MuiDrawer, { shouldForwardProp: (prop) => prop !== 'open'
},
[`& .${classes.listSubHeader}`]: {
backgroundColor: "transparent !important",
boxSizing: "border-box",
maxWidth: "calc(100% - 8px)",
width: "auto",
},
[`& .${classes.listSubHeader}:hover`]: {
color: `${theme.navigation.text} !important`,
@@ -151,7 +154,7 @@ const Drawer = styled(MuiDrawer, { shouldForwardProp: (prop) => prop !== 'open'
backgroundColor: "transparent !important",
border: "0 !important",
borderRadius: "0 !important",
}
},
},
},
},
@@ -171,7 +174,18 @@ const Drawer = styled(MuiDrawer, { shouldForwardProp: (prop) => prop !== 'open'
backgroundColor: "transparent !important",
border: "0 !important",
borderRadius: "0 !important",
}
},
'& .MuiListItemText-root': {
display: "none",
margin: "0 !important",
maxWidth: 0,
minWidth: 0,
overflow: "hidden",
width: 0,
},
'& .MuiListItem-root > .MuiSvgIcon-root': {
display: "none",
},
},
},
},
@@ -189,6 +203,13 @@ export const StyledListItem = styled(ListItem)(
borderRadius: theme.shape.borderRadius,
color: theme.navigation.text,
backgroundColor: "transparent !important",
boxSizing: "border-box",
maxWidth: "calc(100% - 8px)",
width: "auto",
"& .MuiListItemText-root": {
marginLeft: "0.35rem",
minWidth: 0,
},
"& .MuiListItemText-primary": {
fontSize: theme.typography.pxToRem(12.5),
fontWeight: 600,
@@ -203,10 +224,12 @@ export const StyledListItemIcon = styled(ListItemIcon)(
paddingTop:0,
marginTop: 0,
paddingBottom: 0,
minWidth: "32px",
minWidth: "36px",
width: "36px",
justifyContent: "center",
color: theme.navigation.icon,
backgroundColor: "transparent !important",
overflow: "visible",
}),
);
@@ -569,10 +592,9 @@ const AllSettingOptions = [
].sort();
const TopAppBarVerticalAdjustShortcutsDialog = ({onClose}) => {
const theme = useTheme();
const sideShortcuts = useGetMythicSetting({setting_name: "sideShortcuts", default_value: defaultShortcuts})
const [currentShortcuts, setCurrentShortcuts] = React.useState(sideShortcuts);
const [updateSetting, _] = useSetMythicSetting();
const [updateSetting] = useSetMythicSetting();
const reset = () => {
setCurrentShortcuts(defaultShortcuts);
}
@@ -611,75 +633,84 @@ const TopAppBarVerticalAdjustShortcutsDialog = ({onClose}) => {
return (
<React.Fragment>
<DialogTitle id="form-dialog-title">Configure Side Shortcuts</DialogTitle>
<div style={{height: "calc(70vh)", display: "flex", flexDirection: "column"}}>
<Paper elevation={5} style={{backgroundColor: theme.pageHeader.main, color: theme.pageHeaderText.main, marginBottom: "5px"}}>
<Button size={"small"} style={{color: "white", marginRight: "20px",}}
onClick={() => addShortcut(currentShortcuts.length)}
startIcon={<AddCircleIcon color="success" style={{backgroundColor: "white", borderRadius: "10px"}}/>}
<DialogContent dividers={true} sx={{p: 0}}>
<MythicDialogBody sx={{height: "min(70vh, 42rem)", p: 1}}>
<MythicDialogSection
title="Side Shortcuts"
actions={
<>
<Button
size="small"
onClick={() => addShortcut(currentShortcuts.length)}
startIcon={<AddCircleIcon fontSize="small" />}
>
Shortcut
</Button>
<Button size="small" onClick={reset} color="warning">
Reset
</Button>
</>
}
sx={{display: "flex", flexDirection: "column", flex: "1 1 auto", minHeight: 0}}
>
Shortcut
</Button>
<Button size={"small"} onClick={reset} color={"warning"}>
Reset To Defaults
</Button>
</Paper>
<TableContainer className="mythicElement" style={{flexGrow: 1}}>
<Table size="small" style={{width: "100%", "overflow": "scroll", tableLayout: "fixed"}}>
<TableHead>
<TableRow>
<MythicStyledTableCell style={{width: "2rem"}}></MythicStyledTableCell>
<MythicStyledTableCell style={{width: "2rem"}}></MythicStyledTableCell>
<MythicStyledTableCell></MythicStyledTableCell>
</TableRow>
</TableHead>
<DragDropContext onDragEnd={onDragEnd}>
<Droppable droppableId="vertical-shortcuts-column-list">
{(provided) => (
<TableBody ref={provided.innerRef} {...provided.droppableProps}>
<div className="mythic-reorder-list" ref={provided.innerRef} {...provided.droppableProps}>
{currentShortcuts.map((c, i) => (
<Draggable key={c + i} draggableId={c} index={i}>
<Draggable key={c + i} draggableId={`shortcut-${c}-${i}`} index={i}>
{(provided2, snapshot) => (
<TableRow hover ref={provided2.innerRef}
{...provided2.draggableProps}
{...provided2.dragHandleProps}
<div
ref={provided2.innerRef}
className={`mythic-reorder-row${snapshot.isDragging ? " mythic-reorder-row-dragging" : ""}`}
{...provided2.draggableProps}
>
<MythicStyledTableCell style={{width: "2rem"}}>
<DragHandleIcon/>
</MythicStyledTableCell>
<MythicStyledTableCell style={{width: "2rem"}}>
<IconButton onClick={() => removeShortcut(i)}>
<DeleteIcon color={"error"}/>
</IconButton>
</MythicStyledTableCell>
<MythicStyledTableCell style={{}}>
<span className="mythic-reorder-drag-handle" {...provided2.dragHandleProps}>
<DragHandleIcon fontSize="small" />
</span>
<div className="mythic-reorder-row-main">
<Select
className="mythic-reorder-select"
fullWidth
size="small"
value={c}
onChange={(e) => onChangeShortcutValue(e, i)}
input={<Input style={{width: "100%"}}/>}
>
{AllSettingOptions.map((opt) => (
<MenuItem value={opt} key={opt}>{opt}</MenuItem>
))}
</Select>
</MythicStyledTableCell>
</TableRow>)}
</div>
<div className="mythic-reorder-row-actions">
<IconButton
aria-label={`Remove ${c}`}
className="mythic-table-row-icon-action mythic-table-row-icon-action-hover-danger"
size="small"
onClick={() => removeShortcut(i)}
>
<DeleteIcon fontSize="small" />
</IconButton>
</div>
</div>
)}
</Draggable>
))}
</TableBody>)}
{provided.placeholder}
</div>
)}
</Droppable>
</DragDropContext>
</Table>
</TableContainer>
</div>
<DialogActions>
<Button onClick={onClose} variant="contained" color="primary">
</MythicDialogSection>
</MythicDialogBody>
</DialogContent>
<MythicDialogFooter>
<MythicDialogButton onClick={onClose}>
Cancel
</Button>
<Button onClick={onUpdate} variant="contained" color="success">
Update
</Button>
</DialogActions>
</MythicDialogButton>
<MythicDialogButton onClick={onUpdate} intent="primary">
Save
</MythicDialogButton>
</MythicDialogFooter>
</React.Fragment>
)
}
@@ -796,17 +827,27 @@ export function TopAppBarVertical(props) {
<List style={{paddingTop: 0, marginTop: 0, height: "100%", display: "flex", flexDirection: "column",
backgroundColor: "transparent !important",
border: "0 !important", borderRadius: 0}}>
<ListItem className={classes.listSubHeader} style={{margin: "4px 4px 6px", paddingTop: "5px", paddingLeft: "4px", paddingBottom: "5px", borderRadius: theme.shape.borderRadius}}>
<ListItemIcon >
<img src={ReactLogo} onClick={()=>navigate('/new')} width={"40px"} height={"35px"} alt="Mythic" style={{cursor: "pointer"}}/>
</ListItemIcon>
<ListItem className={classes.listSubHeader} style={{
alignItems: "center",
height: "52px",
margin: "4px 4px 6px",
minHeight: "52px",
paddingTop: "5px",
paddingLeft: "8px",
paddingRight: "8px",
paddingBottom: "5px",
borderRadius: theme.shape.borderRadius,
}}>
<StyledListItemIcon>
<img src={ReactLogo} onClick={()=>navigate('/new')} width={"32px"} height={"28px"} alt="Mythic" style={{cursor: "pointer"}}/>
</StyledListItemIcon>
<ListItemText style={{margin: 0}} primary={
<>
<Typography style={{ fontSize: 12, color: theme.navigation.muted, display: "inline-block", lineHeight: 1.35}}>
<b>Mythic:</b> v{serverVersion}<br/>
<b>UI:</b> v{mythicUIVersion}<br/>
</Typography>
<IconButton onClick={props.toggleTheme} style={{float:"right", display: menuOpen ? "" : "none", color: theme.navigation.icon}} >
<IconButton onClick={props.toggleTheme} size="small" style={{float:"right", display: menuOpen ? "" : "none", color: theme.navigation.icon}} >
{theme.palette.mode === 'light' &&
<DarkModeTwoToneIcon style={{color: theme.navigation.icon}} fontSize={"medium"} className="mythicElement" />
}
@@ -1051,10 +1092,10 @@ function TopBarRightShortcutsVertical({me, menuOpen, serverName}){
<StyledListItemIcon>
<MythicStyledTooltip title={"Documentation Links"} tooltipStyle={{display: "inline-flex"}}>
<HelpTwoToneIcon style={{color: theme.navBarTextIconColor}} fontSize={"medium"} className="mythicElement"/>
<KeyboardArrowDownIcon style={{color: theme.navBarTextIconColor, display: menuOpen ? "" : "none"}} />
</MythicStyledTooltip>
</StyledListItemIcon>
<ListItemText primary={"Help"} />
<KeyboardArrowDownIcon style={{color: theme.navBarTextIconColor, display: menuOpen ? "" : "none", flex: "0 0 auto", marginLeft: "auto"}} />
</StyledListItem>
<StyledListItem className={classes.listSubHeader} component={Link} to='/new/EventFeed' >
@@ -1070,10 +1111,10 @@ function TopBarRightShortcutsVertical({me, menuOpen, serverName}){
<StyledListItemIcon>
<MythicStyledTooltip title={"User Settings"} tooltipStyle={{display: "inline-flex"}}>
<ManageAccountsTwoToneIcon style={{color: theme.navBarTextIconColor}} fontSize={"medium"} className="mythicElement" />
<KeyboardArrowDownIcon style={{color: theme.navBarTextIconColor, display: menuOpen ? "" : "none"}} />
</MythicStyledTooltip>
</StyledListItemIcon>
<ListItemText primary={"Settings"} />
<KeyboardArrowDownIcon style={{color: theme.navBarTextIconColor, display: menuOpen ? "" : "none", flex: "0 0 auto", marginLeft: "auto"}} />
</StyledListItem>
</>
)
@@ -5,35 +5,46 @@ 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 Paper from '@mui/material/Paper';
import Typography from '@mui/material/Typography';
import { BrowserScriptsTableRow } from './BrowserScriptsTableRow';
import {useTheme} from '@mui/material/styles';
import AddCircleIcon from '@mui/icons-material/AddCircle';
import Button from '@mui/material/Button';
import { MythicDialog } from '../../MythicComponents/MythicDialog';
import {EditScriptDialog} from './EditScriptDialog';
import {MythicPageHeader} from "../../MythicComponents/MythicPageHeader";
import {MythicPageHeader, MythicPageHeaderChip} from "../../MythicComponents/MythicPageHeader";
import {MythicToolbarButton} from "../../MythicComponents/MythicTableToolbar";
import {MythicTableEmptyState} from "../../MythicComponents/MythicStateDisplay";
export function BrowserScriptsTable(props){
const theme = useTheme();
const [openNewScriptDialog, setOpenNewScriptDialog] = React.useState(false);
const scriptCountLabel = props.browserscripts.length === 1 ? "1 script" : `${props.browserscripts.length} scripts`;
const activeCount = props.browserscripts.filter((script) => script.active).length;
const activeCountLabel = activeCount === 1 ? "1 active" : `${activeCount} active`;
const modifiedCount = props.browserscripts.filter((script) => script.user_modified).length;
return (
<>
<MythicPageHeader title={"Browser Scripts"}>
<Button size="small" onClick={() => setOpenNewScriptDialog(true)} style={{whiteSpace: "nowrap", color: "white"}}
startIcon={<AddCircleIcon color={"success"} style={{backgroundColor: "white", borderRadius: "10px"}}/>} >
New Script
</Button>
{openNewScriptDialog &&
<MythicDialog fullWidth={true} maxWidth="xl" open={openNewScriptDialog}
onClose={()=>{setOpenNewScriptDialog(false);}}
innerDialog={
<EditScriptDialog me={props.me} onClose={()=>{setOpenNewScriptDialog(false);}} title="Create New Browser Script" new={true} onSubmitEdit={props.onSubmitNew} />
} />
<MythicPageHeader
title={"Browser Scripts"}
subtitle={"Manage custom browser script renderers for task output in the current UI."}
meta={
<>
<MythicPageHeaderChip label={scriptCountLabel} />
<MythicPageHeaderChip label={activeCountLabel} />
{modifiedCount > 0 && <MythicPageHeaderChip label={`${modifiedCount} modified`} />}
</>
}
</MythicPageHeader>
actions={
<MythicToolbarButton variant="contained" color="primary" onClick={() => setOpenNewScriptDialog(true)} startIcon={<AddCircleIcon />}>
Script
</MythicToolbarButton>
}
/>
{openNewScriptDialog &&
<MythicDialog fullWidth={true} maxWidth="xl" open={openNewScriptDialog}
onClose={()=>{setOpenNewScriptDialog(false);}}
innerDialog={
<EditScriptDialog me={props.me} onClose={()=>{setOpenNewScriptDialog(false);}} title="Create New Browser Script" new={true} onSubmitEdit={props.onSubmitNew} />
} />
}
<TableContainer className="mythicElement" style={{height: "100%", flexGrow: 1}}>
<Table stickyHeader={true} size="small" style={{"tableLayout": "fixed", "maxWidth": "100%", "overflow": "scroll"}}>
<TableHead>
@@ -47,6 +58,14 @@ export function BrowserScriptsTable(props){
</TableRow>
</TableHead>
<TableBody>
{props.browserscripts.length === 0 &&
<MythicTableEmptyState
colSpan={6}
compact
title="No browser scripts"
description="Create or import browser scripts to customize task output rendering."
/>
}
{props.browserscripts.map( (op) => (
<BrowserScriptsTableRow
me={props.me}
@@ -63,4 +82,3 @@ export function BrowserScriptsTable(props){
)
}
@@ -4,7 +4,7 @@ import TableCell from '@mui/material/TableCell';
import TableRow from '@mui/material/TableRow';
import { MythicDialog } from '../../MythicComponents/MythicDialog';
import {EditScriptDialog} from './EditScriptDialog';
import SettingsIcon from '@mui/icons-material/Settings';
import EditIcon from '@mui/icons-material/Edit';
import IconButton from '@mui/material/IconButton';
import {MythicStyledTooltip} from "../../MythicComponents/MythicStyledTooltip";
import {MythicAgentSVGIcon} from "../../MythicComponents/MythicAgentSVGIcon";
@@ -24,7 +24,9 @@ export function BrowserScriptsTableRow(props){
<React.Fragment>
<TableRow key={"payload" + props.id} hover>
<TableCell >
<IconButton size="small" onClick={()=>{setOpenEdit(true);}} color="info" variant="contained"><SettingsIcon color="info" /></IconButton>
<IconButton className="mythic-table-row-icon-action mythic-table-row-icon-action-hover-info" size="small" onClick={()=>{setOpenEdit(true);}}>
<EditIcon fontSize="small" />
</IconButton>
</TableCell>
<TableCell >
<Switch
@@ -56,4 +58,3 @@ export function BrowserScriptsTableRow(props){
</React.Fragment>
)
}
@@ -1,6 +1,4 @@
import React, {useEffect} from 'react';
import Button from '@mui/material/Button';
import DialogActions from '@mui/material/DialogActions';
import React from 'react';
import DialogContent from '@mui/material/DialogContent';
import DialogTitle from '@mui/material/DialogTitle';
import List from '@mui/material/List';
@@ -8,11 +6,14 @@ import ListItem from '@mui/material/ListItem';
import ListItemIcon from '@mui/material/ListItemIcon';
import ListItemText from '@mui/material/ListItemText';
import Checkbox from '@mui/material/Checkbox';
import CardHeader from '@mui/material/CardHeader';
import {gql, useQuery} from '@apollo/client';
import { CardContent } from '@mui/material';
import {classes, StyledButton, StyledDivider} from '../../MythicComponents/MythicTransferList';
import {MythicDraggableDialogTitle} from "../../MythicComponents/MythicDraggableDialogTitle";
import {classes, StyledButton} from '../../MythicComponents/MythicTransferList';
import {
MythicDialogButton,
MythicDialogBody,
MythicDialogFooter,
MythicDialogSection
} from '../../MythicComponents/MythicDialogLayout';
const getCommandsQuery = gql`
query GetCallbackDetails($callback_id: Int!) {
@@ -68,15 +69,14 @@ const CustomList = ({title, items, handleToggle}) => {
))
}, [items, handleToggle]);
return (
<>
<CardHeader className={classes.cardHeader} title={title}/>
<StyledDivider classes={{root: classes.divider}}/>
<CardContent style={{flexGrow: 1, overflowY: "auto", padding: 0}}>
<div className="mythic-transfer-list">
<div className="mythic-transfer-list-header">{title}</div>
<div className="mythic-transfer-list-body">
<List dense component="div" role="list" style={{padding: 0, width: "100%"}}>
{renderedList}
</List>
</CardContent>
</>
</div>
</div>
)
};
export function AddRemoveCallbackCommandsDialog(props) {
@@ -187,67 +187,71 @@ export function AddRemoveCallbackCommandsDialog(props) {
<React.Fragment>
<DialogTitle>Add or Remove Commands for Callback {props.display_id} </DialogTitle>
<DialogContent dividers={true} style={{height: "100%", display: "flex", flexDirection: "column", position: "relative", overflowY: "auto"}}>
This will add or remove commands associated with this callback from Mythic's perspective.
This does NOT add or remove commands within the payload itself that's beaconing out to Mythic.
<div style={{display: "flex", flexDirection: "row", overflowY: "auto", flexGrow: 1, minHeight: 0}}>
<div style={{paddingLeft: 0, flexGrow: 1, marginLeft: 0, marginRight: "10px", position: "relative", overflowY: "auto", display: "flex", flexDirection: "column" }}>
<CustomList title={leftTitle} items={left} handleToggle={handleToggle}/>
</div>
<div style={{display: "flex", flexDirection: "column", justifyContent: "center"}}>
<StyledButton
variant="contained"
size="small"
className={classes.button}
onClick={handleAllRight}
disabled={left.length === 0}
aria-label="move all right"
>
&gt;&gt;
</StyledButton>
<StyledButton
variant="contained"
size="small"
className={classes.button}
onClick={handleCheckedRight}
aria-label="move selected right"
>
&gt;
</StyledButton>
<StyledButton
variant="contained"
size="small"
className={classes.button}
onClick={handleCheckedLeft}
aria-label="move selected left"
>
&lt;
</StyledButton>
<StyledButton
variant="contained"
size="small"
className={classes.button}
onClick={handleAllLeft}
disabled={right.length === 0}
aria-label="move all left"
>
&lt;&lt;
</StyledButton>
</div>
<div style={{marginLeft: "10px", position: "relative", flexGrow: 1, display: "flex", flexDirection: "column" }}>
<CustomList title={rightTitle} items={right} handleToggle={handleToggle} />
</div>
</div>
<MythicDialogBody>
<MythicDialogSection
title="Command Availability"
description="Updates Mythic's callback command association without changing commands inside the active payload."
>
<div style={{display: "flex", flexDirection: "row", overflowY: "auto", flexGrow: 1, minHeight: 0}}>
<div style={{paddingLeft: 0, flexGrow: 1, marginLeft: 0, marginRight: "10px", position: "relative", overflowY: "auto", display: "flex", flexDirection: "column" }}>
<CustomList title={leftTitle} items={left} handleToggle={handleToggle}/>
</div>
<div className="mythic-transfer-controls">
<StyledButton
variant="contained"
size="small"
className={classes.button}
onClick={handleAllRight}
disabled={left.length === 0}
aria-label="move all right"
>
&gt;&gt;
</StyledButton>
<StyledButton
variant="contained"
size="small"
className={classes.button}
onClick={handleCheckedRight}
aria-label="move selected right"
>
&gt;
</StyledButton>
<StyledButton
variant="contained"
size="small"
className={classes.button}
onClick={handleCheckedLeft}
aria-label="move selected left"
>
&lt;
</StyledButton>
<StyledButton
variant="contained"
size="small"
className={classes.button}
onClick={handleAllLeft}
disabled={right.length === 0}
aria-label="move all left"
>
&lt;&lt;
</StyledButton>
</div>
<div style={{marginLeft: "10px", position: "relative", flexGrow: 1, display: "flex", flexDirection: "column" }}>
<CustomList title={rightTitle} items={right} handleToggle={handleToggle} />
</div>
</div>
</MythicDialogSection>
</MythicDialogBody>
</DialogContent>
<DialogActions>
<Button onClick={props.onClose} variant="contained" color="primary">
<MythicDialogFooter>
<MythicDialogButton onClick={props.onClose}>
Close
</Button>
<Button onClick={setFinalTags} variant="contained" color="success">
</MythicDialogButton>
<MythicDialogButton intent="primary" onClick={setFinalTags}>
Submit
</Button>
</DialogActions>
</MythicDialogButton>
</MythicDialogFooter>
</React.Fragment>
);
}
@@ -517,7 +517,7 @@ const getTaskWidth = (node) => {
return Math.max(325, (nodeText.length * 8) + 10)
}
const getEventNodeWidth = (node) => {
return (node.maxNameLength * 8) + 10 + 100;
return Math.max(190, (node.maxNameLength * 8) + 120);
}
const getBrowserscriptWidth = (node) => {
let nodeText = " ";
@@ -534,7 +534,7 @@ const getHeight = (node) => {
return 0;
}
if(node.type === "eventNode"){
return 20;
return 88;
}
return 80;
}
@@ -1,24 +1,25 @@
import React from 'react';
import Button from '@mui/material/Button';
import DialogActions from '@mui/material/DialogActions';
import DialogContent from '@mui/material/DialogContent';
import DialogTitle from '@mui/material/DialogTitle';
import IconButton from '@mui/material/IconButton';
import { useTheme } from '@mui/material/styles';
import {reorder} from "../../MythicComponents/MythicDraggableList";
import {
Draggable,
DragDropContext,
Droppable,
} from "@hello-pangea/dnd";
import { List, ListItem, ListItemText, Paper } from '@mui/material';
import DragHandleIcon from '@mui/icons-material/DragHandle';
import VisibilityOffIcon from '@mui/icons-material/VisibilityOff';
import VisibilityIcon from '@mui/icons-material/Visibility';
import {
MythicDialogBody,
MythicDialogButton,
MythicDialogFooter,
MythicDialogSection,
} from "../../MythicComponents/MythicDialogLayout";
export function CallbacksTableColumnsReorderDialog({initialItems, onSubmit, onClose, onReset, visible, hidden}) {
const [items, setItems] = React.useState(initialItems);
const theme = useTheme();
const onDragEnd = ({ destination, source }) => {
// dropped outside the list
if (!destination) return;
@@ -26,13 +27,12 @@ export function CallbacksTableColumnsReorderDialog({initialItems, onSubmit, onCl
setItems(newItems);
};
React.useEffect( () => {
const newItems = items.map( c => {
setItems((currentItems) => currentItems.map( c => {
if(visible.includes(c.name)){
return {...c, visible: true};
}
return {...c, visible: false};
})
setItems(newItems);
}));
}, [visible, hidden]);
const onToggleVisibility = (i) => {
const newItems = items.map( (c, index) => {
@@ -49,21 +49,28 @@ export function CallbacksTableColumnsReorderDialog({initialItems, onSubmit, onCl
return (
<React.Fragment>
<DialogTitle id="form-dialog-title">Drag and Drop to Adjust the Order and Toggle Visibility</DialogTitle>
<DialogContent dividers={true} style={{height: "100%", margin: 0, padding: 0, background: theme.palette.background.main}}>
<DraggableList items={items} onToggleVisibility={onToggleVisibility} onDragEnd={onDragEnd} />
<DialogTitle id="form-dialog-title">Column Layout</DialogTitle>
<DialogContent dividers={true} sx={{p: 0}}>
<MythicDialogBody sx={{height: "min(70vh, 42rem)", p: 1}}>
<MythicDialogSection
title="Columns"
sx={{display: "flex", flexDirection: "column", flex: "1 1 auto", minHeight: 0}}
>
<DraggableList items={items} onToggleVisibility={onToggleVisibility} onDragEnd={onDragEnd} />
</MythicDialogSection>
</MythicDialogBody>
</DialogContent>
<DialogActions>
<Button onClick={onClose} variant="contained" color="primary">
<MythicDialogFooter>
<MythicDialogButton onClick={onClose}>
Close
</Button>
<Button onClick={onReset} variant="contained" color="warning">
</MythicDialogButton>
<MythicDialogButton onClick={onReset} intent="warning">
Reset
</Button>
<Button onClick={onFinish} variant="contained" color="success">
Submit
</Button>
</DialogActions>
</MythicDialogButton>
<MythicDialogButton onClick={onFinish} intent="primary">
Save
</MythicDialogButton>
</MythicDialogFooter>
</React.Fragment>
);
}
@@ -73,46 +80,48 @@ export const DraggableList = ({ items, onDragEnd, onToggleVisibility }) => {
<DragDropContext onDragEnd={onDragEnd}>
<Droppable droppableId="callback-table-column-list">
{(provided) => (
<List style={{border: 0, padding: 0}} ref={provided.innerRef} {...provided.droppableProps}>
<div className="mythic-reorder-list" ref={provided.innerRef} {...provided.droppableProps}>
{items.map((item, index) => (
<DraggableListItem onToggleVisibility={onToggleVisibility} item={item} index={index} key={item.key} />
))}
{provided.placeholder}
</List>
</div>
)}
</Droppable>
</DragDropContext>
);
};
export const DraggableListItem = ({ item, index, onToggleVisibility }) => {
const theme = useTheme();
return (
<Draggable draggableId={item.key} index={index}>
{(provided, snapshot) => (
<ListItem
<div
ref={provided.innerRef}
className={`mythic-reorder-row${snapshot.isDragging ? " mythic-reorder-row-dragging" : ""}${item.visible ? "" : " mythic-reorder-row-disabled"}`}
{...provided.draggableProps}
{...provided.dragHandleProps}
component={Paper}
disabled={!item.visible}
sx={snapshot.isDragging ? { background: theme.palette.secondary.main } : {
}}
>
<DragHandleIcon style={{marginRight: "10px"}}/>
<ListItemText primary={item.name} />
<IconButton onClick={() => onToggleVisibility(index)} style={{float: "right", margin: 0, padding: 0}}>
{item.visible ? (
<VisibilityIcon color={"success"} />
) : (
<VisibilityOffIcon color={"error"} />
)
}
</IconButton>
</ListItem>
<span className="mythic-reorder-drag-handle" {...provided.dragHandleProps}>
<DragHandleIcon fontSize="small" />
</span>
<div className="mythic-reorder-row-main">
<span className="mythic-reorder-row-title">{item.name}</span>
</div>
<div className="mythic-reorder-row-actions">
<IconButton
aria-label={item.visible ? `Hide ${item.name}` : `Show ${item.name}`}
className={`mythic-table-row-icon-action ${item.visible ? "mythic-table-row-icon-action-hover-danger" : "mythic-table-row-icon-action-hover-info"}`}
size="small"
onClick={() => onToggleVisibility(index)}
>
{item.visible ? (
<VisibilityIcon fontSize="small" />
) : (
<VisibilityOffIcon fontSize="small" />
)}
</IconButton>
</div>
</div>
)}
</Draggable>
);
};
@@ -476,7 +476,8 @@ export const CallbacksTabsCustomFileBasedBrowserTable = (props) => {
{
name: removeDisplay, type: "item",
disabled: removeCommand === undefined,
icon: <DeleteIcon color="error" style={{ paddingRight: '5px' }} />,
danger: true,
icon: <DeleteIcon style={{ paddingRight: '5px' }} />,
click: ({event}) => {
event.stopPropagation();
props.onTaskRowAction({
@@ -546,7 +547,8 @@ export const CallbacksTabsCustomFileBasedBrowserTable = (props) => {
{
name: `Remove All Selected`, type: "item",
disabled: removeCommand === undefined,
icon: <DeleteIcon color="error" style={{ paddingRight: '5px' }} />,
danger: true,
icon: <DeleteIcon style={{ paddingRight: '5px' }} />,
click: ({event}) => {
event.stopPropagation();
let newTasks = [];
@@ -973,4 +975,4 @@ export const TableRowSizeCell = ({ cellData, rowData }) => {
}
};
return getStringSize(cellData);
};
};
@@ -539,7 +539,8 @@ export const CallbacksTabsFileBrowserTable = (props) => {
},
{
name: removeDisplay, type: "item", disabled: removeCommand === undefined,
icon: <DeleteIcon color="error" style={{ paddingRight: '5px' }} />,
danger: true,
icon: <DeleteIcon style={{ paddingRight: '5px' }} />,
click: ({event}) => {
event.stopPropagation();
props.onTaskRowAction({
@@ -582,7 +583,8 @@ export const CallbacksTabsFileBrowserTable = (props) => {
},
{
name: `Remove All Selected`, type: "item",
icon: <DeleteIcon color="error" style={{ paddingRight: '5px' }} />,
danger: true,
icon: <DeleteIcon style={{ paddingRight: '5px' }} />,
click: ({event}) => {
event.stopPropagation();
let newTasks = [];
@@ -1016,6 +1018,7 @@ const FileBrowserTableRowActionCell = ({ rowData, cellData, onTaskRowAction, tre
<DropdownMenuItem
key={option.name}
disabled={option.disabled}
className={option.danger ? "mythic-menu-item-hover-danger" : undefined}
onClick={(event) => handleMenuItemClick(event, option.click)}
>
{option.icon} {option.name}
@@ -1029,6 +1032,7 @@ const FileBrowserTableRowActionCell = ({ rowData, cellData, onTaskRowAction, tre
<DropdownMenuItem
key={menuOption.name}
disabled={menuOption.disabled}
className={menuOption.danger ? "mythic-menu-item-hover-danger" : undefined}
onClick={(event) => handleMenuItemClick(event, menuOption.click)}
>
{menuOption.icon}{menuOption.name}
@@ -8,8 +8,6 @@ import TableHead from '@mui/material/TableHead';
import Typography from '@mui/material/Typography';
import {useQuery, gql, useMutation} from '@apollo/client';
import LinearProgress from '@mui/material/LinearProgress';
import Paper from '@mui/material/Paper';
import {useTheme} from '@mui/material/styles';
import DialogActions from '@mui/material/DialogActions';
import DialogContent from '@mui/material/DialogContent';
import DialogTitle from '@mui/material/DialogTitle';
@@ -34,6 +32,7 @@ import {HostFileDialog} from "../Payloads/HostFileDialog";
import PublicIcon from '@mui/icons-material/Public';
import {payloadsCallbackAllowed} from "../Payloads/Payloads";
import Switch from '@mui/material/Switch';
import {MythicSectionHeader} from "../../MythicComponents/MythicPageHeader";
const GET_Payload_Details = gql`
query GetCallbackDetails($callback_id: Int!) {
@@ -184,7 +183,6 @@ mutation removeLoadedCommand($id: Int!){
}
`;
export function DetailedCallbackTable(props){
const theme = useTheme();
const me = useReactiveVar(meState);
const [openHostDialog, setOpenHostDialog] = React.useState(false);
const [openDetailedView, setOpenDetailedView] = React.useState(false);
@@ -335,18 +333,10 @@ export function DetailedCallbackTable(props){
<React.Fragment>
<DialogTitle id="form-dialog-title">Callback Configuration</DialogTitle>
<DialogContent dividers={true}>
<Paper elevation={5} style={{backgroundColor: theme.pageHeader.main, color: theme.pageHeaderText.main,marginBottom: "5px", marginTop: "10px"}} variant={"elevation"}>
<Typography variant="h6" style={{textAlign: "left", display: "inline-block", marginLeft: "20px", color: theme.pageHeaderColor}}>
Callback Information
</Typography>
</Paper>
<MythicSectionHeader title="Callback Information" sx={{mt: 0}} />
<ExpandedCallbackSideDetailsTable {...data.callback_by_pk} />
<Paper elevation={5} style={{backgroundColor: theme.pageHeader.main, color: theme.pageHeaderText.main,marginBottom: "5px", marginTop: "10px"}} variant={"elevation"}>
<Typography variant="h6" style={{textAlign: "left", display: "inline-block", marginLeft: "20px", color: theme.pageHeaderColor}}>
Payload Information
</Typography>
</Paper>
<MythicSectionHeader title="Payload Information" />
<TableContainer className="mythicElement">
<Table size="small" aria-label="details" style={{ "overflowWrap": "break-word"}}>
<TableHead>
@@ -454,11 +444,7 @@ export function DetailedCallbackTable(props){
</TableBody>
</Table>
</TableContainer>
<Paper elevation={5} style={{backgroundColor: theme.pageHeader.main, color: theme.pageHeaderText.main,marginBottom: "5px", marginTop: "10px"}} variant={"elevation"}>
<Typography variant="h6" style={{textAlign: "left", display: "inline-block", marginLeft: "20px", color: theme.pageHeaderColor}}>
Build Parameters
</Typography>
</Paper>
<MythicSectionHeader title="Build Parameters" />
<TableContainer className="mythicElement">
<Table size="small" aria-label="details" style={{ "overflowWrap": "break-word"}}>
<TableHead>
@@ -496,11 +482,7 @@ export function DetailedCallbackTable(props){
</TableBody>
</Table>
</TableContainer>
<Paper elevation={5} style={{backgroundColor: theme.pageHeader.main, color: theme.pageHeaderText.main,marginBottom: "5px", marginTop: "10px"}} variant={"elevation"}>
<Typography variant="h6" style={{textAlign: "left", display: "inline-block", marginLeft: "20px", color: theme.pageHeaderColor}}>
Build Steps
</Typography>
</Paper>
<MythicSectionHeader title="Build Steps" />
<TableContainer className="mythicElement">
<Table size="small" aria-label="details" style={{ "overflowWrap": "break-word"}}>
<TableHead>
@@ -530,11 +512,7 @@ export function DetailedCallbackTable(props){
</TableContainer>
{ c2Profiles.map( (c2) => (
<React.Fragment key={"c2frag" + data.callback_by_pk.payload.id + c2.c2_profile}>
<Paper elevation={5} style={{backgroundColor: theme.pageHeader.main, color: theme.pageHeaderText.main,marginBottom: "5px", marginTop: "10px"}} variant={"elevation"}>
<Typography variant="h6" style={{textAlign: "left", display: "inline-block", marginLeft: "20px", color: theme.pageHeaderColor}}>
{c2.c2_profile}
</Typography>
</Paper>
<MythicSectionHeader title={c2.c2_profile} />
<TableContainer className="mythicElement">
<Table size="small" aria-label="details" style={{"overflowWrap": "break-word"}}>
<TableHead>
@@ -565,13 +543,12 @@ export function DetailedCallbackTable(props){
</TableContainer>
</React.Fragment>
))}
<Paper elevation={5} style={{backgroundColor: theme.pageHeader.main, color: theme.pageHeaderText.main,marginBottom: "5px", marginTop: "10px"}} variant={"elevation"}>
<Typography variant="h6" style={{textAlign: "left", display: "inline-block", marginLeft: "20px", color: theme.pageHeaderColor}}>
Loaded Commands
</Typography>
<Button style={{float: "right"}} variant="contained" size="small" onClick={()=>{setOpenAddRemoveCommandsDialog(true)}} >Add/Remove Commands</Button>
</Paper>
<MythicSectionHeader
title="Loaded Commands"
actions={
<Button variant="outlined" size="small" onClick={()=>{setOpenAddRemoveCommandsDialog(true)}}>Add/Remove Commands</Button>
}
/>
<TableContainer className="mythicElement">
<Table size="small" aria-label="details" style={{"overflowWrap": "break-word"}}>
<TableHead>
@@ -594,10 +571,16 @@ export function DetailedCallbackTable(props){
<TableCell>{cmd.mythic}</TableCell>
<TableCell>{cmd.payload}</TableCell>
<TableCell>
<IconButton variant="contained" target="_blank"
href={"/docs/agents/" + cmd.payload_type + "/commands/" + cmd.cmd}>
<MenuBookIcon />
</IconButton>
<MythicStyledTooltip title="Open command documentation">
<IconButton
className="mythic-table-row-icon-action mythic-table-row-icon-action-hover-info"
size="small"
target="_blank"
href={"/docs/agents/" + cmd.payload_type + "/commands/" + cmd.cmd}
>
<MenuBookIcon fontSize="small" />
</IconButton>
</MythicStyledTooltip>
</TableCell>
</TableRow>
))
@@ -1,10 +1,15 @@
import React from 'react';
import Button from '@mui/material/Button';
import DialogActions from '@mui/material/DialogActions';
import DialogContent from '@mui/material/DialogContent';
import DialogTitle from '@mui/material/DialogTitle';
import {gql, useMutation} from '@apollo/client';
import { snackActions } from '../../utilities/Snackbar';
import {DragAndDropFileUpload} from './TaskParametersDialogRow';
import {
MythicDialogBody,
MythicDialogButton,
MythicDialogFooter,
MythicDialogSection
} from '../../MythicComponents/MythicDialogLayout';
const import_callback = gql`
mutation importCallbackConfigMutation($config: jsonb!) {
@@ -35,35 +40,36 @@ export function ImportCallbackConfigDialog(props) {
snackActions.error("Failed to parse configuration")
}
}
const onFileChange = (evt) => {
const onFileChange = (newFile) => {
const reader = new FileReader();
reader.onload = (e) => {
const contents = e.target.result;
setFileValue({name: evt.target.files[0].name, contents: contents});
setFileValue({name: newFile.name, contents: contents});
}
reader.readAsBinaryString(evt.target.files[0]);
reader.readAsBinaryString(newFile);
}
return (
<React.Fragment>
<DialogTitle id="form-dialog-title">Import Callback Config From Other Mythic Server</DialogTitle>
<DialogContent dividers={true}>
Export a callback config from another Mythic server and import it here to interact with that callback from this server.
<br/>
<Button variant="contained" component="label">
{ fileValue.name === "" ? "Select File" : fileValue.name }
<input onChange={onFileChange} type="file" hidden />
</Button>
<MythicDialogBody>
<MythicDialogSection
title="Callback Config File"
description="Upload an exported callback configuration from another Mythic server."
>
<DragAndDropFileUpload value={fileValue} multiple={false} onChange={onFileChange} />
</MythicDialogSection>
</MythicDialogBody>
</DialogContent>
<DialogActions>
<Button variant="contained" onClick={props.onClose} color="primary">
<MythicDialogFooter>
<MythicDialogButton onClick={props.onClose}>
Close
</Button>
<Button variant="contained" onClick={onCommitSubmit} color="success">
</MythicDialogButton>
<MythicDialogButton intent="primary" onClick={onCommitSubmit}>
Submit
</Button>
</DialogActions>
</MythicDialogButton>
</MythicDialogFooter>
</React.Fragment>
);
}
@@ -151,7 +151,7 @@ export function ModifyCallbackMythicTreeGroupsDialog(props){
{groups.map( (a, i) => (
<TableRow key={'array' + props.name + i} >
<MythicStyledTableCell style={{width: "2rem", paddingLeft:"0"}}>
<IconButton onClick={(e) => {removeArrayValue(i)}} size="large"><DeleteIcon color="error" /> </IconButton>
<IconButton className="mythic-table-row-icon-action mythic-table-row-icon-action-hover-danger" size="small" onClick={(e) => {removeArrayValue(i)}}><DeleteIcon fontSize="small" /> </IconButton>
</MythicStyledTableCell>
<MythicStyledTableCell>
<MythicTextField required={props.required} fullWidth={true} placeholder={""} value={a} multiline={true} autoFocus={ i > 0}
@@ -18,6 +18,7 @@ import {GetMythicSetting} from "../../MythicComponents/MythicSavedUserSetting";
import {ResponseDisplayGraph} from "./ResponseDisplayGraph";
import {operatorSettingDefaults} from "../../../cache";
import {ResponseDisplayTabs} from "./ResponseDisplayTabs";
import {MythicEmptyState, MythicLoadingState, MythicSearchEmptyState} from "../../MythicComponents/MythicStateDisplay";
const subResponsesStream = gql`
subscription subResponsesStream($task_id: Int!){
@@ -588,12 +589,47 @@ const ResponseDisplayComponent = ({rawResponses, viewBrowserScript, output, comm
fetchScripts({variables: {command_id: command_id}});
}
}, [command_id, task.id]);
const stateSx = {
backgroundColor: "transparent",
color: "inherit",
height: expand ? "100%" : undefined,
};
if(loadingBrowserScript){
return null
return (
<MythicLoadingState
compact
title="Preparing output"
description="Loading the response renderer for this command."
sx={stateSx}
/>
)
}
if(rawResponses.length === 0 && output.length === 0){
return search ? (
<MythicSearchEmptyState
compact
description="No task output matched the current search."
sx={stateSx}
/>
) : (
<MythicEmptyState
compact
title="No output yet"
description="Task output will appear here as responses arrive."
sx={stateSx}
/>
)
}
const shouldUseBrowserScript = viewBrowserScript && localViewBrowserScript;
if(shouldUseBrowserScript && browserScriptData === undefined){
return null
return (
<MythicLoadingState
compact
title="Rendering output"
description="Preparing the browser script view."
sx={stateSx}
/>
)
}
return (
shouldUseBrowserScript ? (
@@ -23,6 +23,7 @@ import FormControl from "@mui/material/FormControl";
import WrapTextIcon from '@mui/icons-material/WrapText';
import {useTheme} from '@mui/material/styles';
import HeightIcon from '@mui/icons-material/Height';
import {MythicEmptyState} from "../../MythicComponents/MythicStateDisplay";
const getInteractiveTaskingQuery = gql`
${taskingDataFragment}
@@ -297,6 +298,7 @@ const InteractiveTerminalDisplay = ({data, useASNIColor, showTaskStatus, wrapTex
const terminalRef = React.useRef(null);
const fitAddonRef = React.useRef(null);
const resizeObserverRef = React.useRef(null);
const fitAnimationFrameRef = React.useRef(null);
const replayStateRef = React.useRef({settingsKey: "", signatures: []});
const autoScrollRef = React.useRef(autoScroll);
const wrapTextRef = React.useRef(wrapText);
@@ -333,6 +335,15 @@ const InteractiveTerminalDisplay = ({data, useASNIColor, showTaskStatus, wrapTex
console.error(error);
}
}, []);
const scheduleFitTerminal = React.useCallback( () => {
if(fitAnimationFrameRef.current !== null){
return;
}
fitAnimationFrameRef.current = window.requestAnimationFrame(() => {
fitAnimationFrameRef.current = null;
fitTerminal();
});
}, [fitTerminal]);
React.useEffect( () => {
autoScrollRef.current = autoScroll;
if(autoScroll){
@@ -341,16 +352,16 @@ const InteractiveTerminalDisplay = ({data, useASNIColor, showTaskStatus, wrapTex
}, [autoScroll]);
React.useEffect( () => {
wrapTextRef.current = wrapText;
fitTerminal();
}, [wrapText, fitTerminal]);
scheduleFitTerminal();
}, [wrapText, scheduleFitTerminal]);
React.useEffect( () => {
if(wrapText){
unwrappedColumnCountRef.current = 0;
} else {
unwrappedColumnCountRef.current = Math.min(Math.max(longestLineLength + 2, 1), INTERACTIVE_TERMINAL_MAX_UNWRAPPED_COLS);
}
fitTerminal();
}, [fitTerminal, longestLineLength, wrapText]);
scheduleFitTerminal();
}, [longestLineLength, scheduleFitTerminal, wrapText]);
React.useEffect( () => {
if(!terminalElementRef.current){
return;
@@ -370,15 +381,20 @@ const InteractiveTerminalDisplay = ({data, useASNIColor, showTaskStatus, wrapTex
terminal.open(terminalElementRef.current);
terminalRef.current = terminal;
fitAddonRef.current = fitAddon;
resizeObserverRef.current = new ResizeObserver(() => fitTerminal());
resizeObserverRef.current = new ResizeObserver(() => scheduleFitTerminal());
if(terminalScrollContainerRef.current){
resizeObserverRef.current.observe(terminalScrollContainerRef.current);
}
window.requestAnimationFrame(() => {
fitAnimationFrameRef.current = window.requestAnimationFrame(() => {
fitAnimationFrameRef.current = null;
fitTerminal();
setTerminalReady(true);
});
return () => {
if(fitAnimationFrameRef.current !== null){
window.cancelAnimationFrame(fitAnimationFrameRef.current);
fitAnimationFrameRef.current = null;
}
resizeObserverRef.current?.disconnect();
resizeObserverRef.current = null;
fitAddonRef.current = null;
@@ -396,7 +412,7 @@ const InteractiveTerminalDisplay = ({data, useASNIColor, showTaskStatus, wrapTex
return;
}
const terminal = terminalRef.current;
fitTerminal();
scheduleFitTerminal();
const nextSignatures = data.map(getInteractiveTerminalEntrySignature);
const settingsKey = `${useASNIColor}:${showTaskStatus}:${wrapText}`;
const previousReplayState = replayStateRef.current;
@@ -429,7 +445,7 @@ const InteractiveTerminalDisplay = ({data, useASNIColor, showTaskStatus, wrapTex
} else {
afterWrite();
}
}, [data, showTaskStatus, terminalReady, useASNIColor, wrapText]);
}, [data, scheduleFitTerminal, showTaskStatus, terminalReady, useASNIColor, wrapText]);
return (
<div
ref={terminalScrollContainerRef}
@@ -695,12 +711,15 @@ export const ResponseDisplayInteractive = (props) =>{
}, 2000);
return () => clearTimeout(timeoutID);
}, [backdropOpen]);
const panelHeight = props.expand ? "100%" : "clamp(260px, 36vh, 500px)";
const outputMinHeight = props.expand ? 0 : "160px";
return (
<div style={{
display: "flex", overflowY: "auto",
position: "relative",
height: props.expand ? "100%" : undefined,
height: panelHeight,
minHeight: props.expand ? 0 : "260px",
maxHeight: props.expand ? "100%" : "500px",
flexDirection: "column",
width: "100%",
@@ -725,14 +744,23 @@ export const ResponseDisplayInteractive = (props) =>{
<SearchBar onSubmitSearch={onSubmitSearch}/>
}
<div style={{overflow: "hidden", width: "100%", marginBottom: "5px", height: props.expand ? "100%": undefined,
flexGrow: 1, minHeight: "50px"}} ref={props.responseRef}
flexGrow: 1, flexShrink: 1, minHeight: outputMinHeight}} ref={props.responseRef}
id={`ptytask${props.task.id}`}>
<InteractiveTerminalDisplay data={visibleOutput}
useASNIColor={useASNIColor}
showTaskStatus={showTaskStatus}
wrapText={wrapText}
autoScroll={autoScroll}
theme={theme}/>
{visibleOutput.length === 0 && !backdropOpen ? (
<MythicEmptyState
compact
title="No interactive output yet"
description="Interactive tasking and responses will appear here as they arrive."
sx={{backgroundColor: "transparent", color: "inherit", height: "100%"}}
/>
) : (
<InteractiveTerminalDisplay data={visibleOutput}
useASNIColor={useASNIColor}
showTaskStatus={showTaskStatus}
wrapText={wrapText}
autoScroll={autoScroll}
theme={theme}/>
)}
</div>
{!props.task?.is_interactive_task &&
<div style={{width: "100%", display: "inline-flex", alignItems: "flex-end"}}>
@@ -1,5 +1,5 @@
import React, {useEffect, useLayoutEffect, useRef} from 'react';
import { styled } from '@mui/material/styles';
import { alpha, styled } from '@mui/material/styles';
import {IconButton, Link} from '@mui/material';
import Paper from '@mui/material/Paper';
import Typography from '@mui/material/Typography';
@@ -9,7 +9,6 @@ import AccordionSummary from '@mui/material/AccordionSummary';
import ExpandMoreIcon from '@mui/icons-material/ExpandMore';
import ExpandLessIcon from '@mui/icons-material/ExpandLess';
import ChatOutlinedIcon from '@mui/icons-material/ChatOutlined';
import Badge from '@mui/material/Badge';
import {useTheme} from '@mui/material/styles';
import {gql, useSubscription } from '@apollo/client';
import {TaskDisplayContainer, TaskDisplayContainerConsole} from './TaskDisplayContainer';
@@ -31,6 +30,7 @@ import ComputerIcon from '@mui/icons-material/Computer';
import PublicIcon from '@mui/icons-material/Public';
import WarningAmberIcon from '@mui/icons-material/WarningAmber';
import NumbersIcon from '@mui/icons-material/Numbers';
import {getMythicStatusFromTaskStatus, MythicStatusChip} from '../../MythicComponents/MythicStatusChip';
const PREFIX = 'TaskDisplay';
@@ -43,7 +43,22 @@ export const classes = {
secondaryHeadingExpanded: `${PREFIX}-secondaryHeadingExpanded`,
icon: `${PREFIX}-icon`,
details: `${PREFIX}-details`,
column: `${PREFIX}-column`
column: `${PREFIX}-column`,
taskHeaderBody: `${PREFIX}-taskHeaderBody`,
taskMetaRow: `${PREFIX}-taskMetaRow`,
taskMetaItem: `${PREFIX}-taskMetaItem`,
taskMetaIcon: `${PREFIX}-taskMetaIcon`,
taskHeaderBodyCompact: `${PREFIX}-taskHeaderBodyCompact`,
taskHeaderActions: `${PREFIX}-taskHeaderActions`,
taskIconButton: `${PREFIX}-taskIconButton`,
taskCommandRow: `${PREFIX}-taskCommandRow`,
taskCommandText: `${PREFIX}-taskCommandText`,
taskCommandTextCompact: `${PREFIX}-taskCommandTextCompact`,
taskCommandName: `${PREFIX}-taskCommandName`,
taskCommandParams: `${PREFIX}-taskCommandParams`,
taskChildToggle: `${PREFIX}-taskChildToggle`,
taskCommentBlock: `${PREFIX}-taskCommentBlock`,
consolePrompt: `${PREFIX}-consolePrompt`
};
export const accordionClasses = {
root: `${ACCORDION_PREFIX}-root`,
@@ -68,6 +83,11 @@ export const StyledPaper = styled(Paper)((
backgroundColor: theme.palette.background.paper,
border: `1px solid ${theme.borderColor}`,
borderRadius: theme.shape.borderRadius,
transition: "background-color 120ms ease, border-color 120ms ease, box-shadow 120ms ease",
"&:hover": {
borderColor: theme.palette.mode === "dark" ? alpha(theme.palette.common.white, 0.22) : alpha(theme.palette.common.black, 0.18),
backgroundColor: theme.palette.mode === "dark" ? alpha(theme.palette.common.white, 0.035) : alpha(theme.palette.common.black, 0.015),
},
},
[`& .${classes.heading}`]: {
@@ -123,6 +143,137 @@ export const StyledPaper = styled(Paper)((
display: "inline-block",
margin: 0,
height: "auto",
},
[`& .${classes.taskHeaderBody}`]: {
display: "flex",
flexDirection: "column",
gap: 5,
minWidth: 0,
width: "100%",
},
[`& .${classes.taskHeaderBodyCompact}`]: {
gap: 4,
},
[`& .${classes.taskMetaRow}`]: {
alignItems: "center",
color: theme.palette.text.secondary,
display: "flex",
flexWrap: "wrap",
gap: 5,
minWidth: 0,
},
[`& .${classes.taskHeaderBodyCompact} .${classes.taskMetaRow}`]: {
flexWrap: "nowrap",
maxHeight: 24,
overflow: "hidden",
},
[`& .${classes.taskMetaItem}`]: {
alignItems: "center",
backgroundColor: theme.palette.mode === "dark" ? alpha(theme.palette.common.white, 0.05) : alpha(theme.palette.common.black, 0.035),
border: `1px solid ${theme.palette.mode === "dark" ? alpha(theme.palette.common.white, 0.08) : alpha(theme.palette.common.black, 0.07)}`,
borderRadius: 5,
color: theme.palette.text.secondary,
display: "inline-flex",
fontSize: theme.typography.pxToRem(11.5),
fontWeight: 600,
gap: 4,
lineHeight: 1.2,
maxWidth: "18rem",
minHeight: 22,
minWidth: 0,
padding: "2px 6px",
whiteSpace: "nowrap",
"& > span:last-child": {
minWidth: 0,
overflow: "hidden",
textOverflow: "ellipsis",
},
"& a": {
color: "inherit",
fontWeight: 700,
},
},
[`& .${classes.taskHeaderBodyCompact} .${classes.taskMetaItem}`]: {
flex: "0 0 auto",
},
[`& .${classes.taskMetaIcon}`]: {
flexShrink: 0,
fontSize: "0.86rem",
height: "0.86rem",
width: "0.86rem",
},
[`& .${classes.taskHeaderActions}`]: {
alignItems: "center",
display: "inline-flex",
flexWrap: "wrap",
gap: 4,
minWidth: 0,
},
[`& .${classes.taskHeaderBodyCompact} .${classes.taskHeaderActions}`]: {
flex: "0 0 auto",
flexWrap: "nowrap",
},
[`& .${classes.taskIconButton}`]: {
border: `1px solid ${theme.palette.mode === "dark" ? alpha(theme.palette.common.white, 0.08) : alpha(theme.palette.common.black, 0.08)}`,
borderRadius: 5,
color: theme.palette.text.secondary,
height: 24,
padding: 0,
width: 24,
"& svg": {
fontSize: "1rem",
},
},
[`& .${classes.taskCommandRow}`]: {
alignItems: "flex-start",
display: "flex",
gap: 6,
minWidth: 0,
width: "100%",
},
[`& .${classes.taskCommandText}`]: {
color: theme.taskPromptCommandTextColor,
flex: "1 1 auto",
fontSize: theme.typography.pxToRem(14),
lineHeight: 1.35,
minWidth: 0,
},
[`& .${classes.taskCommandTextCompact}`]: {
display: "-webkit-box",
overflow: "hidden",
WebkitBoxOrient: "vertical",
WebkitLineClamp: "2",
},
[`& .${classes.taskCommandName}`]: {
color: theme.taskPromptCommandTextColor,
fontWeight: 800,
},
[`& .${classes.taskCommandParams}`]: {
color: theme.palette.text.primary,
overflowWrap: "anywhere",
},
[`& .${classes.taskChildToggle}`]: {
color: theme.palette.text.secondary,
height: 24,
marginTop: -2,
padding: 0,
width: 24,
},
[`& .${classes.taskCommentBlock}`]: {
backgroundColor: theme.palette.mode === "dark" ? alpha(theme.palette.common.white, 0.045) : alpha(theme.palette.common.black, 0.025),
border: `1px solid ${theme.palette.mode === "dark" ? alpha(theme.palette.common.white, 0.08) : alpha(theme.palette.common.black, 0.07)}`,
borderRadius: 5,
color: theme.palette.text.primary,
fontSize: theme.typography.pxToRem(12.5),
lineHeight: 1.35,
padding: "6px 8px",
},
[`& .${classes.consolePrompt}`]: {
color: theme.palette.text.secondary,
flexShrink: 0,
fontFamily: 'Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace',
fontSize: theme.typography.pxToRem(13),
fontWeight: 800,
}
}));
@@ -190,77 +341,131 @@ function TaskDisplayConsolePreMemo({task, me, filterOptions, newlyIssuedTasks}){
);
}
export const TaskDisplayConsole = React.memo(TaskDisplayConsolePreMemo);
const TaskStatusDisplay = ({task, theme}) => {
const TaskStatusDisplay = ({task}) => {
const chipSx = {
display: "inline-flex",
height: 22,
mx: 0.5,
verticalAlign: "middle",
"& .MuiChip-icon": {
fontSize: "0.86rem",
},
"& .MuiChip-label": {
px: 0.7,
},
};
if(task.status.toLowerCase().includes("error")){
return (<Typography size="small" component="span" style={{color: theme.palette.error.main, display: "inline-block", fontSize: theme.typography.pxToRem(15)}}>{task.status.toLowerCase()}</Typography>)
return <MythicStatusChip component="span" label={task.status.toLowerCase()} status="error" sx={{...chipSx, maxWidth: "16rem"}} />
}else if(task.status === "cleared"){
return (<Typography size="small" component="span" style={{padding: "0", color: theme.palette.warning.main, display: "inline-block", fontSize: theme.typography.pxToRem(15)}}>cleared</Typography>)
return <MythicStatusChip component="span" label="cleared" status="warning" sx={chipSx} />
}else if(task.status === "completed" || task.status === "success"){
return null//return (<Typography size="small" style={{padding: "0", color: theme.palette.success.main, marginLeft: "5%", display: "inline-block", fontSize: theme.typography.pxToRem(15)}}>completed</Typography>)
}else if(task.status === "submitted"){
return (<Typography size="small" component="span" style={{padding: "0", color: theme.palette.info.main, display: "inline-block", fontSize: theme.typography.pxToRem(15)}}>{task.status.toLowerCase()}</Typography>)
}else if(task.status.toLowerCase().includes("processing")){
return (<Typography size="small" component="span" style={{padding: "0", color: theme.palette.warning.main, display: "inline-block", fontSize: theme.typography.pxToRem(15)}}>{task.status.toLowerCase()}</Typography>)
}else if(task.opsec_pre_blocked && !task.opsec_pre_bypassed){
return (<Typography size="small" component="span" style={{padding: "0", color: theme.palette.warning.main, display: "inline-block", fontSize: theme.typography.pxToRem(15)}}>OPSEC BLOCKED (PRE)</Typography>)
return <MythicStatusChip component="span" label="OPSEC blocked pre" status="blocked" sx={chipSx} />
}else if(task.opsec_post_blocked && !task.opsec_post_bypassed){
return (<Typography size="small" component="span" style={{padding: "0", color: theme.palette.warning.main, display: "inline-block", fontSize: theme.typography.pxToRem(15)}}>OPSEC BLOCKED (POST)</Typography>)
return <MythicStatusChip component="span" label="OPSEC blocked post" status="blocked" sx={chipSx} />
}else{
return (<Typography size="small" component="span" style={{padding: "0", color: theme.palette.info.main, display: "inline-block", fontSize: theme.typography.pxToRem(15)}}>{task.status.toLowerCase()}</Typography>)
return (
<MythicStatusChip
component="span"
label={task.status.toLowerCase()}
status={getMythicStatusFromTaskStatus(task.status)}
sx={{...chipSx, maxWidth: "16rem"}}
/>
)
}
}
const getTaskAccentColor = (task, theme) => {
if(task.status.toLowerCase().includes("error")){
return theme.palette.error.main;
}
if(task.status.toLowerCase() === "cleared"){
return theme.palette.warning.main;
}
if(task.status === "submitted"){
return theme.palette.info.main;
}
if(task.opsec_pre_blocked && !task.opsec_pre_bypassed){
return theme.palette.warning.main;
}
if(task.opsec_post_blocked && !task.opsec_post_bypassed){
return theme.palette.warning.main;
}
if(task.status.toLowerCase().includes("processing")){
return theme.palette.warning.main;
}
if(task.status === "completed" || task.completed){
return theme.palette.success.main;
}
return theme.palette.info.main;
}
const getCallbackPrimaryIP = (task) => {
try{
return JSON.parse(task.callback.ip)[0] || "";
}catch(error){
return "";
}
}
const TaskMetaItem = ({children, icon, title, style}) => {
const item = (
<span className={classes.taskMetaItem} style={style}>
{icon && React.cloneElement(icon, {
className: classes.taskMetaIcon,
fontSize: "inherit",
})}
<span>{children}</span>
</span>
);
if(title){
return (
<MythicStyledTooltip title={title}>
{item}
</MythicStyledTooltip>
)
}
return item;
}
const TaskHeaderAction = ({title, children, ...props}) => (
<MythicStyledTooltip title={title}>
<IconButton className={classes.taskIconButton} size="small" color="inherit"
disableFocusRipple={true} disableRipple={true} {...props}>
{children}
</IconButton>
</MythicStyledTooltip>
)
const TaskTagDisplay = ({task}) => {
return (
<TagsDisplay tags={task.tags} />
)
}
const ColoredTaskDisplay = ({task, theme, children, expanded}) => {
const [themeColor, setThemeColor] = React.useState(theme.palette.info.main);
useEffect( () => {
if(task.status.toLowerCase().includes("error")){
setThemeColor(theme.palette.error.main);
}else if(task.status.toLowerCase() === "cleared"){
setThemeColor(theme.palette.warning.main);
}else if(task.status === "submitted"){
setThemeColor(theme.palette.info.main);
}else if(task.opsec_pre_blocked && !task.opsec_pre_bypassed){
setThemeColor(theme.palette.warning.main);
}else if(task.opsec_post_blocked && !task.opsec_post_bypassed){
setThemeColor(theme.palette.warning.main);
}else if(task.status.toLowerCase().includes("processing")){
setThemeColor(theme.palette.warning.main);
}else if(task.status === "completed" || task.completed){
setThemeColor(theme.palette.success.main);
}else{
setThemeColor(theme.palette.info.main);
}
}, [task.status, task.completed])
return(
<span style={{display: "flex", margin: 0, borderWidth: 0, padding: 0, minHeight: "48px", alignItems: "center",
height: "100%", borderLeft: "4px solid " + themeColor, paddingLeft: "7px", width: "100%", maxWidth: "100%",
borderTopLeftRadius: theme.shape.borderRadius, borderBottomLeftRadius: expanded ? 0 : theme.shape.borderRadius}}>
{children}
</span>
)
const themeColor = getTaskAccentColor(task, theme);
return(
<span style={{display: "flex", margin: 0, borderWidth: 0, padding: "8px 10px 8px 9px", minHeight: "58px", alignItems: "center",
height: "100%", borderLeft: "4px solid " + themeColor, width: "100%", maxWidth: "100%",
borderTopLeftRadius: theme.shape.borderRadius, borderBottomLeftRadius: expanded ? 0 : theme.shape.borderRadius}}>
{children}
</span>
)
}
const GetOperatorDisplay = ({initialHideUsernameValue, task}) => {
if(initialHideUsernameValue){
return '';
}
return " / " + task.operator.username;
}
export const ColoredTaskLabel = ({task, theme, me, taskDivID, onClick, displayChildren, toggleDisplayChildren, expanded }) => {
export const ColoredTaskLabel = ({task, theme, me, taskDivID, onClick, displayChildren, toggleDisplayChildren, expanded, compact=false }) => {
const [displayComment, setDisplayComment] = React.useState(false);
const [alertBadges, setAlertBadges] = React.useState(0);
const initialHideUsernameValue = GetMythicSetting({setting_name: "hideUsernames", default_value: operatorSettingDefaults.hideUsernames});
const initialShowIPValue = GetMythicSetting({setting_name: "showIP", default_value: operatorSettingDefaults.showIP});
const ipValue = JSON.parse(task.callback.ip)[0];
const ipValue = getCallbackPrimaryIP(task);
const initialShowHostnameValue = GetMythicSetting({setting_name: "showHostname", default_value: operatorSettingDefaults.showHostname});
const initialShowCallbackGroupsValue = GetMythicSetting({setting_name: "showCallbackGroups", default_value: operatorSettingDefaults.showCallbackGroups});
const initialTaskTimestampDisplayField = GetMythicSetting({setting_name: "taskTimestampDisplayField", default_value: operatorSettingDefaults.taskTimestampDisplayField});
const initialShowOPSECBypassUsername = GetMythicSetting({setting_name: "showOPSECBypassUsername", default_value: operatorSettingDefaults.showOPSECBypassUsername});
const displayTimestamp = task[initialTaskTimestampDisplayField] ? task[initialTaskTimestampDisplayField] : task.timestamp;
const [openKillTaskButton, setOpenKillTaskButton] = React.useState({open: false});
const command = task?.command?.cmd || task.command_name;
const commandLine = command + " " + task.display_params;
const callbackGroups = task.callback.mythictree_groups?.join(', ') || "";
const opsecBypassUsers = [
task?.opsec_pre_bypass_user?.username,
task?.opsec_post_bypass_user?.username,
].filter(Boolean).join(", ");
const toggleDisplayComment = (evt) => {
evt.stopPropagation();
setDisplayComment(!displayComment);
@@ -282,117 +487,111 @@ export const ColoredTaskLabel = ({task, theme, me, taskDivID, onClick, displayCh
}
return (
<ColoredTaskDisplay task={task} theme={theme} expanded={expanded} >
<div id={taskDivID} style={{width: "100%"}}>
<div id={taskDivID} className={compact ? `${classes.taskHeaderBody} ${classes.taskHeaderBodyCompact}` : classes.taskHeaderBody}>
{displayComment && (
<React.Fragment>
<Typography className={classes.taskAndTimeDisplay} onClick={preventPropagation}>{task.commentOperator.username}</Typography><br/>
<Typography className={classes.heading} onClick={preventPropagation}>{task.comment}</Typography>
</React.Fragment>
<div className={classes.taskCommentBlock} onClick={preventPropagation}>
<Typography component="div" sx={{fontSize: "0.72rem", fontWeight: 800, color: theme.palette.text.secondary, mb: 0.5}}>
{task.commentOperator?.username || "comment"}
</Typography>
<Typography component="div" sx={{fontSize: "0.82rem", overflowWrap: "anywhere"}}>
{task.comment}
</Typography>
</div>
)}
<div style={{lineHeight: 0}} onClick={onLocalClick}>
<Typography className={classes.taskAndTimeDisplay} onClick={preventPropagation}>
[{toLocalTime(displayTimestamp, me?.user?.view_utc_time || false)}]
{" / "}
<div className={classes.taskMetaRow} onClick={preventPropagation}>
<TaskMetaItem title={"Task timestamp"} icon={<AccessTimeIcon />}>
{toLocalTime(displayTimestamp, me?.user?.view_utc_time || false)}
</TaskMetaItem>
<TaskMetaItem title={"View Task in separate page"} icon={<NumbersIcon />}>
<Link underline="hover" target="_blank" href={"/new/task/" + task.display_id}>T-{task.display_id}</Link>
</TaskMetaItem>
{!initialHideUsernameValue &&
<TaskMetaItem title={"Operator"} icon={<PersonOutlineIcon />}>
{task.operator.username}
</TaskMetaItem>
}
<TaskMetaItem title={"View Callback in separate page"}>
<Link underline="hover" target="_blank" href={"/new/callbacks/" + task.callback.display_id}>C-{task.callback.display_id}</Link>
</TaskMetaItem>
{initialShowHostnameValue &&
<TaskMetaItem title={"Host"} icon={<ComputerIcon />}>
{task.callback.host}
</TaskMetaItem>
}
{initialShowIPValue && ipValue !== "" &&
<TaskMetaItem title={"IP address"} icon={<PublicIcon />}>
{ipValue}
</TaskMetaItem>
}
{initialShowCallbackGroupsValue && callbackGroups !== "" &&
<TaskMetaItem title={"Callback groups"}>
{callbackGroups}
</TaskMetaItem>
}
{task?.command?.payloadtype?.name &&
<TaskMetaItem title={"Payload type"}>
{task.command.payloadtype.name}
</TaskMetaItem>
}
{initialShowOPSECBypassUsername && opsecBypassUsers !== "" &&
<TaskMetaItem title={"The specified usernames approved OPSEC bypasses for this task"} icon={<LockOpenIcon />}>
{opsecBypassUsers}
</TaskMetaItem>
}
{task.comment.length > 0 &&
<TaskMetaItem title={task.comment} style={{maxWidth: "24rem"}}>
{task.comment}
</TaskMetaItem>
}
<span className={classes.taskHeaderActions}>
{task.has_intercepted_response &&
<span style={{display:"inline-flex", whiteSpace: "pre-wrap"}}>
<MythicStyledTooltip
title={"This task has responses that have been intercepted and changed due to a workflow container"}>
<IconButton color={"inherit"} style={{padding: 0}} disableFocusRipple={true}
disableRipple={true}>
<CropRotateTwoToneIcon style={{height: "15px", cursor: "default"}}/>
</IconButton>
</MythicStyledTooltip>
{" / "}
</span>
<TaskHeaderAction title={"This task has responses that have been intercepted and changed due to a workflow container"}>
<CropRotateTwoToneIcon />
</TaskHeaderAction>
}
{task?.eventstepinstance !== null &&
<span style={{display:"inline-flex", whiteSpace: "pre-wrap"}}>
<MythicStyledTooltip title={"Task created via Eventing, click to view entire event flow in separate page"} >
<IconButton component={Link} href={'/new/eventing?eventgroup=' +
task?.eventstepinstance?.eventgroupinstance?.eventgroup?.id +
"&eventgroupinstance=" + task?.eventstepinstance?.eventgroupinstance?.id
} target={"_blank"} style={{padding: 0, }}
color="inherit" disableFocusRipple={true}
disableRipple={true}>
<PlayCircleFilledTwoToneIcon fontSize={"small"} />
</IconButton>
</MythicStyledTooltip>
{" / "}
</span>
{task?.eventstepinstance &&
<TaskHeaderAction title={"Task created via Eventing, click to view entire event flow in separate page"}
component={Link}
href={'/new/eventing?eventgroup=' +
task?.eventstepinstance?.eventgroupinstance?.eventgroup?.id +
"&eventgroupinstance=" + task?.eventstepinstance?.eventgroupinstance?.id
}
target={"_blank"}>
<PlayCircleFilledTwoToneIcon />
</TaskHeaderAction>
}
<span style={{display:"inline-flex", whiteSpace: "pre-wrap"}}>
<MythicStyledTooltip title={"View Task in separate page"} >
<Link style={{wordBreak: "break-all", color: theme.taskPromptTextColor,}} underline={"always"} target={"_blank"}
href={"/new/task/" + task.display_id}>T-{task.display_id}</Link>
</MythicStyledTooltip>
{!task.completed && task.status_timestamp_processing &&
<MythicStyledTooltip title={"Task the agent to kill this task"} >
<FontAwesomeIcon size={"sm"} icon={faSkullCrossbones} onClick={(e) => onClickKillIcon(e, true)}
style={{cursor: "pointer", height: "12px", marginLeft: "5px"}} />
</MythicStyledTooltip>
}
</span>
<GetOperatorDisplay initialHideUsernameValue={initialHideUsernameValue} task={task}/>
{" / "}
<MythicStyledTooltip title={"View Callback in separate page"}>
<Link style={{wordBreak: "break-all", color: theme.taskPromptTextColor}} underline="always" target="_blank"
href={"/new/callbacks/" + task.callback.display_id}>C-{task.callback.display_id}</Link>
</MythicStyledTooltip>
{initialShowHostnameValue ? ` / ${task.callback.host} ` : ''}
{initialShowIPValue ? `/ ${ipValue} ` : ''}
{initialShowCallbackGroupsValue ? `/ ${task.callback.mythictree_groups.join(', ')} ` : ''}
{" / "}
{task?.command?.payloadtype?.name}
{initialShowOPSECBypassUsername && <>
{(task?.opsec_pre_bypass_user?.username || task?.opsec_post_bypass_user?.username) && <span
style={{display:"inline-flex", whiteSpace: "pre-wrap"}}>
{" /"}
<MythicStyledTooltip title={"The specified usernames approved OPSEC bypasses for this task"}
tooltipStyle={{padding: 0}}>
<LockOpenIcon color={"inherit"} style={{height: "15px"}}/>
</MythicStyledTooltip>
{task?.opsec_pre_bypass_user?.username}
{task?.opsec_pre_bypass_user?.username && task?.opsec_post_bypass_user?.username && ", "}
{task?.opsec_post_bypass_user?.username}
</span>}
</>}
{" "}
<TaskStatusDisplay task={task} theme={theme}/>
{!task.completed && task.status_timestamp_processing &&
<TaskHeaderAction title={"Task the agent to kill this task"} onClick={(e) => onClickKillIcon(e, true)}>
<FontAwesomeIcon size={"sm"} icon={faSkullCrossbones} style={{height: "0.82rem"}} />
</TaskHeaderAction>
}
<TaskStatusDisplay task={task}/>
{task.comment.length > 0 &&
<span className={classes.column}>
{" / "}
<IconButton size="small" style={{padding: "0"}}
onClick={toggleDisplayComment}><ChatOutlinedIcon fontSize={"small"}/></IconButton>
</span>
<TaskHeaderAction title={displayComment ? "Hide comment" : "Show comment"} onClick={toggleDisplayComment}>
<ChatOutlinedIcon />
</TaskHeaderAction>
}
{task.comment}
</span>
</div>
<div className={classes.taskCommandRow} onClick={onLocalClick}>
{task.tasks.length > 0 &&
<IconButton className={classes.taskChildToggle} size="small" onClick={toggleDisplayChildren}>
{displayChildren ? <ExpandLessIcon fontSize="small" /> : <ExpandMoreIcon fontSize="small" />}
</IconButton>
}
<MythicStyledTooltip maxWidth={"calc(80vw)"}
enterDelay={2000}
placement={"top"}
title={commandLine} >
<Typography className={compact ? `${classes.taskCommandText} ${classes.taskCommandTextCompact}` : classes.taskCommandText} component="div">
<span className={classes.taskCommandName}>{command}</span>
{task.display_params !== "" &&
<span className={classes.taskCommandParams}> {task.display_params}</span>
}
</Typography>
<TaskTagDisplay task={task}/>
</div>
<div onClick={onLocalClick}>
<div className={classes.column} >
<Badge badgeContent={alertBadges} color="warning" anchorOrigin={{vertical: 'top', horizontal: 'left'}}>
{task.tasks.length > 0 && !displayChildren &&
<ExpandMoreIcon onClick={toggleDisplayChildren} />
}
{task.tasks.length > 0 && displayChildren &&
<ExpandLessIcon onClick={toggleDisplayChildren} />
}
<MythicStyledTooltip maxWidth={"calc(80vw)"}
enterDelay={2000}
placement={"top"}
title={(task?.command?.cmd || task.command_name) + " " + task.display_params} >
<Typography className={classes.heading} style={{color: theme.taskPromptCommandTextColor}} onClick={onLocalClick} >
{(task?.command?.cmd || task.command_name) + " " + task.display_params}
</Typography>
</MythicStyledTooltip>
</Badge>
</div>
</MythicStyledTooltip>
</div>
<TaskTagDisplay task={task}/>
</div>
{openKillTaskButton.open &&
<TaskFromUIButton ui_feature={"task:job_kill"}
@@ -752,7 +951,14 @@ const TaskLabel = ({task, dropdownOpen, toggleTaskDropdown, me, newlyIssuedTasks
<StyledPaper className={classes.root + " no-box-shadow"} elevation={5} id={`taskHeader-${task.id}`}>
<Accordion TransitionProps={{ unmountOnExit: true, onEnter: scrollContent }} defaultExpanded={false}
onChange={toggleTaskDropdown} expanded={dropdownOpen}
style={{backgroundColor: "unset", backgroundImage: "unset"}}
sx={{
backgroundColor: "unset",
backgroundImage: "unset",
border: 0,
boxShadow: "unset",
"&:before": {display: "none"},
"&.Mui-expanded": {margin: 0},
}}
>
<StyledAccordionSummary
expandIcon={<ExpandMoreIcon />}
@@ -815,6 +1021,7 @@ export const TaskLabelFlat = ({task, me, showOnSelectTask, onSelectTask, graphVi
>
<ColoredTaskLabel theme={theme} task={task} me={me} taskDivID={`scrolltotasksplit${task.id}`} onClick={onClickEntry}
displayChildren={displayChildren} toggleDisplayChildren={toggleDisplayChildren} expanded={false}
compact={showOnSelectTask}
/>
</StyledPaper>
)
@@ -822,10 +1029,11 @@ export const TaskLabelFlat = ({task, me, showOnSelectTask, onSelectTask, graphVi
const ColoredTaskDisplayConsole = ({task, theme, children, expanded}) => {
const themeColor = getTaskAccentColor(task, theme);
return(
<span style={{display: "flex", margin: 0, borderWidth: 0, padding: 0, minHeight: "30px", alignItems: "center",
height: "100%", paddingLeft: "5px", width: "100%",
borderTopLeftRadius: "4px", borderBottomLeftRadius: expanded ? 0 : "4px"}}>
<span style={{display: "flex", margin: 0, borderWidth: 0, padding: "8px 10px 8px 9px", minHeight: "58px", alignItems: "center",
height: "100%", borderLeft: "4px solid " + themeColor, width: "100%",
borderTopLeftRadius: theme.shape.borderRadius, borderBottomLeftRadius: expanded ? 0 : theme.shape.borderRadius}}>
{children}
</span>
)
@@ -953,6 +1161,9 @@ export const ColoredTaskLabelConsole = ({task, theme, me, taskDivID, onClick, di
const initialShowHostnameValue = GetMythicSetting({setting_name: "showHostname", default_value: operatorSettingDefaults.showHostname});
const initialShowIPValue = GetMythicSetting({setting_name: "showIP", default_value: operatorSettingDefaults.showIP});
const [openKillTaskButton, setOpenKillTaskButton] = React.useState({open: false});
const ipValue = getCallbackPrimaryIP(task);
const command = task?.command?.cmd || task.command_name;
const commandLine = command + " " + task.display_params;
const preventPropagation = (e) => {
e.stopPropagation();
//e.preventDefault();
@@ -963,102 +1174,66 @@ export const ColoredTaskLabelConsole = ({task, theme, me, taskDivID, onClick, di
}
setOpenKillTaskButton({open: open});
}
const [themeColor, setThemeColor] = React.useState(theme.palette.info.main);
useEffect( () => {
if(task.status.toLowerCase().includes("error")){
setThemeColor(theme.palette.error.main);
}else if(task.status.toLowerCase() === "cleared"){
setThemeColor(theme.palette.warning.main);
}else if(task.status === "submitted"){
setThemeColor(theme.palette.info.main);
}else if(task.opsec_pre_blocked && !task.opsec_pre_bypassed){
setThemeColor(theme.palette.warning.main);
}else if(task.opsec_post_blocked && !task.opsec_post_bypassed){
setThemeColor(theme.palette.warning.main);
}else if(task.status.toLowerCase().includes("processing")){
setThemeColor(theme.palette.warning.main);
}else if(task.status === "completed" || (task.status === "success" && task.completed)){
setThemeColor(theme.palette.success.main);
}else{
setThemeColor(theme.palette.info.main);
}
}, [task.status, task.completed])
return (
<ColoredTaskDisplayConsole task={task} theme={theme} expanded={expanded}>
<div id={taskDivID} style={{ width: "100%" }}>
<Typography sx={{ color: theme.taskPromptTextColor, display: "flex", alignItems: "center" }} style={{ fontFamily: "monospace" }}>
<span style={{ fontFamily: "monospace" }}>[</span>
<AccessTimeIcon fontSize="small" style={{ margin: "0 3px 0 0px", verticalAlign: "middle" }} />
{toLocalTimeShort(displayTimestamp, me?.user?.view_utc_time || false)}
{"]"}
{!initialHideUsernameValue &&
<>
{"-["}
<PersonOutlineIcon fontSize="small" style={{ margin: "0 3px 0 0px", verticalAlign: "middle" }} />
{task.operator.username}
{"]"}
</>
<div id={taskDivID} className={classes.taskHeaderBody}>
<div className={classes.taskMetaRow} onClick={preventPropagation}>
<TaskMetaItem title={"Task timestamp"} icon={<AccessTimeIcon />}>
{toLocalTimeShort(displayTimestamp, me?.user?.view_utc_time || false)}
</TaskMetaItem>
{!initialHideUsernameValue &&
<TaskMetaItem title={"Operator"} icon={<PersonOutlineIcon />}>
{task.operator.username}
</TaskMetaItem>
}
<TaskMetaItem title={"View Task in separate page"} icon={<NumbersIcon />}>
<Link underline="hover" target="_blank" href={"/new/task/" + task.display_id}>T-{task.display_id}</Link>
</TaskMetaItem>
{initialShowHostnameValue &&
<TaskMetaItem title={"Host"} icon={<ComputerIcon />}>
{task.callback.host}
</TaskMetaItem>
}
{initialShowIPValue && ipValue !== "" &&
<TaskMetaItem title={"IP address"} icon={<PublicIcon />}>
{ipValue}
</TaskMetaItem>
}
{(task.opsec_pre_blocked && !task.opsec_pre_bypassed) &&
<TaskMetaItem title={"OPSEC blocked before tasking"} icon={<WarningAmberIcon />}>
OPSEC pre
</TaskMetaItem>
}
{(task.opsec_post_blocked && !task.opsec_post_bypassed) &&
<TaskMetaItem title={"OPSEC blocked after tasking"} icon={<WarningAmberIcon />}>
OPSEC post
</TaskMetaItem>
}
<span className={classes.taskHeaderActions}>
{!task.completed && task.status_timestamp_processing &&
<TaskHeaderAction title={"Task the agent to kill this task"} onClick={(e) => onClickKillIcon(e, true)}>
<FontAwesomeIcon size={"sm"} icon={faSkullCrossbones} style={{height: "0.82rem"}} />
</TaskHeaderAction>
}
<TaskStatusDisplay task={task}/>
</span>
</div>
<MythicStyledTooltip
title={commandLine}
maxWidth="calc(80vw)"
enterDelay={2000}
placement="top">
<Typography className={classes.taskCommandText} component="div"
sx={{display: "flex", alignItems: "baseline", gap: "6px", fontFamily: 'Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace'}}>
<span className={classes.consolePrompt}>{">"}</span>
<span style={{minWidth: 0}}>
<span className={classes.taskCommandName}>{command}</span>
{task.display_params !== "" &&
<span className={classes.taskCommandParams}> {task.display_params}</span>
}
{"-["}
<NumbersIcon fontSize="small" style={{ margin: "0 3px 0 0px", verticalAlign: "middle" }} />
<MythicStyledTooltip title={"View Task in separate page"} >
<Link style={{wordBreak: "break-all", color: theme.taskPromptTextColor,}} underline={"always"} target={"_blank"}
href={"/new/task/" + task.display_id}>T-{task.display_id}</Link>
</MythicStyledTooltip>
{!task.completed && task.status_timestamp_processing &&
<>
<MythicStyledTooltip title={"Task the agent to kill this task"} >
<FontAwesomeIcon size={"sm"} icon={faSkullCrossbones} onClick={(e) => onClickKillIcon(e, true)}
style={{cursor: "pointer", height: "15px", marginLeft: "5px"}} />
</MythicStyledTooltip>
</>
}
{"]"}
{initialShowHostnameValue ? (
<>
{"-["}
<ComputerIcon fontSize="small" style={{ margin: "0 3px 0 0px", verticalAlign: "middle" }} />
{task.callback.host}
{"]"}
</>) : null}
{initialShowIPValue ? (
<>
{"-["}
<PublicIcon fontSize="small" style={{ margin: "0 3px 0 0px", verticalAlign: "middle" }} />
{JSON.parse(task.callback.ip)[0]}
{"]"}
</> ) : null}
{(task.opsec_pre_blocked && !task.opsec_pre_bypassed) ? (
<>
{"-["}
<WarningAmberIcon fontSize="small" style={{ margin: "0 3px 0 0px", verticalAlign: "middle", color: theme.palette.warning.main }} />
{"OPSEC BLOCKED (PRE)]"}
</>) : null}
{(task.opsec_post_blocked && !task.opsec_post_bypassed) ? (
<>
{"-["}
<WarningAmberIcon fontSize="small" style={{ margin: "0 3px 0 0px", verticalAlign: "middle", color: theme.palette.warning.main }} />
{"OPSEC BLOCKED (POST)]"}
</>) : null}
{")"}
</Typography>
<MythicStyledTooltip title={task.status}>
<span style={{ fontFamily: "monospace", marginRight: 2, color: theme.taskPromptTextColor }}></span>
<b style={{ fontFamily: "monospace", color: themeColor, fontWeight: 600 }}>{">_"}</b>{" "}
</MythicStyledTooltip>
<MythicStyledTooltip
title={(task?.command?.cmd || task.command_name) + " " + task.display_params}
maxWidth="calc(80vw)"
enterDelay={2000}
placement="top">
<Typography
sx={{ fontSize: 15, display: "flex", alignItems: "center", marginLeft: 0}}
style={{ fontFamily: "monospace" }}>
<span style={{ marginLeft: 4, color: theme.taskPromptCommandTextColor }}>
<b>{(task?.command?.cmd || task.command_name)}</b> {task.display_params}
</span>
</Typography>
</MythicStyledTooltip>
</span>
</Typography>
</MythicStyledTooltip>
</div>
{openKillTaskButton.open &&
<TaskFromUIButton ui_feature={"task:job_kill"}
@@ -1097,10 +1272,9 @@ const TaskLabelConsole = ({task, me}) => {
}
return (
<StyledPaper className={classes.root + " no-box-shadow no-border"} elevation={5} style={{marginRight: 0, marginBottom: "5px"}} id={`taskHeader-${task.id}`}>
<StyledPaper className={classes.root + " no-box-shadow"} elevation={5} style={{marginRight: 0, marginBottom: "5px"}} id={`taskHeader-${task.id}`}>
<ColoredTaskLabelConsole theme={theme} task={task} me={me} taskDivID={`scrolltotaskconsole${task.id}`} expanded={true}/>
<TaskDisplayContainerConsole me={me} task={task} />
<div style={{borderBottom: "0px dashed grey", width: "100%", height: "5px", marginTop: "5px"}}/>
</StyledPaper>
);
}
@@ -26,15 +26,18 @@ import {FontAwesomeIcon} from '@fortawesome/react-fontawesome';
import {faExclamationTriangle} from '@fortawesome/free-solid-svg-icons';
import { faExternalLinkAlt, faExpandArrowsAlt } from '@fortawesome/free-solid-svg-icons';
import SearchIcon from '@mui/icons-material/Search';
import SpeedDial from '@mui/material/SpeedDial';
import SpeedDialAction from '@mui/material/SpeedDialAction';
import { Backdrop } from '@mui/material';
import Button from '@mui/material/Button';
import Menu from '@mui/material/Menu';
import MenuItem from '@mui/material/MenuItem';
import ListItemIcon from '@mui/material/ListItemIcon';
import ListItemText from '@mui/material/ListItemText';
import Divider from '@mui/material/Divider';
import {downloadFileFromMemory} from '../../utilities/Clipboard';
import InsertPhotoIcon from '@mui/icons-material/InsertPhoto';
import html2canvas from 'html2canvas';
import CloseFullscreenIcon from '@mui/icons-material/CloseFullscreen';
import CodeOffIcon from '@mui/icons-material/CodeOff';
import SettingsTwoToneIcon from '@mui/icons-material/SettingsTwoTone';
import MoreHorizIcon from '@mui/icons-material/MoreHoriz';
import {b64DecodeUnicode} from './ResponseDisplay';
import {MythicStyledTooltip} from "../../MythicComponents/MythicStyledTooltip";
import IconButton from '@mui/material/IconButton';
@@ -88,17 +91,16 @@ export const TaskDisplayContainer = ({task, me}) => {
return (
<>
<Grid container spacing={0} style={{width: "100%"}}>
<SpeedDialDisplayGeneric toggleViewBrowserScript={toggleViewBrowserScript}
<Grid size={12}>
<TaskActionsToolbarGeneric toggleViewBrowserScript={toggleViewBrowserScript}
toggleSelectAllOutput={toggleSelectAllOutput}
toggleOpenSearch={toggleOpenSearch}
taskData={task}
me={me}
responseRef={responseRef}
viewBrowserScript={viewBrowserScript}
style={{position: "relative", zIndex: 2, display: "flex", flexDirection: "row-reverse", width: '100%',
bottom: "25px", right: "4px", height: 0}}
fabStyle={{ }}
viewAllOutput={selectAllOutput}/>
</Grid>
<Grid size={12}>
<ResponseDisplay
task={task}
@@ -449,14 +451,14 @@ const SideDisplayGeneric = ({toggleViewBrowserScript, toggleSelectAllOutput,
</div>
)
}
const SpeedDialDisplayGeneric = ({toggleViewBrowserScript, toggleSelectAllOutput,
const TaskActionsToolbarGeneric = ({toggleViewBrowserScript, toggleSelectAllOutput,
toggleOpenSearch, taskData, viewAllOutput, me,
responseRef, style, fabStyle, viewBrowserScript}) => {
responseRef, viewBrowserScript}) => {
const theme = useTheme();
const [task, setTask] = React.useState(taskData || {});
const eventingDataRef = React.useRef({name: "", value: 0});
const [openEventingDialog, setOpenEventingDialog] = React.useState(false);
const [openSpeedDial, setOpenSpeedDial] = React.useState(false);
const [actionsMenuAnchor, setActionsMenuAnchor] = React.useState(null);
const [openTaskTagDialog, setOpenTaskTagDialog] = React.useState(false);
const [openCommentDialog, setOpenCommentDialog] = React.useState(false);
const [openParametersDialog, setOpenParametersDialog] = React.useState(false);
@@ -478,9 +480,15 @@ const SpeedDialDisplayGeneric = ({toggleViewBrowserScript, toggleSelectAllOutput
React.useEffect( () => {
setTask(taskData);
}, [taskData.id, taskData.token, taskData.original_params, taskData.opsec_pre_blocked, taskData.opsec_pre_bypassed, taskData.opsec_post_blocked, taskData.opsec_post_bypassed])
const openActionsMenu = (event) => {
setActionsMenuAnchor(event.currentTarget);
};
const closeActionsMenu = () => {
setActionsMenuAnchor(null);
};
const onDownloadResponses = () => {
downloadResponses({variables: {task_id: task.id}});
setOpenSpeedDial(false);
closeActionsMenu();
};
const copyToClipboard = () => {
let command = task?.command?.cmd || task.command_name;
@@ -490,7 +498,7 @@ const SpeedDialDisplayGeneric = ({toggleViewBrowserScript, toggleSelectAllOutput
}else{
snackActions.error("Failed to copy text");
}
setOpenSpeedDial(false);
closeActionsMenu();
};
const [reissueTask] = useMutation(ReissueTaskMutationGQL, {
onCompleted: data => {
@@ -540,12 +548,15 @@ const SpeedDialDisplayGeneric = ({toggleViewBrowserScript, toggleSelectAllOutput
fakeLink.remove();
})();
closeActionsMenu();
};
const onReissueTask = () => {
reissueTask({variables: {task_id: task.id}});
closeActionsMenu();
}
const onReissueTaskHandler = () => {
reissueTaskHandler({variables: {task_id: task.id}});
closeActionsMenu();
}
const onTriggerEventing = () => {
eventingDataRef.current = {
@@ -553,10 +564,10 @@ const SpeedDialDisplayGeneric = ({toggleViewBrowserScript, toggleSelectAllOutput
value: task.id
};
setOpenEventingDialog(true);
closeActionsMenu();
}
return (
<React.Fragment>
<Backdrop open={openSpeedDial} onClick={()=>{setOpenSpeedDial(false);}} style={{zIndex: 2, position: "absolute"}}/>
{openTaskTagDialog && <MythicDialog fullWidth={true} maxWidth="lg" open={openTaskTagDialog}
onClose={()=>{setOpenTaskTagDialog(false);}}
innerDialog={<ViewEditTagsDialog me={me} target_object={"task_id"} target_object_id={task.id} onClose={()=>{setOpenTaskTagDialog(false);}} />}
@@ -601,165 +612,128 @@ const SpeedDialDisplayGeneric = ({toggleViewBrowserScript, toggleSelectAllOutput
}
/>
}
<SpeedDial
ariaLabel="Task Speeddial"
icon={<SettingsTwoToneIcon fontSize={"large"} />}
style={{...style}}
onClick={()=>{setOpenSpeedDial(!openSpeedDial)}}
FabProps={{...fabStyle, color: "secondary", size: "small", sx: {minHeight: "30px", height: "30px", width: "30px"}}}
open={openSpeedDial}
direction="right"
>
<SpeedDialAction
icon={viewBrowserScript ? <CodeOffIcon color={"error"} /> : <CodeIcon color={"success"}/>}
arrow
tooltipPlacement={"top"}
tooltipTitle={"Toggle BrowserScript"}
onClick={() => {toggleViewBrowserScript();setOpenSpeedDial(false);}}
/>
<SpeedDialAction
icon={viewAllOutput ? <CloseFullscreenIcon color={"error"} /> : <FontAwesomeIcon style={{color: theme.palette.success.main}} icon={faExpandArrowsAlt} size="lg" />}
arrow
tooltipPlacement={"top"}
tooltipTitle={viewAllOutput ? "View Paginated Output" : "View All Output"}
onClick={() => {toggleSelectAllOutput();setOpenSpeedDial(false);}}
/>
<SpeedDialAction
icon={<SearchIcon color={"info"} />}
arrow
tooltipPlacement={"top"}
tooltipTitle={"Search Output"}
onClick={() => {toggleOpenSearch();setOpenSpeedDial(false);}}
/>
<SpeedDialAction
icon={<GetAppIcon color={"success"}/>}
arrow
tooltipPlacement={"top"}
tooltipTitle={"Download output"}
onClick={onDownloadResponses}
/>
<SpeedDialAction
icon={<InsertPhotoIcon/>}
arrow
tooltipPlacement={"top"}
tooltipTitle={"Download screenshot of output"}
onClick={onDownloadImageClickPng}
/>
<SpeedDialAction
icon={<LocalOfferOutlinedIcon/>}
arrow
tooltipPlacement={"top"}
tooltipTitle={"Edit Tags"}
onClick={()=>{setOpenTaskTagDialog(true);setOpenSpeedDial(false);}}
/>
<SpeedDialAction
icon={<FontAwesomeIcon icon={faExternalLinkAlt} size="lg" />}
arrow
tooltipPlacement={"top"}
tooltipTitle={"Open Task in New Window"}
onClick={()=> {window.open('/new/task/' + task.display_id, "_blank")}}
/>
<SpeedDialAction
icon={<FileCopyOutlinedIcon/>}
arrow
tooltipPlacement={"top"}
tooltipTitle={"Copy original params to clipboard"}
onClick={copyToClipboard}
/>
<SpeedDialAction
icon={<RateReviewOutlinedIcon/>}
arrow
tooltipPlacement={"top"}
tooltipTitle={"Edit Comment"}
onClick={()=>{setOpenCommentDialog(true);setOpenSpeedDial(false);}}
/>
<SpeedDialAction
icon={<KeyboardIcon/>}
arrow
tooltipPlacement={"top"}
tooltipTitle={"View All Parameters And Timestamps"}
onClick={()=>{setOpenParametersDialog(true);setOpenSpeedDial(false);}}
/>
<SpeedDialAction
icon={<FontAwesomeIcon style={{color: theme.palette.error.main}} icon={faExclamationTriangle} size="lg" />}
arrow
tooltipPlacement={"top"}
tooltipTitle={"View Stdout/Stderr of Task"}
onClick={()=>{setOpenStdoutStderrDialog(true);setOpenSpeedDial(false);}}
/>
<SpeedDialAction
icon={<PlayCircleFilledTwoToneIcon />}
arrow
tooltipPlacement={"top"}
tooltipTitle={"Trigger Eventing Based on Task"}
onClick={()=>{onTriggerEventing();setOpenSpeedDial(false);}}
/>
{task.opsec_pre_blocked === null ? null : ( task.opsec_pre_bypassed === false ? (
<SpeedDialAction
icon={<LockIcon style={{color: theme.palette.error.main}}/>}
arrow
tooltipPlacement={"top"}
tooltipTitle={"Submit OPSEC PreCheck Bypass Request"}
onClick={()=>{setOpenOpsecDialog({open: true, view: "pre"});setOpenSpeedDial(false);}}
/>
): (
<SpeedDialAction
icon={<LockOpenIcon style={{color: theme.palette.success.main}}/>}
arrow
tooltipPlacement={"top"}
tooltipTitle={"View OPSEC PreCheck Data"}
onClick={()=>{setOpenOpsecDialog({open: true, view: "pre"});setOpenSpeedDial(false);}}
/>
)
)
}
{task.opsec_post_blocked === null ? null : ( task.opsec_post_bypassed === false ? (
<SpeedDialAction
icon={<LockIcon style={{color: theme.palette.error.main}}/>}
arrow
tooltipPlacement={"top"}
tooltipTitle={"Submit OPSEC PostCheck Bypass Request"}
onClick={()=>{setOpenOpsecDialog({open: true, view: "post"});setOpenSpeedDial(false);}}
/>
): (
<SpeedDialAction
icon={<LockOpenIcon style={{color: theme.palette.success.main}}/>}
arrow
tooltipPlacement={"top"}
tooltipTitle={"View OPSEC PostCheck Data"}
onClick={()=>{setOpenOpsecDialog({open: true, view: "post"});setOpenSpeedDial(false);}}
/>
)
)
}
{task.token === null ? null : (
<SpeedDialAction
icon={<ConfirmationNumberIcon />}
arrow
tooltipPlacement={"top"}
tooltipTitle={"View Token Information"}
onClick={()=>{setOpenTokenDialog(true);setOpenSpeedDial(false);}}
/>
)}
{task.status.toLowerCase().includes("error: container") ? (
<SpeedDialAction
icon={<ReplayIcon style={{color: theme.palette.warning.main}}/>}
arrow
tooltipPlacement={"top"}
tooltipTitle={"Resubmit Tasking"}
onClick={onReissueTask}
/>
) : null}
{task.status.toLowerCase().includes("error: task") ? (
<SpeedDialAction
icon={<ReplayIcon style={{color: theme.palette.warning.main}}/>}
arrow
tooltipPlacement={"top"}
tooltipTitle={"Resubmit Task Handler"}
onClick={onReissueTaskHandler}
/>
):null}
</SpeedDial>
<Paper elevation={0} sx={{
alignItems: "center",
backgroundColor: theme.palette.mode === "dark" ? "rgba(255,255,255,0.03)" : "rgba(0,0,0,0.02)",
borderLeft: 0,
borderRight: 0,
borderTop: 0,
borderRadius: 0,
display: "flex",
gap: 0.5,
minHeight: 36,
overflowX: "auto",
px: 1,
py: 0.5,
}}>
<MythicStyledTooltip title={"Toggle BrowserScript"}>
<IconButton size="small" onClick={toggleViewBrowserScript}>
{viewBrowserScript ? <CodeOffIcon color={"error"} fontSize="small" /> : <CodeIcon color={"success"} fontSize="small" />}
</IconButton>
</MythicStyledTooltip>
<MythicStyledTooltip title={viewAllOutput ? "View Paginated Output" : "View All Output"}>
<IconButton size="small" onClick={toggleSelectAllOutput}>
{viewAllOutput ? <CloseFullscreenIcon color={"error"} fontSize="small" /> : <FontAwesomeIcon style={{color: theme.palette.success.main}} icon={faExpandArrowsAlt} size="sm" />}
</IconButton>
</MythicStyledTooltip>
<MythicStyledTooltip title={"Search Output"}>
<IconButton size="small" onClick={toggleOpenSearch}>
<SearchIcon color={"info"} fontSize="small" />
</IconButton>
</MythicStyledTooltip>
<MythicStyledTooltip title={"Download output"}>
<IconButton size="small" onClick={onDownloadResponses}>
<GetAppIcon color={"success"} fontSize="small" />
</IconButton>
</MythicStyledTooltip>
<MythicStyledTooltip title={"Download screenshot of output"}>
<IconButton size="small" onClick={onDownloadImageClickPng}>
<InsertPhotoIcon fontSize="small" />
</IconButton>
</MythicStyledTooltip>
<Button size="small" variant="text" startIcon={<MoreHorizIcon fontSize="small" />} onClick={openActionsMenu}
sx={{ml: 0.25, minWidth: "auto", textTransform: "none"}}>
More
</Button>
<Menu anchorEl={actionsMenuAnchor}
open={Boolean(actionsMenuAnchor)}
onClose={closeActionsMenu}
anchorOrigin={{vertical: "bottom", horizontal: "right"}}
transformOrigin={{vertical: "top", horizontal: "right"}}>
<MenuItem onClick={()=>{setOpenTaskTagDialog(true);closeActionsMenu();}}>
<ListItemIcon><LocalOfferOutlinedIcon fontSize="small" /></ListItemIcon>
<ListItemText>Edit Tags</ListItemText>
</MenuItem>
<MenuItem onClick={()=> {window.open('/new/task/' + task.display_id, "_blank");closeActionsMenu();}}>
<ListItemIcon><FontAwesomeIcon icon={faExternalLinkAlt} size="sm" /></ListItemIcon>
<ListItemText>Open Task in New Window</ListItemText>
</MenuItem>
<MenuItem onClick={copyToClipboard}>
<ListItemIcon><FileCopyOutlinedIcon fontSize="small" /></ListItemIcon>
<ListItemText>Copy original params</ListItemText>
</MenuItem>
<MenuItem onClick={()=>{setOpenCommentDialog(true);closeActionsMenu();}}>
<ListItemIcon><RateReviewOutlinedIcon fontSize="small" /></ListItemIcon>
<ListItemText>Edit Comment</ListItemText>
</MenuItem>
<MenuItem onClick={()=>{setOpenParametersDialog(true);closeActionsMenu();}}>
<ListItemIcon><KeyboardIcon fontSize="small" /></ListItemIcon>
<ListItemText>View Parameters And Timestamps</ListItemText>
</MenuItem>
<MenuItem onClick={()=>{setOpenStdoutStderrDialog(true);closeActionsMenu();}}>
<ListItemIcon><FontAwesomeIcon style={{color: theme.palette.error.main}} icon={faExclamationTriangle} size="sm" /></ListItemIcon>
<ListItemText>View Stdout/Stderr</ListItemText>
</MenuItem>
<MenuItem onClick={onTriggerEventing}>
<ListItemIcon><PlayCircleFilledTwoToneIcon fontSize="small" /></ListItemIcon>
<ListItemText>Trigger Eventing Based on Task</ListItemText>
</MenuItem>
{(task.opsec_pre_blocked !== null || task.opsec_post_blocked !== null || task.token !== null ||
task.status.toLowerCase().includes("error: container") || task.status.toLowerCase().includes("error: task")) &&
<Divider />
}
{task.opsec_pre_blocked !== null && (
<MenuItem onClick={()=>{setOpenOpsecDialog({open: true, view: "pre"});closeActionsMenu();}}>
<ListItemIcon>
{task.opsec_pre_bypassed === false ?
<LockIcon fontSize="small" style={{color: theme.palette.error.main}}/> :
<LockOpenIcon fontSize="small" style={{color: theme.palette.success.main}}/>
}
</ListItemIcon>
<ListItemText>{task.opsec_pre_bypassed === false ? "Submit OPSEC PreCheck Bypass Request" : "View OPSEC PreCheck Data"}</ListItemText>
</MenuItem>
)}
{task.opsec_post_blocked !== null && (
<MenuItem onClick={()=>{setOpenOpsecDialog({open: true, view: "post"});closeActionsMenu();}}>
<ListItemIcon>
{task.opsec_post_bypassed === false ?
<LockIcon fontSize="small" style={{color: theme.palette.error.main}}/> :
<LockOpenIcon fontSize="small" style={{color: theme.palette.success.main}}/>
}
</ListItemIcon>
<ListItemText>{task.opsec_post_bypassed === false ? "Submit OPSEC PostCheck Bypass Request" : "View OPSEC PostCheck Data"}</ListItemText>
</MenuItem>
)}
{task.token !== null && (
<MenuItem onClick={()=>{setOpenTokenDialog(true);closeActionsMenu();}}>
<ListItemIcon><ConfirmationNumberIcon fontSize="small" /></ListItemIcon>
<ListItemText>View Token Information</ListItemText>
</MenuItem>
)}
{task.status.toLowerCase().includes("error: container") && (
<MenuItem onClick={onReissueTask}>
<ListItemIcon><ReplayIcon fontSize="small" style={{color: theme.palette.warning.main}} /></ListItemIcon>
<ListItemText>Resubmit Tasking</ListItemText>
</MenuItem>
)}
{task.status.toLowerCase().includes("error: task") && (
<MenuItem onClick={onReissueTaskHandler}>
<ListItemIcon><ReplayIcon fontSize="small" style={{color: theme.palette.warning.main}} /></ListItemIcon>
<ListItemText>Resubmit Task Handler</ListItemText>
</MenuItem>
)}
</Menu>
</Paper>
</React.Fragment>
)
@@ -818,8 +818,8 @@ export function TaskParametersDialogRow(props){
<TableRow key={'array' + props.name + i} >
<MythicStyledTableCell style={{width: "2rem"}}>
<MythicStyledTooltip title={"Remove array element"}>
<IconButton onClick={(e) => {removeArrayValue(i)}} color="error">
<DeleteIcon />
<IconButton className="mythic-table-row-icon-action mythic-table-row-icon-action-hover-danger" size="small" onClick={(e) => {removeArrayValue(i)}}>
<DeleteIcon fontSize="small" />
</IconButton>
</MythicStyledTooltip>
</MythicStyledTableCell>
@@ -862,7 +862,7 @@ export function TaskParametersDialogRow(props){
{typedArrayValue.map( (a, i) => (
<TableRow key={'typedarray' + props.name + i} >
<MythicStyledTableCell style={{width: "2rem", paddingLeft:"0"}}>
<IconButton onClick={(e) => {removeTypedArrayValue(i)}} size="large"><DeleteIcon color="error" /> </IconButton>
<IconButton className="mythic-table-row-icon-action mythic-table-row-icon-action-hover-danger" size="small" onClick={(e) => {removeTypedArrayValue(i)}}><DeleteIcon fontSize="small" /> </IconButton>
</MythicStyledTableCell>
<MythicStyledTableCell>
<div style={{display: "inline-flex", alignItems: "center", width: "100%"}}>
@@ -1082,8 +1082,8 @@ export function TaskParametersDialogRow(props){
</MythicStyledTableCell>
<MythicStyledTableCell>
<MythicStyledTooltip title={"Mark associated payload as no longer on host and not available for linking"}>
<Button component="span" style={{color: theme.palette.error.main, padding: 0}}
onClick={onAgentConnectRemovePayloadOnHost}><DeleteIcon />Remove Listed</Button>
<Button className="mythic-table-row-action mythic-table-row-action-hover-danger" component="span" size="small"
startIcon={<DeleteIcon fontSize="small" />} onClick={onAgentConnectRemovePayloadOnHost}>Remove Listed</Button>
</MythicStyledTooltip>
</MythicStyledTableCell>
</TableRow>
@@ -1172,12 +1172,9 @@ export function TaskParametersDialogRow(props){
borderRadius: "10px",
marginRight: "5px"
}}/> Credential</Button>
<Button color={"warning"} component="span" style={{padding: 0}} onClick={() =>{
<Button className="mythic-table-row-action mythic-table-row-action-hover-danger" component="span" size="small" startIcon={<DeleteIcon fontSize="small" />} onClick={() =>{
onDeleteCredential();
}}><DeleteIcon style={{
color: theme.palette.error.main,
marginRight: "5px"
}}/> Credential</Button>
}}>Credential</Button>
</React.Fragment>
)
@@ -16,17 +16,17 @@ export function CreatePayloadNavigationButtons(props){
<div style={{marginLeft: "20px"}}>
<Button
className="mythic-table-row-action"
disabled={props.first}
color="info"
variant={"outlined"}
variant="contained"
onClick={props.canceled}
style={{marginRight: "10px"}}
>
Back
</Button>
<Button
className={`mythic-table-row-action ${props.last ? "mythic-table-row-action-hover-success" : "mythic-table-row-action-hover-info"}`}
variant="contained"
color={props.last ? "success" : "primary"}
onClick={props.finished}
disabled={disabledButtons}
>
@@ -36,8 +36,8 @@ export function CreatePayloadNavigationButtons(props){
{props.last && props.showExtraOptions &&
<React.Fragment>
<Button
className="mythic-table-row-action mythic-table-row-action-hover-warning"
variant="contained"
color="warning"
onClick={props.startOver}
style={{marginLeft: "10px"}}
>
@@ -45,8 +45,8 @@ export function CreatePayloadNavigationButtons(props){
</Button>
{props.showExtraOptions &&
<Button
className="mythic-table-row-action mythic-table-row-action-hover-info"
variant="contained"
color="info"
component={Link}
style={{marginLeft: "10px"}}
to={"/new/createwrapper"}
@@ -508,8 +508,8 @@ export function CreatePayloadParameter({onChange, parameter_type, default_value,
</FormControl>
{dynamic_query_function !== "" && dynamic_query_function !== undefined &&
<MythicStyledTooltip title={"ReIssue Dynamic Query Function"} tooltipStyle={{display: "inline-block"}}>
<IconButton onClick={reIssueDynamicQueryFunction}>
<RefreshIcon />
<IconButton className="mythic-table-row-icon-action mythic-table-row-icon-action-hover-info" size="small" onClick={reIssueDynamicQueryFunction}>
<RefreshIcon fontSize="small" />
</IconButton>
</MythicStyledTooltip>
}
@@ -543,8 +543,8 @@ export function CreatePayloadParameter({onChange, parameter_type, default_value,
/>
{dynamic_query_function !== "" && dynamic_query_function !== undefined &&
<MythicStyledTooltip title={"ReIssue Dynamic Query Function"} tooltipStyle={{display: "inline-block"}}>
<IconButton onClick={reIssueDynamicQueryFunction}>
<RefreshIcon />
<IconButton className="mythic-table-row-icon-action mythic-table-row-icon-action-hover-info" size="small" onClick={reIssueDynamicQueryFunction}>
<RefreshIcon fontSize="small" />
</IconButton>
</MythicStyledTooltip>
}
@@ -577,8 +577,8 @@ export function CreatePayloadParameter({onChange, parameter_type, default_value,
</FormControl>
{dynamic_query_function !== "" && dynamic_query_function !== undefined &&
<MythicStyledTooltip title={"ReIssue Dynamic Query Function"} tooltipStyle={{display: "inline-block"}}>
<IconButton onClick={reIssueDynamicQueryFunction}>
<RefreshIcon />
<IconButton className="mythic-table-row-icon-action mythic-table-row-icon-action-hover-info" size="small" onClick={reIssueDynamicQueryFunction}>
<RefreshIcon fontSize="small" />
</IconButton>
</MythicStyledTooltip>
}
@@ -592,8 +592,8 @@ export function CreatePayloadParameter({onChange, parameter_type, default_value,
{arrayValue.map( (a, i) => (
<TableRow key={'array' + name + i} style={{}} >
<MythicStyledTableCell style={{width: "2rem"}}>
<IconButton onClick={(e) => {removeArrayValue(i)}} color="error">
<DeleteIcon/>
<IconButton className="mythic-table-row-icon-action mythic-table-row-icon-action-hover-danger" size="small" onClick={(e) => {removeArrayValue(i)}}>
<DeleteIcon fontSize="small" />
</IconButton>
</MythicStyledTableCell>
<MythicStyledTableCell>
@@ -606,7 +606,7 @@ export function CreatePayloadParameter({onChange, parameter_type, default_value,
))}
<TableRow >
<MythicStyledTableCell style={{width: "3rem"}}>
<IconButton onClick={addNewArrayValue} size="large"> <AddCircleIcon color="success" /> </IconButton>
<IconButton className="mythic-table-row-icon-action mythic-table-row-icon-action-hover-success" size="small" onClick={addNewArrayValue}> <AddCircleIcon fontSize="small" /> </IconButton>
</MythicStyledTableCell>
<MythicStyledTableCell></MythicStyledTableCell>
</TableRow>
@@ -622,7 +622,7 @@ export function CreatePayloadParameter({onChange, parameter_type, default_value,
{typedArrayValue.map( (a, i) => (
<TableRow key={'typedarray' + name + i} >
<MythicStyledTableCell style={{width: "2rem", paddingLeft:"0"}}>
<IconButton onClick={(e) => {removeTypedArrayValue(i)}} size="large"><DeleteIcon color="error" /> </IconButton>
<IconButton className="mythic-table-row-icon-action mythic-table-row-icon-action-hover-danger" size="small" onClick={(e) => {removeTypedArrayValue(i)}}><DeleteIcon fontSize="small" /> </IconButton>
</MythicStyledTableCell>
<MythicStyledTableCell>
<div style={{display: "inline-flex", alignItems: "center", width: "100%"}}>
@@ -650,7 +650,7 @@ export function CreatePayloadParameter({onChange, parameter_type, default_value,
))}
<TableRow >
<MythicStyledTableCell style={{width: "5rem", paddingLeft:"0"}}>
<IconButton onClick={addNewTypedArrayValue} size="large"> <AddCircleIcon color="success" /> </IconButton>
<IconButton className="mythic-table-row-icon-action mythic-table-row-icon-action-hover-success" size="small" onClick={addNewTypedArrayValue}> <AddCircleIcon fontSize="small" /> </IconButton>
</MythicStyledTableCell>
<MythicStyledTableCell></MythicStyledTableCell>
</TableRow>
@@ -663,7 +663,7 @@ export function CreatePayloadParameter({onChange, parameter_type, default_value,
<React.Fragment>
{dictValue.map( (opt, i) => (
<div key={"dictval" + i}>
<IconButton style={{width: "5%"}} onClick={(e) => {removeDictEntry(i)}} size="large"><DeleteIcon color="error" /> </IconButton>
<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>
@@ -671,7 +671,7 @@ export function CreatePayloadParameter({onChange, parameter_type, default_value,
)}
{dictSelectOptions.length > 0 ? (
<div>
<IconButton onClick={addDictValEntry} size="large"> <AddCircleIcon color="success" /> </IconButton>
<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) => (
<MenuItem key={"selectopt" + name + i} value={selectOpt}>{selectOpt.name}</MenuItem>
@@ -715,8 +715,8 @@ export function CreatePayloadParameter({onChange, parameter_type, default_value,
{mapArray.map( (val, i) => (
<TableRow key={"payloadtype" + i}>
<MythicStyledTableCell style={{width: "2rem"}}>
<IconButton onClick={(e) => {removeMapArrayMap(i)}} color="error">
<DeleteIcon/>
<IconButton className="mythic-table-row-icon-action mythic-table-row-icon-action-hover-danger" size="small" onClick={(e) => {removeMapArrayMap(i)}}>
<DeleteIcon fontSize="small" />
</IconButton>
</MythicStyledTableCell>
<MythicStyledTableCell>
@@ -728,8 +728,8 @@ export function CreatePayloadParameter({onChange, parameter_type, default_value,
{val[1].map( (v, j) => (
<TableRow key={"payloadtypevalue" + i + j}>
<MythicStyledTableCell style={{width: "2rem"}}>
<IconButton onClick={(e) => {removeMapArray(i, j)}} color="error">
<DeleteIcon/>
<IconButton className="mythic-table-row-icon-action mythic-table-row-icon-action-hover-danger" size="small" onClick={(e) => {removeMapArray(i, j)}}>
<DeleteIcon fontSize="small" />
</IconButton>
</MythicStyledTableCell>
<MythicStyledTableCell>
@@ -740,7 +740,7 @@ export function CreatePayloadParameter({onChange, parameter_type, default_value,
))}
<TableRow>
<MythicStyledTableCell style={{width: "3rem"}}>
<IconButton onClick={() => addMapArray(i)} size="large"> <AddCircleIcon color="success" /> </IconButton>
<IconButton className="mythic-table-row-icon-action mythic-table-row-icon-action-hover-success" size="small" onClick={() => addMapArray(i)}> <AddCircleIcon fontSize="small" /> </IconButton>
</MythicStyledTableCell>
<MythicStyledTableCell/>
</TableRow>
@@ -751,7 +751,7 @@ export function CreatePayloadParameter({onChange, parameter_type, default_value,
))}
<TableRow>
<MythicStyledTableCell style={{width: "3rem"}}>
<IconButton onClick={addMapArrayMap} size="large"> <AddCircleIcon color="success" /> </IconButton>
<IconButton className="mythic-table-row-icon-action mythic-table-row-icon-action-hover-success" size="small" onClick={addMapArrayMap}> <AddCircleIcon fontSize="small" /> </IconButton>
</MythicStyledTableCell>
<MythicStyledTableCell/>
<MythicStyledTableCell/>
@@ -149,7 +149,7 @@ const SnackMessageError = (props) => {
tabSize: 4
}}/>
<DialogActions>
<Button variant="contained" onClick={props.onClose} color="primary">
<Button className="mythic-table-row-action" variant="contained" onClick={props.onClose}>
Close
</Button>
</DialogActions>
@@ -231,4 +231,3 @@ export function PayloadSubscriptionNotification(props) {
innerDialog={<SnackMessageError payloadData={payloadData} onClose={handleErrorClose} />}
/>;
}
@@ -548,9 +548,13 @@ export const StartFromExistingPayloadOrStartFresh = (
<div style={{flexShrink: 0}}>
<Typography variant={"p"} style={{fontWeight: 600}}>
3. Continue from Existing Payload or
<Button size="small" color={"primary"} variant={"contained"} style={{marginLeft: "10px", color: "white", marginBottom: "5px"}}
<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 color={"success"} style={{backgroundColor: "white", borderRadius: "10px"}}/>} >
startIcon={<AddCircleIcon fontSize="small" />} >
Start Fresh
</Button>
</Typography>
@@ -892,4 +896,4 @@ export const ConfigurationSummary = ({buildParameters, os, c2_name}) => {
</div>
))
)
}
}
@@ -412,10 +412,10 @@ function CommandTransferSelect(props) {
<ListItemText id={labelId} primary={
<div style={{display: "flex", alignItems: "center", justifyContent: "space-between"}}>
{value}
<IconButton variant="contained" onClick={(e) => e.stopPropagation()}
<IconButton className="mythic-table-row-icon-action mythic-table-row-icon-action-hover-info" size="small" onClick={(e) => e.stopPropagation()}
href={"/docs/agents/" + props.payload_type + "/commands/" + value}
style={{marginLeft: "10px", float: "right"}} target="_blank">
<MenuBookIcon/>
<MenuBookIcon fontSize="small"/>
</IconButton>
</div>
} />
@@ -435,7 +435,7 @@ function CommandTransferSelect(props) {
<StyledButton
variant="contained"
size="small"
className={classes.button}
className={`${classes.button} mythic-table-row-action`}
onClick={handleAllRight}
disabled={commands.filter(c => c.left && !c.disabled).length === 0}
aria-label="move all right"
@@ -445,7 +445,7 @@ function CommandTransferSelect(props) {
<StyledButton
variant="contained"
size="small"
className={classes.button}
className={`${classes.button} mythic-table-row-action`}
onClick={handleCheckedRight}
disabled={commands.filter(c => c.left && c.selected).length === 0}
aria-label="move selected right"
@@ -455,7 +455,7 @@ function CommandTransferSelect(props) {
<StyledButton
variant="contained"
size="small"
className={classes.button}
className={`${classes.button} mythic-table-row-action`}
onClick={handleCheckedLeft}
disabled={commands.filter( c => c.right && c.selected).length === 0}
aria-label="move selected left"
@@ -465,7 +465,7 @@ function CommandTransferSelect(props) {
<StyledButton
variant="contained"
size="small"
className={classes.button}
className={`${classes.button} mythic-table-row-action`}
onClick={handleAllLeft}
disabled={commands.filter(c => c.right && !c.disabled).length === 0}
aria-label="move all left"
@@ -484,4 +484,4 @@ function CommandTransferSelect(props) {
canceled={canceled} finished={finished}/>
</div>
*/
*/
@@ -425,10 +425,14 @@ export function Step4C2Profiles(props){
))
}
</Select>
<Button size="small" color={"primary"} variant={"contained"} style={{marginLeft: "10px", color: "white", marginBottom: "5px"}}
<Button
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 color={"success"} style={{backgroundColor: "white", borderRadius: "10px"}}/>} >
startIcon={<AddCircleIcon fontSize="small" />} >
Include Profile
</Button>
</div>
@@ -656,8 +660,8 @@ const C2ProfileTabs = ({includedC2Profiles, onChange, os, onCloseTab, onChangeCr
<Tab key={c.name + index} label={
<div style={{display: "flex", alignItems: "center"}}>
{c.name}
<IconButton size='small' onClick={(e) => onCloseTabLocal(e, index)} >
<CloseIcon />
<IconButton className="mythic-table-row-icon-action mythic-table-row-icon-action-hover-danger" size='small' onClick={(e) => onCloseTabLocal(e, index)} >
<CloseIcon fontSize="small" />
</IconButton>
</div>
} {...a11yProps(index)} style={{flexShrink: 0}} />
@@ -226,7 +226,7 @@ export function Step5Build(props){
<Typography variant={"p"} style={{}}>
<b>OS: </b>{props.buildOptions[1].os}
<MythicStyledTooltip title={"Edit OS / Payload Type"}>
<IconButton color={"primary"} onClick={() => props.moveToStep(0)}>
<IconButton className="mythic-table-row-icon-action mythic-table-row-icon-action-hover-info" size="small" onClick={() => props.moveToStep(0)}>
<DriveFileRenameOutlineIcon />
</IconButton>
</MythicStyledTooltip>
@@ -275,7 +275,7 @@ export function Step5Build(props){
<Typography textAlign="center" variant={"h7"} style={{fontWeight: 600, width: "100%"}}>
2. Build Parameter Configuration
<MythicStyledTooltip title={"Edit Build Parameters"}>
<IconButton color={"primary"} onClick={() => props.moveToStep(1)}>
<IconButton className="mythic-table-row-icon-action mythic-table-row-icon-action-hover-info" size="small" onClick={() => props.moveToStep(1)}>
<DriveFileRenameOutlineIcon />
</IconButton>
</MythicStyledTooltip>
@@ -297,7 +297,7 @@ export function Step5Build(props){
<Typography textAlign="center" variant={"h7"} style={{fontWeight: 600, width: "100%"}}>
3. Command Selection
<MythicStyledTooltip title={"Edit Commands"}>
<IconButton color={"primary"} onClick={() => props.moveToStep(2)}>
<IconButton className="mythic-table-row-icon-action mythic-table-row-icon-action-hover-info" size="small" onClick={() => props.moveToStep(2)}>
<DriveFileRenameOutlineIcon />
</IconButton>
</MythicStyledTooltip>
@@ -305,10 +305,10 @@ export function Step5Build(props){
{props.buildOptions[2]?.map(c => (
<div key={c} style={{display: "flex", alignItems: "center", justifyContent: "space-between"}}>
{c}
<IconButton variant="contained" onClick={(e) => e.stopPropagation()}
<IconButton className="mythic-table-row-icon-action mythic-table-row-icon-action-hover-info" size="small" onClick={(e) => e.stopPropagation()}
href={"/docs/agents/" + props.buildOptions[1].payload_type + "/commands/" + c}
style={{marginLeft: "10px", float: "right"}} target="_blank">
<MenuBookIcon/>
<MenuBookIcon fontSize="small"/>
</IconButton>
</div>
))}
@@ -329,7 +329,7 @@ export function Step5Build(props){
<Typography textAlign="center" variant={"h7"} style={{fontWeight: 600, width: "100%"}}>
4. C2 Configuration
<MythicStyledTooltip title={"Edit C2 Parameters"}>
<IconButton color={"primary"} onClick={() => props.moveToStep(3)}>
<IconButton className="mythic-table-row-icon-action mythic-table-row-icon-action-hover-info" size="small" onClick={() => props.moveToStep(3)}>
<DriveFileRenameOutlineIcon />
</IconButton>
</MythicStyledTooltip>
@@ -378,4 +378,4 @@ export function Step5Build(props){
<PayloadSubscriptionNotification me={props.me} subscriptionID={subscriptionID} fromNow={fromNow}/>}
</div>
*/
*/
@@ -15,17 +15,17 @@ export function CreatePayloadNavigationButtons(props){
return (
<div style={{marginLeft: "20px"}}>
<Button
className="mythic-table-row-action"
disabled={props.first}
color="info"
variant={"outlined"}
variant="contained"
style={{marginRight: "10px"}}
onClick={props.canceled}
>
Back
</Button>
<Button
className={`mythic-table-row-action ${props.last ? "mythic-table-row-action-hover-success" : "mythic-table-row-action-hover-info"}`}
variant="contained"
color={props.last ? "success" : "primary"}
onClick={props.finished}
disabled={props.disableNext || disabledButtons}
>
@@ -34,8 +34,8 @@ export function CreatePayloadNavigationButtons(props){
{props.last && props.showExtraOptions &&
<React.Fragment>
<Button
className="mythic-table-row-action mythic-table-row-action-hover-warning"
variant="contained"
color="warning"
style={{marginLeft: "10px"}}
onClick={props.startOver}
>
@@ -43,8 +43,8 @@ export function CreatePayloadNavigationButtons(props){
</Button>
{props.showExtraOptions &&
<Button
className="mythic-table-row-action mythic-table-row-action-hover-info"
variant="contained"
color="info"
component={Link}
style={{marginLeft: "10px"}}
to={"/new/createpayload"}
@@ -238,18 +238,20 @@ export function PayloadsTableRow(props){
<React.Fragment>
<TableRow key={"payload" + props.payload.uuid} hover>
<TableCell>
<Button size="small" onClick={onSelected} color="primary" variant="contained">Select</Button>
<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>
<div className="mythic-payload-progress-cell">
<PayloadsTableRowBuildStatus {...props.payload} />
<PayloadsTableRowBuildProgress {...props.payload} />
</div>
</TableCell>
<TableCell>{props.payload.description}</TableCell>
<TableCell>
<IconButton size="small" color="info" onClick={() => setOpenDetailedView(true)}>
<InfoIconOutline />
<IconButton className="mythic-table-row-icon-action mythic-table-row-icon-action-hover-info" size="small" onClick={() => setOpenDetailedView(true)}>
<InfoIconOutline fontSize="small" />
</IconButton>
</TableCell>
</TableRow>
@@ -182,7 +182,7 @@ export function Step5Build(props){
<Typography variant={"p"} style={{}}>
<b>OS: </b>{props.buildOptions[1].os}
<MythicStyledTooltip title={"Edit OS / Payload Type"}>
<IconButton color={"primary"} onClick={() => props.moveToStep(0)}>
<IconButton className="mythic-table-row-icon-action mythic-table-row-icon-action-hover-info" size="small" onClick={() => props.moveToStep(0)}>
<DriveFileRenameOutlineIcon />
</IconButton>
</MythicStyledTooltip>
@@ -231,7 +231,7 @@ export function Step5Build(props){
<Typography textAlign="center" variant={"h7"} style={{fontWeight: 600, width: "100%"}}>
2. Build Parameter Configuration
<MythicStyledTooltip title={"Edit Build Parameters"}>
<IconButton color={"primary"} onClick={() => props.moveToStep(1)}>
<IconButton className="mythic-table-row-icon-action mythic-table-row-icon-action-hover-info" size="small" onClick={() => props.moveToStep(1)}>
<DriveFileRenameOutlineIcon />
</IconButton>
</MythicStyledTooltip>
@@ -253,7 +253,7 @@ export function Step5Build(props){
<Typography textAlign="center" variant={"h7"} style={{fontWeight: 600, width: "100%"}}>
3. Embedded Payload Configuration
<MythicStyledTooltip title={"Edit Selected Payload"}>
<IconButton color={"primary"} onClick={() => props.moveToStep(2)}>
<IconButton className="mythic-table-row-icon-action mythic-table-row-icon-action-hover-info" size="small" onClick={() => props.moveToStep(2)}>
<DriveFileRenameOutlineIcon />
</IconButton>
</MythicStyledTooltip>
@@ -1,36 +1,38 @@
import React from 'react';
import { EventFeedTableEvents } from './EventFeedTableEvents';
import Typography from '@mui/material/Typography';
import Paper from '@mui/material/Paper';
import {useTheme} from '@mui/material/styles';
import Pagination from '@mui/material/Pagination';
import MythicTextField from "../../MythicComponents/MythicTextField";
import SearchIcon from '@mui/icons-material/Search';
import Tooltip from '@mui/material/Tooltip';
import IconButton from '@mui/material/IconButton';
import AutoFixHighIcon from '@mui/icons-material/AutoFixHigh';
import HealingIcon from '@mui/icons-material/Healing';
import MenuItem from '@mui/material/MenuItem';
import Select from '@mui/material/Select';
import Grid from '@mui/material/Grid';
import {alertCount} from "../../../cache";
import {levelOptions} from "./EventFeed";
import {MythicPageBody} from "../../MythicComponents/MythicPageBody";
import {MythicPageHeader, MythicPageHeaderChip} from "../../MythicComponents/MythicPageHeader";
import {MythicTablePagination} from "../../MythicComponents/MythicTablePagination";
import {MythicSearchField, MythicTableToolbar, MythicTableToolbarGroup, MythicToolbarButton, MythicToolbarSelect} from "../../MythicComponents/MythicTableToolbar";
import {MythicEmptyState} from "../../MythicComponents/MythicStateDisplay";
const EventList = ({onUpdateLevel, onUpdateResolution, operationeventlog}) => {
return (
<div style={{ flexGrow: 1}}>
{operationeventlog.map( o => <EventFeedTableEvents {...o}
key={o.id}
onUpdateLevel={onUpdateLevel}
onUpdateResolution={onUpdateResolution}
/>)}
{operationeventlog.length === 0 ? (
<MythicEmptyState
compact
title="No event feed entries"
description="Events matching the current level and search will appear here."
minHeight={180}
/>
) : (
operationeventlog.map( o => <EventFeedTableEvents {...o}
key={o.id}
onUpdateLevel={onUpdateLevel}
onUpdateResolution={onUpdateResolution}
/>)
)}
</div>
)
};
export function EventFeedTable(props){
const theme = useTheme();
const [search, setSearch] = React.useState("");
const [level, setLevel] = React.useState("info");
@@ -51,30 +53,37 @@ export function EventFeedTable(props){
props.onLevelChange("warning (unresolved)");
}
}, []);
const visibleEventsLabel = props.operationeventlog.length === 1 ? "1 shown" : `${props.operationeventlog.length} shown`;
const totalEventsLabel = props.pageData.totalCount === 1 ? "1 total" : `${props.pageData.totalCount} total`;
return (
<MythicPageBody>
<Grid container spacing={1} style={{ marginTop: "0px"}}>
<Grid style={{paddingTop: 0}} size={10}>
<MythicTextField placeholder="Search..." value={search} marginBottom={"0px"} marginTop={"0px"}
onChange={handleSearchValueChange} onEnter={submitSearch} InputProps={{
endAdornment:
<React.Fragment>
<Tooltip title="Search">
<IconButton onClick={submitSearch} size="large"><SearchIcon style={{color: theme.palette.info.main}}/></IconButton>
</Tooltip>
<Tooltip title="Resolve Viewable Errors">
<IconButton onClick={props.resolveViewableErrors} size="large"><AutoFixHighIcon style={{color: theme.palette.success.main}}/></IconButton>
</Tooltip>
<Tooltip title="Resolve All Errors">
<IconButton onClick={props.resolveAllErrors} size="large"><HealingIcon style={{color: theme.palette.success.main}}/></IconButton>
</Tooltip>
</React.Fragment>,
style: {padding: 0}
}}/>
</Grid>
<Grid style={{paddingTop: 0, paddingLeft: 0}} size={2}>
<Select
style={{width: "100%"}}
<MythicPageHeader
title={"Event Feed"}
subtitle={"Review operation messages, warnings, and service events as they arrive."}
meta={
<>
<MythicPageHeaderChip label={level} />
<MythicPageHeaderChip label={visibleEventsLabel} />
<MythicPageHeaderChip label={totalEventsLabel} />
</>
}
actions={
<>
<MythicToolbarButton onClick={props.resolveViewableErrors} color="success" variant="outlined" startIcon={<AutoFixHighIcon />}>
Resolve Viewable
</MythicToolbarButton>
<MythicToolbarButton onClick={props.resolveAllErrors} color="success" variant="outlined" startIcon={<HealingIcon />}>
Resolve All
</MythicToolbarButton>
</>
}
/>
<MythicTableToolbar>
<MythicTableToolbarGroup grow>
<MythicSearchField value={search} onChange={handleSearchValueChange} onEnter={submitSearch} onSearch={submitSearch} />
</MythicTableToolbarGroup>
<MythicTableToolbarGroup>
<MythicToolbarSelect
value={level}
onChange={handleLevelValueChange}
>
@@ -83,9 +92,9 @@ export function EventFeedTable(props){
<MenuItem key={"levelFilter" + opt} value={opt}>{opt}</MenuItem>
))
}
</Select>
</Grid>
</Grid>
</MythicToolbarSelect>
</MythicTableToolbarGroup>
</MythicTableToolbar>
<div style={{display: "flex", flexDirection: "column", overflowY: "auto", flexGrow: 1, overflowX: "Hidden"}}>
<EventList
@@ -93,11 +102,7 @@ export function EventFeedTable(props){
onUpdateLevel={props.onUpdateLevel}
operationeventlog={props.operationeventlog}/>
</div>
<div style={{background: "transparent", display: "flex", justifyContent: "center", alignItems: "center", paddingTop: "5px", paddingBottom: "10px"}}>
<Pagination count={Math.ceil(props.pageData.totalCount / props.pageData.fetchLimit)} variant="outlined" color="primary" boundaryCount={1}
siblingCount={1} onChange={props.onChangePage} showFirstButton={true} showLastButton={true} style={{padding: "20px"}}/>
<Typography style={{paddingLeft: "10px"}}>Total Results: {props.pageData.totalCount}</Typography>
</div>
<MythicTablePagination totalCount={props.pageData.totalCount} fetchLimit={props.pageData.fetchLimit} onChange={props.onChangePage} />
</MythicPageBody>
);
}
}
@@ -6,8 +6,8 @@ import { meState } from '../../../cache';
import {useReactiveVar} from '@apollo/client';
import ListItem from '@mui/material/ListItem';
import ListItemText from '@mui/material/ListItemText';
import {useTheme} from '@mui/material/styles';
import {EventFeedTableEventsActions} from './EventFeedTableEventsActions';
import {MythicStatusChip} from '../../MythicComponents/MythicStatusChip';
const PREFIX = 'EventFeedTableEvents';
@@ -30,35 +30,23 @@ const StyledListItem = styled(ListItem)((
}
}));
const GetPreAdornment = ({message}) => {
const theme = useTheme();
const getColor = React.useCallback(() => {
if(message.warning){
if(message.resolved){
return theme.palette.success.main;
} else {
return theme.palette.error.main;
}
}
return theme.palette.info.main;
}, [theme, message.level, message.resolved, message.warning]);
const getSymbol = React.useCallback(() => {
if(message.warning){
if(message.resolved){
return "+";
} else {
return "!";
}
}
return "*";
}, [theme, message.level, message.resolved, message.warning]);
const GetEventStatusChip = ({message}) => {
if(message.warning){
return (
<MythicStatusChip
label={message.resolved ? "Resolved" : "Warning"}
status={message.resolved ? "success" : "error"}
sx={{mr: 1, flex: "0 0 auto"}}
/>
);
}
return (
<>
<p style={{display: "inline-block", fontWeight: "bold", color: getColor(), margin: "0 0 0 0"}}>[</p>
<p style={{display: "inline-block", fontWeight: "bold", color: getColor(), margin: "0 0 0 0"}}>{getSymbol()}</p>
<p style={{display: "inline-block", fontWeight: "bold", color: getColor(), margin: "0 10px 0 0"}}>]</p>
</>
)
<MythicStatusChip
label={message.level}
status={message.level === "warning" ? "warning" : "info"}
sx={{mr: 1, flex: "0 0 auto"}}
/>
);
}
export function EventFeedTableEvents(props){
@@ -89,8 +77,8 @@ export function EventFeedTableEvents(props){
</React.Fragment>
}
secondary={
<div style={{display: "flex", overflowX: "auto"}}>
<GetPreAdornment message={props} />
<div style={{display: "flex", alignItems: "flex-start", overflowX: "auto"}}>
<GetEventStatusChip message={props} />
<pre style={{ margin: "0 0 0 0px", overflowX: "auto", maxWidth: "90%", wordBreak: "break-all", whiteSpace: "pre-wrap"}}>
{props.message}
</pre>
@@ -105,4 +93,3 @@ export function EventFeedTableEvents(props){
);
}
@@ -27,8 +27,9 @@ export function EventFeedTableEventsActions(props){
<IconButton
aria-controls={"eventmenu" + props.id}
aria-haspopup="true"
className="mythic-table-row-icon-action"
onClick={handleClick}
size="large"><MoreVertIcon/>
size="small"><MoreVertIcon fontSize="small" />
</IconButton>
<Menu elevation={5} id={"eventmenu" + props.id} anchorEl={anchorEl} open={Boolean(anchorEl)} onClose={handleClose} >
{
@@ -45,4 +46,3 @@ export function EventFeedTableEventsActions(props){
</ListItemSecondaryAction>
);
}
@@ -85,19 +85,24 @@ export function TestEventGroupFileDialog({onClose, initialWorkflow}){
}
return (
<React.Fragment>
<DialogTitle id="form-dialog-title">
Create and Verify Eventing Workflow
<MythicStyledTooltip title={"Preview Graph"} tooltipStyle={{float: "right"}}>
<IconButton color={"info"} variant={"contained"} onClick={previewGraph}>
<AccountTreeIcon />
</IconButton>
</MythicStyledTooltip>
<MythicStyledTooltip title={"Create with GUI Wizard"} tooltipStyle={{float: "right"}} >
<IconButton style={{float: "right", margin: 0}} color={"success"} variant={"contained"}
onClick={() => setOpenCreateEventingStepper(true)}>
<CategoryIcon />
</IconButton>
</MythicStyledTooltip>
<DialogTitle id="form-dialog-title" className="mythic-eventing-editor-dialog-title">
<div>
<div className="mythic-eventing-editor-title">Create and verify eventing workflow</div>
<div className="mythic-eventing-editor-subtitle">Edit workflow source and validate it before upload.</div>
</div>
<div className="mythic-eventing-editor-title-actions">
<MythicStyledTooltip title={"Preview graph"}>
<IconButton className="mythic-table-row-icon-action mythic-table-row-icon-action-hover-info" size="small" onClick={previewGraph}>
<AccountTreeIcon fontSize="small" />
</IconButton>
</MythicStyledTooltip>
<MythicStyledTooltip title={"Create with GUI wizard"} >
<IconButton className="mythic-table-row-icon-action mythic-table-row-icon-action-hover-success" size="small"
onClick={() => setOpenCreateEventingStepper(true)}>
<CategoryIcon fontSize="small" />
</IconButton>
</MythicStyledTooltip>
</div>
{openEventStepRender.open &&
<MythicDialog fullWidth={true} maxWidth="xl" open={openEventStepRender.open}
onClose={() => {
@@ -115,20 +120,20 @@ export function TestEventGroupFileDialog({onClose, initialWorkflow}){
/>
}
</DialogTitle>
<DialogContent style={{height: "calc(95vh)", margin: 0, padding: 0}}>
<DialogContent className="mythic-eventing-editor-dialog-content">
<ResponseDisplayPlaintext plaintext={fileText.current} onChangeContent={onChangeFileText} initial_mode={"yaml"} expand={true} />
</DialogContent>
<DialogActions>
<Button variant="contained" onClick={onClose} color="primary">
<DialogActions className="mythic-eventing-wizard-actions">
<Button className="mythic-table-row-action" variant="outlined" onClick={onClose}>
Close
</Button>
<Button variant={"contained"} color={"success"} onClick={testFile}>
<Button className="mythic-table-row-action mythic-table-row-action-hover-info" variant={"outlined"} onClick={testFile}>
Test
</Button>
<Button variant={"contained"} color={"warning"} onClick={submitAsFile}>
<Button className="mythic-table-row-action mythic-table-row-action-hover-success" variant={"outlined"} onClick={submitAsFile}>
Save and Submit
</Button>
</DialogActions>
</React.Fragment>
)
}
}
File diff suppressed because it is too large Load Diff
@@ -16,6 +16,7 @@ import {toLocalTime} from "../../utilities/Time";
import CheckCircleTwoToneIcon from '@mui/icons-material/CheckCircleTwoTone';
import CancelTwoToneIcon from '@mui/icons-material/CancelTwoTone';
import {snackActions} from "../../utilities/Snackbar";
import {MythicStatusChip} from "../../MythicComponents/MythicStatusChip";
const updateApprovalStatusMutation = gql`
mutation updateApprovalStatus($eventgroupapproval_id: Int!, $approved: Boolean!) {
@@ -62,7 +63,7 @@ export function EventGroupTableRunAsDialog({eventgroupapprovals, me, onClose, se
<DialogContent dividers={true} style={{maxHeight: "calc(70vh)"}}>
<DialogContentText>
Individual users must approve workflows to run under their account and operation leads must approve bot workflows.<br/>
<b>Run As: </b>{selectedEventGroup.run_as} <br/>
<b>Run as: </b>{selectedEventGroup.run_as} <br/>
{getRunAsHelp(selectedEventGroup.run_as)}
</DialogContentText>
<TableContainer className="mythicElement">
@@ -80,43 +81,40 @@ export function EventGroupTableRunAsDialog({eventgroupapprovals, me, onClose, se
<TableCell>{e.operator.username}</TableCell>
<TableCell>
{e.approved ? (
<>
<Button disabled style={{marginRight: "10px"}} variant={"contained"} color={"success"}
onClick={() => onApprovalClick({id: e.id, approved: true})}>
<CheckCircleTwoToneIcon color={"success"} style={{marginRight: "5px"}} /> Approved
</Button>
<Button disabled={e.operator.id !== me?.user?.id} variant={"contained"} color={"warning"}
<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})}>
<CancelTwoToneIcon style={{marginRight: "5px"}} /> Deny
Deny
</Button>
</>
</div>
) : e.created_at === e.updated_at ? (
<>
<Button disabled={e.operator.id !== me?.user?.id} style={{marginRight: "20px"}}
variant={"contained"} color={"success"}
<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})}>
<CheckCircleTwoToneIcon style={{marginRight: "5px"}} /> {"Approve "}
Approve
</Button>
<Button disabled={e.operator.id !== me?.user?.id} variant={"contained"}
color={"warning"}
<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})}>
<CancelTwoToneIcon style={{marginRight: "5px"}} /> Deny
Deny
</Button>
</>
</div>
) : (
<>
<Button style={{marginRight: "20px"}} variant={"contained"} color={"success"}
<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})}>
<CheckCircleTwoToneIcon style={{marginRight: "5px"}} /> Approve
</Button>
<Button disabled variant={"contained"} color={"warning"}
onClick={() => onApprovalClick({id: e.id, approved: false})}>
<CancelTwoToneIcon color={"warning"} style={{marginRight: "5px"}} /> Denied
Approve
</Button>
<MythicStatusChip label="Denied" status="warning" icon={<CancelTwoToneIcon />} />
</>
</div>
)}
</TableCell>
<TableCell>
@@ -132,7 +130,7 @@ export function EventGroupTableRunAsDialog({eventgroupapprovals, me, onClose, se
</DialogContent>
<DialogActions>
<Button onClick={onClose} variant="contained" color="primary">
<Button className="mythic-table-row-action" onClick={onClose} variant="contained">
Close
</Button>
</DialogActions>
@@ -114,14 +114,13 @@ export function EventGroupTableEditDialog({onClose, selectedEventGroup, includeS
<ResponseDisplayPlaintext plaintext={workflowRef.current} onChangeContent={updateWorkflow} initial_mode={outputFormat} expand={true} />
</DialogContent>
<DialogActions>
<Button onClick={onClose} variant="contained" color="primary">
<Button className="mythic-table-row-action" onClick={onClose} variant="contained">
Close
</Button>
<Button onClick={includeSteps ? submitAsFile : onUpdateClick} variant="contained" color="success">
<Button className="mythic-table-row-action mythic-table-row-action-hover-success" onClick={includeSteps ? submitAsFile : onUpdateClick} variant="contained">
{includeSteps ? "Create New Workflow": "Update"}
</Button>
</DialogActions>
</React.Fragment>
);
}
@@ -21,7 +21,7 @@ import MythicStyledTableCell from "../../MythicComponents/MythicTableCell";
import {faPhotoVideo} from '@fortawesome/free-solid-svg-icons';
import {FontAwesomeIcon} from '@fortawesome/react-fontawesome';
import DeleteIcon from '@mui/icons-material/Delete';
import {Link, Typography} from '@mui/material';
import {IconButton, Link, Typography} from '@mui/material';
import {MythicConfirmDialog} from "../../MythicComponents/MythicConfirmDialog";
import CloudUploadIcon from '@mui/icons-material/CloudUpload';
@@ -38,15 +38,19 @@ export function EventFileManageDialog({onClose, selectedEventGroup}) {
return (
<React.Fragment>
<DialogTitle id="form-dialog-title">
Add or Remove Files associated with this workflow
<Button size={"small"} style={{float: "right", marginRight: "10px"}}
variant={"contained"}
color={"success"} component="label"
startIcon={<CloudUploadIcon />}
>
New Files
<input onChange={onFileChange} type="file" multiple hidden/>
</Button>
<div className="mythic-dialog-title-row">
<span>Add or Remove Files associated with this workflow</span>
<Button
className="mythic-table-row-action mythic-table-row-action-hover-success"
component="label"
size="small"
startIcon={<CloudUploadIcon fontSize="small" />}
variant="contained"
>
New Files
<input onChange={onFileChange} type="file" multiple hidden/>
</Button>
</div>
</DialogTitle>
<DialogContent dividers={true} style={{maxHeight: "calc(70vh)"}}>
@@ -64,7 +68,7 @@ export function EventFileManageDialog({onClose, selectedEventGroup}) {
</TableContainer>
</DialogContent>
<DialogActions>
<Button onClick={onClose} variant="contained" color="primary">
<Button className="mythic-table-row-action" onClick={onClose} variant="contained">
Close
</Button>
</DialogActions>
@@ -102,11 +106,15 @@ function EventFileManageDialogTableRow({eventFile}) {
{openDelete &&
<MythicConfirmDialog onClose={() => {setOpenDelete(false);}} onSubmit={onAcceptDelete} open={openDelete}/>
}
<DeleteIcon color={"error"} fontSize={"large"} style={{height: "35px", cursor: "pointer"}}
onClick={()=>{setOpenDelete(true);}}/>
<MythicStyledTooltip title={"Delete file"}>
<IconButton className="mythic-table-row-icon-action mythic-table-row-icon-action-hover-danger" size="small" onClick={()=>{setOpenDelete(true);}}>
<DeleteIcon fontSize="small" />
</IconButton>
</MythicStyledTooltip>
<MythicStyledTooltip title={"Preview Media"}>
<FontAwesomeIcon icon={faPhotoVideo} style={{height: "25px", bottom: "5px", position: "relative", cursor: "pointer", display: "inline-block"}}
onClick={onPreviewMedia} />
<IconButton className="mythic-table-row-icon-action mythic-table-row-icon-action-hover-info" size="small" onClick={onPreviewMedia}>
<FontAwesomeIcon icon={faPhotoVideo} />
</IconButton>
</MythicStyledTooltip>
{openPreviewMediaDialog &&
<MythicDialog fullWidth={true} maxWidth="xl" open={openPreviewMediaDialog}
@@ -42,7 +42,7 @@ export function EventGroupConsumingContainersDialog({onClose, selectedEventGroup
</TableContainer>
</DialogContent>
<DialogActions>
<Button onClick={onClose} variant="contained" color="primary">
<Button className="mythic-table-row-action" onClick={onClose} variant="contained">
Close
</Button>
</DialogActions>
@@ -274,33 +274,33 @@ function EventGroupInstancesTableMaterialReactTablePreMemo({eventgroups, me, set
return <CallbacksTableStringCell rowData={row} cellData={row.id} />
case "Status":
return (
<div style={{display: "flex", flexDirection:"row", alignItems: "center"}}>
<div className="mythic-table-row-actions mythic-table-row-actions-nowrap">
<GetStatusSymbol data={row} />
{
selectedInstanceID === 0 ?
(
<MythicStyledTooltip title={"View Graph Above"} >
<IconButton onClick={() => {setSelectedInstance(row.id);}} >
<CastConnectedTwoToneIcon />
<IconButton className="mythic-table-row-icon-action mythic-table-row-icon-action-hover-info" size="small" onClick={() => {setSelectedInstance(row.id);}} >
<CastConnectedTwoToneIcon fontSize="small" />
</IconButton>
</MythicStyledTooltip>
) :
(
<MythicStyledTooltip title={"Stop viewing graph"} >
<IconButton onClick={() => {setSelectedInstance(0);}} >
<CancelTwoToneIcon />
<IconButton className="mythic-table-row-icon-action mythic-table-row-icon-action-hover-danger" size="small" onClick={() => {setSelectedInstance(0);}} >
<CancelTwoToneIcon fontSize="small" />
</IconButton>
</MythicStyledTooltip>
)
}
<MythicStyledTooltip title={"Open Graph in Modal"}>
<IconButton onClick={() => {openViewInstanceLargeDialog(row)}}>
<OpenInNewTwoToneIcon />
<IconButton className="mythic-table-row-icon-action mythic-table-row-icon-action-hover-info" size="small" onClick={() => {openViewInstanceLargeDialog(row)}}>
<OpenInNewTwoToneIcon fontSize="small" />
</IconButton>
</MythicStyledTooltip>
<MythicStyledTooltip title={"Copy shareable link to workflow"}>
<IconButton onClick={() => onSaveToClipboard(row)}>
<IosShareIcon />
<IconButton className="mythic-table-row-icon-action mythic-table-row-icon-action-hover-info" size="small" onClick={() => onSaveToClipboard(row)}>
<IosShareIcon fontSize="small" />
</IconButton>
</MythicStyledTooltip>
</div>
@@ -344,26 +344,26 @@ function EventGroupInstancesTableMaterialReactTablePreMemo({eventgroups, me, set
return <CallbacksTableStringCell rowData={row} cellData={row?.operator?.username} />
case "Action":
return (
<div style={{display: "flex", alignItems: "center"}}>
<div className="mythic-table-row-actions mythic-table-row-actions-nowrap">
{row.end_timestamp === null ? (
<MythicStyledTooltip title={"Cancel Eventing"} >
<IconButton onClick={() => {onOpenCancelDialog({id: row.id});}}
color={"warning"}>
<CancelTwoToneIcon />
className="mythic-table-row-icon-action mythic-table-row-icon-action-hover-warning" size="small">
<CancelTwoToneIcon fontSize="small" />
</IconButton>
</MythicStyledTooltip>
) : row.status === "error" || row.status === "cancelled" ? (
<MythicStyledTooltip title={"Retry Failed / Canceled Steps"} >
<IconButton onClick={() => {onOpenRetryDialog({id: row.id});}}
color={"warning"}>
<ReplayIcon />
className="mythic-table-row-icon-action mythic-table-row-icon-action-hover-warning" size="small">
<ReplayIcon fontSize="small" />
</IconButton>
</MythicStyledTooltip>
) : row.status === "success" ? (
<MythicStyledTooltip title={"Run Again"} >
<IconButton onClick={() => {onOpenRunAgainDialog({id: row.id});}}
color={"success"}>
<ReplayIcon />
className="mythic-table-row-icon-action mythic-table-row-icon-action-hover-success" size="small">
<ReplayIcon fontSize="small" />
</IconButton>
</MythicStyledTooltip>
) : null}
@@ -471,4 +471,4 @@ function EventGroupInstancesTableMaterialReactTablePreMemo({eventgroups, me, set
)
}
export const EventGroupInstancesTableMaterialReactTable = React.memo(EventGroupInstancesTableMaterialReactTablePreMemo)
export const EventGroupInstancesTableMaterialReactTable = React.memo(EventGroupInstancesTableMaterialReactTablePreMemo)
@@ -1,8 +1,6 @@
import React, {useCallback} from 'react';
import React from 'react';
import { gql, useMutation } from '@apollo/client';
import {snackActions} from '../../utilities/Snackbar';
import {useTheme} from '@mui/material/styles';
import Typography from '@mui/material/Typography';
import Button from '@mui/material/Button';
import {toLocalTime} from "../../utilities/Time";
import NotificationsActiveTwoToneIcon from '@mui/icons-material/NotificationsActiveTwoTone';
@@ -16,7 +14,6 @@ import {
EventStepRenderFlowWithProvider
} from "./EventStepRender";
import OpenInNewTwoToneIcon from '@mui/icons-material/OpenInNewTwoTone';
import PermMediaTwoToneIcon from '@mui/icons-material/PermMediaTwoTone';
import AddCircleIcon from '@mui/icons-material/AddCircle';
import Table from '@mui/material/Table';
import TableBody from '@mui/material/TableBody';
@@ -44,6 +41,7 @@ import {EventGroupConsumingContainersDialog} from "./EventGroupConsumingContaine
import CalendarMonthTwoToneIcon from '@mui/icons-material/CalendarMonthTwoTone';
import {EventGroupTableEditDialog} from "./EventEditEventGroupDialog";
import EditIcon from '@mui/icons-material/Edit';
import {MythicPageHeader, MythicPageHeaderChip} from "../../MythicComponents/MythicPageHeader";
const updateDeleteStatusMutation = gql(`
mutation updateDeleteStatusMutation($eventgroup_id: Int!, $deleted: Boolean!) {
@@ -142,75 +140,73 @@ export function EventGroupTable({selectedEventGroup, me, showInstances, showGrap
triggerManually({variables: {eventgroup_id: selectedEventGroup.id}});
}
return (
<div style={{marginLeft: "5px", display: "flex", overflowY: "auto", flexDirection: "column", height: height || "100%"}}>
<div className="mythic-eventing-detail" style={{height: height || "100%"}}>
{selectedEventGroup.id === 0 &&
<Typography variant={"h4"}><strong>All Eventing Runs</strong></Typography>
<MythicPageHeader
dense
title="All Eventing Runs"
subtitle="Review recent workflow executions across all registered event groups."
/>
}
{selectedEventGroup.id !== 0 &&
<div>
{selectedEventGroup.deleted ? (
<div style={{float: "right", cursor: "pointer", paddingTop: "5px"}}>
<MythicStyledTooltip title={"Restore event group workflow"}>
<Button color={"secondary"} size="small" onClick={onAcceptDelete} >
<RestoreFromTrashIcon color="success"/>
Restore
</Button>
</MythicStyledTooltip>
</div>
) : (
<div style={{float: "right", cursor: "pointer", paddingTop: "5px"}}>
<MythicStyledTooltip
title={"Mark event group as deleted so no new triggers for it will work and it will be removed from view"}>
<Button color={"secondary"} size="small" onClick={() => {
setOpenDeleteDialog(true);
}} >
<DeleteIcon color="error"/> Delete
</Button>
</MythicStyledTooltip>
{openDeleteDialog &&
<MythicConfirmDialog onClose={() => {
setOpenDeleteDialog(false);
}} onSubmit={onAcceptDelete} open={openDeleteDialog}/>
}
</div>
)}
<Typography variant={"h4"} style={{
display: "inline-block",
marginRight: "10px"
}}><strong>{selectedEventGroup?.name}</strong></Typography>
{selectedEventGroup.active ?
(
<>
<MythicPageHeader
dense
title={selectedEventGroup?.name}
subtitle={selectedEventGroup?.description}
meta={
<>
<MythicStyledTooltip title={"Disable Event Group"}>
<Button color={"secondary"} onClick={() => {
setOpenActiveDialog(true);
}}>
<NotificationsActiveTwoToneIcon size={"small"} color={"success"}/>
Enabled
</Button>
</MythicStyledTooltip>
{openActiveDialog &&
<MythicConfirmDialog onClose={() => {
setOpenActiveDialog(false);
}} onSubmit={onAcceptActive} open={openActiveDialog}
acceptText={"Disable"}
/>
}
<MythicPageHeaderChip
icon={selectedEventGroup.active ? <NotificationsActiveTwoToneIcon /> : <NotificationsOffTwoToneIcon />}
label={selectedEventGroup.active ? "Enabled" : "Disabled"}
status={selectedEventGroup.active ? "success" : undefined}
className={selectedEventGroup.active ? "" : "mythic-eventing-header-chip-disabled"}
/>
<MythicPageHeaderChip label={selectedEventGroup.trigger} />
<MythicPageHeaderChip label={selectedEventGroup.run_as || "unknown"} />
<MythicPageHeaderChip
label={selectedEventGroup.approved_to_run ? "Approved" : "Needs approval"}
status={selectedEventGroup.approved_to_run ? "success" : "warning"}
/>
{selectedEventGroup.deleted && <MythicPageHeaderChip label="Deleted" status="error" />}
</>
) :
(
<MythicStyledTooltip title={"Enable Event Group"}>
<Button color={"secondary"} onClick={onAcceptActive}>
<NotificationsOffTwoToneIcon size={"small"} color={"warning"}/>
Disabled
</Button>
</MythicStyledTooltip>
)
}
actions={
<>
{selectedEventGroup.deleted ? (
<Button className="mythic-table-row-action-hover-success" variant="outlined" size="small" startIcon={<RestoreFromTrashIcon fontSize="small" />} onClick={onAcceptDelete}>
Restore
</Button>
) : (
<Button className="mythic-table-row-action-hover-danger" variant="outlined" size="small" startIcon={<DeleteIcon fontSize="small" />} onClick={() => setOpenDeleteDialog(true)}>
Delete
</Button>
)}
{selectedEventGroup.active ? (
<Button className="mythic-table-row-action-hover-warning" variant="outlined" size="small" startIcon={<NotificationsActiveTwoToneIcon fontSize="small" />} onClick={() => setOpenActiveDialog(true)}>
Disable
</Button>
) : (
<Button className="mythic-table-row-action-hover-success" variant="outlined" size="small" startIcon={<NotificationsOffTwoToneIcon fontSize="small" />} onClick={onAcceptActive}>
Enable
</Button>
)}
</>
}
/>
{openDeleteDialog &&
<MythicConfirmDialog onClose={() => {
setOpenDeleteDialog(false);
}} onSubmit={onAcceptDelete} open={openDeleteDialog}/>
}
{openActiveDialog &&
<MythicConfirmDialog onClose={() => {
setOpenActiveDialog(false);
}} onSubmit={onAcceptActive} open={openActiveDialog}
acceptText={"Disable"}
/>
}
<br/><Typography variant={"h8"}>{selectedEventGroup?.description}</Typography>
<TableContainer className="mythicElement">
<Table>
<TableHead>
@@ -220,7 +216,7 @@ export function EventGroupTable({selectedEventGroup, me, showInstances, showGrap
<TableCell>Keywords</TableCell>
<TableCell style={{width: "4rem"}}>Context</TableCell>
<TableCell style={{width: "3rem"}}>Env</TableCell>
<TableCell style={{width: "10rem"}}>Run As</TableCell>
<TableCell style={{width: "10rem"}}>Run as</TableCell>
<TableCell style={{width: "15rem"}}>Actions</TableCell>
</TableRow>
</TableHead>
@@ -244,8 +240,8 @@ export function EventGroupTable({selectedEventGroup, me, showInstances, showGrap
{selectedEventGroup.trigger_data &&
<IconButton onClick={() => {
setOpenTriggerDataView(true);
}} size={"small"} variant={"outlined"} color={"info"} style={{}}>
<InfoTwoToneIcon/>
}} className="mythic-table-row-icon-action mythic-table-row-icon-action-hover-info" size="small">
<InfoTwoToneIcon fontSize="small" />
</IconButton>
}
</MythicTableCell>
@@ -253,8 +249,8 @@ export function EventGroupTable({selectedEventGroup, me, showInstances, showGrap
{selectedEventGroup.environment &&
<IconButton onClick={() => {
setOpenEnvView(true);
}} size={"small"} variant={"outlined"} color={"info"} style={{}}>
<InfoTwoToneIcon/>
}} className="mythic-table-row-icon-action mythic-table-row-icon-action-hover-info" size="small">
<InfoTwoToneIcon fontSize="small" />
</IconButton>
}
</MythicTableCell>
@@ -263,16 +259,16 @@ export function EventGroupTable({selectedEventGroup, me, showInstances, showGrap
{selectedEventGroup.approved_to_run ?
(
<MythicStyledTooltip title={"all user approvals received"}>
<IconButton onClick={() => setOpenApprovalDialog(true)}>
<ChecklistRtlTwoToneIcon color={"success"} />
<IconButton className="mythic-table-row-icon-action mythic-table-row-icon-action-success" size="small" onClick={() => setOpenApprovalDialog(true)}>
<ChecklistRtlTwoToneIcon fontSize="small" />
</IconButton>
</MythicStyledTooltip>
) : (
<MythicStyledTooltip title={"missing user approvals"}>
<IconButton onClick={() => setOpenApprovalDialog(true)}>
<RuleTwoToneIcon color={"error"} />
<IconButton className="mythic-table-row-icon-action mythic-table-row-icon-action-danger" size="small" onClick={() => setOpenApprovalDialog(true)}>
<RuleTwoToneIcon fontSize="small" />
</IconButton>
</MythicStyledTooltip>
@@ -284,39 +280,39 @@ export function EventGroupTable({selectedEventGroup, me, showInstances, showGrap
</MythicTableCell>
<MythicTableCell>
<div className="mythic-table-row-actions mythic-table-row-actions-nowrap">
<MythicStyledTooltip title={"Edit Workflow Metadata (not steps)"}>
<IconButton onClick={() => {setOpenEditDialog(true)}}>
<EditIcon color={"info"}/>
<IconButton className="mythic-table-row-icon-action mythic-table-row-icon-action-hover-info" size="small" onClick={() => {setOpenEditDialog(true)}}>
<EditIcon fontSize="small" />
</IconButton>
</MythicStyledTooltip>
{selectedEventGroup.trigger === "manual" &&
<MythicStyledTooltip title={"Trigger manually now"}>
<IconButton onClick={onTriggerManual}>
<PlayCircleFilledTwoToneIcon color={"success"}/>
<IconButton className="mythic-table-row-icon-action mythic-table-row-icon-action-hover-success" size="small" onClick={onTriggerManual}>
<PlayCircleFilledTwoToneIcon fontSize="small" />
</IconButton>
</MythicStyledTooltip>
}
<MythicStyledTooltip title={"Large Graph View"}>
<IconButton onClick={() => {
setOpenEventStepRender(true);
}} size={"small"} variant={"outlined"} color={"secondary"} style={{}}>
<OpenInNewTwoToneIcon color={"info"} style={{cursor: "pointer", marginRight: "5px"}}/>
}} className="mythic-table-row-icon-action mythic-table-row-icon-action-hover-info" size="small">
<OpenInNewTwoToneIcon fontSize="small" />
</IconButton>
</MythicStyledTooltip>
<MythicStyledTooltip title={"Create New Workflow From This One"}>
<IconButton onClick={() => {
setOpenFileView(true);
}} size={"small"} color={"secondary"} style={{}}>
<PermMediaTwoToneIcon color={"info"} style={{cursor: "pointer", marginRight: "5px"}}/>
<AddCircleIcon color={"success"} style={{marginLeft: "-8px", marginTop: "7px", backgroundColor: "white", borderRadius: "10px"}} fontSize={"small"} />
}} className="mythic-table-row-icon-action mythic-table-row-icon-action-hover-success" size="small">
<AddCircleIcon fontSize="small" />
</IconButton>
</MythicStyledTooltip>
<MythicStyledTooltip title={"Manage Associated Files"}>
<IconButton onClick={() => {
setOpenFileManageView(true);
}} size={"small"} variant={"outlined"} color={"secondary"} style={{}}>
}} className="mythic-table-row-icon-action mythic-table-row-icon-action-hover-info" size="small">
<Badge badgeContent={selectedEventGroup.filemeta.length} color="secondary">
<AttachFileIcon color={"info"} style={{cursor: "pointer", marginRight: "5px"}}/>
<AttachFileIcon fontSize="small" />
</Badge>
</IconButton>
</MythicStyledTooltip>
@@ -324,8 +320,8 @@ export function EventGroupTable({selectedEventGroup, me, showInstances, showGrap
<MythicStyledTooltip title={"Manually execute via keyword"}>
<IconButton onClick={() => {
setOpenTriggerKeyword(true);
}} size={"small"} variant={"outlined"} color={"secondary"} style={{}}>
<SpellcheckIcon color={"success"} style={{cursor: "pointer", marginRight: "5px"}}/>
}} className="mythic-table-row-icon-action mythic-table-row-icon-action-hover-success" size="small">
<SpellcheckIcon fontSize="small" />
</IconButton>
</MythicStyledTooltip>
}
@@ -334,20 +330,20 @@ export function EventGroupTable({selectedEventGroup, me, showInstances, showGrap
<Badge badgeContent={consumingContainersErrors} color={"error"}>
<IconButton onClick={() => {
setOpenConsumingContainerDialog(true);
}} size={"small"} variant={"outlined"} color={"secondary"}>
<LayersTwoToneIcon color={consumingContainersErrors > 0 ? "error": "info"} style={{marginRight: "-3px"}} />
}} className={`mythic-table-row-icon-action ${consumingContainersErrors > 0 ? "mythic-table-row-icon-action-danger" : "mythic-table-row-icon-action-hover-info"}`} size="small">
<LayersTwoToneIcon fontSize="small" />
</IconButton>
</Badge>
</MythicStyledTooltip>
}
</div>
</MythicTableCell>
</TableRow>
</TableBody>
</Table>
</TableContainer>
</div>
</>
}
{showGraph && <RenderSteps selectedInstanceID={selectedInstanceID} selectedEventGroup={selectedEventGroup} />}
{showInstances && <EventGroupInstances setSelectedInstance={setSelectedInstanceID}
@@ -455,7 +451,6 @@ export function EventGroupTable({selectedEventGroup, me, showInstances, showGrap
}
function RenderSteps({selectedEventGroup, selectedInstanceID}){
const theme = useTheme();
const getRenderer = () => {
if(selectedInstanceID > 0){
return <EventStepInstanceRenderFlowWithProvider selectedEventGroupInstance={selectedInstanceID} />
@@ -467,13 +462,7 @@ function RenderSteps({selectedEventGroup, selectedInstanceID}){
}
return (
<div style={{
height: "200px",
minHeight: "200px",
maxHeight: "200px",
border: `1px solid ${theme.palette.secondary.main}`,
borderRadius: "5px"
}}>
<div className="mythic-eventing-graph-panel">
{getRenderer()}
</div>
)
File diff suppressed because it is too large Load Diff
@@ -21,6 +21,7 @@ import {snackActions} from "../../utilities/Snackbar";
import {MeContext} from "../../App";
import {EventGroupTable} from "./EventGroupTable";
import {MythicDraggableDialogTitle} from "../../MythicComponents/MythicDraggableDialogTitle";
import AddCircleIcon from '@mui/icons-material/AddCircle';
const triggerManualMutation = gql(`
mutation triggerManualMutation($eventgroup_id: Int!, $env_data: jsonb){
@@ -236,8 +237,8 @@ export function EventTriggerContextSelectDialog({onClose, triggerContext}) {
{dictionaryData.map( (e, index) => (
<TableRow key={"dictionarydata" + index}>
<MythicStyledTableCell style={{display: "flex"}} >
<IconButton color={"error"} onClick={() => removeRow(index)} >
<DeleteIcon />
<IconButton className="mythic-table-row-icon-action mythic-table-row-icon-action-hover-danger" size="small" onClick={() => removeRow(index)} >
<DeleteIcon fontSize="small" />
</IconButton>
<Select
style={{}}
@@ -263,7 +264,7 @@ export function EventTriggerContextSelectDialog({onClose, triggerContext}) {
))}
<TableRow>
<MythicStyledTableCell>
<Button onClick={addRow} color={"success"}>
<Button className="mythic-table-row-action mythic-table-row-action-hover-success" onClick={addRow} variant="contained" startIcon={<AddCircleIcon fontSize="small" />}>
Add Entry
</Button>
</MythicStyledTableCell>
@@ -275,10 +276,10 @@ export function EventTriggerContextSelectDialog({onClose, triggerContext}) {
</DialogContent>
<DialogActions>
<Button onClick={onClose} variant="contained" color="primary">
<Button className="mythic-table-row-action" onClick={onClose} variant="contained">
Close
</Button>
<Button onClick={onSubmit} variant="contained" color="success">
<Button className="mythic-table-row-action mythic-table-row-action-hover-success" onClick={onSubmit} variant="contained">
Submit
</Button>
</DialogActions>
@@ -18,6 +18,7 @@ import MythicStyledTableCell from "../../MythicComponents/MythicTableCell";
import DeleteIcon from '@mui/icons-material/Delete';
import {TextField} from '@mui/material';
import {snackActions} from "../../utilities/Snackbar";
import AddCircleIcon from '@mui/icons-material/AddCircle';
const triggerKeywordMutation = gql(`
mutation triggerKeywordMutation($keyword: String!, $keywordEnvData: jsonb!){
@@ -133,8 +134,8 @@ export function EventTriggerKeywordDialog({onClose, selectedEventGroup}) {
{dictionaryData.map( (e, index) => (
<TableRow key={"dictionarydata" + index}>
<MythicStyledTableCell style={{display: "flex"}} >
<IconButton color={"error"} onClick={() => removeRow(index)} >
<DeleteIcon />
<IconButton className="mythic-table-row-icon-action mythic-table-row-icon-action-hover-danger" size="small" onClick={() => removeRow(index)} >
<DeleteIcon fontSize="small" />
</IconButton>
<Select
style={{}}
@@ -160,7 +161,7 @@ export function EventTriggerKeywordDialog({onClose, selectedEventGroup}) {
))}
<TableRow>
<MythicStyledTableCell>
<Button onClick={addRow} color={"success"}>
<Button className="mythic-table-row-action mythic-table-row-action-hover-success" onClick={addRow} variant="contained" startIcon={<AddCircleIcon fontSize="small" />}>
Add Entry
</Button>
</MythicStyledTableCell>
@@ -172,10 +173,10 @@ export function EventTriggerKeywordDialog({onClose, selectedEventGroup}) {
</DialogContent>
<DialogActions>
<Button onClick={onClose} variant="contained" color="primary">
<Button className="mythic-table-row-action" onClick={onClose} variant="contained">
Close
</Button>
<Button onClick={onSubmit} variant="contained" color="success">
<Button className="mythic-table-row-action mythic-table-row-action-hover-success" onClick={onSubmit} variant="contained">
Submit
</Button>
</DialogActions>
@@ -1,28 +1,26 @@
import React, {useCallback} from 'react';
import React from 'react';
import { gql, useSubscription, useQuery } from '@apollo/client';
import {snackActions} from '../../utilities/Snackbar';
import {useTheme} from '@mui/material/styles';
import Button from '@mui/material/Button';
import List from '@mui/material/List';
import ListSubheader from '@mui/material/ListSubheader';
import Divider from '@mui/material/Divider';
import ListItem from '@mui/material/ListItem';
import ListItemText from '@mui/material/ListItemText';
import TextField from '@mui/material/TextField';
import InputAdornment from '@mui/material/InputAdornment';
import {EventGroupTable} from "./EventGroupTable";
import {UploadEventFile} from "../../MythicComponents/MythicFileUpload";
import VisibilityOffIcon from '@mui/icons-material/VisibilityOff';
import VisibilityIcon from '@mui/icons-material/Visibility';
import {MythicStyledTooltip} from "../../MythicComponents/MythicStyledTooltip";
import { IconButton } from '@mui/material';
import NotificationsOffTwoToneIcon from '@mui/icons-material/NotificationsOffTwoTone';
import CloudUploadIcon from '@mui/icons-material/CloudUpload';
import SearchIcon from '@mui/icons-material/Search';
import {MythicDialog} from "../../MythicComponents/MythicDialog";
import {TestEventGroupFileDialog} from "./CreateEventWorkflowDialog";
import AddCircleIcon from '@mui/icons-material/AddCircle';
import Split from 'react-split';
import Paper from '@mui/material/Paper';
import {CreateEventingStepper} from "./CreateEventingStepper";
import CategoryIcon from '@mui/icons-material/Category';
import {MythicPageBody} from "../../MythicComponents/MythicPageBody";
import {MythicPageHeader, MythicPageHeaderChip} from "../../MythicComponents/MythicPageHeader";
import {MythicToolbarButton, MythicToolbarToggle} from "../../MythicComponents/MythicTableToolbar";
const get_eventgroups = gql`
query GetEventGroups {
@@ -164,12 +162,31 @@ steps:
outputs:
SCRIPT_TASK_ID: id
`;
const sidebarFilterOptionsBase = [
{key: "all", label: "All"},
{key: "runnable", label: "Runnable"},
{key: "needs_approval", label: "Needs approval"},
{key: "disabled", label: "Disabled"},
];
const getEventGroupStatus = (eventGroup) => {
if(eventGroup.deleted){
return {key: "deleted", label: "Deleted", rank: 3};
}
if(!eventGroup.active){
return {key: "disabled", label: "Disabled", rank: 2};
}
if(!eventGroup.approved_to_run){
return {key: "needs_approval", label: "Needs approval", rank: 1};
}
return {key: "runnable", label: "Runnable", rank: 0};
};
export function Eventing({me}){
const theme = useTheme();
const [openTestModal, setOpenTestModal] = React.useState(false);
const [openCreateEventingStepper, setOpenCreateEventingStepper] = React.useState(false);
const [eventgroups, setEventgroups] = React.useState([]);
const [showDeleted, setShowDeleted] = React.useState(false);
const [sidebarSearch, setSidebarSearch] = React.useState("");
const [sidebarFilter, setSidebarFilter] = React.useState("all");
const [selectedEventGroup, setSelectedEventGroup] = React.useState({id: 0});
const foundQueryEvent = React.useRef(false);
useQuery(get_eventgroups, {
@@ -255,42 +272,98 @@ export function Eventing({me}){
}
}, [eventgroups]);
const visibleEventGroups = React.useMemo(() => {
return eventgroups.filter(e => showDeleted || !e.deleted);
}, [eventgroups, showDeleted]);
const activeEventGroups = React.useMemo(() => {
return eventgroups.filter(e => !e.deleted && e.active);
}, [eventgroups]);
React.useEffect(() => {
if(!showDeleted && sidebarFilter === "deleted"){
setSidebarFilter("all");
}
}, [showDeleted, sidebarFilter]);
const sidebarFilterCounts = React.useMemo(() => {
return visibleEventGroups.reduce((prev, cur) => {
const status = getEventGroupStatus(cur);
return {...prev, [status.key]: (prev[status.key] || 0) + 1};
}, {runnable: 0, needs_approval: 0, disabled: 0, deleted: 0});
}, [visibleEventGroups]);
const sidebarFilterOptions = React.useMemo(() => {
if(showDeleted){
return [...sidebarFilterOptionsBase, {key: "deleted", label: "Deleted"}];
}
return sidebarFilterOptionsBase;
}, [showDeleted]);
const filteredEventGroups = React.useMemo(() => {
const search = sidebarSearch.trim().toLowerCase();
return visibleEventGroups.filter((eventGroup) => {
const status = getEventGroupStatus(eventGroup);
if(sidebarFilter !== "all" && sidebarFilter !== status.key){
return false;
}
if(search === ""){
return true;
}
const searchText = [
eventGroup.name,
eventGroup.description,
eventGroup.trigger,
eventGroup.run_as,
eventGroup.operator?.username,
...(Array.isArray(eventGroup.keywords) ? eventGroup.keywords : []),
].filter(Boolean).join(" ").toLowerCase();
return searchText.includes(search);
}).sort((a, b) => {
const statusA = getEventGroupStatus(a);
const statusB = getEventGroupStatus(b);
if(statusA.rank !== statusB.rank){
return statusA.rank - statusB.rank;
}
return (a.id > b.id) ? -1 : ((b.id > a.id) ? 1 : 0);
});
}, [sidebarFilter, sidebarSearch, visibleEventGroups]);
const selectedEventLabel = selectedEventGroup.id === 0 ? "All runs" : selectedEventGroup.name;
return (
<div style={{display: "flex", flexDirection: "column", height: "100%", overflowY: "auto"}}>
<div style={{display: "flex", flexDirection: "row", height: "100%"}}>
<Split direction="horizontal" style={{width: "100%", height: "100%", display: "flex", overflow: "hidden"}} sizes={[20, 80]} >
<div className="bg-gray-base" style={{display: "inline-flex"}}>
<MythicPageBody>
<MythicPageHeader
title="Eventing"
subtitle="Create, review, and manually trigger operation workflows."
meta={
<>
<MythicPageHeaderChip label={`${visibleEventGroups.length} shown`} />
<MythicPageHeaderChip label={`${activeEventGroups.length} active`} />
<MythicPageHeaderChip label={selectedEventLabel} />
{showDeleted && <MythicPageHeaderChip label="Deleted visible" />}
</>
}
actions={
<>
<MythicToolbarButton className="mythic-table-row-action-hover-info" variant="outlined" component="label" startIcon={<CloudUploadIcon fontSize="small" />}>
Upload
<input onChange={onFileChange} type="file" multiple hidden/>
</MythicToolbarButton>
<MythicToolbarButton className="mythic-table-row-action-hover-success" variant="outlined" onClick={()=>setOpenTestModal(true)} startIcon={<AddCircleIcon fontSize="small" />}>
Text
</MythicToolbarButton>
<MythicToolbarButton className="mythic-table-row-action-hover-success" variant="outlined" onClick={()=>setOpenCreateEventingStepper(true)} startIcon={<CategoryIcon fontSize="small" />}>
Wizard
</MythicToolbarButton>
<MythicToolbarToggle
className={showDeleted ? "mythic-table-row-action-hover-warning" : "mythic-table-row-action-hover-info"}
checked={showDeleted}
onClick={() => setShowDeleted(!showDeleted)}
label="Deleted"
activeIcon={<VisibilityIcon fontSize="small" />}
inactiveIcon={<VisibilityOffIcon fontSize="small" />}
/>
</>
}
/>
<div className="mythic-eventing-workspace">
<Split direction="horizontal" className="mythic-eventing-split" sizes={[30, 70]} minSize={[360, 520]} >
<div className="mythic-eventing-sidebar">
<div style={{width: "100%", height: '100%', display: "flex", flexDirection: "column"}}>
<Paper style={{marginBottom: "5px"}}>
<MythicStyledTooltip title={"Upload complete workflow files"} >
<Button size={"small"} style={{display: "inline-flex", marginRight: "10px", marginLeft: "10px", marginTop: "5px"}}
color={"info"} component="label" startIcon={<CloudUploadIcon />}
>
Upload
<input onChange={onFileChange} type="file" multiple hidden/>
</Button>
</MythicStyledTooltip>
<MythicStyledTooltip title={"Create Workflow with Text Editor"} >
<Button size={"small"} color={"success"}
style={{display: "inline-flex", marginRight: "10px", marginLeft: "10px", marginTop: "5px"}}
onClick={()=>setOpenTestModal(true)}
startIcon={<AddCircleIcon style={{backgroundColor: "white", borderRadius: "10px"}} />}
>
Text
</Button>
</MythicStyledTooltip>
<MythicStyledTooltip title={"Create Workflow with GUI Wizard"} tooltipStyle={{}} >
<Button size={"small"} color={"success"}
style={{display: "inline-flex", marginRight: "10px", marginLeft: "10px", marginTop: "5px"}}
onClick={()=>setOpenCreateEventingStepper(true)}
startIcon={<CategoryIcon />}
>
Wizard
</Button>
</MythicStyledTooltip>
</Paper>
{openTestModal &&
<MythicDialog fullWidth={true} maxWidth="xl" open={openTestModal}
onClose={(e) => {
@@ -309,54 +382,86 @@ export function Eventing({me}){
innerDialog={<CreateEventingStepper onClose={onCloseStepper} />}
/>
}
<div className="mythic-eventing-sidebar-toolbar">
<div className="mythic-eventing-sidebar-title-row">
<div>
<div className="mythic-eventing-sidebar-title">Registered event groups</div>
<div className="mythic-eventing-sidebar-subtitle">Browse workflows by run state</div>
</div>
<span className="mythic-eventing-sidebar-count">{filteredEventGroups.length}/{visibleEventGroups.length}</span>
</div>
<TextField
className="mythic-eventing-sidebar-search"
size="small"
fullWidth
placeholder="Search workflows"
value={sidebarSearch}
onChange={(event) => setSidebarSearch(event.target.value)}
InputProps={{
startAdornment: (
<InputAdornment position="start">
<SearchIcon fontSize="small" />
</InputAdornment>
)
}}
/>
<div className="mythic-eventing-filter-row">
{sidebarFilterOptions.map((filterOption) => {
const filterCount = filterOption.key === "all" ? visibleEventGroups.length : sidebarFilterCounts[filterOption.key];
return (
<button
key={filterOption.key}
type="button"
onClick={() => setSidebarFilter(filterOption.key)}
className={`mythic-eventing-filter-button ${sidebarFilter === filterOption.key ? "mythic-eventing-filter-button-active" : ""}`.trim()}
>
<span>{filterOption.label}</span>
<span className="mythic-eventing-filter-count">{filterCount || 0}</span>
</button>
);
})}
</div>
</div>
<ListItem onClick={() => setSelectedEventGroup({id: 0})}
className={selectedEventGroup.id === 0 ? "selectedCallback": ""}
style={selectedEventGroup.id === 0 ?
{paddingTop: 0, paddingBottom: 0, borderLeft: `5px solid ${theme.palette.info.main}`,
borderRight: `5px solid ${theme.palette.info.main}`} :
{paddingTop: 0, paddingBottom: 0}}>
<ListItemText primary={"View All Instances"} />
className={`mythic-eventing-list-item mythic-eventing-list-item-all ${selectedEventGroup.id === 0 ? "mythic-eventing-list-item-selected" : ""}`.trim()}>
<div className="mythic-eventing-status-dot mythic-eventing-status-all" />
<div className="mythic-eventing-list-item-content">
<div className="mythic-eventing-list-item-main">
<span className="mythic-eventing-list-item-name">All workflow runs</span>
<span className="mythic-eventing-row-chip mythic-eventing-row-chip-all">{visibleEventGroups.length}</span>
</div>
<div className="mythic-eventing-list-item-meta">Review instances across all event groups</div>
</div>
</ListItem>
<div style={{flexGrow: 1, overflowY: "auto", height:"100%"}}>
<List style={{border: 0, backgroundColor: "unset"}}
subheader={
<ListSubheader style={{lineHeight: "30px"}} component="div">
Registered Event Groups
{showDeleted ? (
<MythicStyledTooltip title={"Hide Deleted Services"} tooltipStyle={{float: "right"}}>
<IconButton size="small" style={{float: "right", }} variant="contained" onClick={() => setShowDeleted(!showDeleted)}><VisibilityIcon /></IconButton>
</MythicStyledTooltip>
) : (
<MythicStyledTooltip title={"Show Deleted Services"} tooltipStyle={{float: "right"}}>
<IconButton size="small" style={{float: "right", }} variant="contained" onClick={() => setShowDeleted(!showDeleted)} ><VisibilityOffIcon /></IconButton>
</MythicStyledTooltip>
)}
</ListSubheader>
}>
{eventgroups.map( (e, i) => (
(showDeleted || !e.deleted) &&
<ListItem key={e.id + e.name} onClick={() => setSelectedEventGroup(e)}
className={selectedEventGroup.id === e.id ? "selectedCallback": ""}
style={selectedEventGroup.id === e.id ?
{paddingTop: 0, paddingBottom: 0,
borderLeft: `5px solid ${theme.palette.info.main}`, borderRight: `5px solid ${theme.palette.info.main}`,
} :
{paddingTop: 0, paddingBottom: 0}}>
{!e.active &&
<NotificationsOffTwoToneIcon color={"warning"} />
}
<ListItemText primary={e.name} style={{
textDecoration: e.deleted ? "line-through" : ""
}} />
</ListItem>
))}
<div className="mythic-eventing-list-scroll">
<List className="mythic-eventing-list">
{filteredEventGroups.length === 0 ? (
<div className="mythic-eventing-list-empty">No workflows match this view</div>
) : filteredEventGroups.map( (eventGroup) => {
const status = getEventGroupStatus(eventGroup);
return (
<ListItem key={eventGroup.id + eventGroup.name} onClick={() => setSelectedEventGroup(eventGroup)}
className={`mythic-eventing-list-item ${selectedEventGroup.id === eventGroup.id ? "mythic-eventing-list-item-selected" : ""} mythic-eventing-list-item-${status.key}`.trim()}>
<div className={`mythic-eventing-status-dot mythic-eventing-status-${status.key}`} />
<div className="mythic-eventing-list-item-content">
<div className="mythic-eventing-list-item-main">
<span className={`mythic-eventing-list-item-name ${eventGroup.deleted ? "mythic-eventing-list-item-name-deleted" : ""}`.trim()}>{eventGroup.name}</span>
<span className={`mythic-eventing-row-chip mythic-eventing-row-chip-${status.key}`}>{status.label}</span>
</div>
<div className="mythic-eventing-list-item-meta">
<span>{eventGroup.trigger || "No trigger"}</span>
<span className="mythic-eventing-runas-chip">{eventGroup.run_as || "unknown"}</span>
</div>
</div>
</ListItem>
);
})}
</List>
</div>
<Divider />
</div>
</div>
<div className="bg-gray-light" style={{display: "inline-flex"}}>
<div className="mythic-eventing-content">
<div style={{width: "100%", height: "100%"}}>
<EventGroupTable selectedEventGroup={selectedEventGroup} me={me} showInstances={true} showGraph={true} />
</div>
@@ -364,6 +469,6 @@ export function Eventing({me}){
</Split>
</div>
</div>
</MythicPageBody>
)
}
}
@@ -5,10 +5,8 @@ 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 Paper from '@mui/material/Paper';
import Typography from '@mui/material/Typography';
import { toLocalTime } from '../../utilities/Time';
import {useTheme} from '@mui/material/styles';
import LockIcon from '@mui/icons-material/Lock';
import LockOpenIcon from '@mui/icons-material/LockOpen';
import { MythicDialog } from '../../MythicComponents/MythicDialog';
@@ -23,17 +21,12 @@ import {
import {updateSleepInfoCallbackMutation} from "../Callbacks/CallbackMutations";
import {snackActions} from "../../utilities/Snackbar";
import {TagsDisplay, ViewEditTags} from "../../MythicComponents/MythicTag";
import {MythicSectionHeader} from "../../MythicComponents/MythicPageHeader";
export function ExpandedCallbackSideDetails(props){
const theme = useTheme();
return (
<div style={{ width: "100%", height: "100%", overflowY: "scroll" }}>
<Paper elevation={5} style={{backgroundColor: theme.pageHeader.main, color: theme.pageHeaderText.main, marginBottom: "5px"}}
variant={"elevation"}>
<Typography variant="h5" style={{textAlign: "left", display: "inline-block", marginLeft: "20px"}}>
Callback {props.callback.display_id}
</Typography>
</Paper>
<MythicSectionHeader title={`Callback ${props.callback.display_id}`} dense={false} sx={{mt: 0}} />
<ExpandedCallbackSideDetailsTable {...props.callback} />
</div>
)
@@ -5,11 +5,10 @@ import Typography from '@mui/material/Typography';
import {useNavigate} from 'react-router-dom';
import {getStringSize} from "../Callbacks/ResponseDisplayTable";
import {getSkewedNow} from "../../utilities/Time";
import Paper from '@mui/material/Paper';
import MenuItem from '@mui/material/MenuItem';
import Button from '@mui/material/Button';
import TextField from '@mui/material/TextField';
import {CallbackDataCard, GaugeCard, LineTimeMultiChartCard, PieChartCard, TableDataCard} from './DashboardComponents';
import {CallbackDataCard, DashboardEmptyCard, GaugeCard, LineTimeMultiChartCard, PieChartCard, TableDataCard} from './DashboardComponents';
import {MythicAgentSVGIcon} from "../../MythicComponents/MythicAgentSVGIcon";
import {MythicStyledTooltip} from "../../MythicComponents/MythicStyledTooltip";
import {b64DecodeUnicode} from "../Callbacks/ResponseDisplay";
@@ -58,7 +57,9 @@ import MythicStyledTableCell from "../../MythicComponents/MythicTableCell";
import {faCopy} from '@fortawesome/free-solid-svg-icons';
import MythicTextField from "../../MythicComponents/MythicTextField";
import {ImageWithAuth} from "../../utilities/ImageWithAuth";
import {MythicPageHeader} from "../../MythicComponents/MythicPageHeader";
import {MythicPageHeader, MythicPageHeaderChip} from "../../MythicComponents/MythicPageHeader";
import {MythicStatusChip} from "../../MythicComponents/MythicStatusChip";
import {MythicLoadingState} from "../../MythicComponents/MythicStateDisplay";
const LeadDashboardQuery = gql`
${taskingDataFragment}
@@ -268,7 +269,7 @@ const ActiveCallbacksDashboardElement = ({me, data, editing, removeElement}) =>
return (
<CallbackDataCard data={active}
mainTitle={
<div style={{display: "flex", flexDirection: "row", justifyContent: "space-between", marginBottom: "0px" }}>
<div className="mythic-dashboard-metric-title-row">
{"Active Callbacks"}
<MythicTextField type={"number"} value={recentHours} name={"recent hours"} onChange={onChangeRecent}
showLabel={true} inline={true} marginBottom={"0px"}
@@ -282,20 +283,16 @@ const ActiveCallbacksDashboardElement = ({me, data, editing, removeElement}) =>
}
mainElement={
<>
<Typography variant={"h1"} style={{
marginLeft: "5px",
fontWeight: "bold",
display: "inline-block"
}} onClick={() => navigate("/new/callbacks")}>
<Typography className="mythic-dashboard-metric-value" variant={"h1"} onClick={() => navigate("/new/callbacks")}>
{active.active}
</Typography>
<Typography variant={"h5"} style={{display: "inline-block"}}>
<Typography className="mythic-dashboard-metric-total" variant={"h5"}>
/ {active.total}
</Typography>
</>
}
secondaryElement={
<Typography variant={"h2"} style={{marginLeft: "5px", fontWeight: "bold"}}>
<Typography className="mythic-dashboard-metric-secondary" variant={"h2"}>
{active.recent}
</Typography>
}
@@ -492,7 +489,10 @@ const ProxyUsageDashboardElement = ({me, data, editing, removeElement}) => {
</TableBody>
}
editing={editing}
removeElement={removeElement}/>
removeElement={removeElement}
empty={callbackPorts.length === 0}
emptyTitle="No proxy activity"
emptyDescription="Port forward activity will appear here once callbacks send or receive proxy traffic."/>
)
}
const Top10UserContextsDashboardElement = ({me, data, editing, removeElement}) => {
@@ -581,6 +581,9 @@ const Top10UserContextsDashboardElement = ({me, data, editing, removeElement}) =
}
editing={editing}
removeElement={removeElement}
empty={taskedUser.length === 0}
emptyTitle="No user context activity"
emptyDescription="Tasked user contexts will appear here once tasks run against callbacks."
/>
)
}
@@ -664,7 +667,10 @@ const Top10HostContextsDashboardElement = ({me, data, editing, removeElement}) =
</TableBody>
}
editing={editing}
removeElement={removeElement}/>
removeElement={removeElement}
empty={taskedHosts.length === 0}
emptyTitle="No host context activity"
emptyDescription="Tasked host contexts will appear here once tasks run against callbacks."/>
)
}
const Top10RecentPayloadsDashboardElement = ({me, data, editing, removeElement}) => {
@@ -699,33 +705,23 @@ const Top10RecentPayloadsDashboardElement = ({me, data, editing, removeElement})
}, [data]);
if( data.payload && payloads.length === 0){
return (
<div style={{
marginRight: "5px",
width: "100%",
height: "100%",
border: "1px solid gray",
position: "relative",
borderRadius: "4px",
}} >
<h3 style={{marginTop: 0, marginLeft: "5px", marginBottom: 0, paddingBottom: 0}}>
{"No Payloads Created"}
</h3>
<div style={{height: 200, overflowY: "auto"}}>
<div style={{
position: "absolute",
top: "45%",
left: "15%",
borderRadius: "4px",
}}>
<Button color={"success"} variant={"contained"}
onClick={() => navigate("/new/createpayload")}
style={{marginRight: "20px", float: "right"}}
startIcon={<AddCircleIcon color="success" style={{backgroundColor: "white", borderRadius: "10px"}}/>} >
Create Your First Payload
</Button>
</div>
</div>
</div>
<DashboardEmptyCard
editing={editing}
removeElement={removeElement}
title={"No Payloads Created"}
action={
<Button
className="mythic-dashboard-primary-button"
onClick={() => navigate("/new/createpayload")}
startIcon={<AddCircleIcon fontSize="small" />}
variant="outlined"
>
Create Your First Payload
</Button>
}
>
Build a payload to start populating this view.
</DashboardEmptyCard>
)
}
return (
@@ -754,8 +750,8 @@ const Top10RecentPayloadsDashboardElement = ({me, data, editing, removeElement})
removeElement={removeElement}
customizeElement={
<MythicStyledTooltip title={"Create new Payload"}>
<IconButton style={{padding: 0}} size={"small"} onClick={() => navigate("/new/createpayload")}>
<AddCircleIcon color="success" style={{backgroundColor: "white", borderRadius: "10px"}}/>
<IconButton className="mythic-dashboard-icon-button" size={"small"} onClick={() => navigate("/new/createpayload")}>
<AddCircleIcon fontSize="small" />
</IconButton>
</MythicStyledTooltip>
}
@@ -810,45 +806,37 @@ const Top10RecentWorkflowsDashboardElement = ({me, data, editing, removeElement}
}, [data]);
if( data.eventgroupinstance && workflows.length === 0){
return (
<div style={{
marginRight: "5px",
width: "100%",
height: "100%",
border: "1px solid gray",
position: "relative",
borderRadius: "4px",
}} >
<h3 style={{marginTop: 0, marginLeft: "5px", marginBottom: 0, paddingBottom: 0}}>
{"No Workflows Created"}
</h3>
<div style={{height: 200, overflowY: "auto"}}>
<div style={{
position: "absolute",
top: "45%",
left: "15%",
borderRadius: "4px",
}}>
<Button color={"success"} variant={"contained"}
onClick={() => setOpenNewWorkflowModal(true)}
style={{marginRight: "20px", float: "right"}}
startIcon={<AddCircleIcon color="success" style={{backgroundColor: "white", borderRadius: "10px"}}/>} >
<>
<DashboardEmptyCard
editing={editing}
removeElement={removeElement}
title={"No Workflows Created"}
action={
<Button
className="mythic-dashboard-primary-button"
onClick={() => setOpenNewWorkflowModal(true)}
startIcon={<AddCircleIcon fontSize="small" />}
variant="outlined"
>
Create a Workflow
</Button>
</div>
{openNewWorkflowModal &&
<MythicDialog fullWidth={true} maxWidth="xl" open={openNewWorkflowModal}
}
>
Create a workflow to see recent eventing executions here.
</DashboardEmptyCard>
{openNewWorkflowModal &&
<MythicDialog fullWidth={true} maxWidth="xl" open={openNewWorkflowModal}
onClose={(e) => {
setOpenNewWorkflowModal(false);
}}
innerDialog={<TestEventGroupFileDialog
initialWorkflow={initialWorkflow}
onClose={(e) => {
setOpenNewWorkflowModal(false);
}}
innerDialog={<TestEventGroupFileDialog
initialWorkflow={initialWorkflow}
onClose={(e) => {
setOpenNewWorkflowModal(false);
}}/>}
/>
}
</div>
</div>
}}/>}
/>
}
</>
)
}
return (
@@ -877,8 +865,8 @@ const Top10RecentWorkflowsDashboardElement = ({me, data, editing, removeElement}
removeElement={removeElement}
customizeElement={
<MythicStyledTooltip title={"Create new Workflow"}>
<IconButton style={{padding: 0}} size={"small"} onClick={() => setOpenNewWorkflowModal(true)}>
<AddCircleIcon color="success" style={{backgroundColor: "white", borderRadius: "10px"}}/>
<IconButton className="mythic-dashboard-icon-button" size={"small"} onClick={() => setOpenNewWorkflowModal(true)}>
<AddCircleIcon fontSize="small" />
</IconButton>
</MythicStyledTooltip>
}
@@ -1016,74 +1004,49 @@ const MyOperationsDashboardElement = ({me, data, reloadDashboard, editing, remov
return {
operation: p,
id: p.id,
label: <>
{p.name}
</>,
value: <>
{p.id === me.user.current_operation_id ? ("Current Operation") : (
<React.Fragment>
<Button size="small" startIcon={<PlayArrowIcon/>}
onClick={()=>makeCurrentOperation(p)}
color="info">
Make Current
</Button>
</React.Fragment>
)}
</>
label: p.name,
}
}) || [];
setOperations(newPayloadData);
}, [data]);
if( data.operation && operations.length === 0){
return (
<div style={{
marginRight: "5px",
width: "100%",
height: "100%",
border: "1px solid gray",
position: "relative",
borderRadius: "4px",
}} >
<h3 style={{marginTop: 0, marginLeft: "5px", marginBottom: 0, paddingBottom: 0}}>
{"You Belong to No Operations"}
</h3>
<div style={{height: 200, overflowY: "auto"}}>
<div style={{
position: "absolute",
top: "45%",
left: "15%",
borderRadius: "4px",
}}>
{data?.operator[0]?.admin &&
<Button color={"success"} variant={"contained"}
onClick={() => setOpenNewOperationDialog(true)}
style={{marginRight: "20px", float: "right"}}
startIcon={<AddCircleIcon color="success" style={{backgroundColor: "white", borderRadius: "10px"}}/>} >
Create Operation
</Button>
}
{!data?.operator[0]?.admin &&
<Typography>
{"Ask an admin to create an operation for you or a lead to add you to an existing operation"}
</Typography>
}
</div>
{openNewOperation &&
<MythicDialog
fullWidth={true}
open={openNewOperation}
onClose={() => {setOpenNewOperationDialog(false);}}
innerDialog={
<MythicModifyStringDialog title={"New Operation's Name"}
onClose={() => {setOpenNewOperationDialog(false);}}
value={""}
onSubmit={onSubmitNewOperation}
/>
}
/>
<>
<DashboardEmptyCard
editing={editing}
removeElement={removeElement}
title={"You Belong to No Operations"}
action={data?.operator[0]?.admin ? (
<Button
className="mythic-dashboard-primary-button"
onClick={() => setOpenNewOperationDialog(true)}
startIcon={<AddCircleIcon fontSize="small" />}
variant="outlined"
>
Create Operation
</Button>
) : null}
>
{data?.operator[0]?.admin ?
"Create an operation to begin organizing callbacks, payloads, and tasking." :
"Ask an admin to create an operation for you or a lead to add you to an existing operation."
}
</div>
</div>
</DashboardEmptyCard>
{openNewOperation &&
<MythicDialog
fullWidth={true}
open={openNewOperation}
onClose={() => {setOpenNewOperationDialog(false);}}
innerDialog={
<MythicModifyStringDialog title={"New Operation's Name"}
onClose={() => {setOpenNewOperationDialog(false);}}
value={""}
onSubmit={onSubmitNewOperation}
/>
}
/>
}
</>
)
}
return (
@@ -1101,13 +1064,20 @@ const MyOperationsDashboardElement = ({me, data, reloadDashboard, editing, remov
}
tableBody={
<TableBody >
{operations.map( (d, index) => (
<TableRow hover key={d.label + index} onClick={() => {handleHostContextClick(d)}} style={{cursor: "pointer"}}>
<MythicTableCell>{d.label}</MythicTableCell>
<MythicTableCell>
<Button size="small" onClick={()=>{setOpenUpdateNotifications(true);}} startIcon={<EditIcon/>}
disabled={me?.user?.current_operation_id !== d.operation.id}
color={d.operation.complete ? "success" : "primary"} variant="contained">Edit</Button>
{operations.map( (d) => (
<TableRow hover key={d.id} onClick={() => {handleHostContextClick(d)}} style={{cursor: "pointer"}}>
<MythicTableCell>
<div className="mythic-status-stack">
<span>{d.label}</span>
{d.operation.complete &&
<MythicStatusChip label="Completed" status="completed" />
}
</div>
</MythicTableCell>
<MythicTableCell>
<Button className="mythic-dashboard-table-action" size="small" onClick={()=>{setOpenUpdateNotifications(true);}} startIcon={<EditIcon/>}
disabled={me?.user?.current_operation_id !== d.operation.id}
variant="outlined">Edit</Button>
{openUpdateNotifications &&
<MythicDialog open={openUpdateNotifications} fullWidth maxWidth={"lg"}
onClose={()=>{setOpenUpdateNotifications(false);}}
@@ -1116,9 +1086,9 @@ const MyOperationsDashboardElement = ({me, data, reloadDashboard, editing, remov
}
</MythicTableCell>
<MythicTableCell>
<Button size="small" onClick={()=>{setOpenUpdateOperators(true);}}
<Button className="mythic-dashboard-table-action" size="small" onClick={()=>{setOpenUpdateOperators(true);}}
disabled={me?.user?.current_operation_id !== d.operation.id}
startIcon={<AssignmentIndIcon/>} color={d.operation.complete ? "success" : "primary"} variant="contained">Edit</Button>
startIcon={<AssignmentIndIcon/>} variant="outlined">Edit</Button>
{openUpdateOperators &&
<MythicDialog open={openUpdateOperators} maxHeight={"calc(80vh)"} fullWidth maxWidth={"lg"}
onClose={()=>{setOpenUpdateOperators(false);}}
@@ -1126,18 +1096,28 @@ const MyOperationsDashboardElement = ({me, data, reloadDashboard, editing, remov
/>
}
</MythicTableCell>
<MythicTableCell>{d.value}</MythicTableCell>
</TableRow>
)
)}
<MythicTableCell>
{d.id === me.user.current_operation_id ? (
<MythicStatusChip label="Current" status="active" />
) : (
<Button className="mythic-dashboard-table-action" size="small" startIcon={<PlayArrowIcon/>}
onClick={()=>makeCurrentOperation(d.operation)}
variant="outlined">
Make Current
</Button>
)}
</MythicTableCell>
</TableRow>
)
)}
</TableBody>
}
editing={editing}
removeElement={removeElement}
customizeElement={
<MythicStyledTooltip title={"Create new Operation"}>
<IconButton style={{padding: 0}} size={"small"} onClick={() => setOpenNewOperationDialog(true)}>
<AddCircleIcon color="success" style={{backgroundColor: "white", borderRadius: "10px"}}/>
<IconButton className="mythic-dashboard-icon-button" size={"small"} onClick={() => setOpenNewOperationDialog(true)}>
<AddCircleIcon fontSize="small" />
</IconButton>
</MythicStyledTooltip>
}
@@ -1482,6 +1462,9 @@ const Top10RecentFileDownloadsDashboardElement = ({me, data, editing, removeElem
}
editing={editing}
removeElement={removeElement}
empty={files.length === 0}
emptyTitle="No downloaded files"
emptyDescription="Recent downloads will appear here once file transfers complete."
/>
{openPreviewMediaDialog.open &&
<MythicDialog fullWidth={true} maxWidth="xl" open={openPreviewMediaDialog.open}
@@ -1528,25 +1511,27 @@ const Top10RecentScreenshotsDashboardElement = ({me, data, editing, removeElemen
tableBody={
<TableBody>
{files.length > 0 &&
<TableRow style={{display: "flex", height: "100%", width: "100%", alignItems: "center", justifyContent: "space-between"}}>
<MythicTableCell style={{borderBottom: 0}}>
<Button onClick={handleBack} disabled={activeStep === 0}>
{<KeyboardArrowLeft/>}
</Button>
</MythicTableCell>
<MythicTableCell>
<ImageWithAuth src={"/screencaptures/" + files[activeStep] + "?" + now}
style={{height: "200px", cursor: "pointer"}}
onClick={(e) => onPreviewMedia(e, activeStep)}
/>
</MythicTableCell>
<MythicTableCell style={{borderBottom: 0}} >
<Button
onClick={handleNext}
<TableRow className="mythic-dashboard-screenshot-row">
<MythicTableCell className="mythic-dashboard-screenshot-nav-cell">
<IconButton className="mythic-dashboard-icon-button" onClick={handleBack} disabled={activeStep === 0} size="small">
<KeyboardArrowLeft fontSize="small" />
</IconButton>
</MythicTableCell>
<MythicTableCell className="mythic-dashboard-screenshot-preview-cell">
<ImageWithAuth src={"/screencaptures/" + files[activeStep] + "?" + now}
className="mythic-dashboard-screenshot-image"
onClick={(e) => onPreviewMedia(e, activeStep)}
/>
</MythicTableCell>
<MythicTableCell className="mythic-dashboard-screenshot-nav-cell">
<IconButton
className="mythic-dashboard-icon-button"
onClick={handleNext}
disabled={activeStep === maxSteps - 1}
size="small"
>
{<KeyboardArrowRight/>}
</Button>
<KeyboardArrowRight fontSize="small" />
</IconButton>
</MythicTableCell>
</TableRow>
}
@@ -1559,6 +1544,9 @@ const Top10RecentScreenshotsDashboardElement = ({me, data, editing, removeElemen
removeElement={
removeElement
}
empty={files.length === 0}
emptyTitle="No screenshots"
emptyDescription="Recent screenshots will appear here once screenshot files are created."
/>
{ openScreenshot.open &&
<MythicDialog fullWidth={true} maxWidth="xl" open={openScreenshot.open}
@@ -1614,6 +1602,9 @@ const Top10RecentTasksDashboardElement = ({me, data, editing, removeElement}) =>
removeElement={
removeElement
}
empty={tasks.length === 0}
emptyTitle="No recent tasking"
emptyDescription="Recent tasks will appear here once operators issue tasking."
/>
</>
@@ -1651,7 +1642,7 @@ const Top10RecentCredentialsDashboardElement = ({me, data, editing, removeElemen
<TableRow key={c.id} hover>
<MythicStyledTableCell style={{ whiteSpace: "pre-line",wordBreak: "break-all", display: "flex", flexDirection: "row"}}>
<MythicStyledTooltip title={"Copy Credential to clipboard"}>
<IconButton onClick={() => onCopyToClipboard(c.credential_text)} size="small">
<IconButton className="mythic-dashboard-icon-button" onClick={() => onCopyToClipboard(c.credential_text)} size="small">
<FontAwesomeIcon icon={faCopy}/>
</IconButton>
</MythicStyledTooltip>
@@ -1674,6 +1665,9 @@ const Top10RecentCredentialsDashboardElement = ({me, data, editing, removeElemen
removeElement={
removeElement
}
empty={credentials.length === 0}
emptyTitle="No recent credentials"
emptyDescription="Captured credentials will appear here once credentials are created."
/>
</>
@@ -1707,20 +1701,20 @@ const OperatorDashboard = ({me, setLoading, loading}) => {
}
return (
<>
<div style={{display: "flex", marginLeft: "0.5rem", marginBottom: "0.5rem",}}>
<div className="mythic-dashboard-row">
<ActiveCallbacksDashboardElement me={me} data={analysisData}/>
<Top10RecentPayloadsDashboardElement me={me} data={analysisData}/>
<Top10RecentWorkflowsDashboardElement me={me} data={analysisData}/>
</div>
<div style={{display: "flex", marginLeft: "0.5rem", marginBottom: "0.5rem"}}>
<div className="mythic-dashboard-row">
<ProxyUsageDashboardElement me={me} data={analysisData}/>
<Top10RecentFileDownloadsDashboardElement me={me} data={analysisData}/>
<Top10RecentCredentialsDashboardElement me={me} data={analysisData}/>
</div>
<div style={{display: "flex", marginLeft: "0.5rem", marginBottom: "0.5rem"}}>
<div className="mythic-dashboard-row">
<Top10RecentTasksDashboardElement me={me} data={analysisData}/>
</div>
<div style={{display: "flex", marginLeft: "0.5rem", marginBottom: "0.5rem"}}>
<div className="mythic-dashboard-row">
<Top10RecentScreenshotsDashboardElement me={me} data={analysisData}/>
<MyOperationsDashboardElement me={me} data={analysisData} reloadDashboard={reloadDashboard}/>
</div>
@@ -1747,16 +1741,16 @@ const LeadDashboard = ({me, setLoading, loading}) => {
}, []);
return (
<>
<div style={{display: "flex", marginLeft: "0.5rem", marginBottom: "0.5rem",}}>
<div className="mythic-dashboard-row">
<ActiveCallbacksDashboardElement me={me} data={analysisData} />
<Top10CommandStatsDashboardElement me={me} data={analysisData} />
<Top10UserContextsDashboardElement me={me} data={analysisData} />
<Top10HostContextsDashboardElement me={me} data={analysisData} />
</div>
<div style={{display: "flex", marginLeft: "0.5rem", marginBottom: "0.5rem"}}>
<div className="mythic-dashboard-row">
<ActivityPerDayDashboardElement me={me} data={analysisData} />
</div>
<div style={{display: "flex", marginLeft: "0.5rem"}}>
<div className="mythic-dashboard-row">
<TaskStatusDashboardElement me={me} data={analysisData} />
<OperatorActivityDashboardElement me={me} data={analysisData} />
<Top10ArtifactsDashboardElement me={me} data={analysisData} />
@@ -1922,25 +1916,25 @@ const CustomDashboard = ({me, setLoading, loading, editing}) => {
}
}, [me, analysisData, removeDashboardElement]);
return (
<div style={{height: "100%", overflowY: "auto"}}>
<div className="mythic-dashboard-custom">
{dashboards.map((d, i) => (
<div key={"dashboardRow" + i}
style={{display: "flex", marginLeft: "0.5rem", marginBottom: "0.5rem", borderRadius: "5px", }}>
className="mythic-dashboard-row">
{editing &&
<div style={{display: "flex", flexDirection: "column", width: "40px", justifyContent: "center"}}>
<div className="mythic-dashboard-edit-rail">
<MythicStyledTooltip title={"Add New Chart To This Row"}>
<IconButton onClick={() => setOpenAddElement({open: true, row: i})} >
<AddchartIcon color={"success"} />
<IconButton className="mythic-dashboard-icon-button" onClick={() => setOpenAddElement({open: true, row: i})} size="small">
<AddchartIcon fontSize="small" />
</IconButton>
</MythicStyledTooltip>
<MythicStyledTooltip title={"Add Row After This Row"}>
<IconButton onClick={() => addDashboardRow(i)}>
<FormatListBulletedAddIcon color={"success"} />
<IconButton className="mythic-dashboard-icon-button" onClick={() => addDashboardRow(i)} size="small">
<FormatListBulletedAddIcon fontSize="small" />
</IconButton>
</MythicStyledTooltip>
<MythicStyledTooltip title={"Remove This Row"}>
<IconButton onClick={() => removeDashboardRow(i)}>
<PlaylistRemoveIcon color={"error"} />
<IconButton className="mythic-dashboard-icon-button mythic-dashboard-icon-button-hover-danger" onClick={() => removeDashboardRow(i)} size="small">
<PlaylistRemoveIcon fontSize="small" />
</IconButton>
</MythicStyledTooltip>
</div>
@@ -1985,20 +1979,42 @@ export function CallbacksCard({me}) {
}
return (
<>
<MythicPageHeader title={
<>
Welcome <b>{me.user.username}</b>
{me.user.current_operation_id === 0 ? null : (
<>
{" to"} <b>{me.user.current_operation}</b>'s Dashboard
</>
)}
</>
}>
<div style={{display: "flex", alignItems: "center"}}>
<MythicPageHeader
title={
<>
Welcome <b>{me.user.username}</b>
</>
}
subtitle={me.user.current_operation_id === 0 ? "No operation selected" : `${me.user.current_operation}'s Dashboard`}
meta={
<>
<MythicPageHeaderChip label={`${dashboard} perspective`} />
{editing && <MythicPageHeaderChip label="Editing layout" />}
</>
}
actions={
<>
<TextField
style={{color: "white", width: "10rem", margin: "0.5rem"}}
InputProps={{style: {color: "white"}}}
sx={{
width: "11rem",
"& .MuiInputBase-root": {
backgroundColor: "rgba(255, 255, 255, 0.08)",
color: "inherit",
minHeight: 30,
},
"& .MuiInputLabel-root": {
color: "currentColor",
},
"& .MuiOutlinedInput-notchedOutline": {
borderColor: "rgba(255, 255, 255, 0.24)",
},
"& .MuiSelect-select": {
py: "4px",
},
"& .MuiSvgIcon-root": {
color: "inherit",
},
}}
size={"small"}
select={true}
label={"Dashboard Perspective"}
@@ -2012,39 +2028,31 @@ export function CallbacksCard({me}) {
}
</TextField>
<MythicStyledTooltip title={"Analyze Operation Data Again"} tooltipStyle={{display: "inline-block"}}>
<IconButton style={{color: "white"}} onClick={() => setLoading(true)}>
<IconButton onClick={() => setLoading(true)}>
<ReplayIcon />
</IconButton>
</MythicStyledTooltip>
{dashboard === "custom" &&
<MythicStyledTooltip title={editing ? "Stop Editing Dashboard Contents":"Edit Dashboard Contents"}>
<IconButton style={{color: "white"}} onClick={() => setEditing(!editing)}>
<IconButton onClick={() => setEditing(!editing)}>
<EditIcon />
</IconButton>
</MythicStyledTooltip>
}
</div>
</MythicPageHeader>
<div style={{width: "100%", height: "100%", display: "flex", flexDirection: "column", position: "relative", overflowY: "auto"}}>
</>
}
/>
<div className="mythic-dashboard-content">
{loading &&
<div style={{
overflow: "hidden",
zIndex: "5",
position: "absolute",
height: "100%",
width: "100%",
backgroundColor: "rgba(37,37,37,0.35)"
}}>
<div style={{
position: "absolute",
left: "45%",
top: "40%",
borderRadius: "4px",
border: "1px solid black",
padding: "5px",
backgroundColor: "rgba(37,37,37,0.92)", color: "white",
}}>
{"Analyzing Operation..."}
<div className="mythic-dashboard-loading-overlay">
<div className="mythic-dashboard-loading-card">
<MythicLoadingState
compact
title="Analyzing operation"
description="Refreshing dashboard data."
minHeight={96}
sx={{p: 0}}
/>
</div>
</div>
}
@@ -1,14 +1,10 @@
import React from 'react';
import {useTheme} from '@mui/material/styles';
import Typography from '@mui/material/Typography';
import { LineChart } from '@mui/x-charts/LineChart';
import Slider from '@mui/material/Slider';
import { PieChart, pieArcLabelClasses } from '@mui/x-charts/PieChart';
import { axisClasses } from '@mui/x-charts/ChartsAxis';
import {Gauge, GaugeContainer,
GaugeValueArc,
GaugeReferenceArc,
useGaugeState} from '@mui/x-charts/Gauge';
import {Gauge} from '@mui/x-charts/Gauge';
import Table from '@mui/material/Table';
import TableContainer from '@mui/material/TableContainer';
import { BarChart } from '@mui/x-charts/BarChart';
@@ -18,20 +14,110 @@ import IconButton from '@mui/material/IconButton';
import VisibilityOffIcon from '@mui/icons-material/VisibilityOff';
import VisibilityIcon from '@mui/icons-material/Visibility';
import Paper from "@mui/material/Paper";
import {MythicEmptyState} from "../../MythicComponents/MythicStateDisplay";
const normalColors = [
const fallbackDashboardColors = [
'#09bdff',
'#4cd5ff',
'#1F94AD',
'#2a7b9b',
'#007FFF',
'#0754a2',
'#4b47a2',
'#635bce',
'#878be7',
'#39b86f',
'#d69d2d',
'#d65c6b',
'#7b6fd6',
'#24a3a3',
'#9c6ade',
'#d47f38',
'#4e7ad7',
'#90a955',
];
const getDashboardColors = (theme) => [
theme.palette.info.main,
theme.palette.success.main,
theme.palette.warning.main,
theme.palette.error.main,
theme.palette.secondary.main,
theme.palette.primary.main,
...fallbackDashboardColors,
];
const DashboardCard = ({
actions,
bodyClassName = "",
children,
className = "",
editing,
removeElement,
title,
width = "100%",
}) => {
return (
<Paper
className={`mythic-dashboard-card ${className}`.trim()}
elevation={0}
style={{width}}
>
{(title || actions || editing) &&
<div className="mythic-dashboard-card-header">
<div className="mythic-dashboard-card-title">
{title}
</div>
{(editing || actions) &&
<div className="mythic-dashboard-card-actions">
{actions}
{editing &&
<MythicStyledTooltip title={"Remove element"}>
<IconButton
className="mythic-dashboard-icon-button mythic-dashboard-icon-button-hover-danger"
onClick={removeElement}
size="small"
>
<DeleteIcon fontSize="small" />
</IconButton>
</MythicStyledTooltip>
}
</div>
}
</div>
}
<div className={`mythic-dashboard-card-body ${bodyClassName}`.trim()}>
{children}
</div>
</Paper>
);
};
export const DashboardEmptyCard = ({action, children, editing, removeElement, title, width = "100%"}) => (
<DashboardCard
bodyClassName="mythic-dashboard-card-body-empty"
editing={editing}
removeElement={removeElement}
title={title}
width={width}
>
<div className="mythic-dashboard-empty-state">
<div className="mythic-dashboard-empty-copy">
{children}
</div>
{action &&
<div className="mythic-dashboard-empty-action">
{action}
</div>
}
</div>
</DashboardCard>
);
const DashboardNoDataState = ({
title = "No data yet",
description = "This dashboard element will populate when matching operation activity exists.",
}) => (
<MythicEmptyState
compact
title={title}
description={description}
minHeight={132}
sx={{p: 0}}
/>
);
export const PieChartCard = ({
data, width = "100%", additionalStyles, innerElement,
@@ -40,7 +126,7 @@ export const PieChartCard = ({
right: 10,
top: 10,
bottom: 10,
}, colors = normalColors,
}, colors,
onClick, title = "", editing, removeElement, customizeElement
}) => {
const [showLegend, setShowLegend] = React.useState(true);
@@ -48,113 +134,79 @@ export const PieChartCard = ({
setShowLegend(!showLegend);
}
const theme = useTheme();
const chartData = Array.isArray(data) ? data : [];
const hasChartData = chartData.length > 0;
return (
<Paper variant={"elevation"} elevation={3} style={{
marginRight: "0.5rem",
width: width,
height: "100%",
border: `1px solid ${theme.borderColor}`,
borderRadius: theme.shape.borderRadius,
overflow: "hidden",
}}>
<h3 style={{marginTop: 0, marginLeft: "0.5rem", marginBottom: 0, paddingBottom: 0,}}>
{editing &&
<span>
<MythicStyledTooltip title={"Remove element"}>
<IconButton onClick={removeElement}>
<DeleteIcon color={"error"}/>
</IconButton>
</MythicStyledTooltip>
</span>
}
{title}
<span style={{float: "right"}}>
<DashboardCard
actions={
<>
{customizeElement}
<MythicStyledTooltip title={showLegend ? "Hide Legend" : "Show Legend"}>
<IconButton onClick={toggleLegend} >
{showLegend ? <VisibilityIcon /> : <VisibilityOffIcon />}
<IconButton className="mythic-dashboard-icon-button" onClick={toggleLegend} size="small">
{showLegend ? <VisibilityIcon fontSize="small" /> : <VisibilityOffIcon fontSize="small" />}
</IconButton>
</MythicStyledTooltip>
</span>
</h3>
<PieChart
skipAnimation={true}
series={[
{
// item has id, label, value, data
//arcLabel: (item) => `${item.label}`,
//arcLabelMinAngle: 35,
//arcLabelRadius: "60%",
data: data,
highlightScope: {fade: 'global', highlighted: 'item'},
faded: {innerRadius: 0, additionalRadius: -10, color: 'gray'},
paddingAngle: 1,
cornerRadius: 4,
innerRadius: 0,
...additionalStyles
},
]}
height={200}
margin={margin}
sx={{
[`& .${pieArcLabelClasses.root}`]: {
fill: 'white',
fontWeight: 'bold',
},
}}
colors={colors}
onItemClick={onClick}
hideLegend={!showLegend}
slotProps={{
legend: {
direction: "vertical", // "horizontal"
sx: {
gap: "4px", // itemGap (distance between legend items)
// CSS class
['.MuiChartsLegend-series']: {
gap: '8px', // markGap (distance between legend dot and text)
},
[`.MuiChartsLegend-mark`]: {
height: 12, // size of the legend dot
width: 12,
},
</>
}
editing={editing}
removeElement={removeElement}
title={title}
width={width}
>
{hasChartData ? (
<PieChart
skipAnimation={true}
series={[
{
// item has id, label, value, data
//arcLabel: (item) => `${item.label}`,
//arcLabelMinAngle: 35,
//arcLabelRadius: "60%",
data: chartData,
highlightScope: {fade: 'global', highlighted: 'item'},
faded: {innerRadius: 0, additionalRadius: -10, color: 'gray'},
paddingAngle: 1,
cornerRadius: 4,
innerRadius: 0,
...additionalStyles
},
]}
height={200}
margin={margin}
sx={{
[`& .${pieArcLabelClasses.root}`]: {
fill: 'white',
fontWeight: 'bold',
},
}}
colors={colors || getDashboardColors(theme)}
onItemClick={onClick}
hideLegend={!showLegend}
slotProps={{
legend: {
direction: "vertical", // "horizontal"
sx: {
gap: "4px", // itemGap (distance between legend items)
// CSS class
['.MuiChartsLegend-series']: {
gap: '8px', // markGap (distance between legend dot and text)
},
[`.MuiChartsLegend-mark`]: {
height: 12, // size of the legend dot
width: 12,
},
}
}
}
}}>
{innerElement}
</PieChart>
</Paper>
}}>
{innerElement}
</PieChart>
) : (
<DashboardNoDataState />
)}
</DashboardCard>
);
}
function GaugePointer() {
const { valueAngle, outerRadius, cx, cy } = useGaugeState();
if (valueAngle === null) {
// No value to display
return null;
}
const target = {
x: cx + outerRadius * Math.sin(valueAngle),
y: cy - outerRadius * Math.cos(valueAngle),
};
return (
<g>
<circle cx={cx} cy={cy} r={5} fill="red" />
<path
d={`M ${cx} ${cy} L ${target.x} ${target.y}`}
stroke="red"
strokeWidth={3}
/>
</g>
);
}
export const GaugeCard = ({data, width = "100%", additionalStyles, innerElement, hidden, margin = {
left: 10,
right: 10,
top: 10,
bottom: 10,
}, colors = normalColors, onClick, title = "", editing, removeElement, customizeElement }) => {
export const GaugeCard = ({data, width = "100%", title = "", editing, removeElement, customizeElement }) => {
const theme = useTheme();
const getFillColor = () => {
if(data['total'] === 0){return theme.palette.text.disabled}
@@ -168,29 +220,14 @@ export const GaugeCard = ({data, width = "100%", additionalStyles, innerElement,
}
}
return (
<Paper variant={"elevation"} elevation={3} style={{
marginRight: "0.5rem",
width: width,
height: "100%",
border: `1px solid ${theme.borderColor}`,
borderRadius: theme.shape.borderRadius,
overflow: "hidden",
}}>
<h3 style={{marginTop: 0, marginLeft: "0.5rem", marginBottom: 0, paddingBottom: 0,}}>
{editing &&
<span>
<MythicStyledTooltip title={"Remove element"}>
<IconButton onClick={removeElement}>
<DeleteIcon color={"error"}/>
</IconButton>
</MythicStyledTooltip>
</span>
}
{title}
<span style={{float: "right"}}>
{customizeElement}
</span>
</h3>
<DashboardCard
actions={customizeElement}
bodyClassName="mythic-dashboard-card-body-centered"
editing={editing}
removeElement={removeElement}
title={title}
width={width}
>
<Gauge
height={200}
width={200}
@@ -210,82 +247,68 @@ export const GaugeCard = ({data, width = "100%", additionalStyles, innerElement,
})}
>
</Gauge>
</Paper>
</DashboardCard>
);
}
export const CallbackDataCard = ({mainTitle, secondTitle, mainElement, secondaryElement, width="100%",
editing, removeElement}) => {
const theme = useTheme();
return (
<Paper variant={"elevation"} elevation={3} style={{
marginRight: "0.5rem",
width: width,
height: "100%",
border: `1px solid ${theme.borderColor}`,
borderRadius: theme.shape.borderRadius,
overflow: "hidden",
}} >
<h2 style={{marginTop: 0, marginLeft: "0.5rem", marginBottom: 0, paddingBottom: 0}}>
{editing &&
<span >
<MythicStyledTooltip title={"Remove element"}>
<IconButton onClick={removeElement} >
<DeleteIcon color={"error"}/>
</IconButton>
</MythicStyledTooltip>
</span>
}
{mainTitle}
</h2>
<div style={{height: 180, cursor: "pointer"}}>
<DashboardCard
bodyClassName="mythic-dashboard-metric-body"
editing={editing}
removeElement={removeElement}
title={mainTitle}
width={width}
>
<div className="mythic-dashboard-metric-content">
<MythicStyledTooltip title={"Go to Active Callbacks"}>
{mainElement}
<h4 style={{marginTop: 0, marginLeft: "5px", marginBottom: 0, paddingBottom: 0}}>
{secondTitle}
</h4>
{secondaryElement}
<div className="mythic-dashboard-metric-link">
{mainElement}
<div className="mythic-dashboard-metric-label">
{secondTitle}
</div>
{secondaryElement}
</div>
</MythicStyledTooltip>
</div>
</Paper>
</DashboardCard>
)
}
export const TableDataCard = ({title, width = "100%", tableHead, tableBody, editing, removeElement, customizeElement}) => {
const theme = useTheme();
export const TableDataCard = ({
title,
width = "100%",
tableHead,
tableBody,
editing,
removeElement,
customizeElement,
empty = false,
emptyTitle,
emptyDescription,
}) => {
return (
<Paper variant={"elevation"} elevation={3} style={{
marginRight: "0.5rem",
width: width,
height: "100%",
border: `1px solid ${theme.borderColor}`,
borderRadius: theme.shape.borderRadius,
overflow: "hidden",
}}>
<h3 style={{marginTop: 0, marginLeft: "0.5rem", marginBottom: 0, paddingBottom: 0}}>
{editing &&
<span >
<MythicStyledTooltip title={"Remove element"}>
<IconButton onClick={removeElement} >
<DeleteIcon color={"error"}/>
</IconButton>
</MythicStyledTooltip>
</span>
}
{title}
<span style={{float: "right", marginRight: "0.5rem"}}>
{customizeElement}
</span>
</h3>
<TableContainer className="mythicElement" style={{height: 200, overflowY: "auto", width: "100%"}}>
<Table style={{ maxWidth: "100%", overflow: "auto"}} stickyHeader size="small">
{tableHead}
{tableBody}
</Table>
</TableContainer>
</Paper>
<DashboardCard
actions={customizeElement}
bodyClassName="mythic-dashboard-table-body"
editing={editing}
removeElement={removeElement}
title={title}
width={width}
>
{empty ? (
<DashboardNoDataState title={emptyTitle} description={emptyDescription} />
) : (
<TableContainer className="mythic-dashboard-table-container mythicElement">
<Table className="mythic-dashboard-table" stickyHeader size="small">
{tableHead}
{tableBody}
</Table>
</TableContainer>
)}
</DashboardCard>
)
}
export const LineTimeChartCard = ({data, additionalStyles}) => {
const theme = useTheme();
const [value, setValue] = React.useState([0, 0]);
const [range, setRange] = React.useState([0, 0]);
React.useEffect( () => {
@@ -315,17 +338,7 @@ export const LineTimeChartCard = ({data, additionalStyles}) => {
}
};
return (
<Paper variant={"elevation"} elevation={3} style={{
marginRight: "0.5rem",
width: "100%",
height: "100%",
border: `1px solid ${theme.borderColor}`,
borderRadius: theme.shape.borderRadius,
overflow: "hidden",
}} >
<Typography variant={"h3"} style={{margin: 0, padding: 0, position: "relative", left: "30%"}}>
Tasks Issued per Day
</Typography>
<DashboardCard title="Tasks Issued per Day">
<LineChart
xAxis={[
{
@@ -364,7 +377,7 @@ export const LineTimeChartCard = ({data, additionalStyles}) => {
}}
margin={{ top: 10 }}
dataset={data}
height={300}
height={200}
{...additionalStyles}
></LineChart>
<Slider
@@ -373,16 +386,18 @@ export const LineTimeChartCard = ({data, additionalStyles}) => {
valueLabelDisplay="auto"
min={range[0]}
max={range[1]}
sx={{ mt: 2, width: "80%", left: "10%" }}
className="mythic-dashboard-slider"
sx={{ width: "80%" }}
/>
</Paper>
</DashboardCard>
)
}
export const LineTimeMultiChartCard = ({data, additionalStyles, colors=normalColors, view_utc_time, editing, removeElement, customizeElement}) => {
export const LineTimeMultiChartCard = ({data, additionalStyles, colors, view_utc_time, editing, removeElement, customizeElement}) => {
const theme = useTheme();
const [value, setValue] = React.useState([0, 0]);
const [range, setRange] = React.useState([0, 0]);
const hasChartData = (data?.x?.length || 0) > 0 && (data?.y?.length || 0) > 0;
React.useEffect( () => {
if(data.x.length > 0){
@@ -430,105 +445,91 @@ export const LineTimeMultiChartCard = ({data, additionalStyles, colors=normalCol
}
}
return (
<Paper variant={"elevation"} elevation={3} style={{
width: "100%",
marginRight: "0.5rem",
height: "100%",
border: `1px solid ${theme.borderColor}`,
borderRadius: theme.shape.borderRadius,
overflow: "hidden",
}} >
<Typography variant={"h3"} style={{margin: 0, padding: 0, position: "relative", left: "30%"}}>
{editing &&
<span>
<MythicStyledTooltip title={"Remove element"}>
<IconButton onClick={removeElement}>
<DeleteIcon color={"error"}/>
</IconButton>
</MythicStyledTooltip>
</span>
}
Activity per Day {view_utc_time ? "( UTC )" : "( " + Intl?.DateTimeFormat()?.resolvedOptions()?.timeZone + " )"}
<span style={{}}>
<DashboardCard
actions={
<>
{customizeElement}
<MythicStyledTooltip title={showLegend ? "Hide Legend" : "Show Legend"}>
<IconButton onClick={toggleLegend} >
{showLegend ? <VisibilityIcon /> : <VisibilityOffIcon />}
<IconButton className="mythic-dashboard-icon-button" onClick={toggleLegend} size="small">
{showLegend ? <VisibilityIcon fontSize="small" /> : <VisibilityOffIcon fontSize="small" />}
</IconButton>
</MythicStyledTooltip>
</span>
</Typography>
<LineChart
colors={colors}
hideLegend={!showLegend}
xAxis={[
{
data: data.x,
scaleType: "time",
min: data?.x?.[value[0]] || 0,
max: data?.x?.[value[1]] || 0,
id: 'bottomAxis',
tickMinStep: 86400000,
labelStyle: {
fontSize: 10,
},
tickLabelStyle: {
angle: 25,
textAnchor: 'start',
fontSize: 5,
},
},
]}
yAxis={[
{id: "taskAxis", scaleType: "linear", label: "Tasks Issued"},
{id: "callbackAxis", scaleType: "linear", label: "Active Callbacks", position: "right"}
]}
series={data.y}
sx={{
[`.${axisClasses.left} .${axisClasses.label}`]: {
//transform: 'translate(-25px, 0)',
},
[`.${axisClasses.right} .${axisClasses.label}`]: {
//transform: 'translate(30px, 0)',
},
}}
margin={{ }}
height={200}
{...additionalStyles}
></LineChart>
<Slider
value={value}
onChange={handleChange}
color={"info"}
size={"small"}
valueLabelDisplay={"auto"}
valueLabelFormat={sliderVal => sliderDate(sliderVal, view_utc_time)}
min={range[0]}
max={range[1]}
sx={{ mt: 2, width: "80%", left: "10%" }}
/>
</Paper>
</>
}
editing={editing}
removeElement={removeElement}
title={`Activity per Day ${view_utc_time ? "(UTC)" : `(${Intl?.DateTimeFormat()?.resolvedOptions()?.timeZone})`}`}
>
{hasChartData ? (
<>
<LineChart
colors={colors || getDashboardColors(theme)}
hideLegend={!showLegend}
xAxis={[
{
data: data.x,
scaleType: "time",
min: data?.x?.[value[0]] || 0,
max: data?.x?.[value[1]] || 0,
id: 'bottomAxis',
tickMinStep: 86400000,
labelStyle: {
fontSize: 10,
},
tickLabelStyle: {
angle: 25,
textAnchor: 'start',
fontSize: 5,
},
},
]}
yAxis={[
{id: "taskAxis", scaleType: "linear", label: "Tasks Issued"},
{id: "callbackAxis", scaleType: "linear", label: "Active Callbacks", position: "right"}
]}
series={data.y}
sx={{
[`.${axisClasses.left} .${axisClasses.label}`]: {
//transform: 'translate(-25px, 0)',
},
[`.${axisClasses.right} .${axisClasses.label}`]: {
//transform: 'translate(30px, 0)',
},
}}
margin={{ }}
height={200}
{...additionalStyles}
></LineChart>
<Slider
value={value}
onChange={handleChange}
size={"small"}
valueLabelDisplay={"auto"}
valueLabelFormat={sliderVal => sliderDate(sliderVal, view_utc_time)}
min={range[0]}
max={range[1]}
className="mythic-dashboard-slider"
sx={{ width: "80%" }}
/>
</>
) : (
<DashboardNoDataState
title="No activity yet"
description="Task and callback activity will appear here once the operation has timeline data."
/>
)}
</DashboardCard>
)
}
export const StackedBarChartCard = ({data, labels, title, width="100%", hidden, colors=normalColors, margin={
export const StackedBarChartCard = ({data, labels, title, width="100%", hidden, colors, margin={
right: 10,
top: 40,
bottom: 10,
}}) => {
const theme = useTheme();
return (
<Paper variant={"elevation"} elevation={3} style={{
marginRight: "0.5rem",
width: width,
height: "100%",
border: `1px solid ${theme.borderColor}`,
borderRadius: theme.shape.borderRadius,
overflow: "hidden",
}} >
<h3 style={{marginTop: 0, marginLeft: "5px", marginBottom: 0, paddingBottom: 0, position: "absolute"}}>
{title}
</h3>
<DashboardCard title={title} width={width}>
<BarChart
xAxis={[{
scaleType: "band",
@@ -539,7 +540,7 @@ export const StackedBarChartCard = ({data, labels, title, width="100%", hidden,
layout={"vertical"}
series={data}
height={200}
colors={colors}
colors={colors || getDashboardColors(theme)}
hideLegend={hidden}
slotProps={{
legend: {
@@ -553,6 +554,6 @@ export const StackedBarChartCard = ({data, labels, title, width="100%", hidden,
}
}
}} />
</Paper>
</DashboardCard>
);
}
@@ -1,9 +1,6 @@
import React from 'react';
import Paper from '@mui/material/Paper';
import Button from '@mui/material/Button';
import Typography from '@mui/material/Typography';
import {useTheme} from '@mui/material/styles';
import ButtonGroup from '@mui/material/ButtonGroup';
import ArrowDropDownIcon from '@mui/icons-material/ArrowDropDown';
import Grow from '@mui/material/Grow';
import Popper from '@mui/material/Popper';
@@ -16,26 +13,36 @@ import {CircularProgress} from '@mui/material';
import { MythicDisplayTextDialog} from '../../MythicComponents/MythicDisplayTextDialog';
import { MythicDialog } from '../../MythicComponents/MythicDialog';
import { SelectPayloadTypeDialog } from './SelectPayloadTypeDialog';
import {MythicPageHeader} from "../../MythicComponents/MythicPageHeader";
import {MythicPageHeader, MythicPageHeaderChip} from "../../MythicComponents/MythicPageHeader";
export function MitreGrid({entries, onGetCommands, onGetTasks, onGetCommandsFiltered, onGetTasksFiltered, onFilterByTags, showCountGrouping}){
const theme = useTheme();
const [backdropOpen, setBackdropOpen] = React.useState(false);
const techniqueCount = Object.values(entries || {}).reduce((total, tactic) => total + (tactic?.rows?.length || 0), 0);
return (
<>
<MythicPageHeader title={"MITRE ATT&CK Mappings"}>
<PoperDropdown onGetCommands={onGetCommands}
onGetTasks={onGetTasks}
onGetCommandsFiltered={onGetCommandsFiltered}
onGetTasksFiltered={onGetTasksFiltered}
onFilterByTags={onFilterByTags}
setBackdropOpen={setBackdropOpen}
showCountGrouping={showCountGrouping}
entries={entries}
/>
</MythicPageHeader>
<MythicPageHeader
title={"MITRE ATT&CK Mappings"}
subtitle={"Review command and task coverage across enterprise ATT&CK tactics."}
meta={
<>
<MythicPageHeaderChip label={`${techniqueCount} techniques`} />
{showCountGrouping && <MythicPageHeaderChip label={`Grouped by ${showCountGrouping}`} />}
</>
}
actions={
<PoperDropdown onGetCommands={onGetCommands}
onGetTasks={onGetTasks}
onGetCommandsFiltered={onGetCommandsFiltered}
onGetTasksFiltered={onGetTasksFiltered}
onFilterByTags={onFilterByTags}
setBackdropOpen={setBackdropOpen}
showCountGrouping={showCountGrouping}
entries={entries}
/>
}
/>
<div style={{display: "flex", flexGrow: 1, overflow: "auto"}}>
{backdropOpen && <Backdrop open={backdropOpen} style={{zIndex: 2, position: "absolute"}} invisible={false}>
@@ -239,14 +246,18 @@ function PoperDropdown({onGetCommands, onGetTasks, onGetCommandsFiltered, onGetT
}
return (
<React.Fragment>
<ButtonGroup variant="text" ref={dropdownAnchorRef} aria-label="split button" style={{marginRight: "10px", float: "right", color: "white"}} >
<Button size="small" style={{color: "white"}} aria-controls={dropdownOpen ? 'split-button-menu' : undefined}
aria-expanded={dropdownOpen ? 'true' : undefined}
aria-haspopup="menu"
onClick={() => setDropdownOpen(!dropdownOpen)}>
Actions <ArrowDropDownIcon />
</Button>
</ButtonGroup>
<Button
ref={dropdownAnchorRef}
size="small"
variant="outlined"
aria-controls={dropdownOpen ? 'split-button-menu' : undefined}
aria-expanded={dropdownOpen ? 'true' : undefined}
aria-haspopup="menu"
onClick={() => setDropdownOpen(!dropdownOpen)}
endIcon={<ArrowDropDownIcon fontSize="small" />}
>
Actions
</Button>
<Popper open={dropdownOpen} anchorEl={dropdownAnchorRef.current} role={undefined} transition style={{zIndex: 10}}>
{({ TransitionProps, placement }) => (
<Grow
@@ -309,4 +320,4 @@ export function MitreGridDisplay({entries, showCountGrouping}){
<MitreGridColumn key={t} column={entries[t]} showCountGrouping={showCountGrouping} />
))
)
}
}
@@ -6,19 +6,18 @@ 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 Paper from '@mui/material/Paper';
import Typography from '@mui/material/Typography';
import AddCircleIcon from '@mui/icons-material/AddCircle';
import { MythicDialog } from '../../MythicComponents/MythicDialog';
import {useTheme} from '@mui/material/styles';
import {EditBlockListDialog} from './EditBlockListDialog';
import {snackActions} from '../../utilities/Snackbar';
import {useMutation, gql} from '@apollo/client';
import DeleteIcon from '@mui/icons-material/Delete';
import IconButton from '@mui/material/IconButton';
import {MythicConfirmDialog} from '../../MythicComponents/MythicConfirmDialog';
import TuneIcon from '@mui/icons-material/Tune';
import {MythicPageHeader} from "../../MythicComponents/MythicPageHeader";
import EditIcon from '@mui/icons-material/Edit';
import {MythicPageHeader, MythicPageHeaderChip} from "../../MythicComponents/MythicPageHeader";
import {MythicToolbarButton} from "../../MythicComponents/MythicTableToolbar";
import {MythicTableEmptyState} from "../../MythicComponents/MythicStateDisplay";
const newBlockListEntry = gql`
mutation newBlockListsEntries($entries: [disabledcommandsprofile_insert_input!]!) {
@@ -58,7 +57,6 @@ mutation deleteBlockListEntries($name: String!, $entries: [Int!]!){
`;
export function CommandBlockListTable(props){
const theme = useTheme();
const [openNew, setOpenNewDialog] = React.useState(false);
const [blockLists, setBlockLists] = React.useState([]);
const [newBlockListEntries] = useMutation(newBlockListEntry, {
@@ -174,22 +172,26 @@ export function CommandBlockListTable(props){
if(props?.me?.user?.current_operation_id === 0){
return null;
}
const blockListCountLabel = blockLists.length === 1 ? "1 list" : `${blockLists.length} lists`;
return (
<>
<MythicPageHeader title={"Command Block Lists"}>
<Button size="small"
onClick={()=>{setOpenNewDialog(true);}}
style={{color: "white", whiteSpace: "nowrap"}}
startIcon={<AddCircleIcon color="success" style={{backgroundColor: "white", borderRadius: "10px"}}/>}
>New Block List</Button>
{openNew &&
<MythicDialog open={openNew} fullWidth={true} maxWidth="lg"
onClose={()=>{setOpenNewDialog(false);}}
innerDialog={<EditBlockListDialog editable={true} currentSelected={[]} onSubmit={onSubmitNewBlockList} dialogTitle="Create New Block List" onClose={() => setOpenNewDialog(false)}
/>}
/>
<MythicPageHeader
title={"Command Block Lists"}
subtitle={"Control which commands are blocked for selected payload types."}
meta={<MythicPageHeaderChip label={blockListCountLabel} />}
actions={
<MythicToolbarButton variant="contained" color="primary" onClick={()=>{setOpenNewDialog(true);}} startIcon={<AddCircleIcon />}>
Block List
</MythicToolbarButton>
}
</MythicPageHeader>
/>
{openNew &&
<MythicDialog open={openNew} fullWidth={true} maxWidth="lg"
onClose={()=>{setOpenNewDialog(false);}}
innerDialog={<EditBlockListDialog editable={true} currentSelected={[]} onSubmit={onSubmitNewBlockList} dialogTitle="Create New Block List" onClose={() => setOpenNewDialog(false)}
/>}
/>
}
<TableContainer className="mythicElement">
<Table size="small" style={{"tableLayout": "fixed", "maxWidth": "calc(100vw)", "overflow": "scroll"}}>
<TableHead>
@@ -201,6 +203,14 @@ export function CommandBlockListTable(props){
</TableRow>
</TableHead>
<TableBody>
{blockLists.length === 0 &&
<MythicTableEmptyState
colSpan={4}
compact
title="No command block lists"
description="Create a block list to prevent selected commands from being used in this operation."
/>
}
{
blockLists.map( b => (
<CommandBlockListTableRow key={b.name} {...b} onAcceptDelete={onAcceptDelete} onSubmitEdits={onSubmitEdits}/>
@@ -214,7 +224,6 @@ export function CommandBlockListTable(props){
}
function CommandBlockListTableRow(props){
const theme = useTheme();
const [openDelete, setOpenDeleteDialog] = React.useState(false);
const [blockedCommandDisplay, setBlockedCommandDisplay] = React.useState([]);
const [openUpdate, setOpenUpdateDialog] = React.useState(false);
@@ -242,11 +251,11 @@ function CommandBlockListTableRow(props){
return (
<TableRow hover>
<TableCell>
<IconButton size="small" onClick={()=>{setOpenDeleteDialog(true);}} color={"error"} variant="contained"><DeleteIcon/></IconButton>
<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}/>
</TableCell>
<TableCell>
<Button size="small" onClick={()=>{setOpenUpdateDialog(true);}} startIcon={<TuneIcon/>} color="primary" variant="contained">Edit</Button>
<Button className="mythic-table-row-action" size="small" onClick={()=>{setOpenUpdateDialog(true);}} startIcon={<EditIcon/>} variant="outlined">Edit</Button>
{openUpdate &&
<MythicDialog open={openUpdate} fullWidth maxWidth={"lg"}
onClose={()=>{setOpenUpdateDialog(false);}}
@@ -266,4 +275,3 @@ function CommandBlockListTableRow(props){
</TableRow>
)
}
@@ -1,6 +1,4 @@
import React, {useEffect} from 'react';
import Button from '@mui/material/Button';
import DialogActions from '@mui/material/DialogActions';
import DialogContent from '@mui/material/DialogContent';
import DialogTitle from '@mui/material/DialogTitle';
import Grid from '@mui/material/Grid';
@@ -9,20 +7,20 @@ import ListItem from '@mui/material/ListItem';
import ListItemIcon from '@mui/material/ListItemIcon';
import ListItemText from '@mui/material/ListItemText';
import Checkbox from '@mui/material/Checkbox';
import Paper from '@mui/material/Paper';
import Card from '@mui/material/Card';
import CardHeader from '@mui/material/CardHeader';
import CardContent from '@mui/material/CardContent';
import {useQuery, gql} from '@apollo/client';
import {useTheme} from '@mui/material/styles';
import Typography from '@mui/material/Typography';
import MythicTextField from '../../MythicComponents/MythicTextField';
import { snackActions } from '../../utilities/Snackbar';
import {classes, StyledButton, StyledDivider} from '../../MythicComponents/MythicTransferList';
import {classes, StyledButton} from '../../MythicComponents/MythicTransferList';
import {
MythicDialogBody,
MythicDialogButton,
MythicDialogFooter,
MythicDialogSection,
MythicFormField
} from '../../MythicComponents/MythicDialogLayout';
function PayloadTypeBlockListPreMemo(props){
const theme = useTheme();
const [checked, setChecked] = React.useState([]);
const [left, setLeft] = React.useState([]);
const [right, setRight] = React.useState(props.right);
@@ -101,14 +99,10 @@ function PayloadTypeBlockListPreMemo(props){
props.onChange({selected: right, name: props.name});
}, [right])
const customList = (title, items) => (
<Paper style={{width:"100%", marginTop:"5px"}}>
<Card>
<CardHeader
title={title}
/>
<StyledDivider classes={{root: classes.divider}}/>
<CardContent style={{height: "calc(30vh)", overflow: "auto"}} >
<List dense component="div" role="list" style={{padding:0}} >
<div className="mythic-transfer-list">
<div className="mythic-transfer-list-header">{title}</div>
<div className="mythic-transfer-list-body">
<List dense component="div" role="list" style={{padding:0}}>
{items.map((valueObj) => {
const value = props.itemKey === undefined ? valueObj : valueObj[props.itemKey];
const labelId = `transfer-list-item-${value}-label`;
@@ -127,23 +121,16 @@ function PayloadTypeBlockListPreMemo(props){
);
})}
</List>
</CardContent>
</Card>
</Paper>
</div>
</div>
);
return (
<Grid container spacing={0} justifyContent="center" alignItems="center" className={classes.root}>
<Grid size={12}>
<Paper elevation={5} style={{backgroundColor: theme.pageHeader.main, color: theme.pageHeaderText.main}} variant={"elevation"}>
<Typography variant="h3" style={{textAlign: "left", display: "inline-block", marginLeft: "20px"}}>
{props.name}
</Typography>
</Paper>
</Grid>
<MythicDialogSection title={props.name} className="mythic-transfer-section">
<Grid container spacing={1} justifyContent="center" alignItems="stretch">
<Grid size={5}>{customList(leftTitle, left)}</Grid>
<Grid>
<Grid container direction="column" alignItems="center">
<div className="mythic-transfer-controls">
<StyledButton
variant="contained"
size="small"
@@ -184,10 +171,11 @@ function PayloadTypeBlockListPreMemo(props){
>
</StyledButton>
</Grid>
</div>
</Grid>
<Grid size={5}>{customList(rightTitle, right)}</Grid>
</Grid>
</MythicDialogSection>
);
}
const PayloadTypeBlockList = React.memo(PayloadTypeBlockListPreMemo);
@@ -267,20 +255,25 @@ export function EditBlockListDialog({dialogTitle, onSubmit, blockListName: propB
<>
<DialogTitle id="form-dialog-title">{dialogTitle}</DialogTitle>
<DialogContent dividers={true}>
<MythicTextField disabled={!editable} onChange={onChangeBlockListName} value={blockListName} name="Block List Name" autoFocus requiredValue/>
{payloadtypes.map(p => (
<PayloadTypeBlockList key={p.name} leftTitle={"Not Blocked"} onChange={onChange} rightTitle={"Blocked Commands"} itemKey={"cmd"} right={p.selected} left={p.commands} name={p.name}/>
))}
<MythicDialogBody>
<MythicDialogSection title="Block List">
<MythicFormField label="Block List Name" required>
<MythicTextField disabled={!editable} onChange={onChangeBlockListName} value={blockListName} name="Block List Name" showLabel={false} autoFocus requiredValue marginTop="0px" marginBottom="0px"/>
</MythicFormField>
</MythicDialogSection>
{payloadtypes.map(p => (
<PayloadTypeBlockList key={p.name} leftTitle={"Not Blocked"} onChange={onChange} rightTitle={"Blocked Commands"} itemKey={"cmd"} right={p.selected} left={p.commands} name={p.name}/>
))}
</MythicDialogBody>
</DialogContent>
<DialogActions>
<Button onClick={onClose} variant="contained" color="primary">
<MythicDialogFooter>
<MythicDialogButton onClick={onClose}>
Close
</Button>
<Button onClick={submit} variant="contained" color="success">
</MythicDialogButton>
<MythicDialogButton intent="primary" onClick={submit}>
Submit
</Button>
</DialogActions>
</MythicDialogButton>
</MythicDialogFooter>
</>
);
}
@@ -1,28 +1,24 @@
import React from 'react';
import {Button} from '@mui/material';
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 Paper from '@mui/material/Paper';
import { OperationTableRow } from './OperationTableRow';
import Typography from '@mui/material/Typography';
import AddCircleIcon from '@mui/icons-material/AddCircle';
import { MythicDialog } from '../../MythicComponents/MythicDialog';
import {useTheme} from '@mui/material/styles';
import {SettingsOperatorDialog} from '../Settings/SettingsOperatorDialog';
import {snackActions} from '../../utilities/Snackbar';
import {useMutation, gql} from '@apollo/client';
import {MythicModifyStringDialog} from '../../MythicComponents/MythicDialog';
import VisibilityOffIcon from '@mui/icons-material/VisibilityOff';
import VisibilityIcon from '@mui/icons-material/Visibility';
import { MythicStyledTooltip } from '../../MythicComponents/MythicStyledTooltip';
import { IconButton } from '@mui/material';
import PersonAddIcon from '@mui/icons-material/PersonAdd';
import {meState} from "../../../cache";
import {MythicPageHeader} from "../../MythicComponents/MythicPageHeader";
import {MythicPageHeader, MythicPageHeaderChip} from "../../MythicComponents/MythicPageHeader";
import {MythicToolbarButton, MythicToolbarToggle} from "../../MythicComponents/MythicTableToolbar";
import {MythicTableEmptyState} from "../../MythicComponents/MythicStateDisplay";
const newOperatorMutation = gql`
mutation NewOperator($username: String!, $password: String!) {
@@ -59,7 +55,6 @@ mutation newOperationMutation($name: String){
`;
export function OperationTable(props){
const theme = useTheme();
const [openNewOperator, setOpenNewOperatorDialog] = React.useState(false);
const [openNewOperation, setOpenNewOperationDialog] = React.useState(false);
const [showDeleted, setShowDeleted] = React.useState(false);
@@ -143,72 +138,60 @@ export function OperationTable(props){
const onSubmitNewOperation = (operation_name) => {
newOperation({variables: {name: operation_name}})
}
const visibleOperations = props.operations.filter((op) => showDeleted || !op.deleted);
const visibleOperationsLabel = visibleOperations.length === 1 ? "1 shown" : `${visibleOperations.length} shown`;
const totalOperationsLabel = props.operations.length === 1 ? "1 total" : `${props.operations.length} total`;
return (
<>
<MythicPageHeader title={"Operations"}>
<Button size="small" onClick={() => {setOpenNewOperationDialog(true);}}
style={{ color: "white", whiteSpace: "nowrap"}}
startIcon={<AddCircleIcon color="success" style={{backgroundColor: "white", borderRadius: "10px"}}/>} >
New Operation
</Button>
<MythicStyledTooltip title={"Create new operator"} tooltipStyle={{}}>
<IconButton size="small"
onClick={()=>{setOpenNewOperatorDialog(true);}}
style={{}}
variant="contained">
<PersonAddIcon />
</IconButton>
</MythicStyledTooltip>
{showDeleted ? (
<MythicStyledTooltip title={"Hide Deleted Operations"} tooltipStyle={{}}>
<IconButton size="small" style={{}}
variant="contained" onClick={() => setShowDeleted(!showDeleted)}>
<VisibilityIcon />
</IconButton>
</MythicStyledTooltip>
) : (
<MythicStyledTooltip title={"Show Deleted Operations"} tooltipStyle={{}}>
<IconButton size="small" style={{}}
variant="contained" onClick={() => setShowDeleted(!showDeleted)} >
<VisibilityOffIcon />
</IconButton>
</MythicStyledTooltip>
)}
{openNewOperator &&
<MythicDialog open={openNewOperator}
onClose={()=>{setOpenNewOperatorDialog(false);}}
innerDialog={<SettingsOperatorDialog title="New Operator" onAccept={onSubmitNewOperator} handleClose={()=>{setOpenNewOperatorDialog(false);}} {...props}/>}
/>
<MythicPageHeader
title={"Operations"}
subtitle={"Create operations, assign operators, and manage engagement state."}
meta={
<>
<MythicPageHeaderChip label={visibleOperationsLabel} />
<MythicPageHeaderChip label={totalOperationsLabel} />
{showDeleted && <MythicPageHeaderChip label="Deleted visible" />}
</>
}
{openNewOperation &&
<MythicDialog
fullWidth={true}
open={openNewOperation}
onClose={() => {setOpenNewOperationDialog(false);}}
innerDialog={
<MythicModifyStringDialog title={"New Operation's Name"}
onClose={() => {setOpenNewOperationDialog(false);}}
value={""}
onSubmit={onSubmitNewOperation}
/>
}
/>
actions={
<>
<MythicToolbarButton variant="contained" color="primary" onClick={() => {setOpenNewOperationDialog(true);}} startIcon={<AddCircleIcon />}>
Operation
</MythicToolbarButton>
<MythicToolbarButton variant="outlined" color="primary" onClick={()=>{setOpenNewOperatorDialog(true);}} startIcon={<PersonAddIcon />}>
Operator
</MythicToolbarButton>
<MythicToolbarToggle
checked={showDeleted}
onClick={() => setShowDeleted(!showDeleted)}
label="Deleted"
activeIcon={<VisibilityIcon fontSize="small" />}
inactiveIcon={<VisibilityOffIcon fontSize="small" />}
/>
</>
}
</MythicPageHeader>
/>
{openNewOperator &&
<MythicDialog open={openNewOperator}
onClose={()=>{setOpenNewOperatorDialog(false);}}
innerDialog={<SettingsOperatorDialog title="New Operator" onAccept={onSubmitNewOperator} handleClose={()=>{setOpenNewOperatorDialog(false);}} {...props}/>}
/>
}
{openNewOperation &&
<MythicDialog
fullWidth={true}
open={openNewOperation}
onClose={() => {setOpenNewOperationDialog(false);}}
innerDialog={
<MythicModifyStringDialog title={"New Operation's Name"}
onClose={() => {setOpenNewOperationDialog(false);}}
value={""}
onSubmit={onSubmitNewOperation}
/>
}
/>
}
<TableContainer className="mythicElement">
{props.operations.length === 0 &&
<div style={{display: "flex", flexDirection: "column", flexGrow: 1}}>
<div style={{
position: "absolute",
left: "35%",
top: "50%"
}}>
{"No Operations available!"}<br/>
{"Ask a Mythic admin or operation lead to add you to an operation."}
</div>
</div>
}
<Table size="small" style={{"tableLayout": "fixed", "overflow": "scroll"}}>
<TableHead>
<TableRow>
@@ -222,22 +205,27 @@ export function OperationTable(props){
</TableRow>
</TableHead>
<TableBody>
{props.operations.map((op) => (
showDeleted || !op.deleted ? (
{visibleOperations.length === 0 &&
<MythicTableEmptyState
colSpan={7}
compact
title={props.operations.length === 0 ? "No operations available" : "No visible operations"}
description={props.operations.length === 0 ? "Ask a Mythic admin or operation lead to add you to an operation." : "Deleted operations are hidden. Toggle Deleted to include them."}
/>
}
{visibleOperations.map((op) => (
<OperationTableRow
me={props.me}
key={"operation" + op.id}
onUpdateOperation={onUpdateOperation}
onUpdateCurrentOperation={props.onUpdateCurrentOperation}
updateDeleted={props.updateDeleted}
{...op} operator={props.operator}
/>
) : null
))}
onUpdateOperation={onUpdateOperation}
onUpdateCurrentOperation={props.onUpdateCurrentOperation}
updateDeleted={props.updateDeleted}
{...op} operator={props.operator}
/>
))}
</TableBody>
</Table>
</TableContainer>
</>
)
}
@@ -17,6 +17,7 @@ import DeleteIcon from '@mui/icons-material/Delete';
import RestoreFromTrashOutlinedIcon from '@mui/icons-material/RestoreFromTrashOutlined';
import {restartWebsockets} from '../../../index';
import {useNavigate} from 'react-router-dom';
import {MythicStatusChip} from '../../MythicComponents/MythicStatusChip';
export const updateCurrentOperationMutation = gql`
mutation updateCurrentOpertionMutation($operator_id: Int!, $operation_id: Int!) {
@@ -121,11 +122,25 @@ export function OperationTableRow(props){
<TableRow key={props.id} hover>
<TableCell>
{props.deleted ? (
<IconButton size="small" onClick={()=>{setOpenDeleteDialog(true);}} color="success" variant="contained"
disabled={me?.user?.current_operation_id !== props.id}><RestoreFromTrashOutlinedIcon/> </IconButton>
<IconButton
aria-label="restore operation"
className="mythic-table-row-icon-action mythic-table-row-icon-action-success"
size="small"
onClick={()=>{setOpenDeleteDialog(true);}}
disabled={me?.user?.current_operation_id !== props.id}
>
<RestoreFromTrashOutlinedIcon fontSize="small"/>
</IconButton>
) : (
<IconButton size="small" onClick={()=>{setOpenDeleteDialog(true);}} color="error" variant="contained"
disabled={me?.user?.current_operation_id !== props.id}><DeleteIcon/> </IconButton>
<IconButton
aria-label="delete operation"
className="mythic-table-row-icon-action mythic-table-row-icon-action-hover-danger"
size="small"
onClick={()=>{setOpenDeleteDialog(true);}}
disabled={me?.user?.current_operation_id !== props.id}
>
<DeleteIcon fontSize="small"/>
</IconButton>
)}
{openDelete &&
<MythicConfirmDialog onClose={() => {setOpenDeleteDialog(false);}} onSubmit={onAcceptDelete}
@@ -134,9 +149,9 @@ export function OperationTableRow(props){
acceptColor={props.deleted ? "success": "error"} />
}
</TableCell>
<TableCell><Button size="small" onClick={()=>{setOpenUpdateNotifications(true);}} startIcon={<EditIcon/>}
<TableCell><Button className="mythic-table-row-action" size="small" onClick={()=>{setOpenUpdateNotifications(true);}} startIcon={<EditIcon/>}
disabled={me?.user?.current_operation_id !== props.id}
color={props.complete ? "success" : "primary"} variant="contained">Edit</Button>
variant="outlined">Edit</Button>
{openUpdateNotifications &&
<MythicDialog open={openUpdateNotifications} fullWidth maxWidth={"lg"}
onClose={()=>{setOpenUpdateNotifications(false);}}
@@ -144,9 +159,9 @@ export function OperationTableRow(props){
/>
}
</TableCell>
<TableCell><Button size="small" onClick={()=>{setOpenUpdateOperators(true);}}
<TableCell><Button className="mythic-table-row-action" size="small" onClick={()=>{setOpenUpdateOperators(true);}}
disabled={me?.user?.current_operation_id !== props.id}
startIcon={<AssignmentIndIcon/>} color={props.complete ? "success" : "primary"} variant="contained">Edit</Button>
startIcon={<AssignmentIndIcon/>} variant="outlined">Edit</Button>
{openUpdateOperators &&
<MythicDialog open={openUpdateOperators} maxHeight={"calc(80vh)"} fullWidth maxWidth={"lg"}
onClose={()=>{setOpenUpdateOperators(false);}}
@@ -154,18 +169,30 @@ export function OperationTableRow(props){
/>
}
</TableCell>
<TableCell>{props.name} {props.complete ? " (Completed) " : ""}</TableCell>
<TableCell>
<div className="mythic-status-stack">
<span>{props.name}</span>
{props.complete &&
<MythicStatusChip label="Completed" status="completed" />
}
{props.deleted &&
<MythicStatusChip label="Deleted" status="deleted" />
}
</div>
</TableCell>
<TableCell>{props.admin.username}</TableCell>
<TableCell>
<Button size="small" startIcon={<AssessmentIcon/>}
onClick={() => {navigate("/new")}} color="primary"
<Button className="mythic-table-row-action" size="small" startIcon={<AssessmentIcon/>}
onClick={() => {navigate("/new")}}
disabled={me?.user?.current_operation_id !== props.id}
variant="contained">Analysis</Button>
variant="outlined">Analysis</Button>
</TableCell>
<TableCell>
{props.id === me.user.current_operation_id ? ("Current Operation") : (
{props.id === me.user.current_operation_id ? (
<MythicStatusChip label="Current" status="active" />
) : (
<React.Fragment>
<Button size="small" startIcon={<PlayArrowIcon/>} onClick={makeCurrentOperation} color="info" variant="contained">Make Current</Button>
<Button className="mythic-table-row-action" size="small" startIcon={<PlayArrowIcon/>} onClick={makeCurrentOperation} variant="outlined">Make Current</Button>
</React.Fragment>
)}
</TableCell>
@@ -173,4 +200,3 @@ export function OperationTableRow(props){
</React.Fragment>
)
}
@@ -2,7 +2,6 @@ import React from 'react';
import Button from '@mui/material/Button';
import DialogActions from '@mui/material/DialogActions';
import DialogContent from '@mui/material/DialogContent';
import DialogContentText from '@mui/material/DialogContentText';
import DialogTitle from '@mui/material/DialogTitle';
import MythicTextField from '../../MythicComponents/MythicTextField';
import {useQuery, gql} from '@apollo/client';
@@ -10,7 +9,13 @@ import {snackActions} from '../../utilities/Snackbar';
import Switch from '@mui/material/Switch';
import Typography from '@mui/material/Typography';
import Box from '@mui/material/Box';
import FormControlLabel from '@mui/material/FormControlLabel';
import {getReadableTextColor, MythicColorSwatchInput} from '../../MythicComponents/MythicColorInput';
import {
MythicDialogBody,
MythicDialogGrid,
MythicDialogSection
} from '../../MythicComponents/MythicDialogLayout';
const GET_OperationData = gql`
query GetOperations($operation_id: Int!) {
@@ -89,73 +94,65 @@ export function OperationTableRowNotificationsDialog(props) {
<React.Fragment>
<DialogTitle id="form-dialog-title">Modify {name}</DialogTitle>
<DialogContent dividers={true} style={{maxHeight: "calc(70vh)"}}>
<DialogContentText>
Use this dialog to update some information about an operation.
</DialogContentText>
<MythicTextField
autoFocus
value={name}
onChange={onTextChange}
margin="dense"
name="name"
/>
Complete Operation? <Switch checked={complete} onChange={onBoolChange} color="warning" />
<MythicTextField
margin="dense"
value={channel}
onChange={onTextChange}
name="Webhook Channel"
/>
<MythicTextField
margin="dense"
value={webhook}
onChange={onTextChange}
name="Webhook URL"
/>
<MythicTextField
margin="dense"
value={bannerText}
onChange={onTextChange}
name="Banner Text"
/>
<Box
sx={{
mt: 2,
p: 1.5,
border: "1px solid",
borderColor: "divider",
borderRadius: "8px",
backgroundColor: "background.paper",
}}
<MythicDialogBody>
<MythicDialogSection title="Operation Details">
<MythicDialogGrid>
<MythicTextField
autoFocus
value={name}
onChange={onTextChange}
name="name"
/>
<FormControlLabel
className="mythic-dialog-switch-row"
label="Complete Operation"
labelPlacement="start"
control={<Switch checked={complete} onChange={onBoolChange} color="warning" />}
/>
</MythicDialogGrid>
</MythicDialogSection>
<MythicDialogSection title="Webhook Notifications">
<MythicDialogGrid>
<MythicTextField
value={channel}
onChange={onTextChange}
name="Webhook Channel"
/>
<MythicTextField
value={webhook}
onChange={onTextChange}
name="Webhook URL"
/>
</MythicDialogGrid>
</MythicDialogSection>
<MythicDialogSection
title="Operation Banner"
actions={
<MythicColorSwatchInput
color={bannerColor}
label="Operation banner color"
onChange={setBannerColor}
/>
}
>
<Box sx={{display: "flex", alignItems: "center", justifyContent: "space-between", gap: 2, mb: 1}}>
<Box sx={{minWidth: 0}}>
<Typography variant="body2" sx={{fontWeight: 700}}>Banner Color</Typography>
<Typography variant="caption" sx={{color: "text.secondary"}}>Operation banner background</Typography>
</Box>
<MythicColorSwatchInput
color={bannerColor}
label="Operation banner color"
onChange={setBannerColor}
/>
</Box>
<Box
sx={{
minHeight: 38,
px: 1.5,
display: "flex",
alignItems: "center",
borderRadius: "6px",
backgroundColor: bannerColor,
border: "1px solid",
borderColor: "divider",
}}
>
<Typography sx={{color: getReadableTextColor(bannerColor), fontWeight: 700}}>
{bannerText || "Operation Banner"}
</Typography>
</Box>
</Box>
<MythicTextField
value={bannerText}
onChange={onTextChange}
name="Banner Text"
/>
<Box
className="mythic-dialog-preview"
sx={{
mt: 1,
backgroundColor: bannerColor,
}}
>
<Typography sx={{color: getReadableTextColor(bannerColor), fontWeight: 700}}>
{bannerText || "Operation Banner"}
</Typography>
</Box>
</MythicDialogSection>
</MythicDialogBody>
</DialogContent>
<DialogActions>
<Button onClick={props.onClose} variant="contained" color="primary">
@@ -12,6 +12,10 @@ import TableHead from '@mui/material/TableHead';
import Table from '@mui/material/Table';
import TableBody from '@mui/material/TableBody';
import {OperationTableRowUpdateOperatorsDialogRow} from './OperationTableRowUpdateOperatorsDialogRow';
import {
MythicDialogBody,
MythicDialogSection
} from '../../MythicComponents/MythicDialogLayout';
const GET_Operations = gql`
query GetOperation($operation_id: Int!) {
@@ -207,30 +211,34 @@ export function OperationTableRowUpdateOperatorsDialog(props) {
<React.Fragment>
<DialogTitle id="form-dialog-title">Modify Operator Assignments</DialogTitle>
<DialogContent dividers={true}>
<TableContainer className="mythicElement" style={{marginTop: "0px"}}>
<Table stickyHeader={true} size="small" style={{"tableLayout": "fixed", "maxWidth": "calc(100vw)", "overflow": "scroll"}}>
<TableHead>
<TableRow>
<TableCell style={{width: "8rem"}}>Assign to Operation</TableCell>
<TableCell style={{}}>Operator</TableCell>
<TableCell style={{width: "10rem"}}>Role</TableCell>
<TableCell >Block List</TableCell>
<TableCell style={{width: "4rem"}}></TableCell>
</TableRow>
</TableHead>
<TableBody>
{operators.map( (op) => (
<OperationTableRowUpdateOperatorsDialogRow
key={"operator" + op.id}
operator={op} updateOperator={updateOperator}
commandBlockLists={commandBlockLists}
operation_id={props.id}
/>
))}
</TableBody>
</Table>
</TableContainer>
<MythicDialogBody>
<MythicDialogSection title="Operator Access">
<TableContainer className="mythicElement mythic-dialog-table-wrap" style={{marginTop: "0px"}}>
<Table stickyHeader={true} size="small" style={{"tableLayout": "fixed", "maxWidth": "calc(100vw)", "overflow": "scroll"}}>
<TableHead>
<TableRow>
<TableCell style={{width: "8rem"}}>Assign to Operation</TableCell>
<TableCell style={{}}>Operator</TableCell>
<TableCell style={{width: "10rem"}}>Role</TableCell>
<TableCell >Block List</TableCell>
<TableCell style={{width: "4rem"}}></TableCell>
</TableRow>
</TableHead>
<TableBody>
{operators.map( (op) => (
<OperationTableRowUpdateOperatorsDialogRow
key={"operator" + op.id}
operator={op} updateOperator={updateOperator}
commandBlockLists={commandBlockLists}
operation_id={props.id}
/>
))}
</TableBody>
</Table>
</TableContainer>
</MythicDialogSection>
</MythicDialogBody>
</DialogContent>
<DialogActions>
<Button onClick={props.onClose} variant="contained" color="primary">
@@ -243,4 +251,3 @@ export function OperationTableRowUpdateOperatorsDialog(props) {
</React.Fragment>
);
}
@@ -1,19 +1,12 @@
import React, {useState} from 'react';
import Button from '@mui/material/Button';
import DialogActions from '@mui/material/DialogActions';
import DialogContent from '@mui/material/DialogContent';
import DialogContentText from '@mui/material/DialogContentText';
import DialogTitle from '@mui/material/DialogTitle';
import {useQuery, gql} from '@apollo/client';
import TableCell from '@mui/material/TableCell';
import TableRow from '@mui/material/TableRow';
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 LinearProgress from '@mui/material/LinearProgress';
import { getDefaultChoices } from '../CreatePayload/Step2SelectPayloadType';
import { getDefaultValueForType } from '../CreatePayload/Step2SelectPayloadType';
import {MythicDialogBody, MythicDialogButton, MythicDialogFooter, MythicDialogSection} from "../../MythicComponents/MythicDialogLayout";
import {MythicErrorState, MythicLoadingState} from "../../MythicComponents/MythicStateDisplay";
import {BuildParameterList} from "./InstalledServiceParameterDetails";
const GET_C2_Details = gql`
query GetPC2Details($payload_name: String!) {
@@ -78,64 +71,45 @@ export function C2ProfileBuildDialog(props) {
}
});
if (loading) {
return <LinearProgress />;
return (
<>
<DialogTitle id="form-dialog-title">{props.container_name}'s Build Parameters</DialogTitle>
<DialogContent dividers={true}>
<MythicLoadingState title="Loading build parameters" description="Fetching parameter definitions for this C2 profile." minHeight={180} />
</DialogContent>
</>
);
}
if (error) {
console.error(error);
return <div>Error! {error.message}</div>;
return (
<>
<DialogTitle id="form-dialog-title">{props.container_name}'s Build Parameters</DialogTitle>
<DialogContent dividers={true}>
<MythicErrorState title="Unable to load build parameters" description={error.message} minHeight={180} />
</DialogContent>
</>
);
}
return (
<React.Fragment>
<DialogTitle id="form-dialog-title">{props.container_name}'s Build Parameters</DialogTitle>
<DialogContent dividers={true}>
<DialogContentText>
These are the build parameters associated with this payload
</DialogContentText>
<TableContainer className="mythicElement">
<Table size="small" aria-label="details" style={{"tableLayout": "fixed", "overflowWrap": "break-word"}}>
<TableHead>
<TableRow>
<TableCell style={{width: "20%"}}>Parameter</TableCell>
<TableCell>Value</TableCell>
</TableRow>
</TableHead>
<TableBody>
{
buildParams.map( (param) => (
<TableRow key={"buildprop" + param.id} hover>
<TableCell>{param.description}</TableCell>
<TableCell>
<b>Scripting/Building Name: </b><pre style={{display: "inline-block", whiteSpace: "pre-wrap", margin: 0}}>{param.name}</pre><br/>
<b>Parameter Type: </b><pre style={{display: "inline-block", whiteSpace: "pre-wrap", margin: 0}}>{param.parameter_type}</pre><br/>
<b>Default Value: </b><pre style={{display: "inline-block", whiteSpace: "pre-wrap", margin: 0}}>{param.default_value}</pre><br/>
{param.choices.length > 0 ? (
<React.Fragment>
<b>Parameter Options: </b><pre style={{display: "inline-block", whiteSpace: "pre-wrap", margin: 0}}>{param.choices}</pre><br/>
</React.Fragment>
) : null}
<b>Required? </b><pre style={{display: "inline-block", whiteSpace: "pre-wrap", margin: 0}}>{param.required ? "Yes": "No"}</pre><br/>
<b>Verifier Regex: </b><pre style={{display: "inline-block", whiteSpace: "pre-wrap", margin: 0}}>{param.verifier_regex}</pre><br/>
<b>Randomized: </b><pre style={{display: "inline-block", whiteSpace: "pre-wrap", margin: 0}}>{param.randomize ? "Yes": "No"}</pre><br/>
{param.randomize ? (
<React.Fragment>
<b>Format String: </b><pre style={{display: "inline-block", whiteSpace: "pre-wrap", margin: 0}}>{param.format_string}</pre><br/>
</React.Fragment>
) : (null)}
</TableCell>
</TableRow>
))
}
</TableBody>
</Table>
</TableContainer>
<MythicDialogBody compact>
<MythicDialogSection
title="Build Parameters"
description={`${buildParams.length} parameter${buildParams.length === 1 ? "" : "s"} defined for this C2 profile.`}
>
<BuildParameterList parameters={buildParams} />
</MythicDialogSection>
</MythicDialogBody>
</DialogContent>
<DialogActions>
<Button variant="contained" onClick={props.onClose} color="primary">
<MythicDialogFooter>
<MythicDialogButton onClick={props.onClose}>
Close
</Button>
</DialogActions>
</MythicDialogButton>
</MythicDialogFooter>
</React.Fragment>
);
}
@@ -1,14 +1,14 @@
import React, {useState} from 'react';
import Button from '@mui/material/Button';
import DialogActions from '@mui/material/DialogActions';
import React from 'react';
import DialogContent from '@mui/material/DialogContent';
import DialogTitle from '@mui/material/DialogTitle';
import {useQuery, gql} from '@apollo/client';
import LinearProgress from '@mui/material/LinearProgress';
import 'ace-builds/src-noconflict/mode-json';
import 'ace-builds/src-noconflict/theme-monokai';
import 'ace-builds/src-noconflict/theme-xcode';
import {ResponseDisplayPlaintext} from "../Callbacks/ResponseDisplayPlaintext";
import {textExtensionTypesToSyntax} from "../Callbacks/ResponseDisplayMedia";
import {MythicDialogButton, MythicDialogFooter} from "../../MythicComponents/MythicDialogLayout";
import {MythicErrorState, MythicLoadingState} from "../../MythicComponents/MythicStateDisplay";
const getProfileConfigQuery = gql`
query getProfileConfigOutput($container_name: String!, $filename: String!) {
@@ -36,7 +36,6 @@ const getInitialMode = (filename) => {
}
export function C2ProfileConfigDialog(props) {
const config = React.useRef("");
const [fetchedData, setFetchedData] = React.useState(false);
const { loading, error } = useQuery(getProfileConfigQuery, {
variables: {container_name: props.container_name, filename: props.filename},
onCompleted: data => {
@@ -46,18 +45,31 @@ export function C2ProfileConfigDialog(props) {
//console.log(data);
config.current = atob(data.containerDownloadFile.data);
}
setFetchedData(true);
},
fetchPolicy: "network-only"
});
const initialMode = React.useRef(getInitialMode(props.filename));
if (loading) {
return <LinearProgress />;
return (
<>
<DialogTitle id="form-dialog-title">{props.container_name}'s {props.filename}</DialogTitle>
<DialogContent dividers={true}>
<MythicLoadingState title="Loading file" description="Fetching file contents from the service container." minHeight={180} />
</DialogContent>
</>
);
}
if (error) {
console.error(error);
return <div>Error! {error.message}</div>;
return (
<>
<DialogTitle id="form-dialog-title">{props.container_name}'s {props.filename}</DialogTitle>
<DialogContent dividers={true}>
<MythicErrorState title="Unable to load file" description={error.message} minHeight={180} />
</DialogContent>
</>
);
}
const onConfigSubmit = () => {
props.onConfigSubmit(btoa(config.current));
@@ -70,6 +82,7 @@ export function C2ProfileConfigDialog(props) {
return (
<>
<DialogTitle id="form-dialog-title">{props.container_name}'s {props.filename}</DialogTitle>
<DialogContent dividers={true} style={{padding: 0}}>
<div style={{height: "calc(80vh)"}}>
<ResponseDisplayPlaintext
onChangeContent={setConfig}
@@ -77,15 +90,15 @@ export function C2ProfileConfigDialog(props) {
plaintext={config.current}
expand={true} />
</div>
<DialogActions>
<Button variant="contained" onClick={props.onClose} color="primary">
</DialogContent>
<MythicDialogFooter>
<MythicDialogButton onClick={props.onClose}>
Close
</Button>
<Button variant="contained" onClick={onConfigSubmit} color="warning">
</MythicDialogButton>
<MythicDialogButton intent="warning" onClick={onConfigSubmit}>
Submit
</Button>
</DialogActions>
</MythicDialogButton>
</MythicDialogFooter>
</>
);
}
@@ -1,9 +1,8 @@
import React from 'react';
import Button from '@mui/material/Button';
import DialogActions from '@mui/material/DialogActions';
import DialogContent from '@mui/material/DialogContent';
import DialogTitle from '@mui/material/DialogTitle';
import {useQuery, gql, useMutation, useLazyQuery} from '@apollo/client';
import LinearProgress from '@mui/material/LinearProgress';
import { snackActions } from '../../utilities/Snackbar';
import Table from '@mui/material/Table';
import TableBody from '@mui/material/TableBody';
@@ -20,6 +19,9 @@ import DownloadIcon from '@mui/icons-material/Download';
import DeleteIcon from '@mui/icons-material/Delete';
import {MythicConfirmDialog} from '../../MythicComponents/MythicConfirmDialog';
import {downloadFileFromMemory} from '../../utilities/Clipboard';
import {MythicDialogBody, MythicDialogButton, MythicDialogFooter, MythicDialogSection} from "../../MythicComponents/MythicDialogLayout";
import {MythicErrorState, MythicLoadingState} from "../../MythicComponents/MythicStateDisplay";
import {MythicStyledTooltip} from "../../MythicComponents/MythicStyledTooltip";
export const containerListFilesQuery = gql`
query listC2ProfileFilesQuery($container_name: String!){
@@ -96,21 +98,47 @@ export function C2ProfileListFilesDialog(props) {
}
if (loading) {
return <LinearProgress />;
return (
<>
<DialogTitle id="form-dialog-title">{props.container_name}'s Current Files</DialogTitle>
<DialogContent dividers={true}>
<MythicLoadingState title="Loading container files" description="Fetching files from the service container." minHeight={180} />
</DialogContent>
</>
);
}
if (error) {
console.error(error);
return <div>Error! {error.message}</div>;
return (
<>
<DialogTitle id="form-dialog-title">{props.container_name}'s Current Files</DialogTitle>
<DialogContent dividers={true}>
<MythicErrorState title="Unable to load container files" description={error.message} minHeight={180} />
</DialogContent>
</>
);
}
return (
<React.Fragment>
<DialogTitle id="form-dialog-title">{props.container_name}'s Current Files
<Button color="success" size="small" style={{float: "right"}} variant="contained" component="label">
<FileUploadIcon /> Upload File
<input onChange={onFileChange} type="file" multiple hidden />
</Button>
<DialogTitle id="form-dialog-title">
<div className="mythic-dialog-title-row">
<span>{props.container_name}'s Current Files</span>
<Button
className="mythic-dialog-title-action mythic-dialog-button-success"
component="label"
size="small"
startIcon={<FileUploadIcon fontSize="small" />}
variant="outlined"
>
Upload File
<input onChange={onFileChange} type="file" multiple hidden />
</Button>
</div>
</DialogTitle>
<DialogContent dividers={true}>
<MythicDialogBody compact>
<MythicDialogSection title="Container Files" description="Download, edit, upload, or remove files from this installed service.">
<TableContainer className="mythicElement">
<Table stickyHeader size="small" style={{ "maxWidth": "100%", "overflow": "scroll"}}>
<TableHead>
@@ -128,12 +156,14 @@ export function C2ProfileListFilesDialog(props) {
</TableBody>
</Table>
</TableContainer>
<DialogActions>
<Button variant="contained" onClick={props.onClose} color="primary">
</MythicDialogSection>
</MythicDialogBody>
</DialogContent>
<MythicDialogFooter>
<MythicDialogButton onClick={props.onClose}>
Close
</Button>
</DialogActions>
</MythicDialogButton>
</MythicDialogFooter>
</React.Fragment>
);
}
@@ -200,13 +230,25 @@ const C2ProfileListFilesDialogTableRow = ({container_name, id, filename}) => {
<React.Fragment>
<TableRow hover>
<TableCell>
<IconButton size="small" onClick={()=>{setOpenDeleteDialog(true);}} ><DeleteIcon color="error" /></IconButton>
<MythicStyledTooltip title="Remove file">
<IconButton className="mythic-table-row-icon-action mythic-table-row-icon-action-hover-danger" size="small" onClick={()=>{setOpenDeleteDialog(true);}} >
<DeleteIcon fontSize="small" />
</IconButton>
</MythicStyledTooltip>
</TableCell>
<TableCell>
<IconButton size="small" onClick={downloadFile}><DownloadIcon color="success" /></IconButton>
<MythicStyledTooltip title="Download file">
<IconButton className="mythic-table-row-icon-action mythic-table-row-icon-action-success" size="small" onClick={downloadFile}>
<DownloadIcon fontSize="small" />
</IconButton>
</MythicStyledTooltip>
</TableCell>
<TableCell>
<IconButton size="small" onClick={() => {setOpenProfileConfigDialog(true);}}><EditIcon color="info" /></IconButton>
<MythicStyledTooltip title="Edit file">
<IconButton className="mythic-table-row-icon-action mythic-table-row-icon-action-hover-info" size="small" onClick={() => {setOpenProfileConfigDialog(true);}}>
<EditIcon fontSize="small" />
</IconButton>
</MythicStyledTooltip>
</TableCell>
<TableCell>{filename}</TableCell>
</TableRow>
@@ -1,11 +1,10 @@
import React from 'react';
import Button from '@mui/material/Button';
import DialogActions from '@mui/material/DialogActions';
import DialogContentText from '@mui/material/DialogContentText';
import DialogContent from '@mui/material/DialogContent';
import DialogTitle from '@mui/material/DialogTitle';
import {useQuery, gql} from '@apollo/client';
import {snackActions} from "../../utilities/Snackbar";
import {ResponseDisplayPlaintext} from "../Callbacks/ResponseDisplayPlaintext";
import {MythicDialogButton, MythicDialogFooter, MythicDialogSection} from "../../MythicComponents/MythicDialogLayout";
const getProfileOutputQuery = gql`
query getProfileOutput($id: Int!) {
@@ -45,10 +44,9 @@ export function C2ProfileOutputDialog(props) {
return (
<React.Fragment>
<DialogTitle id="form-dialog-title">{props.container_name}'s Current Stdout/Stderr</DialogTitle>
<DialogContentText>
This is the current Stdout/Stderr for the profile. This goes away once you close this dialog.
</DialogContentText>
<div style={{height: "calc(80vh)", overflowY: "auto"}}>
<DialogContent dividers={true} style={{padding: 0}}>
<MythicDialogSection description="This is the current Stdout/Stderr for the profile. This goes away once you close this dialog.">
<div style={{height: "calc(80vh)", overflowY: "auto", paddingTop: "0.5rem"}}>
<ResponseDisplayPlaintext
initial_mode={"json"}
render_colors={false}
@@ -56,12 +54,13 @@ export function C2ProfileOutputDialog(props) {
plaintext={outputData}
expand={true}/>
</div>
<DialogActions>
<Button variant="contained" onClick={props.onClose} color="primary">
</MythicDialogSection>
</DialogContent>
<MythicDialogFooter>
<MythicDialogButton onClick={props.onClose}>
Close
</Button>
</DialogActions>
</MythicDialogButton>
</MythicDialogFooter>
</React.Fragment>
);
}
@@ -1,24 +1,23 @@
import React, {useState} from 'react';
import {Button, Typography} from '@mui/material';
import DialogActions from '@mui/material/DialogActions';
import DialogContent from '@mui/material/DialogContent';
import DialogTitle from '@mui/material/DialogTitle';
import MythicTextField from '../../MythicComponents/MythicTextField';
import {useQuery, gql, useLazyQuery, useMutation} from '@apollo/client';
import LinearProgress from '@mui/material/LinearProgress';
import {CreatePayloadC2ProfileParametersTable} from '../CreatePayload/CreatePayloadC2ProfileParametersTable';
import FormControl from '@mui/material/FormControl';
import Select from '@mui/material/Select';
import Grid from '@mui/material/Grid';
import InputLabel from '@mui/material/InputLabel';
import MenuItem from '@mui/material/MenuItem';
import {useTheme} from '@mui/material/styles';
import { snackActions } from '../../utilities/Snackbar';
import {getDefaultValueForType, getDefaultChoices} from '../CreatePayload/Step2SelectPayloadType';
import {UploadTaskFile} from "../../MythicComponents/MythicFileUpload";
import IosShareIcon from '@mui/icons-material/IosShare';
import SystemUpdateAltIcon from '@mui/icons-material/SystemUpdateAlt';
import DeleteIcon from '@mui/icons-material/Delete';
import {MythicDialogButton, MythicDialogFooter} from "../../MythicComponents/MythicDialogLayout";
import {MythicLoadingState} from "../../MythicComponents/MythicStateDisplay";
const getProfileConfigQuery = gql`
query getProfileParameters($id: Int!) {
@@ -91,7 +90,6 @@ importC2Instance(
`;
export function C2ProfileSavedInstancesDialog(props) {
const theme = useTheme();
const [instanceName, setInstanceName] = useState("");
const [selectedInstance, setSelectedInstance] = useState("");
const [createdInstances, setCreatedInstances] = useState([]);
@@ -233,7 +231,14 @@ export function C2ProfileSavedInstancesDialog(props) {
}
})
if (loading) {
return <LinearProgress />;
return (
<>
<DialogTitle id="form-dialog-title">Save an Instance of {props.name}'s Parameters</DialogTitle>
<DialogContent dividers={true}>
<MythicLoadingState title="Loading saved instances" description="Fetching saved C2 profile parameter sets." minHeight={180} />
</DialogContent>
</>
);
}
const onConfigSubmit = async () => {
if(instanceName.length === 0){
@@ -396,14 +401,14 @@ export function C2ProfileSavedInstancesDialog(props) {
<Grid size={6}>
{selectedInstance.length > 0 ? (
<>
<Button style={{backgroundColor: theme.palette.error.main, color: "white", marginRight: "10px"}} variant="contained" onClick={deleteInstanceButton}>
<DeleteIcon style={{marginRight: "5px"}} /> Delete</Button>
<Button style={{backgroundColor: theme.palette.success.main, color: "white"}} variant="contained" onClick={exportInstanceButton}>
<IosShareIcon style={{marginRight: "5px"}} /> Export</Button>
<Button className="mythic-table-row-action mythic-table-row-action-hover-danger" startIcon={<DeleteIcon fontSize="small" />} style={{marginRight: "0.5rem"}} variant="outlined" onClick={deleteInstanceButton}>
Delete</Button>
<Button className="mythic-table-row-action mythic-table-row-action-success" startIcon={<IosShareIcon fontSize="small" />} style={{marginRight: "0.5rem"}} variant="outlined" onClick={exportInstanceButton}>
Export</Button>
</>
) : null}
<Button style={{backgroundColor: theme.palette.warning.main, color: "white", marginLeft: "10px"}} component="label" variant="contained">
<SystemUpdateAltIcon style={{marginRight: "5px"}} />Import
<Button className="mythic-table-row-action mythic-table-row-action-hover-info" component="label" startIcon={<SystemUpdateAltIcon fontSize="small" />} variant="outlined">
Import
<input onChange={onFileChange} type="file" hidden />
</Button>
</Grid>
@@ -414,22 +419,22 @@ export function C2ProfileSavedInstancesDialog(props) {
inline={createdInstances.length === 0}/>
{createdInstances.length === 0 &&
<>
<Button style={{backgroundColor: theme.palette.warning.main, color: "white", marginLeft: "10px", marginTop: "20px"}} component="label" variant="contained">
<SystemUpdateAltIcon style={{marginRight: "5px"}} />Import
<Button className="mythic-table-row-action mythic-table-row-action-hover-info" component="label" startIcon={<SystemUpdateAltIcon fontSize="small" />} style={{marginTop: "20px"}} variant="outlined">
Import
<input onChange={onFileChange} type="file" hidden />
</Button>
</>
}
<CreatePayloadC2ProfileParametersTable {...props} returnAllDictValues={true} c2profileparameters={currentParameters} onChange={updateC2Parameter} />
</DialogContent>
<DialogActions>
<Button variant="contained" onClick={props.onClose} color="primary">
<MythicDialogFooter>
<MythicDialogButton onClick={props.onClose}>
Close
</Button>
<Button variant="contained" onClick={onConfigSubmit} color="success">
</MythicDialogButton>
<MythicDialogButton intent="primary" onClick={onConfigSubmit}>
{selectedInstance.length > 0 ? (selectedInstance !== instanceName ? "Create": "Update") : ("Create")}
</Button>
</DialogActions>
</MythicDialogButton>
</MythicDialogFooter>
</React.Fragment>
);
}
@@ -1,19 +1,17 @@
import React from 'react';
import Button from '@mui/material/Button';
import DialogActions from '@mui/material/DialogActions';
import DialogContentText from '@mui/material/DialogContentText';
import DialogContent from '@mui/material/DialogContent';
import DialogTitle from '@mui/material/DialogTitle';
import {ResponseDisplayPlaintext} from "../Callbacks/ResponseDisplayPlaintext";
import {MythicDialogButton, MythicDialogFooter, MythicDialogSection} from "../../MythicComponents/MythicDialogLayout";
export function C2ProfileStartStopOutputDialog(props) {
return (
<>
<DialogTitle id="form-dialog-title">{props.container_name}'s Current Stdout/Stderr</DialogTitle>
<DialogContentText>
This is the current Stdout/Stderr for the profile. This goes away once you close this dialog.
</DialogContentText>
<div style={{height: "calc(80vh)", overflowY: "auto"}}>
<DialogContent dividers={true} style={{padding: 0}}>
<MythicDialogSection description="This is the current Stdout/Stderr for the profile. This goes away once you close this dialog.">
<div style={{height: "calc(80vh)", overflowY: "auto", paddingTop: "0.5rem"}}>
<ResponseDisplayPlaintext
initial_mode={"json"}
render_colors={true}
@@ -21,12 +19,13 @@ export function C2ProfileStartStopOutputDialog(props) {
plaintext={props.output}
expand={true}/>
</div>
<DialogActions>
<Button variant="contained" onClick={props.onClose} color="primary">
</MythicDialogSection>
</DialogContent>
<MythicDialogFooter>
<MythicDialogButton onClick={props.onClose}>
Close
</Button>
</DialogActions>
</MythicDialogButton>
</MythicDialogFooter>
</>
);
}
@@ -17,7 +17,6 @@ import {C2ProfileOutputDialog} from './C2ProfileOutputDialog';
import {C2ProfileConfigDialog} from './C2ProfileConfigDialog';
import {C2ProfileStartStopOutputDialog} from './C2ProfileStartStopOutputDialog';
import {snackActions} from '../../utilities/Snackbar';
import {useTheme} from '@mui/material/styles';
import {C2ProfileSavedInstancesDialog} from './C2ProfileSavedInstancesDialog';
import {FontAwesomeIcon} from '@fortawesome/react-fontawesome';
import { faLink } from '@fortawesome/free-solid-svg-icons';
@@ -27,13 +26,15 @@ import RestoreFromTrashOutlinedIcon from '@mui/icons-material/RestoreFromTrashOu
import {C2ProfileListFilesDialog} from './C2ProfileListFilesDialog';
import MenuBookIcon from '@mui/icons-material/MenuBook';
import IconButton from '@mui/material/IconButton';
import BuildIcon from '@mui/icons-material/Build';
import TuneIcon from '@mui/icons-material/Tune';
import SaveIcon from '@mui/icons-material/Save';
import AttachFileIcon from '@mui/icons-material/AttachFile';
import TableRow from '@mui/material/TableRow';
import MythicTableCell from "../../MythicComponents/MythicTableCell";
import {MythicStyledTooltip} from "../../MythicComponents/MythicStyledTooltip";
import {MythicAgentSVGIcon} from "../../MythicComponents/MythicAgentSVGIcon";
import {MythicStatusChip} from "../../MythicComponents/MythicStatusChip";
import CheckCircleOutlineIcon from '@mui/icons-material/CheckCircleOutline';
const toggleDeleteStatus = gql`
@@ -63,7 +64,6 @@ mutation setProfileConfiguration($container_name: String!, $file_path: String!,
`;
export function C2ProfilesRow({service, showDeleted}) {
const theme = useTheme();
const [openBuildingDialog, setOpenBuildingDialog] = React.useState(false);
const [openListFilesDialog, setOpenListFilesDialog] = React.useState(false);
const [dropdownOpen, setDropdownOpen] = React.useState(false);
@@ -148,17 +148,19 @@ export function C2ProfilesRow({service, showDeleted}) {
if(service.deleted && !showDeleted){
return null;
}
const containerStatus = !service.container_running ? "error" : service.is_p2p ? "success" : "neutral";
const containerIcon = service.container_running && !service.is_p2p ? <CheckCircleOutlineIcon /> : undefined;
return (
<>
<TableRow hover>
<MythicTableCell>
{service.deleted ? (
<IconButton onClick={()=>{setOpenDeleteDialog(true);}} color="success" size="small">
<RestoreFromTrashOutlinedIcon/>
<IconButton className="mythic-table-row-icon-action mythic-table-row-icon-action-success" onClick={()=>{setOpenDeleteDialog(true);}} size="small">
<RestoreFromTrashOutlinedIcon fontSize="small" />
</IconButton>
) : (
<IconButton onClick={()=>{setOpenDeleteDialog(true);}} color="error" size="small">
<DeleteIcon/>
<IconButton className="mythic-table-row-icon-action mythic-table-row-icon-action-hover-danger" onClick={()=>{setOpenDeleteDialog(true);}} size="small">
<DeleteIcon fontSize="small" />
</IconButton>
)}
</MythicTableCell>
@@ -199,27 +201,22 @@ export function C2ProfilesRow({service, showDeleted}) {
<Typography variant="body2" component="p" >
<b>Container Status: </b>
</Typography>
<Typography variant="body2" component="p" color={service.container_running ? theme.palette.success.main : theme.palette.error.main} >
<b>{service.container_running ? "Online" : "Offline"}</b>
</Typography>
{!service.is_p2p && service.running &&
<MythicStatusChip
label={service.container_running ? "Online" : "Offline"}
icon={containerIcon}
status={containerStatus}
showIcon
/>
{!service.is_p2p &&
<React.Fragment>
<Typography variant="body2" component="p" >
<b>C2 Server Status: </b>
</Typography>
<Typography variant="body2" component="p" style={{ color:theme.palette.success.main}}>
<b>{"Accepting Connections"}</b>
</Typography>
</React.Fragment>
}
{!service.is_p2p && !service.running &&
<React.Fragment>
<Typography variant="body2" component="p" >
<b>C2 Server Status: </b>
</Typography>
<Typography variant="body2" component="p" style={{color:theme.palette.error.main}}>
<b>{"Not Accepting Connections"}</b>
</Typography>
<MythicStatusChip
label={!service.container_running ? "Unavailable" : service.running ? "Accepting Connections" : "Not Accepting Connections"}
status={!service.container_running ? "neutral" : service.running ? "success" : "error"}
showIcon={service.container_running}
/>
</React.Fragment>
}
</MythicTableCell>
@@ -227,8 +224,9 @@ export function C2ProfilesRow({service, showDeleted}) {
{service.container_running ? (
service.running ?
(
<ButtonGroup ref={dropdownAnchorRef} aria-label="split button" >
<ButtonGroup className="mythic-split-action-group" ref={dropdownAnchorRef} aria-label="split button" >
<Button size="small"
className="mythic-table-row-action mythic-table-row-action-danger"
disabled={alreadyRunningStartStop}
onClick={onStartStopProfile}
style={{width: "100%"}}>
@@ -236,6 +234,7 @@ export function C2ProfilesRow({service, showDeleted}) {
</Button>
<Button
size="small"
className="mythic-table-row-icon-action mythic-table-row-icon-action-danger"
disabled={alreadyRunningStartStop}
aria-controls={dropdownOpen ? 'split-button-menu' : undefined}
aria-expanded={dropdownOpen ? 'true' : undefined}
@@ -243,15 +242,16 @@ export function C2ProfilesRow({service, showDeleted}) {
aria-haspopup="menu"
onClick={handleDropdownToggle}
>
<ArrowDropDownIcon />
<ArrowDropDownIcon fontSize="small" />
</Button>
</ButtonGroup>
)
:
(
service.is_p2p ? null : (
<ButtonGroup size="small" ref={dropdownAnchorRef} aria-label="split button" >
<ButtonGroup className="mythic-split-action-group" size="small" ref={dropdownAnchorRef} aria-label="split button" >
<Button size="small"
className="mythic-table-row-action mythic-table-row-action-success"
disabled={alreadyRunningStartStop}
onClick={onStartStopProfile}
style={{width: "100%"}}>
@@ -259,6 +259,7 @@ export function C2ProfilesRow({service, showDeleted}) {
</Button>
<Button
size="small"
className="mythic-table-row-icon-action mythic-table-row-icon-action-success"
disabled={alreadyRunningStartStop}
aria-controls={dropdownOpen ? 'split-button-menu' : undefined}
aria-expanded={dropdownOpen ? 'true' : undefined}
@@ -266,44 +267,49 @@ export function C2ProfilesRow({service, showDeleted}) {
aria-haspopup="menu"
onClick={handleDropdownToggle}
>
<ArrowDropDownIcon />
<ArrowDropDownIcon fontSize="small" />
</Button>
</ButtonGroup>
)
)
) : null}
<br/>
<div className="mythic-table-row-actions" style={{marginTop: "0.4rem"}}>
<MythicStyledTooltip title={"Documentation"}>
<IconButton
className="mythic-table-row-icon-action mythic-table-row-icon-action-hover-info"
href={"/docs/c2-profiles/" + service.name.toLowerCase()}
target="_blank"
size="medium">
<MenuBookIcon />
size="small">
<MenuBookIcon fontSize="small" />
</IconButton>
</MythicStyledTooltip>
<MythicStyledTooltip title={"Build Parameters"}>
<IconButton
className="mythic-table-row-icon-action mythic-table-row-icon-action-hover-info"
onClick={()=>{setOpenBuildingDialog(true);}}
size="medium">
<BuildIcon />
size="small">
<TuneIcon fontSize="small" />
</IconButton>
</MythicStyledTooltip>
<MythicStyledTooltip title={"Save/Edit Instances for Building"}>
<IconButton
className="mythic-table-row-icon-action mythic-table-row-icon-action-hover-info"
onClick={() => {setOpenProfileSavedInstancesDialog(true);}}
size="medium">
<SaveIcon />
size="small">
<SaveIcon fontSize="small" />
</IconButton>
</MythicStyledTooltip>
<MythicStyledTooltip title={service.container_running ? "View Files" : "Unable to view files because container is offline"}>
<IconButton
className="mythic-table-row-icon-action mythic-table-row-icon-action-hover-info"
disabled={!service.container_running}
onClick={()=>{setOpenListFilesDialog(true);}}
size="medium">
<AttachFileIcon />
size="small">
<AttachFileIcon fontSize="small" />
</IconButton>
</MythicStyledTooltip>
</div>
<Popper open={dropdownOpen} anchorEl={dropdownAnchorRef.current} role={undefined} transition disablePortal style={{zIndex: 4}}>
{({ TransitionProps, placement }) => (
@@ -385,4 +391,4 @@ export function C2ProfilesRow({service, showDeleted}) {
</>
);
}
}
@@ -1,10 +1,6 @@
import React, {useState} from 'react';
import Button from '@mui/material/Button';
import DialogActions from '@mui/material/DialogActions';
import DialogContent from '@mui/material/DialogContent';
import DialogTitle from '@mui/material/DialogTitle';
import {useQuery, gql} from '@apollo/client';
import LinearProgress from '@mui/material/LinearProgress';
import { snackActions } from '../../utilities/Snackbar';
import AceEditor from 'react-ace';
import 'ace-builds/src-noconflict/mode-json';
@@ -12,6 +8,7 @@ import 'ace-builds/src-noconflict/theme-monokai';
import 'ace-builds/src-noconflict/theme-xcode';
import "ace-builds/src-noconflict/ext-searchbox";
import {useTheme} from '@mui/material/styles';
import {MythicDialogBody, MythicDialogButton, MythicDialogFooter, MythicDialogSection} from "../../MythicComponents/MythicDialogLayout";
export function ConsumingServicesGetIDPMetadataDialog(props) {
@@ -51,6 +48,8 @@ export function ConsumingServicesGetIDPMetadataDialog(props) {
<React.Fragment>
<DialogTitle id="form-dialog-title">{props.container}'s {props.idp} Metadata</DialogTitle>
<DialogContent dividers={true}>
<MythicDialogBody compact>
<MythicDialogSection title="Identity Provider Metadata" description="Container metadata returned by the configured auth service.">
<AceEditor
mode="text"
theme={theme.palette.mode === "dark" ? "monokai" : "xcode"}
@@ -67,13 +66,14 @@ export function ConsumingServicesGetIDPMetadataDialog(props) {
tabSize: 4,
useWorker: false
}}/>
</MythicDialogSection>
</MythicDialogBody>
</DialogContent>
<DialogActions>
<Button variant="contained" onClick={props.onClose} color="primary">
<MythicDialogFooter>
<MythicDialogButton onClick={props.onClose}>
Close
</Button>
</DialogActions>
</MythicDialogButton>
</MythicDialogFooter>
</React.Fragment>
);
}
@@ -2,12 +2,9 @@ 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 Paper from '@mui/material/Paper';
import Typography from '@mui/material/Typography';
import {useTheme} from '@mui/material/styles';
import PublicIcon from '@mui/icons-material/Public';
import {IconButton} from '@mui/material';
import {gql, useMutation} from '@apollo/client';
@@ -23,8 +20,7 @@ import {MythicDialog} from "../../MythicComponents/MythicDialog";
import AttachFileIcon from '@mui/icons-material/AttachFile';
import {ConsumingServicesGetIDPMetadataDialog} from "./ConsumingServicesGetIDPMetadataDialog";
import {C2ProfileListFilesDialog} from "./C2ProfileListFilesDialog";
import VisibilityOffIcon from '@mui/icons-material/VisibilityOff';
import VisibilityIcon from '@mui/icons-material/Visibility';
import {MythicStatusChip} from "../../MythicComponents/MythicStatusChip";
const testWebhookMutation = gql`
mutation testWebhookWorks($service_type: String!){
@@ -53,7 +49,6 @@ const webhook_events = ["new_alert","new_callback","new_custom","new_feedback",
const logging_events = ["new_artifact","new_callback", "new_credential","new_file", "new_keylog", "new_payload", "new_response", "new_task"];
export const ConsumingServicesTableRow = ({service, showDeleted}) => {
const theme = useTheme();
const openListFilesInformation = React.useRef("");
const [testWebhook] = useMutation(testWebhookMutation, {
onCompleted: data => {
@@ -159,6 +154,35 @@ export const ConsumingServicesTableRow = ({service, showDeleted}) => {
default:
}
}, [service]);
const renderDeleteButton = (w) => (
<MythicStyledTooltip title={w.deleted ? "Restore service" : "Remove service"}>
<IconButton
className={`mythic-table-row-icon-action ${w.deleted ? "mythic-table-row-icon-action-success" : "mythic-table-row-icon-action-hover-danger"}`}
onClick={() => adjustingDelete(w)}
size="small"
>
{w.deleted ? <RestoreFromTrashOutlinedIcon fontSize="small" /> : <DeleteIcon fontSize="small" />}
</IconButton>
</MythicStyledTooltip>
);
const renderContainerStatus = (w) => (
<MythicStatusChip
label={w.container_running ? "Online" : "Offline"}
status={w.container_running ? "success" : "error"}
/>
);
const renderFileButton = (w) => (
<MythicStyledTooltip title={w.container_running ? "View Files" : "Unable to view files since container is offline"}>
<IconButton
className="mythic-table-row-icon-action mythic-table-row-icon-action-hover-info"
disabled={!w.container_running}
onClick={()=>{onOpenListFilesDialog(w.name);}}
size="small"
>
<AttachFileIcon fontSize="small" />
</IconButton>
</MythicStyledTooltip>
);
const getTableRow = (w) => {
switch(service.type){
case "webhook":
@@ -166,19 +190,7 @@ export const ConsumingServicesTableRow = ({service, showDeleted}) => {
(showDeleted || !w.deleted) &&
<TableRow key={w.id} hover>
<MythicTableCell>
{w.deleted ? (
<IconButton onClick={() => {
adjustingDelete(w);
}} color="success" size="medium">
<RestoreFromTrashOutlinedIcon/>
</IconButton>
) : (
<IconButton onClick={() => {
adjustingDelete(w);
}} color="error" size="medium">
<DeleteIcon/>
</IconButton>
)}
{renderDeleteButton(w)}
</MythicTableCell>
<MythicTableCell>
<Typography variant={"h5"}>
@@ -190,43 +202,27 @@ export const ConsumingServicesTableRow = ({service, showDeleted}) => {
<Typography variant={"body2"}>
<b>Description: </b>{w.description}
</Typography>
{w.container_running &&
<Typography variant="body2" component="p"
style={{color: theme.palette.success.main}}>
<b>{"Online"}</b>
</Typography>
}
{!w.container_running &&
<Typography variant="body2" component="p"
style={{color: theme.palette.error.main}}>
<b>{"Offline"}</b>
</Typography>
}
{renderContainerStatus(w)}
</MythicTableCell>
<MythicTableCell>
<MythicStyledTooltip title={w.container_running ? "View Files" : "Unable to view files since container is offline"}>
<IconButton
disabled={!w.container_running}
onClick={()=>{onOpenListFilesDialog(w.name);}}
size="medium">
<AttachFileIcon />
</IconButton>
</MythicStyledTooltip>
{renderFileButton(w)}
</MythicTableCell>
<MythicTableCell>
{webhook_events.map(s => (
<MythicStyledTooltip title={"test webhook " + s} key={w.id + "webhook_" + s}>
<IconButton
disabled={!w.subscriptions.includes(s) || !w.container_running}
onClick={() => {
issueTestWebhook(s)
}}
size="medium">
<PublicIcon/>
</IconButton>
</MythicStyledTooltip>
))}
<div className="mythic-table-row-actions mythic-service-actions">
{webhook_events.map(s => (
<MythicStyledTooltip title={"test webhook " + s} key={w.id + "webhook_" + s}>
<IconButton
className="mythic-table-row-icon-action mythic-table-row-icon-action-hover-info"
disabled={!w.subscriptions.includes(s) || !w.container_running}
onClick={() => {
issueTestWebhook(s)
}}
size="small">
<PublicIcon fontSize="small" />
</IconButton>
</MythicStyledTooltip>
))}
</div>
</MythicTableCell>
</TableRow>
)
@@ -235,19 +231,7 @@ export const ConsumingServicesTableRow = ({service, showDeleted}) => {
(showDeleted || !w.deleted) &&
<TableRow key={w.id} hover>
<MythicTableCell>
{w.deleted ? (
<IconButton onClick={() => {
adjustingDelete(w);
}} color="success" size="medium">
<RestoreFromTrashOutlinedIcon/>
</IconButton>
) : (
<IconButton onClick={() => {
adjustingDelete(w);
}} color="error" size="medium">
<DeleteIcon/>
</IconButton>
)}
{renderDeleteButton(w)}
</MythicTableCell>
<MythicTableCell>
<Typography variant={"h5"}>
@@ -259,42 +243,27 @@ export const ConsumingServicesTableRow = ({service, showDeleted}) => {
<Typography variant={"body2"}>
<b>Description: </b>{w.description}
</Typography>
{w.container_running &&
<Typography variant="body2" component="p"
style={{color: theme.palette.success.main}}>
<b>{"Online"}</b>
</Typography>
}
{!w.container_running &&
<Typography variant="body2" component="p"
style={{color: theme.palette.error.main}}>
<b>{"Offline"}</b>
</Typography>
}
{renderContainerStatus(w)}
</MythicTableCell>
<MythicTableCell>
<MythicStyledTooltip title={w.container_running ? "View Files" : "Unable to view files since container is offline"}>
<IconButton
disabled={!w.container_running}
onClick={()=>{onOpenListFilesDialog(w.name);}}
size="medium">
<AttachFileIcon />
</IconButton>
</MythicStyledTooltip>
{renderFileButton(w)}
</MythicTableCell>
<MythicTableCell>
{logging_events.map(s => (
<MythicStyledTooltip title={"test logging " + s} key={w.id + "logging_" + s}>
<IconButton
disabled={!w.subscriptions.includes(s) || !w.container_running}
onClick={() => {
issueTestLog(s)
}}
size="medium">
<SyncAltIcon/>
</IconButton>
</MythicStyledTooltip>
))}
<div className="mythic-table-row-actions mythic-service-actions">
{logging_events.map(s => (
<MythicStyledTooltip title={"test logging " + s} key={w.id + "logging_" + s}>
<IconButton
className="mythic-table-row-icon-action mythic-table-row-icon-action-hover-info"
disabled={!w.subscriptions.includes(s) || !w.container_running}
onClick={() => {
issueTestLog(s)
}}
size="small">
<SyncAltIcon fontSize="small" />
</IconButton>
</MythicStyledTooltip>
))}
</div>
</MythicTableCell>
</TableRow>
)
@@ -303,19 +272,7 @@ export const ConsumingServicesTableRow = ({service, showDeleted}) => {
(showDeleted || !w.deleted) &&
<TableRow key={w.id} hover>
<MythicTableCell>
{w.deleted ? (
<IconButton onClick={() => {
adjustingDelete(w);
}} color="success" size="medium">
<RestoreFromTrashOutlinedIcon/>
</IconButton>
) : (
<IconButton onClick={() => {
adjustingDelete(w);
}} color="error" size="medium">
<DeleteIcon/>
</IconButton>
)}
{renderDeleteButton(w)}
</MythicTableCell>
<MythicTableCell>
<Typography variant={"h5"}>
@@ -327,28 +284,10 @@ export const ConsumingServicesTableRow = ({service, showDeleted}) => {
<Typography variant={"body2"}>
<b>Description: </b>{w.description}
</Typography>
{w.container_running &&
<Typography variant="body2" component="p"
style={{color: theme.palette.success.main}}>
<b>{"Online"}</b>
</Typography>
}
{!w.container_running &&
<Typography variant="body2" component="p"
style={{color: theme.palette.error.main}}>
<b>{"Offline"}</b>
</Typography>
}
{renderContainerStatus(w)}
</MythicTableCell>
<MythicTableCell>
<MythicStyledTooltip title={w.container_running ? "View Files" : "Unable to view files since container is offline"}>
<IconButton
disabled={!w.container_running}
onClick={()=>{onOpenListFilesDialog(w.name);}}
size="medium">
<AttachFileIcon />
</IconButton>
</MythicStyledTooltip>
{renderFileButton(w)}
</MythicTableCell>
<MythicTableCell>
<Table>
@@ -376,19 +315,7 @@ export const ConsumingServicesTableRow = ({service, showDeleted}) => {
(showDeleted || !w.deleted) &&
<TableRow key={w.id} hover>
<MythicTableCell>
{w.deleted ? (
<IconButton onClick={() => {
adjustingDelete(w);
}} color="success" size="medium">
<RestoreFromTrashOutlinedIcon/>
</IconButton>
) : (
<IconButton onClick={() => {
adjustingDelete(w);
}} color="error" size="medium">
<DeleteIcon/>
</IconButton>
)}
{renderDeleteButton(w)}
</MythicTableCell>
<MythicTableCell>
<Typography variant={"h5"}>
@@ -400,36 +327,22 @@ export const ConsumingServicesTableRow = ({service, showDeleted}) => {
<Typography variant={"body2"}>
<b>Description: </b>{w.description}
</Typography>
{w.container_running &&
<Typography variant="body2" component="p"
style={{color: theme.palette.success.main}}>
<b>{"Online"}</b>
</Typography>
}
{!w.container_running &&
<Typography variant="body2" component="p"
style={{color: theme.palette.error.main}}>
<b>{"Offline"}</b>
</Typography>
}
{renderContainerStatus(w)}
</MythicTableCell>
<MythicTableCell>
<MythicStyledTooltip title={w.container_running ? "View Files" : "Unable to view files since container is offline"}>
<IconButton
disabled={!w.container_running}
onClick={()=>{onOpenListFilesDialog(w.name);}}
size="medium">
<AttachFileIcon />
</IconButton>
</MythicStyledTooltip>
{renderFileButton(w)}
</MythicTableCell>
<MythicTableCell>
{w.subscriptions.map(s => (
<Typography key={s.name + s.type + w.name} style={{display: "block"}}>
<MythicStyledTooltip title={"Fetch Container Metadata"} >
<IconButton onClick={() => getIDPMetadata(w.name, s.name)}
disabled={!w.container_running}>
<PermIdentityTwoToneIcon />
<IconButton
className="mythic-table-row-icon-action mythic-table-row-icon-action-hover-info"
onClick={() => getIDPMetadata(w.name, s.name)}
disabled={!w.container_running}
size="small"
>
<PermIdentityTwoToneIcon fontSize="small" />
</IconButton>
</MythicStyledTooltip>
{s.name}
@@ -470,4 +383,3 @@ export const ConsumingServicesTableRow = ({service, showDeleted}) => {
)
}
@@ -1,7 +1,6 @@
import React from 'react';
import AttachFileIcon from '@mui/icons-material/AttachFile';
import Typography from '@mui/material/Typography';
import {useTheme} from '@mui/material/styles';
import IconButton from '@mui/material/IconButton';
import {useMutation, gql} from '@apollo/client';
import {snackActions} from '../../utilities/Snackbar';
@@ -19,6 +18,7 @@ import Table from '@mui/material/Table';
import TableBody from '@mui/material/TableBody';
import TableContainer from '@mui/material/TableContainer';
import TableHead from '@mui/material/TableHead';
import {MythicStatusChip} from "../../MythicComponents/MythicStatusChip";
const toggleDeleteStatus = gql`
mutation toggleCustomBrowserDeleteStatus($custombrowser_id: Int!, $deleted: Boolean!){
@@ -29,7 +29,6 @@ mutation toggleCustomBrowserDeleteStatus($custombrowser_id: Int!, $deleted: Bool
`;
export function CustomBrowserRow({service, showDeleted}) {
const theme = useTheme();
const [openListFilesDialog, setOpenListFilesDialog] = React.useState(false);
const [openDelete, setOpenDeleteDialog] = React.useState(false);
const [updateDeleted] = useMutation(toggleDeleteStatus, {
@@ -55,9 +54,9 @@ export function CustomBrowserRow({service, showDeleted}) {
<TableRow >
<MythicTableCell>
{service.deleted ? (
<IconButton size="small" onClick={()=>{setOpenDeleteDialog(true);}} color="success" variant="contained"><RestoreFromTrashOutlinedIcon/></IconButton>
<IconButton className="mythic-table-row-icon-action mythic-table-row-icon-action-success" size="small" onClick={()=>{setOpenDeleteDialog(true);}}><RestoreFromTrashOutlinedIcon fontSize="small" /></IconButton>
) : (
<IconButton size="small" onClick={()=>{setOpenDeleteDialog(true);}} color="error" variant="contained"><DeleteIcon/></IconButton>
<IconButton className="mythic-table-row-icon-action mythic-table-row-icon-action-hover-danger" size="small" onClick={()=>{setOpenDeleteDialog(true);}}><DeleteIcon fontSize="small" /></IconButton>
)}
{openDelete &&
<MythicConfirmDialog onClose={() => {setOpenDeleteDialog(false);}} onSubmit={onAcceptDelete}
@@ -71,9 +70,9 @@ export function CustomBrowserRow({service, showDeleted}) {
</MythicTableCell>
<MythicTableCell>
{service.name}
<Typography variant="body2" component="p" color={service.container_running ? theme.palette.success.main : theme.palette.error.main} >
<b>{service.container_running ? "Online" : "Offline"}</b>
</Typography>
<div style={{marginTop: "0.35rem"}}>
<MythicStatusChip label={service.container_running ? "Online" : "Offline"} status={service.container_running ? "success" : "error"} />
</div>
</MythicTableCell>
<MythicTableCell>
{service.type}
@@ -96,10 +95,11 @@ export function CustomBrowserRow({service, showDeleted}) {
<MythicTableCell>
<MythicStyledTooltip title={service.container_running ? "View Files" : "Unable to view files because container is offline"}>
<IconButton
className="mythic-table-row-icon-action mythic-table-row-icon-action-hover-info"
disabled={!service.container_running}
onClick={()=>{setOpenListFilesDialog(true);}}
size="medium">
<AttachFileIcon />
size="small">
<AttachFileIcon fontSize="small" />
</IconButton>
</MythicStyledTooltip>
</MythicTableCell>
@@ -0,0 +1,85 @@
import React from 'react';
import {MythicStatusChip} from "../../MythicComponents/MythicStatusChip";
export const formatParameterValue = (value, emptyValue = "Not set") => {
if (value === undefined || value === null || value === "") {
return emptyValue;
}
if (typeof value === "boolean") {
return value ? "Yes" : "No";
}
if (Array.isArray(value)) {
return value.length > 0 ? value.join(", ") : emptyValue;
}
if (typeof value === "object") {
return Object.keys(value).length > 0 ? JSON.stringify(value, null, 2) : emptyValue;
}
return `${value}`;
};
export function ParameterMetadataItem({label, value, code = false, emptyValue = "Not set"}) {
return (
<div className="mythic-metadata-item">
<span className="mythic-metadata-label">{label}</span>
<span className={code ? "mythic-metadata-code" : "mythic-metadata-value"}>
{formatParameterValue(value, emptyValue)}
</span>
</div>
);
}
export function ParameterCodeBlock({children}) {
return (
<code className="mythic-code-block">
{formatParameterValue(children)}
</code>
);
}
export function BuildParameterList({parameters}) {
if (parameters.length === 0) {
return (
<div className="mythic-parameter-card">
<div className="mythic-parameter-title">No build parameters</div>
<div className="mythic-parameter-description">This service does not define build-time parameters.</div>
</div>
);
}
return (
<div className="mythic-parameter-list">
{parameters.map((param) => (
<div className="mythic-parameter-card" key={"buildprop" + param.id}>
<div className="mythic-parameter-card-header">
<div>
<div className="mythic-parameter-title">{param.name}</div>
<div className="mythic-parameter-description">
{param.description || "No description provided."}
</div>
</div>
<div className="mythic-status-stack">
<MythicStatusChip label={param.parameter_type} status="neutral" showIcon={false} />
{param.required &&
<MythicStatusChip label="Required" status="warning" />
}
{param.randomize &&
<MythicStatusChip label="Randomized" status="info" />
}
</div>
</div>
<div className="mythic-metadata-grid">
<ParameterMetadataItem label="Scripting / Building Name" value={param.name} code />
<ParameterMetadataItem label="Default Value" value={param.default_value} code />
<ParameterMetadataItem label="Required" value={param.required} />
<ParameterMetadataItem label="Verifier Regex" value={param.verifier_regex} code />
{(param.choices || "").length > 0 &&
<ParameterMetadataItem label="Parameter Options" value={param.choices} code />
}
{param.randomize &&
<ParameterMetadataItem label="Format String" value={param.format_string} code />
}
</div>
</div>
))}
</div>
);
}
@@ -1,19 +1,12 @@
import React, {useState} from 'react';
import Button from '@mui/material/Button';
import DialogActions from '@mui/material/DialogActions';
import DialogContent from '@mui/material/DialogContent';
import DialogContentText from '@mui/material/DialogContentText';
import DialogTitle from '@mui/material/DialogTitle';
import {useQuery, gql} from '@apollo/client';
import TableCell from '@mui/material/TableCell';
import TableRow from '@mui/material/TableRow';
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 LinearProgress from '@mui/material/LinearProgress';
import { getDefaultChoices } from '../CreatePayload/Step2SelectPayloadType';
import { getDefaultValueForType } from '../CreatePayload/Step2SelectPayloadType';
import {MythicDialogBody, MythicDialogButton, MythicDialogFooter, MythicDialogSection} from "../../MythicComponents/MythicDialogLayout";
import {MythicErrorState, MythicLoadingState} from "../../MythicComponents/MythicStateDisplay";
import {BuildParameterList} from "./InstalledServiceParameterDetails";
const GET_Payload_Details = gql`
query GetPayloadDetails($payload_name: String!) {
@@ -82,68 +75,48 @@ export function PayloadTypeBuildDialog(props) {
return {...param, choices: choices, default_value: default_value}
});
setBuildParams(buildParams);
console.log(buildParams);
}
});
if (loading) {
return <LinearProgress />;
return (
<>
<DialogTitle id="form-dialog-title">{props.payload_name}'s Build Parameters</DialogTitle>
<DialogContent dividers={true}>
<MythicLoadingState title="Loading build parameters" description="Fetching parameter definitions for this payload type." minHeight={180} />
</DialogContent>
</>
);
}
if (error) {
console.error(error);
return <div>Error! {error.message}</div>;
return (
<>
<DialogTitle id="form-dialog-title">{props.payload_name}'s Build Parameters</DialogTitle>
<DialogContent dividers={true}>
<MythicErrorState title="Unable to load build parameters" description={error.message} minHeight={180} />
</DialogContent>
</>
);
}
return (
<React.Fragment>
<DialogTitle id="form-dialog-title">{props.payload_name}'s Build Parameters</DialogTitle>
<DialogContent dividers={true}>
<DialogContentText>
These are the build parameters associated with this payload
</DialogContentText>
<TableContainer className="mythicElement">
<Table size="small" aria-label="details" style={{"tableLayout": "fixed", "overflowWrap": "break-word"}}>
<TableHead>
<TableRow>
<TableCell style={{width: "20%"}}>Parameter</TableCell>
<TableCell>Value</TableCell>
</TableRow>
</TableHead>
<TableBody>
{
buildParams.map( (param) => (
<TableRow key={"buildprop" + param.id} hover>
<TableCell>{param.description}</TableCell>
<TableCell>
<b>Scripting/Building Name: </b><pre style={{display: "inline-block", whiteSpace: "pre-wrap", margin: 0}}>{param.name}</pre><br/>
<b>Parameter Type: </b><pre style={{display: "inline-block", whiteSpace: "pre-wrap", margin: 0}}>{param.parameter_type}</pre><br/>
<b>Default Value: </b><pre style={{display: "inline-block", whiteSpace: "pre-wrap", margin: 0}}>{param.default_value}</pre><br/>
{param.choices.length > 0 ? (
<React.Fragment>
<b>Parameter Options: </b><pre style={{display: "inline-block", whiteSpace: "pre-wrap", margin: 0}}>{param.choices}</pre><br/>
</React.Fragment>
) : (null)}
<b>Required? </b><pre style={{display: "inline-block", whiteSpace: "pre-wrap", margin: 0}}>{param.required ? "Yes": "No"}</pre><br/>
<b>Verifier Regex: </b><pre style={{display: "inline-block", whiteSpace: "pre-wrap", margin: 0}}>{param.verifier_regex}</pre><br/>
<b>Randomized: </b><pre style={{display: "inline-block", whiteSpace: "pre-wrap", margin: 0}}>{param.randomize ? "Yes": "No"}</pre><br/>
{param.randomize ? (
<React.Fragment>
<b>Format String: </b><pre style={{display: "inline-block", whiteSpace: "pre-wrap", margin: 0}}>{param.format_string}</pre><br/>
</React.Fragment>
) : (null)}
</TableCell>
</TableRow>
))
}
</TableBody>
</Table>
</TableContainer>
<MythicDialogBody compact>
<MythicDialogSection
title="Build Parameters"
description={`${buildParams.length} parameter${buildParams.length === 1 ? "" : "s"} defined for this payload type.`}
>
<BuildParameterList parameters={buildParams} />
</MythicDialogSection>
</MythicDialogBody>
</DialogContent>
<DialogActions>
<Button variant="contained" onClick={props.onClose} color="primary">
<MythicDialogFooter>
<MythicDialogButton onClick={props.onClose}>
Close
</Button>
</DialogActions>
</MythicDialogButton>
</MythicDialogFooter>
</React.Fragment>
);
}
@@ -1,6 +1,5 @@
import React, { useEffect } from 'react';
import Typography from '@mui/material/Typography';
import {useTheme} from '@mui/material/styles';
import { MythicDialog } from '../../MythicComponents/MythicDialog';
import {PayloadTypeBuildDialog} from './PayloadTypeBuildDialog';
import {MythicConfirmDialog} from '../../MythicComponents/MythicConfirmDialog';
@@ -8,17 +7,18 @@ import DeleteIcon from '@mui/icons-material/Delete';
import RestoreFromTrashOutlinedIcon from '@mui/icons-material/RestoreFromTrashOutlined';
import MenuBookIcon from '@mui/icons-material/MenuBook';
import IconButton from '@mui/material/IconButton';
import BuildIcon from '@mui/icons-material/Build';
import TuneIcon from '@mui/icons-material/Tune';
import TableRow from '@mui/material/TableRow';
import MythicTableCell from "../../MythicComponents/MythicTableCell";
import {gql, useMutation} from '@apollo/client';
import { snackActions } from '../../utilities/Snackbar';
import {MythicStyledTooltip} from "../../MythicComponents/MythicStyledTooltip";
import SendTwoToneIcon from '@mui/icons-material/SendTwoTone';
import TerminalIcon from '@mui/icons-material/Terminal';
import {PayloadTypeCommandDialog} from "./PayloadTypeCommandsDialog";
import {MythicAgentSVGIcon} from "../../MythicComponents/MythicAgentSVGIcon";
import {C2ProfileListFilesDialog} from "./C2ProfileListFilesDialog";
import AttachFileIcon from '@mui/icons-material/AttachFile';
import {MythicStatusChip} from "../../MythicComponents/MythicStatusChip";
const toggleDeleteStatus = gql`
mutation togglePayloadTypeDeleteStatus($payloadtype_id: Int!, $deleted: Boolean!){
@@ -29,7 +29,6 @@ mutation togglePayloadTypeDeleteStatus($payloadtype_id: Int!, $deleted: Boolean!
`;
export function PayloadTypeRow({service, showDeleted}){
const theme = useTheme();
const [wrappedPayloads, setWrappedPayloads] = React.useState("");
const [openBuildingDialog, setOpenBuildingDialog] = React.useState(false);
const [supportedOS, setSupportedOS] = React.useState("");
@@ -72,12 +71,12 @@ export function PayloadTypeRow({service, showDeleted}){
<TableRow hover>
<MythicTableCell>
{service.deleted ? (
<IconButton size="small" onClick={()=>{setOpenDeleteDialog(true);}} color="success" >
<RestoreFromTrashOutlinedIcon/>
<IconButton className="mythic-table-row-icon-action mythic-table-row-icon-action-success" size="small" onClick={()=>{setOpenDeleteDialog(true);}} >
<RestoreFromTrashOutlinedIcon fontSize="small" />
</IconButton>
) : (
<IconButton size="small" onClick={()=>{setOpenDeleteDialog(true);}} color="error" >
<DeleteIcon/>
<IconButton className="mythic-table-row-icon-action mythic-table-row-icon-action-hover-danger" size="small" onClick={()=>{setOpenDeleteDialog(true);}} >
<DeleteIcon fontSize="small" />
</IconButton>
)}
</MythicTableCell>
@@ -112,41 +111,45 @@ export function PayloadTypeRow({service, showDeleted}){
</Typography>
</MythicTableCell>
<MythicTableCell>
<Typography variant="body2" component="p" color={service.container_running ? theme.palette.success.main : theme.palette.error.main} >
<b>{service.container_running ? "Online" : "Offline"}</b>
</Typography>
<MythicStatusChip label={service.container_running ? "Online" : "Offline"} status={service.container_running ? "success" : "error"} />
</MythicTableCell>
<MythicTableCell>
<div className="mythic-table-row-actions">
<MythicStyledTooltip title={"Documentation"}>
<IconButton
className="mythic-table-row-icon-action mythic-table-row-icon-action-hover-info"
href={service.wrapper ? "/docs/wrappers/" + service.name : "/docs/agents/" + service.name}
target="_blank"
size="medium">
<MenuBookIcon />
size="small">
<MenuBookIcon fontSize="small" />
</IconButton>
</MythicStyledTooltip>
<MythicStyledTooltip title={"Build Parameters"}>
<IconButton
className="mythic-table-row-icon-action mythic-table-row-icon-action-hover-info"
onClick={()=>{setOpenBuildingDialog(true);}}
size="medium">
<BuildIcon />
size="small">
<TuneIcon fontSize="small" />
</IconButton>
</MythicStyledTooltip>
<MythicStyledTooltip title={"Commands"}>
<IconButton
className="mythic-table-row-icon-action mythic-table-row-icon-action-hover-info"
onClick={()=>{setOpenCommandsDialog(true)}}
size={"medium"}>
<SendTwoToneIcon />
size="small">
<TerminalIcon fontSize="small" />
</IconButton>
</MythicStyledTooltip>
<MythicStyledTooltip title={service.container_running ? "View Files" : "Unable to view files because container is offline"}>
<IconButton
className="mythic-table-row-icon-action mythic-table-row-icon-action-hover-info"
disabled={!service.container_running}
onClick={()=>{setOpenListFilesDialog(true);}}
size="medium">
<AttachFileIcon />
size="small">
<AttachFileIcon fontSize="small" />
</IconButton>
</MythicStyledTooltip>
</div>
</MythicTableCell>
</TableRow>
{openDelete &&
@@ -176,4 +179,4 @@ export function PayloadTypeRow({service, showDeleted}){
</>
);
}
}
@@ -1,8 +1,5 @@
import React, {useState} from 'react';
import Button from '@mui/material/Button';
import DialogActions from '@mui/material/DialogActions';
import DialogContent from '@mui/material/DialogContent';
import DialogContentText from '@mui/material/DialogContentText';
import PlayCircleFilledTwoToneIcon from '@mui/icons-material/PlayCircleFilledTwoTone';
import DialogTitle from '@mui/material/DialogTitle';
import {useQuery, gql} from '@apollo/client';
@@ -12,15 +9,21 @@ 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 LinearProgress from '@mui/material/LinearProgress';
import MenuBookIcon from '@mui/icons-material/MenuBook';
import IconButton from '@mui/material/IconButton';
import Typography from '@mui/material/Typography';
import { useTheme } from '@mui/material/styles';
import {MythicDialog} from "../../MythicComponents/MythicDialog";
import Paper from '@mui/material/Paper';
import MythicStyledTableCell from "../../MythicComponents/MythicTableCell";
import {getDynamicQueryParamsString} from "../Callbacks/TaskParametersDialogRow";
import {MythicDialogBody, MythicDialogButton, MythicDialogFooter, MythicDialogSection} from "../../MythicComponents/MythicDialogLayout";
import {MythicErrorState, MythicLoadingState} from "../../MythicComponents/MythicStateDisplay";
import {MythicStatusChip} from "../../MythicComponents/MythicStatusChip";
import {MythicStyledTooltip} from "../../MythicComponents/MythicStyledTooltip";
import {
formatParameterValue,
ParameterCodeBlock,
ParameterMetadataItem
} from "./InstalledServiceParameterDetails";
const GET_Payload_Details = gql`
query GetPayloadDetails($payload_name: String!) {
@@ -76,34 +79,63 @@ export function PayloadTypeCommandDialog({service, payload_name, onClose}) {
const notDeleted = data.command.filter(c => !c.deleted);
setCommands([...notDeleted, ...deleted]);
}
});
});
if (loading) {
return <LinearProgress />;
return (
<>
<DialogTitle id="form-dialog-title">{payload_name}'s Commands</DialogTitle>
<DialogContent dividers={true}>
<MythicLoadingState title="Loading commands" description="Fetching command metadata for this payload type." minHeight={180} />
</DialogContent>
</>
);
}
if (error) {
console.error(error);
return <div>Error! {error.message}</div>;
return (
<>
<DialogTitle id="form-dialog-title">{payload_name}'s Commands</DialogTitle>
<DialogContent dividers={true}>
<MythicErrorState title="Unable to load commands" description={error.message} minHeight={180} />
</DialogContent>
</>
);
}
const onClickOpenScriptDialog = (e, command) => {
e.preventDefault();
e.stopPropagation();
setOpenScriptDialog({open: true, command_id: command.id, command_name: command.cmd});
}
const activeCommandCount = commands.filter((command) => !command.deleted).length;
const deletedCommandCount = commands.length - activeCommandCount;
const adminCommandCount = commands.filter((command) => !command.deleted && command.needs_admin).length;
return (
<React.Fragment>
<DialogTitle id="form-dialog-title">{payload_name}'s Commands</DialogTitle>
<DialogContent dividers={true} style={{padding: 0}}>
<DialogContent dividers={true}>
<MythicDialogBody compact>
<MythicDialogSection
title="Commands"
description={`${activeCommandCount} active command${activeCommandCount === 1 ? "" : "s"} available for this payload type.`}
>
<div className="mythic-status-stack" style={{marginBottom: "0.65rem"}}>
<MythicStatusChip label={`${activeCommandCount} Active`} status="success" />
{adminCommandCount > 0 &&
<MythicStatusChip label={`${adminCommandCount} Admin`} status="warning" />
}
{deletedCommandCount > 0 &&
<MythicStatusChip label={`${deletedCommandCount} Deleted`} status="deleted" />
}
</div>
<TableContainer className="mythicElement" style={{height: "100%"}}>
<Table size="small" stickyHeader={true} aria-label="details"
style={{"tableLayout": "fixed", "overflowWrap": "break-word", overflowY:"auto", width: "100%", height: "100%"}}>
<TableHead>
<TableRow>
<TableCell style={{width: "20%"}}>Command</TableCell>
<TableCell style={{width: "24%"}}>Command</TableCell>
<TableCell style={{width: "6rem"}}>Version</TableCell>
<TableCell style={{width: "5rem"}}>Docs</TableCell>
<TableCell style={{width: "5rem"}}>Script</TableCell>
<TableCell>Description</TableCell>
<TableCell style={{width: "6.5rem"}}>Actions</TableCell>
</TableRow>
</TableHead>
<TableBody>
@@ -111,34 +143,46 @@ export function PayloadTypeCommandDialog({service, payload_name, onClose}) {
commands.map((param) => (
<TableRow key={"command" + param.id} hover style={{backgroundColor: param.deleted? theme.palette.action.disabledBackground : ''}}>
<TableCell>
<Typography style={{textDecoration: param.deleted ? 'line-through' : ''}}>
<Typography className="mythic-parameter-title" style={{textDecoration: param.deleted ? 'line-through' : ''}}>
{param.cmd}
</Typography>
{param.needs_admin && <Typography style={{fontWeight: 600}} color={"warning"}>
{"Needs Admin"}
</Typography>}
<div className="mythic-status-stack" style={{marginTop: "0.35rem"}}>
{param.needs_admin &&
<MythicStatusChip label="Needs Admin" status="warning" />
}
{param.deleted &&
<MythicStatusChip label="Deleted" status="deleted" />
}
</div>
</TableCell>
<TableCell>{param.version}</TableCell>
<TableCell>
<IconButton
href={service.wrapper ? "/docs/wrappers/" + service.name : "/docs/agents/" + service.name}
target="_blank"
size="large">
<MenuBookIcon/>
</IconButton>
</TableCell>
<TableCell>
<IconButton onClick={(e) => onClickOpenScriptDialog(e, param)} >
<PlayCircleFilledTwoToneIcon />
</IconButton>
</TableCell>
<TableCell>{param.description}</TableCell>
<TableCell>
<div className="mythic-table-row-actions mythic-table-row-actions-nowrap">
<MythicStyledTooltip title="Documentation">
<IconButton
className="mythic-table-row-icon-action mythic-table-row-icon-action-hover-info"
href={service.wrapper ? "/docs/wrappers/" + service.name : "/docs/agents/" + service.name}
target="_blank"
size="small">
<MenuBookIcon fontSize="small" />
</IconButton>
</MythicStyledTooltip>
<MythicStyledTooltip title="Scripting parameters">
<IconButton className="mythic-table-row-icon-action mythic-table-row-icon-action-hover-info" onClick={(e) => onClickOpenScriptDialog(e, param)} size="small">
<PlayCircleFilledTwoToneIcon fontSize="small" />
</IconButton>
</MythicStyledTooltip>
</div>
</TableCell>
</TableRow>
))
}
</TableBody>
</Table>
</TableContainer>
</MythicDialogSection>
</MythicDialogBody>
{openScriptDialog.open &&
<MythicDialog fullWidth={true} maxWidth="lg" open={openScriptDialog.open}
onClose={()=>{setOpenScriptDialog({open: false, command_id: 0});}}
@@ -149,11 +193,11 @@ export function PayloadTypeCommandDialog({service, payload_name, onClose}) {
}
/>}
</DialogContent>
<DialogActions>
<Button variant="contained" onClick={onClose} color="primary">
<MythicDialogFooter>
<MythicDialogButton onClick={onClose}>
Close
</Button>
</DialogActions>
</MythicDialogButton>
</MythicDialogFooter>
</React.Fragment>
);
}
@@ -189,204 +233,194 @@ const exampleCredentialJson = `{
`;
export function ScriptingCommandDialog({command_id, command_name, onClose}){
const [params, setParams] = useState([]);
const theme = useTheme();
useQuery(GET_CommandParameters, {
const {loading, error} = useQuery(GET_CommandParameters, {
variables: {command_id: command_id},
onCompleted: data => {
const grouped = data.commandparameters.reduce( (prev, cur) => {
if(prev[cur.parameter_group_name] === undefined){
prev[cur.parameter_group_name] = [cur];
const groupName = cur.parameter_group_name || "Default";
if(prev[groupName] === undefined){
prev[groupName] = [cur];
return prev;
}
prev[cur.parameter_group_name].push(cur);
prev[cur.parameter_group_name].sort((a, b) => {return a.ui_position < b.ui_position ? -1 : 1});
prev[groupName].push(cur);
return prev;
}, {});
let groupedArray = [];
for(const key in grouped){
groupedArray.push(grouped[key]);
}
const groupedArray = Object.keys(grouped).sort((a, b) => a.localeCompare(b)).map((key) => {
return grouped[key].sort((a, b) => a.ui_position < b.ui_position ? -1 : 1);
});
setParams(groupedArray);
console.log(groupedArray);
}
});
if (loading) {
return (
<>
<DialogTitle id="form-dialog-title">{command_name}'s Parameters</DialogTitle>
<DialogContent dividers={true}>
<MythicLoadingState title="Loading command parameters" description="Fetching scripting metadata for this command." minHeight={180} />
</DialogContent>
</>
);
}
if (error) {
return (
<>
<DialogTitle id="form-dialog-title">{command_name}'s Parameters</DialogTitle>
<DialogContent dividers={true}>
<MythicErrorState title="Unable to load command parameters" description={error.message} minHeight={180} />
</DialogContent>
</>
);
}
return (
<React.Fragment>
<DialogTitle id="form-dialog-title">{command_name}'s Parameters</DialogTitle>
<DialogContentText>
<Typography component={"span"} style={{marginLeft: "10px", display: "block"}}>
Each "Parameter Group" is one way to issue this task. These "Parameter Groups" allow a single task to offer a wider range of tasking options with similar code paths without creating new commands for each option and by cleaning up the total parameter options to make it less confusing.
</Typography>
<Typography component={"span"} style={{marginLeft: "10px", display: "block"}}>
<b>Note:</b> Parameters with "Required" must have a value supplied by you, but everything else has a "Default Value" that will be supplied if you choose not to submit one.
</Typography>
</DialogContentText>
<DialogContent dividers={true} style={{padding: 0}}>
{params.map( (paramGroup) => (
<div key={paramGroup[0].parameter_group_name}>
<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}}>
Parameter Group: {paramGroup[0].parameter_group_name}
</Typography>
</Paper>
<TableContainer className="mythicElement">
<Table style={{tableLayout: "fixed"}}>
<TableHead>
<TableRow>
<TableCell style={{width: "40%"}}>Name</TableCell>
<TableCell>Value</TableCell>
</TableRow>
</TableHead>
<TableBody>
{paramGroup.map( (param) => (
<TableRow key={param.id}>
<MythicStyledTableCell >
<Typography style={{fontWeight: "600", wordBreak: "break-all"}} >
{param.name}
</Typography>
<Typography variant={"body2"} style={{
fontSize: theme.typography.pxToRem(14),
wordBreak: "break-all"
}}>
{param.description}
</Typography>
{param.required && <Typography component="div" color={"warning"}>Required</Typography>}
</MythicStyledTableCell>
<TableCell style={{whiteSpace: "pre-wrap"}}>
<Typography>
<b>Value Type:</b> {param.type}
</Typography>
{param.verifier_regex !== "" &&
<Typography>
Note: Value must match the following regex: {param.verifier_regex}
</Typography>
}
<Typography>
<b>Default Value:</b> {param.default_value}
</Typography>
{param.choices.length > 0 &&
<Typography>
Choose from the following: {param.choices.join(", ")}
</Typography>
}
{param.choices_are_all_commands &&
<Typography>
<b>Note:</b> Provide any command name
</Typography>
}
{param.choices_are_loaded_commands &&
<Typography>
<b>Note:</b> Provide any currently loaded command name
</Typography>
}
{param.dynamic_query_function !== "" &&
<Typography>
<b>Note:</b> This command in the UI will dynamically give you a list of options to choose from either through the modal or via tab complete.
The following mutation can be used to dynamically generate the same choices. In this case, "other_parameters", are just the values of the other parameters you've set for this command in case they offer meaningful context for this dynamic function. They are provided as a standard dictionary (not a string)
<div className={"code-box"}>
<code>
{getDynamicQueryParamsString}
</code>
</div>
</Typography>
}
{param.type === "File" &&
<Typography>
<b>Note:</b> This parameter expects a file's AgentFileID (UUID) value. When issuing this task through the UI, you will be prompted to select a file from your file system to upload.
When scripting, you should upload a new file first and provide the returned AgentFileID here.<br/>
<div className={"code-box"}>
<code >
{exampleUploadFile(command_name, param.name)}
</code>
</div>
</Typography>
}
{param.type === "TypedArray" &&
<Typography>
<b>Note:</b> TypedArrays are arrays of strings that also carry along a "type" identifier with it. This is expressed in scripting and the code as an array of arrays.
In the UI, tab complete or the modal will show you how to fill this out.
When scripting, you can provide this nested array explicitly:
<div className={"code-box"}>
<code>
[ ["{param.choices[0]}", "test"], ["{param.choices[0]}", "values"] ]
</code>
</div>
</Typography>
}
{param.type === "AgentConnect" &&
<Typography>
<b>Note:</b> This parameter type is more complex and is meant to provide a handy way in the UI to allow an operator to select a payload or callback to connect to. In the UI, this dynamically resolves hosts and their payloads/callbacks to list out their P2P C2 profile options so that you can link up without remember things like randomized named pipe values.
You can do the same thing programmatically, but you'll have to fetch the pieces of information yourself and set the value as the resulting dictionary:
<div className={"code-box"}>
<code>
{exampleAgentConnect}
</code>
</div>
</Typography>
}
{param.type === "LinkInfo" &&
<Typography>
<b>Note:</b> This parameter type is more complex and is meant to provide a handy way in the UI to allow an operator to select an existing P2P link for either unlinking or relinking. The UI dynamically resolves existing "callbackgraphedge" values and their info.
You can do the same thing programmatically, but you'll have to fetch the pieces of information yourself and set the value as the resulting dictionary. You'll notice that the data is the exact same as if the type was "AgentConnect", it's just displayed in a different format in the UI.
<div className={"code-box"}>
<code>
{exampleAgentConnect}
</code>
</div>
</Typography>
}
{param.type === "PayloadList" &&
<>
<Typography>
<b>Note:</b> This parameter simply supplies a list of payloads for the user to select. From here, the actual value that's passed along is just the UUID of the payload the user selected.
</Typography>
{param.supported_agents.length > 0 &&
<Typography>
<b>Note:</b> This command has an explicit list of agents it supports; this is typically done in conjunction with the "PayloadList" type to help reduce the options to only valid ones.
<br/>The supported agents are: {param.supported_agents.join(", ")}
</Typography>
}
{Object.keys(param.supported_agent_build_parameters).length > 0 &&
<Typography>
<b>Note:</b> This command has an explicit list of build parameter values that must be set for them to be considered valid.
<br/>The supported agents and build parameter values are: {JSON.stringify(param.supported_agent_build_parameters, null, 2)}
</Typography>
}
</>
}
{param.type === "CredentialJson" &&
<>
<Typography>
<b>Note:</b> This parameter type allows the user to explicitly select a credential that exists in Mythic's credential store and pass along all of that data to the command.
The value supplied here is the actual JSON credential data like follows:
<div className={"code-box"}>
<code>{exampleCredentialJson}</code>
</div>
</Typography>
{param.limit_credentials_by_type.length > 0 &&
<Typography>
<b>Note:</b> This command limits the credential to those with the following types: {param.limit_credentials_by_type.join(", ")}
</Typography>
}
</>
}
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</TableContainer>
</div>
))}
<DialogContent dividers={true}>
<MythicDialogBody compact>
{params.length === 0 ? (
<MythicDialogSection title="No Parameters" description="This command does not define scripting parameters." />
) : (
params.map( (paramGroup) => {
const parameterGroupName = paramGroup[0].parameter_group_name || "Default";
return (
<MythicDialogSection
key={parameterGroupName}
title={`Parameter Group: ${parameterGroupName}`}
description={`${paramGroup.length} parameter${paramGroup.length === 1 ? "" : "s"} in this group.`}
>
<div className="mythic-parameter-list">
{paramGroup.map((param) => (
<ScriptingParameterCard key={param.id} commandName={command_name} param={param} />
))}
</div>
</MythicDialogSection>
);
})
)}
</MythicDialogBody>
</DialogContent>
<DialogActions>
<Button variant="contained" onClick={onClose} color="primary">
<MythicDialogFooter>
<MythicDialogButton onClick={onClose}>
Close
</Button>
</DialogActions>
</MythicDialogButton>
</MythicDialogFooter>
</React.Fragment>
)
}
const ScriptingParameterCard = ({commandName, param}) => {
const choices = param.choices || [];
return (
<div className="mythic-parameter-card">
<div className="mythic-parameter-card-header">
<div>
<div className="mythic-parameter-title">{param.name}</div>
<div className="mythic-parameter-description">
{param.description || "No description provided."}
</div>
</div>
<div className="mythic-status-stack">
<MythicStatusChip label={param.type} status="neutral" showIcon={false} />
{param.required &&
<MythicStatusChip label="Required" status="warning" />
}
</div>
</div>
<div className="mythic-metadata-grid">
<ParameterMetadataItem label="Value Type" value={param.type} />
<ParameterMetadataItem label="Default Value" value={param.default_value} code />
{param.display_name &&
<ParameterMetadataItem label="Display Name" value={param.display_name} />
}
{param.cli_name &&
<ParameterMetadataItem label="CLI Name" value={param.cli_name} code />
}
{param.verifier_regex &&
<ParameterMetadataItem label="Verifier Regex" value={param.verifier_regex} code />
}
{choices.length > 0 &&
<ParameterMetadataItem label="Choices" value={choices.join(", ")} code />
}
</div>
<ScriptingParameterNotes commandName={commandName} param={param} />
</div>
);
};
const ScriptingParameterNotes = ({commandName, param}) => {
const choices = param.choices || [];
const typedArrayChoice = choices.length > 0 ? choices[0] : "type";
const supportedAgents = param.supported_agents || [];
const supportedBuildParameters = param.supported_agent_build_parameters || {};
const credentialTypes = param.limit_credentials_by_type || [];
return (
<div className="mythic-parameter-notes">
{param.choices_are_all_commands &&
<div className="mythic-parameter-note"><strong>Command Choice:</strong> Provide any command name.</div>
}
{param.choices_are_loaded_commands &&
<div className="mythic-parameter-note"><strong>Loaded Command Choice:</strong> Provide any command name currently loaded into the callback.</div>
}
{param.dynamic_query_function &&
<div className="mythic-parameter-note">
<strong>Dynamic Choices:</strong> Use this mutation to generate the same choices outside the UI.
<ParameterCodeBlock>{getDynamicQueryParamsString}</ParameterCodeBlock>
</div>
}
{param.type === "File" &&
<div className="mythic-parameter-note">
<strong>File Value:</strong> Upload the file first, then provide the returned AgentFileID.
<ParameterCodeBlock>{exampleUploadFile(commandName, param.name)}</ParameterCodeBlock>
</div>
}
{param.type === "TypedArray" &&
<div className="mythic-parameter-note">
<strong>Typed Array:</strong> Provide nested arrays with a type identifier and value.
<ParameterCodeBlock>{`[ ["${typedArrayChoice}", "test"], ["${typedArrayChoice}", "values"] ]`}</ParameterCodeBlock>
</div>
}
{param.type === "AgentConnect" &&
<div className="mythic-parameter-note">
<strong>Agent Connect:</strong> Provide the connection target and C2 profile parameters as a dictionary.
<ParameterCodeBlock>{exampleAgentConnect}</ParameterCodeBlock>
</div>
}
{param.type === "LinkInfo" &&
<div className="mythic-parameter-note">
<strong>Link Info:</strong> Provide the existing P2P link information as a dictionary.
<ParameterCodeBlock>{exampleAgentConnect}</ParameterCodeBlock>
</div>
}
{param.type === "PayloadList" &&
<>
<div className="mythic-parameter-note">
<strong>Payload Value:</strong> The submitted value is the selected payload UUID.
</div>
{supportedAgents.length > 0 &&
<div className="mythic-parameter-note">
<strong>Supported Agents:</strong> {supportedAgents.join(", ")}
</div>
}
{Object.keys(supportedBuildParameters).length > 0 &&
<div className="mythic-parameter-note">
<strong>Required Build Parameter Values:</strong>
<ParameterCodeBlock>{JSON.stringify(supportedBuildParameters, null, 2)}</ParameterCodeBlock>
</div>
}
</>
}
{param.type === "CredentialJson" &&
<>
<div className="mythic-parameter-note">
<strong>Credential JSON:</strong> Provide the full credential JSON value.
<ParameterCodeBlock>{exampleCredentialJson}</ParameterCodeBlock>
</div>
{credentialTypes.length > 0 &&
<div className="mythic-parameter-note">
<strong>Allowed Credential Types:</strong> {formatParameterValue(credentialTypes)}
</div>
}
</>
}
</div>
);
};
@@ -13,16 +13,12 @@ import AppBar from '@mui/material/AppBar';
import Tabs from '@mui/material/Tabs';
import {MythicSearchTabLabel, MythicTabPanel} from "../../MythicComponents/MythicTabPanel";
import {ConsumingServicesTableRow} from './ConsumingServicesTable';
import {MythicStyledTooltip} from "../../MythicComponents/MythicStyledTooltip";
import Paper from '@mui/material/Paper';
import Typography from '@mui/material/Typography';
import VisibilityOffIcon from '@mui/icons-material/VisibilityOff';
import VisibilityIcon from '@mui/icons-material/Visibility';
import { IconButton } from '@mui/material';
import {useTheme} from '@mui/material/styles';
import {CustomBrowserRow} from "./CustomBrowserCard";
import {MythicPageBody} from "../../MythicComponents/MythicPageBody";
import {MythicPageHeader} from "../../MythicComponents/MythicPageHeader";
import {MythicPageHeader, MythicPageHeaderChip} from "../../MythicComponents/MythicPageHeader";
import {MythicToolbarToggle} from "../../MythicComponents/MythicTableToolbar";
const SUB_Payload_Types = gql`
subscription getPayloadTypesSubscription {
@@ -148,7 +144,6 @@ const filterDeleted = (c, showDeleted) => {
}
export function PayloadTypesC2Profiles({me}){
const theme = useTheme();
const [value, setValue] = React.useState(0);
const [allData, setAllData] = React.useState([]);
const payloadTypes = useCustomSubscription({
@@ -180,57 +175,105 @@ export function PayloadTypesC2Profiles({me}){
const handleChange = (event, newValue) => {
setValue(newValue);
};
const visibleData = allData.filter(c => filterDeleted(c, showDeleted));
const getContainersForTab = (tabName) => {
switch(tabName){
case "Payload Types":
return visibleData.filter(c => c.__typename === "payloadtype" && (c.agent_type === "agent" || c.agent_type === "wrapper"));
case "C2 Profiles":
return visibleData.filter(c => c.__typename === "c2profile");
case "Translators":
return visibleData.filter(c => c.__typename === "translationcontainer");
case "Command Augmentation":
return visibleData.filter(c => c.__typename === "payloadtype" && (c.agent_type === "command_augment"));
case "3rd Party":
return visibleData.filter(c => c.__typename === "payloadtype" && (c.agent_type === "service"));
case "Webhooks":
return visibleData.filter(c => c.__typename === "consuming_container" && c.type === "webhook");
case "Loggers":
return visibleData.filter(c => c.__typename === "consuming_container" && c.type === "logging");
case "Eventing":
return visibleData.filter(c => c.__typename === "consuming_container" && c.type === "eventing");
case "Auth":
return visibleData.filter(c => c.__typename === "consuming_container" && c.type === "auth");
case "Browsers":
return visibleData.filter(c => c.__typename === "custombrowser");
default:
return [];
}
}
const currentTab = tabTypes[value] || tabTypes[0];
const currentContainers = getContainersForTab(currentTab);
const getTabComponent = () => {
switch(value){
case 0:
return <ContainersTabPayloadTypesPanel key={"Payload Types"} type={"Payload Types"} index={value} value={value} showDeleted={showDeleted}
containers={allData.filter(c => filterDeleted(c, showDeleted)).filter(c => c.__typename === "payloadtype" && (c.agent_type === "agent" || c.agent_type === "wrapper"))} />
containers={getContainersForTab("Payload Types")} />
case 1:
return <ContainersTabPayloadTypesPanel key={"C2 Profiles"} type={"C2 Profiles"} index={value} value={value} showDeleted={showDeleted}
containers={allData.filter(c => filterDeleted(c, showDeleted)).filter(c => c.__typename === "c2profile")} />
containers={getContainersForTab("C2 Profiles")} />
case 2:
return <ContainersTabPayloadTypesPanel key={"Translators"} type={"Translators"} index={value} value={value} showDeleted={showDeleted}
containers={allData.filter(c => filterDeleted(c, showDeleted)).filter(c => c.__typename === "translationcontainer")} />
containers={getContainersForTab("Translators")} />
case 3:
return <ContainersTabPayloadTypesPanel key={"Command Augmentation"} type={"Command Augmentation"} index={value} value={value} showDeleted={showDeleted}
containers={allData.filter(c => filterDeleted(c, showDeleted)).filter(c => c.__typename === "payloadtype" && (c.agent_type === "command_augment"))} />
containers={getContainersForTab("Command Augmentation")} />
case 4:
return <ContainersTabPayloadTypesPanel key={"3rd Party"} type={"3rd Party"} index={value} value={value} showDeleted={showDeleted}
containers={allData.filter(c => filterDeleted(c, showDeleted)).filter(c => c.__typename === "payloadtype" && (c.agent_type === "service"))} />
containers={getContainersForTab("3rd Party")} />
case 5:
return <ContainersTabConsumingServicesPanel key={"Webhooks"} type={"Webhooks"} index={value} value={value} showDeleted={showDeleted}
containers={allData.filter(c => filterDeleted(c, showDeleted)).filter(c => c.__typename === "consuming_container" && c.type === "webhook")} />
containers={getContainersForTab("Webhooks")} />
case 6:
return <ContainersTabConsumingServicesPanel key={"Loggers"} type={"Loggers"} index={value} value={value} showDeleted={showDeleted}
containers={allData.filter(c => filterDeleted(c, showDeleted)).filter(c => c.__typename === "consuming_container" && c.type === "logging")} />
containers={getContainersForTab("Loggers")} />
case 7:
return <ContainersTabConsumingServicesPanel key={"Eventing"} type={"Eventing"} index={value} value={value} showDeleted={showDeleted}
containers={allData.filter(c => filterDeleted(c, showDeleted)).filter(c => c.__typename === "consuming_container" && c.type === "eventing")} />
containers={getContainersForTab("Eventing")} />
case 8:
return <ContainersTabConsumingServicesPanel key={"Auth"} type={"Auth"} index={value} value={value} showDeleted={showDeleted}
containers={allData.filter(c => filterDeleted(c, showDeleted)).filter(c => c.__typename === "consuming_container" && c.type === "auth")} />
containers={getContainersForTab("Auth")} />
case 9:
return <ContainersTabCustomBrowsersPanel key={"Browsers"} type={"Browsers"} index={value} value={value} showDeleted={showDeleted}
containers={allData.filter(c => filterDeleted(c, showDeleted)).filter(c => c.__typename === "custombrowser")} />
containers={getContainersForTab("Browsers")} />
default:
return null;
}
}
return (
<MythicPageBody>
<MythicPageHeader title={"Installed Services"}>
{showDeleted ? (
<MythicStyledTooltip title={"Hide Deleted Services"} tooltipStyle={{float: "right"}}>
<IconButton size="small" style={{float: "right"}} variant="contained" onClick={() => setShowDeleted(!showDeleted)}><VisibilityIcon /></IconButton>
</MythicStyledTooltip>
) : (
<MythicStyledTooltip title={"Show Deleted Services"} tooltipStyle={{float: "right"}}>
<IconButton size="small" style={{float: "right"}} variant="contained" onClick={() => setShowDeleted(!showDeleted)} ><VisibilityOffIcon /></IconButton>
</MythicStyledTooltip>
)}
</MythicPageHeader>
<AppBar position="static" color="default" className={"no-box-shadow"}>
<MythicPageHeader
title={"Installed Services"}
subtitle={"Review installed agents, C2 profiles, translators, integrations, and browser services."}
meta={
<>
<MythicPageHeaderChip label={currentTab} />
<MythicPageHeaderChip label={`${currentContainers.length} shown`} />
<MythicPageHeaderChip label={`${allData.length} total`} />
{showDeleted && <MythicPageHeaderChip label="Deleted visible" />}
</>
}
actions={
<MythicToolbarToggle
checked={showDeleted}
onClick={() => setShowDeleted(!showDeleted)}
label="Deleted"
activeIcon={<VisibilityIcon fontSize="small" />}
inactiveIcon={<VisibilityOffIcon fontSize="small" />}
/>
}
/>
<AppBar
position="static"
color="default"
className={"no-box-shadow"}
sx={(theme) => ({
backgroundColor: theme.surfaces?.muted || theme.palette.background.paper,
border: `1px solid ${theme.table?.borderSoft || theme.borderColor}`,
borderRadius: `${theme.shape.borderRadius}px`,
overflow: "hidden",
})}
>
<Tabs
value={value}
onChange={handleChange}
@@ -245,34 +288,34 @@ export function PayloadTypesC2Profiles({me}){
switch (tab){
case "Payload Types":
return <ContainersTabPayloadTypesLabel key={"payloadtypes"}
containers={allData.filter(c => filterDeleted(c, showDeleted)).filter(c => c.__typename === "payloadtype" && (c.agent_type === "agent" || c.agent_type === "wrapper"))}/>;
containers={getContainersForTab("Payload Types")}/>;
case "C2 Profiles":
return <ContainersTabC2ProfilesLabel key={"c2profiles"}
containers={allData.filter(c => filterDeleted(c, showDeleted)).filter(c => c.__typename === "c2profile")}/>;
containers={getContainersForTab("C2 Profiles")}/>;
case "Translators":
return <ContainersTabTranslationsLabel key={"translators"}
containers={allData.filter(c => filterDeleted(c, showDeleted)).filter(c => c.__typename === "translationcontainer")}/>;
containers={getContainersForTab("Translators")}/>;
case "Command Augmentation":
return <ContainersTabCommandAugmentLabel key={"commandaugmentation"}
containers={allData.filter(c => filterDeleted(c, showDeleted)).filter(c => c.__typename === "payloadtype" && (c.agent_type === "command_augment"))}/>;
containers={getContainersForTab("Command Augmentation")}/>;
case "3rd Party":
return <ContainersTab3rdPartyLabel key={"3rdpartyservice"}
containers={allData.filter(c => filterDeleted(c, showDeleted)).filter(c => c.__typename === "payloadtype" && (c.agent_type === "service"))} />;
containers={getContainersForTab("3rd Party")} />;
case "Webhooks":
return <ContainersTabWebhooksLabel key={"webhooks"}
containers={allData.filter(c => filterDeleted(c, showDeleted)).filter(c => c.__typename === "consuming_container" && c.type === "webhook")}/>;
containers={getContainersForTab("Webhooks")}/>;
case "Loggers":
return <ContainersTabLoggersLabel key={"loggers"}
containers={allData.filter(c => filterDeleted(c, showDeleted)).filter(c => c.__typename === "consuming_container" && c.type === "logging")}/>;
containers={getContainersForTab("Loggers")}/>;
case "Eventing":
return <ContainersTabEventingLabel key={"eventing"}
containers={allData.filter(c => filterDeleted(c, showDeleted)).filter(c => c.__typename === "consuming_container" && c.type === "eventing")}/>;
containers={getContainersForTab("Eventing")}/>;
case "Auth":
return <ContainersTabAuthLabel key={"auth"}
containers={allData.filter(c => filterDeleted(c, showDeleted)).filter(c => c.__typename === "consuming_container" && c.type === "auth")}/>;
containers={getContainersForTab("Auth")}/>;
case "Browsers":
return <ContainersTabBrowsersLabel key={"browsers"}
containers={allData.filter(c => filterDeleted(c, showDeleted)).filter(c => c.__typename === "custombrowser")}/>;
containers={getContainersForTab("Browsers")}/>;
default:
return null;
@@ -3,7 +3,6 @@ import AttachFileIcon from '@mui/icons-material/AttachFile';
import Typography from '@mui/material/Typography';
import {FontAwesomeIcon} from '@fortawesome/react-fontawesome';
import { faLanguage } from '@fortawesome/free-solid-svg-icons';
import {useTheme} from '@mui/material/styles';
import IconButton from '@mui/material/IconButton';
import {useMutation, gql} from '@apollo/client';
import {snackActions} from '../../utilities/Snackbar';
@@ -16,6 +15,7 @@ import MythicTableCell from "../../MythicComponents/MythicTableCell";
import {MythicStyledTooltip} from "../../MythicComponents/MythicStyledTooltip";
import {MythicDialog} from "../../MythicComponents/MythicDialog";
import {C2ProfileListFilesDialog} from "./C2ProfileListFilesDialog";
import {MythicStatusChip} from "../../MythicComponents/MythicStatusChip";
const toggleDeleteStatus = gql`
mutation toggleC2ProfileDeleteStatus($translationcontainer_id: Int!, $deleted: Boolean!){
@@ -26,7 +26,6 @@ mutation toggleC2ProfileDeleteStatus($translationcontainer_id: Int!, $deleted: B
`;
export function TranslationContainerRow({service, showDeleted}) {
const theme = useTheme();
const [openListFilesDialog, setOpenListFilesDialog] = React.useState(false);
const [openDelete, setOpenDeleteDialog] = React.useState(false);
const [updateDeleted] = useMutation(toggleDeleteStatus, {
@@ -52,9 +51,9 @@ export function TranslationContainerRow({service, showDeleted}) {
<TableRow hover>
<MythicTableCell>
{service.deleted ? (
<IconButton size="small" onClick={()=>{setOpenDeleteDialog(true);}} color="success" variant="contained"><RestoreFromTrashOutlinedIcon/></IconButton>
<IconButton className="mythic-table-row-icon-action mythic-table-row-icon-action-success" size="small" onClick={()=>{setOpenDeleteDialog(true);}}><RestoreFromTrashOutlinedIcon fontSize="small" /></IconButton>
) : (
<IconButton size="small" onClick={()=>{setOpenDeleteDialog(true);}} color="error" variant="contained"><DeleteIcon/></IconButton>
<IconButton className="mythic-table-row-icon-action mythic-table-row-icon-action-hover-danger" size="small" onClick={()=>{setOpenDeleteDialog(true);}}><DeleteIcon fontSize="small" /></IconButton>
)}
{openDelete &&
<MythicConfirmDialog onClose={() => {setOpenDeleteDialog(false);}} onSubmit={onAcceptDelete}
@@ -84,27 +83,29 @@ export function TranslationContainerRow({service, showDeleted}) {
</Typography>
</MythicTableCell>
<MythicTableCell>
<Typography variant="body2" component="p" color={service.container_running ? theme.palette.success.main : theme.palette.error.main} >
<b>{service.container_running ? "Online" : "Offline"}</b>
</Typography>
<MythicStatusChip label={service.container_running ? "Online" : "Offline"} status={service.container_running ? "success" : "error"} />
</MythicTableCell>
<MythicTableCell>
<div className="mythic-table-row-actions">
<MythicStyledTooltip title={"Documentation"}>
<IconButton
className="mythic-table-row-icon-action mythic-table-row-icon-action-hover-info"
href={"/docs/c2-profiles/" + service.name.toLowerCase()}
target="_blank"
size="medium">
<MenuBookIcon />
size="small">
<MenuBookIcon fontSize="small" />
</IconButton>
</MythicStyledTooltip>
<MythicStyledTooltip title={service.container_running ? "View Files" : "Unable to view files because container is offline"}>
<IconButton
className="mythic-table-row-icon-action mythic-table-row-icon-action-hover-info"
disabled={!service.container_running}
onClick={()=>{setOpenListFilesDialog(true);}}
size="medium">
<AttachFileIcon />
size="small">
<AttachFileIcon fontSize="small" />
</IconButton>
</MythicStyledTooltip>
</div>
</MythicTableCell>
{openListFilesDialog &&
<MythicDialog fullWidth={true} maxWidth="md" open={openListFilesDialog}
@@ -1,6 +1,4 @@
import React, {useEffect} from 'react';
import Button from '@mui/material/Button';
import DialogActions from '@mui/material/DialogActions';
import React from 'react';
import DialogContent from '@mui/material/DialogContent';
import DialogTitle from '@mui/material/DialogTitle';
import List from '@mui/material/List';
@@ -8,10 +6,14 @@ import ListItem from '@mui/material/ListItem';
import ListItemIcon from '@mui/material/ListItemIcon';
import ListItemText from '@mui/material/ListItemText';
import Checkbox from '@mui/material/Checkbox';
import CardHeader from '@mui/material/CardHeader';
import {gql, useQuery} from '@apollo/client';
import { CardContent } from '@mui/material';
import {classes, StyledButton, StyledDivider} from '../../MythicComponents/MythicTransferList';
import {classes, StyledButton} from '../../MythicComponents/MythicTransferList';
import {
MythicDialogButton,
MythicDialogBody,
MythicDialogFooter,
MythicDialogSection
} from '../../MythicComponents/MythicDialogLayout';
const getCommandsQuery = gql`
query getCommandsQuery($uuid: String!) {
@@ -33,17 +35,15 @@ export function AddRemoveCommandsDialog(props) {
const [checked, setChecked] = React.useState([]);
const [left, setLeft] = React.useState([]);
const [originalLeft, setOriginalLeft] = React.useState([]);
const [originalRight, setOriginalRight] = React.useState([]);
const [right, setRight] = React.useState([]);
const [leftTitle, setLeftTitle] = React.useState("Commands Not Included");
const [rightTitle, setRightTitle] = React.useState("Commands Included");
const leftTitle = "Commands Not Included";
const rightTitle = "Commands Included";
const leftChecked = intersection(checked, left);
const rightChecked = intersection(checked, right);
useQuery(getCommandsQuery, {variables: {uuid: props.uuid},
fetchPolicy: "no-cache",
onCompleted: (data) => {
setOriginalLeft(data.command);
setOriginalRight(data.payloadcommand);
const leftData = data.command.reduce( (prev, cur) => {
if( data.payloadcommand.filter(c => c.command.cmd === cur.cmd).length === 0){
@@ -116,10 +116,9 @@ export function AddRemoveCommandsDialog(props) {
setRight([]);
};
const customList = (title, items) => (
<>
<CardHeader title={title} />
<StyledDivider classes={{root: classes.divider}}/>
<CardContent style={{display: "flex", overflow: "auto", flexGrow: 1, width: "100%"}}>
<div className="mythic-transfer-list">
<div className="mythic-transfer-list-header">{title}</div>
<div className="mythic-transfer-list-body">
<List dense component="div" role="list" style={{padding:0, width: "100%", overflow: "auto"}}>
{items.map((valueObj) => {
const value = valueObj.cmd;
@@ -140,16 +139,16 @@ export function AddRemoveCommandsDialog(props) {
})}
<ListItem />
</List>
</CardContent>
</>
</div>
</div>
);
const setFinalTags = () => {
// things to add are in the `right` now but weren't for `originalRight`
const commandsToAdd = right.filter( (command) => {
return originalRight.filter(orig => orig.command.cmd === command.cmd).length == 0;
return originalRight.filter(orig => orig.command.cmd === command.cmd).length === 0;
});
const commandsToRemove = originalRight.filter( (command) => {
return right.filter(newCommand => newCommand.cmd === command.command.cmd).length == 0;
return right.filter(newCommand => newCommand.cmd === command.command.cmd).length === 0;
})
props.onSubmit({commandsToAdd, commandsToRemove});
props.onClose();
@@ -158,69 +157,72 @@ export function AddRemoveCommandsDialog(props) {
<>
<DialogTitle id="form-dialog-title">Add or Remove Commands for Payload {props.filename}</DialogTitle>
<DialogContent dividers={true} style={{height: "100%", display: "flex", flexDirection: "column", position: "relative", maxHeight: "100%"}}>
This will add or remove commands associated with this payload from Mythic's perspective.
This does NOT add or remove commands within the payload itself.
<div style={{display: "flex", flexDirection: "row", overflowY: "auto", height: "100%"}}>
<div style={{paddingLeft: 0, flexGrow: 1, marginLeft: 0, marginRight: "10px", position: "relative", overflowY: "auto", display: "flex", flexDirection: "column", width: "100%" }}>
{customList(leftTitle, left)}
</div>
<div style={{display: "flex", flexDirection: "column", justifyContent: "center"}}>
<StyledButton
variant="contained"
size="small"
className={classes.button}
onClick={handleAllRight}
disabled={left.length === 0}
aria-label="move all right"
>
&gt;&gt;
</StyledButton>
<StyledButton
variant="contained"
size="small"
className={classes.button}
onClick={handleCheckedRight}
disabled={leftChecked.length === 0}
aria-label="move selected right"
>
&gt;
</StyledButton>
<StyledButton
variant="contained"
size="small"
className={classes.button}
onClick={handleCheckedLeft}
disabled={rightChecked.length === 0}
aria-label="move selected left"
>
&lt;
</StyledButton>
<StyledButton
variant="contained"
size="small"
className={classes.button}
onClick={handleAllLeft}
disabled={right.length === 0}
aria-label="move all left"
>
&lt;&lt;
</StyledButton>
</div>
<div style={{marginLeft: "10px", position: "relative", display: "flex", flexDirection: "column", width: "100%" }}>
{customList(rightTitle, right)}
</div>
</div>
<MythicDialogBody>
<MythicDialogSection
title="Command Availability"
description="Updates Mythic's payload command association without changing commands inside the payload."
>
<div style={{display: "flex", flexDirection: "row", overflowY: "auto", height: "100%"}}>
<div style={{paddingLeft: 0, flexGrow: 1, marginLeft: 0, marginRight: "10px", position: "relative", overflowY: "auto", display: "flex", flexDirection: "column", width: "100%" }}>
{customList(leftTitle, left)}
</div>
<div className="mythic-transfer-controls">
<StyledButton
variant="contained"
size="small"
className={classes.button}
onClick={handleAllRight}
disabled={left.length === 0}
aria-label="move all right"
>
&gt;&gt;
</StyledButton>
<StyledButton
variant="contained"
size="small"
className={classes.button}
onClick={handleCheckedRight}
disabled={leftChecked.length === 0}
aria-label="move selected right"
>
&gt;
</StyledButton>
<StyledButton
variant="contained"
size="small"
className={classes.button}
onClick={handleCheckedLeft}
disabled={rightChecked.length === 0}
aria-label="move selected left"
>
&lt;
</StyledButton>
<StyledButton
variant="contained"
size="small"
className={classes.button}
onClick={handleAllLeft}
disabled={right.length === 0}
aria-label="move all left"
>
&lt;&lt;
</StyledButton>
</div>
<div style={{marginLeft: "10px", position: "relative", display: "flex", flexDirection: "column", width: "100%" }}>
{customList(rightTitle, right)}
</div>
</div>
</MythicDialogSection>
</MythicDialogBody>
</DialogContent>
<DialogActions>
<Button onClick={props.onClose} variant="contained" color="primary">
<MythicDialogFooter>
<MythicDialogButton onClick={props.onClose}>
Close
</Button>
<Button onClick={setFinalTags} variant="contained" color="success">
</MythicDialogButton>
<MythicDialogButton intent="primary" onClick={setFinalTags}>
Submit
</Button>
</DialogActions>
</MythicDialogButton>
</MythicDialogFooter>
</>
);
}
@@ -8,7 +8,6 @@ import TableHead from '@mui/material/TableHead';
import Typography from '@mui/material/Typography';
import {useQuery, gql, useMutation} from '@apollo/client';
import LinearProgress from '@mui/material/LinearProgress';
import Paper from '@mui/material/Paper';
import {useTheme} from '@mui/material/styles';
import DialogActions from '@mui/material/DialogActions';
import DialogContent from '@mui/material/DialogContent';
@@ -37,6 +36,8 @@ import {MythicAgentSVGIcon} from "../../MythicComponents/MythicAgentSVGIcon";
import {ViewPayloadConfigJSON} from "./EditPayloadConfigDialog";
import {meState} from "../../../cache";
import { useReactiveVar } from '@apollo/client';
import {MythicErrorState, MythicLoadingState} from "../../MythicComponents/MythicStateDisplay";
import {MythicSectionHeader} from "../../MythicComponents/MythicPageHeader";
const GET_Payload_Details = gql`
@@ -208,19 +209,22 @@ export function DetailedPayloadComparisonTable(props){
return (
<React.Fragment>
<DialogTitle id="form-dialog-title">
Compare Payload Configurations
<Select
value={view}
onChange={handleViewChange}
style={{ float: "right"}}
>
{viewOptions.map(p => (
<MenuItem
key={p}
value={p}
> {p}</MenuItem>
))}
</Select>
<div className="mythic-dialog-title-row">
<span>Compare Payload Configurations</span>
<Select
className="mythic-dialog-title-select"
size="small"
value={view}
onChange={handleViewChange}
>
{viewOptions.map(p => (
<MenuItem
key={p}
value={p}
> {p}</MenuItem>
))}
</Select>
</div>
</DialogTitle>
<DialogContent dividers={true} style={{padding: 0, margin: 0, height: "100%"}}>
<Split direction="horizontal"
@@ -539,19 +543,15 @@ function DetailedPayloadInnerTable(props){
commandMods.current.commandsToRemove = [];
}
if (loading) {
return <LinearProgress style={{marginTop: "10px"}}/>;
return <MythicLoadingState title="Loading payload details" description="Fetching payload configuration, C2 profiles, commands, and build metadata." minHeight={220} />;
}
if (error) {
console.error(error);
return <div>Error! {error.message}</div>;
return <MythicErrorState title="Unable to load payload details" description={error.message} minHeight={220} />;
}
return (
<React.Fragment>
<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}}>
Payload Information
</Typography>
</Paper>
<MythicSectionHeader title="Payload Information" sx={{mt: 0}} />
<TableContainer className="mythicElement">
<Table size="small" aria-label="details" style={{ "overflowWrap": "break-word"}}>
<TableHead>
@@ -592,7 +592,13 @@ function DetailedPayloadInnerTable(props){
{window.location.origin + "/direct/download/" + data.payload[0].filemetum.agent_file_id}
</Link>
<MythicStyledTooltip title={"Host Payload Through C2"} >
<PublicIcon color={"info"} style={{marginLeft: "20px", cursor: "pointer"}} onClick={()=>{setOpenHostDialog(true);}} />
<IconButton
className="mythic-table-row-icon-action mythic-table-row-icon-action-info"
size="small"
onClick={()=>{setOpenHostDialog(true);}}
>
<PublicIcon fontSize="small" />
</IconButton>
</MythicStyledTooltip>
{openHostDialog &&
<MythicDialog fullWidth={true} maxWidth="md" open={openHostDialog}
@@ -659,11 +665,7 @@ function DetailedPayloadInnerTable(props){
</TableBody>
</Table>
</TableContainer>
<Paper elevation={5} style={{backgroundColor: theme.pageHeader.main, color: theme.pageHeaderText.main,marginBottom: "5px", marginTop: "10px"}} variant={"elevation"}>
<Typography variant="h6" style={{textAlign: "left", display: "inline-block", marginLeft: "20px", color: theme.pageHeaderColor}}>
Build Parameters
</Typography>
</Paper>
<MythicSectionHeader title="Build Parameters" />
<TableContainer className="mythicElement">
<Table size="small" aria-label="details" style={{ "overflowWrap": "break-word"}}>
<TableHead>
@@ -700,11 +702,7 @@ function DetailedPayloadInnerTable(props){
</TableBody>
</Table>
</TableContainer>
<Paper elevation={5} style={{backgroundColor: theme.pageHeader.main, color: theme.pageHeaderText.main,marginBottom: "5px", marginTop: "10px"}} variant={"elevation"}>
<Typography variant="h6" style={{textAlign: "left", display: "inline-block", marginLeft: "20px", color: theme.pageHeaderColor}}>
Build Steps
</Typography>
</Paper>
<MythicSectionHeader title="Build Steps" />
<TableContainer className="mythicElement">
<Table size="small" aria-label="details" style={{ "overflowWrap": "break-word"}}>
<TableHead>
@@ -732,11 +730,7 @@ function DetailedPayloadInnerTable(props){
</TableContainer>
{ c2Profiles.map( (c2) => (
<React.Fragment key={"c2frag" + props.payload_id + c2.c2_profile}>
<Paper elevation={5} style={{backgroundColor: theme.pageHeader.main, color: theme.pageHeaderText.main,marginBottom: "5px", marginTop: "10px"}} variant={"elevation"}>
<Typography variant="h6" style={{textAlign: "left", display: "inline-block", marginLeft: "20px", color: theme.pageHeaderColor}}>
{c2.c2_profile}
</Typography>
</Paper>
<MythicSectionHeader title={c2.c2_profile} />
<TableContainer className="mythicElement">
<Table size="small" aria-label="details" style={{"overflowWrap": "break-word"}}>
<TableHead>
@@ -777,12 +771,12 @@ function DetailedPayloadInnerTable(props){
))}
<React.Fragment>
<Paper elevation={5} style={{backgroundColor: theme.pageHeader.main, color: theme.pageHeaderText.main,marginBottom: "5px", marginTop: "10px"}} variant={"elevation"}>
<Typography variant="h6" style={{textAlign: "left", display: "inline-block", marginLeft: "20px", color: theme.pageHeaderColor}}>
Loaded Commands
</Typography>
<Button style={{float: "right"}} variant="contained" size="small" onClick={()=>{setOpenAddRemoveCommandsDialog(true)}} >Add/Remove Commands</Button>
</Paper>
<MythicSectionHeader
title="Loaded Commands"
actions={
<Button variant="outlined" size="small" onClick={()=>{setOpenAddRemoveCommandsDialog(true)}}>Add/Remove Commands</Button>
}
/>
{commands.length > 0 &&
<TableContainer className="mythicElement">
<Table size="small" aria-label="details" style={{"overflowWrap": "break-word"}}>
@@ -802,10 +796,16 @@ function DetailedPayloadInnerTable(props){
<TableCell>{cmd.mythic}</TableCell>
<TableCell>{cmd.payload}</TableCell>
<TableCell>
<IconButton variant="contained" target="_blank"
href={"/docs/agents/" + data.payload[0].payloadtype.name + "/commands/" + cmd.cmd}>
<MenuBookIcon />
</IconButton>
<MythicStyledTooltip title="Open command documentation">
<IconButton
className="mythic-table-row-icon-action mythic-table-row-icon-action-hover-info"
size="small"
target="_blank"
href={"/docs/agents/" + data.payload[0].payloadtype.name + "/commands/" + cmd.cmd}
>
<MenuBookIcon fontSize="small" />
</IconButton>
</MythicStyledTooltip>
</TableCell>
</TableRow>
))
@@ -861,11 +861,7 @@ function DetailedPayloadInnerTable(props){
}
{data.payload[0].wrapped_payload_id !== null &&
<React.Fragment>
<Paper elevation={5} style={{backgroundColor: theme.pageHeader.main, color: theme.pageHeaderText.main,marginBottom: "5px", marginTop: "10px"}} variant={"elevation"}>
<Typography variant="h5" style={{textAlign: "left", display: "inline-block", marginLeft: "20px", color: theme.pageHeaderColor}}>
Wrapped Payload Information
</Typography>
</Paper>
<MythicSectionHeader title="Wrapped Payload Information" dense={false} />
<DetailedPayloadInnerTable me={props.me} payload_id={data.payload[0].wrapped_payload_id} />
</React.Fragment>
}
@@ -1,21 +1,25 @@
import React, {useState} from 'react';
import Button from '@mui/material/Button';
import DialogActions from '@mui/material/DialogActions';
import DialogContent from '@mui/material/DialogContent';
import DialogTitle from '@mui/material/DialogTitle';
import {useQuery, gql, useMutation} from '@apollo/client';
import { snackActions } from '../../utilities/Snackbar';
import MythicTableCell from "../../MythicComponents/MythicTableCell";
import TableRow from '@mui/material/TableRow';
import Table from '@mui/material/Table';
import TableBody from '@mui/material/TableBody';
import TableContainer from '@mui/material/TableContainer';
import MythicTextField from "../../MythicComponents/MythicTextField";
import MenuItem from '@mui/material/MenuItem';
import FormControl from '@mui/material/FormControl';
import Select from '@mui/material/Select';
import Input from '@mui/material/Input';
import InputLabel from '@mui/material/InputLabel';
import Switch from '@mui/material/Switch';
import Box from '@mui/material/Box';
import Typography from '@mui/material/Typography';
import {
MythicDialogBody,
MythicDialogButton,
MythicDialogFooter,
MythicDialogGrid,
MythicDialogSection,
MythicFormField,
MythicFormSwitchRow
} from "../../MythicComponents/MythicDialogLayout";
const hostFileMutation = gql`
mutation hostFileMutation($c2_id: Int!, $file_uuid: String!, $host_url: String!, $alert_on_download: Boolean, $remove: Boolean) {
@@ -67,7 +71,8 @@ export function HostFileDialog(props) {
setMessage(value);
}
const handleChange = (event) => {
setSelectedC2Profile(event.target.value);
const selected = availableC2Profiles.find((profile) => profile.id === event.target.value);
setSelectedC2Profile(selected || {id: 0});
};
const onChangeAlert = (event) => {
setAlertOnDownload(event.target.checked);
@@ -102,59 +107,66 @@ export function HostFileDialog(props) {
<React.Fragment>
<DialogTitle id="form-dialog-title">Host File via C2 Profile</DialogTitle>
<DialogContent dividers={true}>
<TableContainer className="mythicElement">
<Table size="small" aria-label="details" style={{ "overflowWrap": "break-word"}}>
<TableBody>
<TableRow hover>
<MythicTableCell style={{width: "30%"}}>File</MythicTableCell>
<MythicTableCell style={{wordBreak: "all"}}>{props.file_name}</MythicTableCell>
</TableRow>
<TableRow hover>
<MythicTableCell>C2 Profile</MythicTableCell>
<MythicTableCell>
<FormControl style={{width: "100%"}}>
<Select
labelId="demo-dialog-select-label"
id="demo-dialog-select"
value={selectedC2Profile}
onChange={handleChange}
input={<Input style={{width: "100%"}}/>}
>
{availableC2Profiles.map( (opt) => (
<MenuItem value={opt} key={opt.id}>{opt.name}</MenuItem>
) )}
</Select>
</FormControl>
</MythicTableCell>
</TableRow>
<TableRow hover>
<MythicTableCell >Hosting URL Path (with /)</MythicTableCell>
<MythicTableCell>
<MythicTextField value={message} onEnter={submit} onChange={onChangeHostURL} requiredValue={true} >
</MythicTextField>
</MythicTableCell>
</TableRow>
<TableRow hover>
<MythicTableCell>Send alert when file is downloaded?</MythicTableCell>
<MythicTableCell>
<Switch color={"success"} onChange={onChangeAlert} checked={alertOnDownload}></Switch>
</MythicTableCell>
</TableRow>
</TableBody>
</Table>
</TableContainer>
<MythicDialogBody>
<MythicDialogSection title="File">
<Box className="mythic-dialog-preview" sx={{backgroundColor: "background.paper"}}>
<Typography sx={{wordBreak: "break-all"}}>
{props.file_name}
</Typography>
</Box>
</MythicDialogSection>
<MythicDialogSection title="Hosting">
<MythicDialogGrid>
<MythicFormField label="C2 Profile" description="Select a running egress profile to serve this file." required>
<FormControl fullWidth size="small">
<InputLabel id="host-file-c2-profile-label">C2 Profile</InputLabel>
<Select
labelId="host-file-c2-profile-label"
id="host-file-c2-profile"
value={selectedC2Profile.id || ""}
label="C2 Profile"
onChange={handleChange}
>
{availableC2Profiles.map( (opt) => (
<MenuItem value={opt.id} key={opt.id}>{opt.name}</MenuItem>
) )}
</Select>
</FormControl>
</MythicFormField>
<MythicFormField label="Hosting URL Path" description="Path must start with /" required>
<MythicTextField
value={message}
onEnter={submit}
onChange={onChangeHostURL}
requiredValue={true}
name="Hosting URL Path"
showLabel={false}
marginTop="0px"
marginBottom="0px"
/>
</MythicFormField>
</MythicDialogGrid>
</MythicDialogSection>
<MythicDialogSection title="Download Alerts">
<MythicFormSwitchRow
label="Download Alert"
description="Send an alert when the hosted file is downloaded."
control={<Switch color={"success"} onChange={onChangeAlert} checked={alertOnDownload} />}
/>
</MythicDialogSection>
</MythicDialogBody>
</DialogContent>
<DialogActions>
<Button variant="contained" onClick={props.onClose} color="primary">
<MythicDialogFooter>
<MythicDialogButton onClick={props.onClose}>
Close
</Button>
<Button variant="contained" onClick={stopHosting} color="error">
</MythicDialogButton>
<MythicDialogButton intent="destructive" onClick={stopHosting}>
Stop Hosting
</Button>
<Button variant="contained" onClick={submit} color={"success"}>
</MythicDialogButton>
<MythicDialogButton intent="primary" onClick={submit}>
Submit
</Button>
</DialogActions>
</MythicDialogButton>
</MythicDialogFooter>
</React.Fragment>
);
}
@@ -1,11 +1,15 @@
import React from 'react';
import Button from '@mui/material/Button';
import DialogActions from '@mui/material/DialogActions';
import DialogContent from '@mui/material/DialogContent';
import DialogTitle from '@mui/material/DialogTitle';
import {gql, useMutation} from '@apollo/client';
import { snackActions } from '../../utilities/Snackbar';
import {DragAndDropFileUpload} from "../Callbacks/TaskParametersDialogRow";
import {
MythicDialogBody,
MythicDialogButton,
MythicDialogFooter,
MythicDialogSection
} from '../../MythicComponents/MythicDialogLayout';
const create_payload = gql`
mutation createPayloadMutation($payload: String!) {
@@ -44,17 +48,20 @@ export function ImportPayloadConfigDialog(props) {
<React.Fragment>
<DialogTitle id="form-dialog-title">Import Payload Config to Generate New Payload</DialogTitle>
<DialogContent dividers={true}>
<MythicDialogBody>
<MythicDialogSection title="Payload Config File" description="Upload an exported payload configuration.">
<DragAndDropFileUpload value={fileValue} multiple={false} onChange={onFileChange} />
</MythicDialogSection>
</MythicDialogBody>
</DialogContent>
<DialogActions>
<Button variant="contained" onClick={props.onClose} color="primary">
<MythicDialogFooter>
<MythicDialogButton onClick={props.onClose}>
Close
</Button>
<Button variant="contained" onClick={onCommitSubmit} color="success">
</MythicDialogButton>
<MythicDialogButton intent="primary" onClick={onCommitSubmit}>
Submit
</Button>
</DialogActions>
</MythicDialogButton>
</MythicDialogFooter>
</React.Fragment>
);
}
@@ -3,8 +3,8 @@ 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 LinearProgress from '@mui/material/LinearProgress';
import {ResponseDisplayPlaintext} from "../Callbacks/ResponseDisplayPlaintext";
import {MythicErrorState, MythicLoadingState} from "../../MythicComponents/MythicStateDisplay";
const getDescriptionQuery = gql`
query getDescriptionQuery ($payload_id: Int!) {
@@ -36,11 +36,11 @@ export function PayloadBuildMessageDialog(props) {
setViewError(props.viewError);
}, [props.viewError]);
if (loading) {
return <LinearProgress style={{marginTop: "10px"}} />;
return <MythicLoadingState compact title="Loading build output" description="Fetching payload build messages." minHeight={160} />;
}
if (error) {
console.error(error);
return <div>Error!</div>;
return <MythicErrorState compact title="Unable to load build output" description={error.message} minHeight={160} />;
}
return (
@@ -63,4 +63,3 @@ export function PayloadBuildMessageDialog(props) {
</React.Fragment>
);
}
@@ -3,9 +3,9 @@ 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 LinearProgress from '@mui/material/LinearProgress';
import { snackActions } from '../../utilities/Snackbar';
import {ResponseDisplayPlaintext} from "../Callbacks/ResponseDisplayPlaintext";
import {MythicErrorState, MythicLoadingState} from "../../MythicComponents/MythicStateDisplay";
const checkPayloadConfigurationQuery = gql`
query checkPayloadConfigurationQuery($uuid: String!) {
@@ -33,11 +33,11 @@ export function PayloadConfigCheckDialog(props) {
fetchPolicy: "network-only"
});
if (loading) {
return <LinearProgress style={{marginTop: "10px"}} />;
return <MythicLoadingState compact title="Checking configuration" description="Running payload configuration validation." minHeight={160} />;
}
if (error) {
console.error(error);
return <div>Error!</div>;
return <MythicErrorState compact title="Unable to check configuration" description={error.message} minHeight={160} />;
}
return (
@@ -1,9 +1,9 @@
import React, {useState} from 'react';
import {useQuery, gql, useMutation} from '@apollo/client';
import LinearProgress from '@mui/material/LinearProgress';
import { snackActions } from '../../utilities/Snackbar';
import {b64DecodeUnicode} from '../Callbacks/ResponseDisplay';
import {MythicModifyStringDialog} from "../../MythicComponents/MythicDialog";
import {MythicErrorState, MythicLoadingState} from "../../MythicComponents/MythicStateDisplay";
const updateDescriptionMutation = gql`
mutation updateDescription ($file_id: Int!, $filename: bytea!) {
@@ -43,11 +43,11 @@ export function PayloadFilenameDialog(props) {
}
});
if (loading) {
return <LinearProgress style={{marginTop: "10px"}} />;
return <MythicLoadingState compact title="Loading filename" description="Fetching payload filename details." minHeight={140} />;
}
if (error) {
console.error(error);
return <div>Error!</div>;
return <MythicErrorState compact title="Unable to load filename" description={error.message} minHeight={140} />;
}
const onCommitSubmit = (newDescription) => {
updateDescription({variables: {file_id: fileId, filename: newDescription}});
@@ -65,4 +65,3 @@ export function PayloadFilenameDialog(props) {
</React.Fragment>
);
}
@@ -3,9 +3,9 @@ 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 LinearProgress from '@mui/material/LinearProgress';
import { snackActions } from '../../utilities/Snackbar';
import {ResponseDisplayPlaintext} from "../Callbacks/ResponseDisplayPlaintext";
import {MythicErrorState, MythicLoadingState} from "../../MythicComponents/MythicStateDisplay";
const generateIOCMutation = gql`
query generateIOCQuery($uuid: String!) {
@@ -33,11 +33,11 @@ export function PayloadGetIOCDialog(props) {
fetchPolicy: "network-only"
});
if (loading) {
return <LinearProgress style={{marginTop: "10px"}} />;
return <MythicLoadingState compact title="Generating IOCs" description="Fetching network indicators for this payload." minHeight={160} />;
}
if (error) {
console.error(error);
return <div>Error!</div>;
return <MythicErrorState compact title="Unable to generate IOCs" description={error.message} minHeight={160} />;
}
return (
@@ -60,4 +60,3 @@ export function PayloadGetIOCDialog(props) {
</React.Fragment>
);
}
@@ -3,9 +3,9 @@ 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 LinearProgress from '@mui/material/LinearProgress';
import { snackActions } from '../../utilities/Snackbar';
import {ResponseDisplayPlaintext} from "../Callbacks/ResponseDisplayPlaintext";
import {MythicErrorState, MythicLoadingState} from "../../MythicComponents/MythicStateDisplay";
const generateSampleMessageMutation = gql`
query generateSampleMessageQuery($uuid: String!) {
@@ -33,11 +33,11 @@ export function PayloadGetSampleMessageDialog(props) {
fetchPolicy: "network-only"
});
if (loading) {
return <LinearProgress style={{marginTop: "10px"}} />;
return <MythicLoadingState compact title="Generating sample message" description="Fetching a C2 sample message for this payload." minHeight={160} />;
}
if (error) {
console.error(error);
return <div>Error!</div>;
return <MythicErrorState compact title="Unable to generate sample message" description={error.message} minHeight={160} />;
}
return (
@@ -60,4 +60,3 @@ export function PayloadGetSampleMessageDialog(props) {
</React.Fragment>
);
}
@@ -3,9 +3,9 @@ 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 LinearProgress from '@mui/material/LinearProgress';
import { snackActions } from '../../utilities/Snackbar';
import {ResponseDisplayPlaintext} from "../Callbacks/ResponseDisplayPlaintext";
import {MythicErrorState, MythicLoadingState} from "../../MythicComponents/MythicStateDisplay";
const generateRedirectRulesMutation = gql`
query generateRedirectRulesMutation($uuid: String!) {
@@ -33,11 +33,11 @@ export function PayloadRedirectRulesDialog(props) {
fetchPolicy: "network-only"
});
if (loading) {
return <LinearProgress style={{marginTop: "10px"}} />;
return <MythicLoadingState compact title="Generating redirect rules" description="Fetching C2 redirect rules for this payload." minHeight={160} />;
}
if (error) {
console.error(error);
return <div>Error!</div>;
return <MythicErrorState compact title="Unable to generate redirect rules" description={error.message} minHeight={160} />;
}
return (
@@ -5,23 +5,20 @@ 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 Paper from '@mui/material/Paper';
import Button from '@mui/material/Button';
import Typography from '@mui/material/Typography';
import { PayloadsTableRow } from './PayloadsTableRow';
import {ImportPayloadConfigDialog} from './ImportPayloadConfigDialog';
import { MythicDialog } from '../../MythicComponents/MythicDialog';
import ButtonGroup from '@mui/material/ButtonGroup';
import ArrowDropDownIcon from '@mui/icons-material/ArrowDropDown';
import Grow from '@mui/material/Grow';
import Popper from '@mui/material/Popper';
import MenuItem from '@mui/material/MenuItem';
import MenuList from '@mui/material/MenuList';
import ClickAwayListener from '@mui/material/ClickAwayListener';
import {useNavigate} from 'react-router-dom';
import Pagination from '@mui/material/Pagination';
import { Backdrop, CircularProgress } from '@mui/material';
import {MythicPageHeader} from "../../MythicComponents/MythicPageHeader";
import { Backdrop } from '@mui/material';
import {MythicPageHeader, MythicPageHeaderChip} from "../../MythicComponents/MythicPageHeader";
import {MythicTablePagination} from "../../MythicComponents/MythicTablePagination";
import {MythicToolbarButton, MythicToolbarToggle} from "../../MythicComponents/MythicTableToolbar";
import {MythicLoadingState, MythicTableEmptyState} from "../../MythicComponents/MythicStateDisplay";
import AddCircleIcon from '@mui/icons-material/AddCircle';
import FileUploadIcon from '@mui/icons-material/FileUpload';
import Inventory2Icon from '@mui/icons-material/Inventory2';
import VisibilityOffIcon from '@mui/icons-material/VisibilityOff';
import VisibilityIcon from '@mui/icons-material/Visibility';
export function PayloadsTable({payload, onDeletePayload, onUpdateCallbackAlert, onRestorePayload, me,
@@ -30,8 +27,6 @@ export function PayloadsTable({payload, onDeletePayload, onUpdateCallbackAlert,
const [showDeleted, setShowDeleted] = React.useState(false);
const [showAutogenerated, setShowAutogenerated] = React.useState(false);
const [openPayloadImport, setOpenPayloadImport] = React.useState(false);
const dropdownAnchorRef = React.useRef(null);
const [dropdownOpen, setDropdownOpen] = React.useState(false);
const navigate = useNavigate();
const toggleShowDeleted = () => {
setShowDeleted(!showDeleted);
@@ -41,101 +36,83 @@ export function PayloadsTable({payload, onDeletePayload, onUpdateCallbackAlert,
setShowAutogenerated(!showAutogenerated);
onChangeShowAutogenerated(!showAutogenerated);
}
const dropDownOptions = [
{
name: "Generate New Payload",
click: () => {
navigate("/new/createpayload");
}
},
{
name: "Generate New Wrapper Payload",
click: () => {
navigate("/new/createwrapper");
}
},
{
name: "Import Payload Config",
click: () => {
setOpenPayloadImport(true)
}
},
{
name: showDeleted ? "Hide Deleted Payloads" : "Show Deleted Payloads",
click: toggleShowDeleted
},
{
name: showAutogenerated ? "Hide Autogenerated Payloads" : "Show Autogenerated Payloads",
click: toggleShowAutogenerated
}
]
const handleMenuItemClick = (event, index) => {
dropDownOptions[index].click();
setDropdownOpen(false);
};
const payloadCountLabel = payload.length === 1 ? "1 shown" : `${payload.length} shown`;
const totalCountLabel = pageData.totalCount === 1 ? "1 total" : `${pageData.totalCount} total`;
return (
<>
<MythicPageHeader title={"Payloads"}>
<ButtonGroup variant="text" ref={dropdownAnchorRef} aria-label="split button" style={{float: "right", marginRight: "10px", color: "white"}}>
<Button size="small" style={{color: "white"}} aria-controls={dropdownOpen ? 'split-button-menu' : undefined}
aria-expanded={dropdownOpen ? 'true' : undefined}
aria-haspopup="menu"
onClick={() => setDropdownOpen(!dropdownOpen)}>
Actions <ArrowDropDownIcon />
</Button>
</ButtonGroup>
<Popper open={dropdownOpen} anchorEl={dropdownAnchorRef.current} role={undefined} transition disablePortal style={{zIndex: 10}}>
{({ TransitionProps, placement }) => (
<Grow
{...TransitionProps}
style={{
transformOrigin: placement === 'bottom' ? 'center top' : 'center bottom',
}}
>
<Paper className={"dropdownMenuColored"}>
<ClickAwayListener onClickAway={() => setDropdownOpen(false)}>
<MenuList id="split-button-menu">
{dropDownOptions.map((option, index) => (
<MenuItem
key={option.name}
onClick={(event) => handleMenuItemClick(event, index)}
>
{option.name}
</MenuItem>
))}
</MenuList>
</ClickAwayListener>
</Paper>
</Grow>
)}
</Popper>
{openPayloadImport &&
<MythicDialog fullWidth={true} maxWidth="sm" open={openPayloadImport}
onClose={()=>{setOpenPayloadImport(false);}}
innerDialog={<ImportPayloadConfigDialog onClose={()=>{setOpenPayloadImport(false);}} />}
/>
<MythicPageHeader
title={"Payloads"}
subtitle={"Build, import, and manage payload artifacts for the current operation."}
meta={
<>
<MythicPageHeaderChip label={payloadCountLabel} />
<MythicPageHeaderChip label={totalCountLabel} />
{showDeleted && <MythicPageHeaderChip label="Deleted visible" />}
{showAutogenerated && <MythicPageHeaderChip label="Autogenerated" />}
</>
}
</MythicPageHeader>
<TableContainer className="mythicElement" style={{flexGrow: 1, overflowY: "auto", height: "100%", position: "relative"}}>
actions={
<>
<MythicToolbarButton variant="contained" color="primary" startIcon={<AddCircleIcon />} onClick={() => navigate("/new/createpayload")}>
Payload
</MythicToolbarButton>
<MythicToolbarButton variant="outlined" color="primary" startIcon={<Inventory2Icon />} onClick={() => navigate("/new/createwrapper")}>
Wrapper
</MythicToolbarButton>
<MythicToolbarButton variant="outlined" color="primary" startIcon={<FileUploadIcon />} onClick={() => setOpenPayloadImport(true)}>
Import Config
</MythicToolbarButton>
<MythicToolbarToggle
checked={showDeleted}
onClick={toggleShowDeleted}
label="Deleted"
activeIcon={<VisibilityIcon fontSize="small" />}
inactiveIcon={<VisibilityOffIcon fontSize="small" />}
/>
<MythicToolbarToggle
checked={showAutogenerated}
onClick={toggleShowAutogenerated}
label="Autogenerated"
activeIcon={<VisibilityIcon fontSize="small" />}
inactiveIcon={<VisibilityOffIcon fontSize="small" />}
/>
</>
}
/>
{openPayloadImport &&
<MythicDialog fullWidth={true} maxWidth="sm" open={openPayloadImport}
onClose={()=>{setOpenPayloadImport(false);}}
innerDialog={<ImportPayloadConfigDialog onClose={()=>{setOpenPayloadImport(false);}} />}
/>
}
<div style={{display: "flex", flexDirection: "column", flexGrow: 1, minHeight: 0, position: "relative"}}>
{openBackdrop &&
<Backdrop open={openBackdrop} style={{zIndex: 2000, position: "absolute"}}>
<CircularProgress color="inherit" disableShrink />
<MythicLoadingState compact title="Loading payloads" description="Fetching payload records for this page." sx={{color: "inherit"}} />
</Backdrop>
}
<TableContainer className="mythicElement" style={{flexGrow: 1, overflowY: "auto", height: "100%"}}>
<Table stickyHeader size="small" style={{maxWidth: "100%", tableLayout: "fixed"}}>
<TableHead >
<TableRow>
<TableCell style={{width: "6rem"}}></TableCell>
<TableCell style={{width: "3rem"}}></TableCell>
<TableCell>File</TableCell>
<TableCell>Progress</TableCell>
<TableCell>Description</TableCell>
<TableCell style={{width: "6rem"}}></TableCell>
<TableCell style={{width: "3rem"}}></TableCell>
<TableCell>File</TableCell>
<TableCell style={{width: "18rem"}}>Progress</TableCell>
<TableCell>Description</TableCell>
<TableCell >C2 Status</TableCell>
<TableCell>Tags</TableCell>
</TableRow>
</TableHead>
<TableBody>
{payload.length === 0 && !openBackdrop &&
<MythicTableEmptyState
colSpan={7}
compact
title={showDeleted || showAutogenerated ? "No payloads match this view" : "No payloads yet"}
description={showDeleted || showAutogenerated ? "Try changing the deleted or autogenerated filters." : "Created payloads and imported configs will appear here."}
/>
}
{payload.map( (op) => (
<PayloadsTableRow
me={me}
@@ -151,11 +128,8 @@ export function PayloadsTable({payload, onDeletePayload, onUpdateCallbackAlert,
</TableBody>
</Table>
</TableContainer>
<div style={{background: "transparent", display: "flex", justifyContent: "center", alignItems: "center", paddingTop: "5px", paddingBottom: "10px"}}>
<Pagination count={Math.ceil(pageData.totalCount / pageData.fetchLimit)} variant="outlined" color="primary" boundaryCount={1}
siblingCount={1} onChange={onChangePage} showFirstButton={true} showLastButton={true} style={{padding: "20px"}}/>
<Typography style={{paddingLeft: "10px"}}>Total Results: {pageData.totalCount}</Typography>
</div>
<MythicTablePagination totalCount={pageData.totalCount} fetchLimit={pageData.fetchLimit} onChange={onChangePage} />
</>
)
@@ -47,6 +47,7 @@ import NotificationsOffOutlinedIcon from '@mui/icons-material/NotificationsOffOu
import {EditPayloadConfigDialog} from "./EditPayloadConfigDialog";
import DifferenceIcon from '@mui/icons-material/Difference';
import {Dropdown, DropdownMenuItem, DropdownNestedMenuItem} from "../../MythicComponents/MythicNestedMenus";
import {MythicStatusChip} from "../../MythicComponents/MythicStatusChip";
const rebuildPayloadMutation = gql`
mutation triggerRebuildMutation($uuid: String!) {
@@ -312,8 +313,9 @@ export function PayloadsTableRow(props){
{
name: props.deleted ? "Restore Payload" : "Delete the Payload from Disk",
type: "item",
danger: !props.deleted,
icon: props.deleted ? <RestoreFromTrashIcon color={"success"} style={{marginRight: "10px"}} /> :
<DeleteIcon color={"error"} style={{marginRight: "10px"}}/>,
<DeleteIcon style={{marginRight: "10px"}}/>,
click: ({event}) => {
event.preventDefault();
event.stopPropagation();
@@ -346,8 +348,12 @@ export function PayloadsTableRow(props){
<React.Fragment>
<TableRow key={"payload" + props.uuid} hover>
<MythicStyledTableCell>
<Button size="small" onClick={openMenu} >
Actions <ArrowDropDownIcon />
<Button
className="mythic-table-row-action"
size="small"
onClick={openMenu}
>
Actions <ArrowDropDownIcon fontSize="small" />
</Button>
{openUpdate &&
<ClickAwayListener onClickAway={handleClose} mouseEvent={"onMouseDown"}>
@@ -361,6 +367,7 @@ export function PayloadsTableRow(props){
<DropdownMenuItem
key={option.name}
disabled={option.disabled}
className={option.danger ? "mythic-menu-item-hover-danger" : undefined}
onClick={(event) => handleMenuItemClick(event, option.click)}
>
{option.icon}{option.name}
@@ -374,6 +381,7 @@ export function PayloadsTableRow(props){
<DropdownMenuItem
key={menuOption.name}
disabled={menuOption.disabled}
className={menuOption.danger ? "mythic-menu-item-hover-danger" : undefined}
onClick={(event) => handleMenuItemClick(event, menuOption.click)}
>
{menuOption.icon}{menuOption.name}
@@ -477,18 +485,30 @@ export function PayloadsTableRow(props){
}
</MythicStyledTableCell>
<MythicStyledTableCell style={{wordBreak: "break-all"}}>
{props.auto_generated && props.task &&
<MythicStyledTooltip title={"This payload was auto generated by a task"} >
<IconButton href={"/new/task/" + props?.task?.display_id} target={"_blank"} >
<SmartToyTwoToneIcon />
</IconButton>
</MythicStyledTooltip>
}
{b64DecodeUnicode(props.filemetum.filename_text)}
<div className="mythic-status-stack">
{props.auto_generated && props.task &&
<MythicStyledTooltip title={"This payload was auto generated by a task"} >
<IconButton
className="mythic-table-row-icon-action mythic-table-row-icon-action-info"
href={"/new/task/" + props?.task?.display_id}
size="small"
target={"_blank"}
>
<SmartToyTwoToneIcon fontSize="small" />
</IconButton>
</MythicStyledTooltip>
}
<span>{b64DecodeUnicode(props.filemetum.filename_text)}</span>
{props.deleted &&
<MythicStatusChip label="Deleted" status="deleted" />
}
</div>
</MythicStyledTableCell>
<MythicStyledTableCell>
<div className="mythic-payload-progress-cell">
<PayloadsTableRowBuildStatus {...props} />
<PayloadsTableRowBuildProgress {...props} />
</div>
</MythicStyledTableCell>
<MythicStyledTableCell style={{wordBreak: "break-all"}}>{props.description}</MythicStyledTableCell>
<MythicStyledTableCell>
@@ -6,6 +6,7 @@ import HideSourceIcon from '@mui/icons-material/HideSource';
import HighlightOffIcon from '@mui/icons-material/HighlightOff';
import {MythicStyledTooltip} from '../../MythicComponents/MythicStyledTooltip';
import Button from '@mui/material/Button';
import IconButton from '@mui/material/IconButton';
import DialogActions from '@mui/material/DialogActions';
import DialogTitle from '@mui/material/DialogTitle';
import { MythicDialog } from '../../MythicComponents/MythicDialog';
@@ -16,6 +17,26 @@ import TableBody from '@mui/material/TableBody';
import TableContainer from '@mui/material/TableContainer';
import TableHead from '@mui/material/TableHead';
import DialogContent from '@mui/material/DialogContent';
import {MythicStatusChip} from '../../MythicComponents/MythicStatusChip';
const buildStepToneClasses = {
info: "mythic-table-row-icon-action-info",
success: "mythic-table-row-icon-action-success",
error: "mythic-table-row-icon-action-danger",
};
function BuildStepIconButton({children, muted = false, onClick, tone = "info"}) {
return (
<IconButton
className={`mythic-table-row-icon-action ${buildStepToneClasses[tone] || ""}`}
onClick={onClick}
size="small"
style={muted ? {filter: "grayscale(1)", opacity: 0.3} : undefined}
>
{children}
</IconButton>
);
}
export function PayloadsTableRowBuildProgress(props){
const [buildProgressData, setBuildProgressData] = React.useState({
@@ -89,29 +110,29 @@ export function PayloadsTableRowBuildProgress(props){
}
const getButton = (step) => {
if(step.step_skip){
return <HideSourceIcon style={{cursor: "pointer", filter: "grayscale(1)", opacity: 0.3}} onClick={() => buildStepClick(step)}/>
return <BuildStepIconButton muted onClick={() => buildStepClick(step)}><HideSourceIcon fontSize="small" /></BuildStepIconButton>
}
if(step.end_time === null){
// this will either be the current step or a future step
if(step.start_time === null){
// this we have no info on it, so it's just waiting
return <PanoramaFishEyeIcon style={{cursor: "pointer", filter: "grayscale(1)", opacity: 0.3}} onClick={() => buildStepClick(step)} />
return <BuildStepIconButton muted onClick={() => buildStepClick(step)}><PanoramaFishEyeIcon fontSize="small" /></BuildStepIconButton>
} else {
if(props.build_phase === "building"){
return <TimelapseIcon style={{cursor: "pointer"}} color="info" onClick={() => buildStepClick(step)}/>
return <BuildStepIconButton onClick={() => buildStepClick(step)}><TimelapseIcon fontSize="small" /></BuildStepIconButton>
} else {
return <HideSourceIcon style={{cursor: "pointer", filter: "grayscale(1)", opacity: 0.3}} color={"info"} onClick={() => buildStepClick(step)}/>
return <BuildStepIconButton muted onClick={() => buildStepClick(step)}><HideSourceIcon fontSize="small" /></BuildStepIconButton>
}
}
} else if(step.step_success) {
return <CheckCircleOutlineIcon style={{cursor: "pointer"}} color="success" onClick={() => buildStepClick(step)}/>
return <BuildStepIconButton tone="success" onClick={() => buildStepClick(step)}><CheckCircleOutlineIcon fontSize="small" /></BuildStepIconButton>
} else {
return <HighlightOffIcon style={{cursor: "pointer"}} color="error" onClick={() => buildStepClick(step)}/>
return <BuildStepIconButton tone="error" onClick={() => buildStepClick(step)}><HighlightOffIcon fontSize="small" /></BuildStepIconButton>
}
}
return (
<>
<span style={props.build_phase === "success" ? {
<span className="mythic-table-row-actions" style={props.build_phase === "success" ? {
filter: "grayscale(1)",
opacity: 0.5} : {}}>
{buildProgressData.total_steps > 0 &&
@@ -168,6 +189,19 @@ export function PayloadBuildStepStatusDialog(props) {
return "Error";
}
}
const getStatusVariant = () => {
if(props.step.step_skip){
return "skipped";
} else if (props.step.current_step === props.step.step_number) {
return "building";
} else if(props.step.end_time === null) {
return "info";
} else if(props.step.step_success){
return "success";
} else {
return "error";
}
}
return (
<React.Fragment>
<DialogTitle id="form-dialog-title">Step {props.step.step_number + 1} - {props.step.step_name}</DialogTitle>
@@ -195,7 +229,8 @@ export function PayloadBuildStepStatusDialog(props) {
</TableRow>
<TableRow hover>
<TableCell>Status</TableCell>
<TableCell> {getStatusMessage()}
<TableCell>
<MythicStatusChip label={getStatusMessage()} status={getStatusVariant()} />
</TableCell>
</TableRow>
<TableRow hover>
@@ -291,24 +326,24 @@ export function PayloadsTableRowBuildProcessPerStep(props){
}
const getButton = (step) => {
if(step.step_skip){
return <HideSourceIcon style={{cursor: "pointer"}} color="info" onClick={() => buildStepClick(step)}/>
return <BuildStepIconButton onClick={() => buildStepClick(step)}><HideSourceIcon fontSize="small" /></BuildStepIconButton>
}
if(step.end_time === null){
// this will either be the current step or a future step
if(step.start_time === null){
// this we have no info on it, so it's just waiting
return <PanoramaFishEyeIcon style={{cursor: "pointer"}} onClick={() => buildStepClick(step)} />
return <BuildStepIconButton muted onClick={() => buildStepClick(step)}><PanoramaFishEyeIcon fontSize="small" /></BuildStepIconButton>
} else {
if(props.build_status === "building"){
return <TimelapseIcon style={{cursor: "pointer"}} color="info" onClick={() => buildStepClick(step)}/>
return <BuildStepIconButton onClick={() => buildStepClick(step)}><TimelapseIcon fontSize="small" /></BuildStepIconButton>
} else {
return <HideSourceIcon style={{cursor: "pointer"}} color="info" onClick={() => buildStepClick(step)}/>
return <BuildStepIconButton onClick={() => buildStepClick(step)}><HideSourceIcon fontSize="small" /></BuildStepIconButton>
}
}
} else if(step.step_success) {
return <CheckCircleOutlineIcon style={{cursor: "pointer"}} color="success" onClick={() => buildStepClick(step)}/>
return <BuildStepIconButton tone="success" onClick={() => buildStepClick(step)}><CheckCircleOutlineIcon fontSize="small" /></BuildStepIconButton>
} else {
return <HighlightOffIcon style={{cursor: "pointer"}} color="error" onClick={() => buildStepClick(step)}/>
return <BuildStepIconButton tone="error" onClick={() => buildStepClick(step)}><HighlightOffIcon fontSize="small" /></BuildStepIconButton>
}
}
return (
@@ -1,11 +1,10 @@
import IconButton from '@mui/material/IconButton';
import CircularProgress from '@mui/material/CircularProgress';
import React from 'react';
import GetAppIcon from '@mui/icons-material/GetApp';
import ErrorIcon from '@mui/icons-material/Error';
import {MythicStyledTooltip} from '../../MythicComponents/MythicStyledTooltip';
import { MythicDialog } from '../../MythicComponents/MythicDialog';
import {PayloadBuildMessageDialog} from './PayloadBuildMessageDialog';
import {MythicStatusChip} from '../../MythicComponents/MythicStatusChip';
export function PayloadsTableRowBuildStatus(props){
const [openBuildMessage, setOpenBuildMessageDialog] = React.useState(false);
@@ -19,20 +18,31 @@ export function PayloadsTableRowBuildStatus(props){
<React.Fragment>
{props.build_phase === "success" ?
( <MythicStyledTooltip title="Download payload">
<a href={"/direct/download/" + props.filemetum.agent_file_id} >
<GetAppIcon color="success" style={{marginRight: "10px"}} />
</a>
<MythicStatusChip
component="a"
href={"/direct/download/" + props.filemetum.agent_file_id}
clickable
label="Ready"
status="success"
icon={<GetAppIcon />}
/>
</MythicStyledTooltip>
)
:
(props.build_phase === "building" ?
(<MythicStyledTooltip title="Payload still building">
<CircularProgress size={20} thickness={4} color="info" style={{marginRight: "10px"}}/>
<MythicStatusChip label="Building" status="building" />
</MythicStyledTooltip>) :
(<>
<MythicStyledTooltip title="Failed to build!" tooltipStyle={{}}>
<ErrorIcon color="error" onClick={onErrorClick} style={{cursor: "pointer", marginRight: "10px"}} />
<MythicStatusChip
clickable
label="Failed"
status="error"
icon={<ErrorIcon />}
onClick={onErrorClick}
/>
</MythicStyledTooltip>
{openBuildMessage &&
<MythicDialog fullWidth={true} maxWidth="lg" open={openBuildMessage}
@@ -46,4 +56,3 @@ export function PayloadsTableRowBuildStatus(props){
</React.Fragment>
);
}
@@ -1,44 +1,67 @@
import React from 'react';
import Typography from '@mui/material/Typography';
import CancelIcon from '@mui/icons-material/Cancel';
import CheckCircleOutlineIcon from '@mui/icons-material/CheckCircleOutline';
import {MythicStyledTooltip} from '../../MythicComponents/MythicStyledTooltip';
import PermScanWifiIcon from '@mui/icons-material/PermScanWifi';
import {MythicStatusChip} from '../../MythicComponents/MythicStatusChip';
const getC2Status = (c2) => {
if(c2.c2profile.is_p2p){
return c2.c2profile.container_running ? {
label: c2.c2profile.name,
status: "success",
icon: <CheckCircleOutlineIcon />,
tooltip: "C2 Container online",
} : {
label: c2.c2profile.name,
status: "error",
icon: <CancelIcon />,
tooltip: "C2 Container offline",
};
}
if(c2.c2profile.running){
return {
label: c2.c2profile.name,
status: "success",
icon: <CheckCircleOutlineIcon />,
tooltip: "C2 Internal Server Running",
};
}
if(c2.c2profile.container_running){
return {
label: c2.c2profile.name,
status: "warning",
icon: <PermScanWifiIcon />,
tooltip: "C2 Internal Server Not Running, but Container Online",
};
}
return {
label: c2.c2profile.name,
status: "error",
icon: <CancelIcon />,
tooltip: "C2 Container offline",
};
};
export function PayloadsTableRowC2Status(props){
return (
<React.Fragment>
<div className="mythic-status-stack">
{
props.payloadc2profiles.map( (c2, i) => (
<Typography key={c2.c2profile.name + props.uuid + i} style={{display: "flex"}}>
{c2.c2profile.is_p2p ?
( c2.c2profile.container_running ?
<MythicStyledTooltip title="C2 Container online">
<CheckCircleOutlineIcon color="success"/>
</MythicStyledTooltip>:
<MythicStyledTooltip title="C2 Container offline">
<CancelIcon color="error"/>
</MythicStyledTooltip> )
:
( c2.c2profile.running ?
<MythicStyledTooltip title="C2 Internal Server Running">
<CheckCircleOutlineIcon color="success"/>
</MythicStyledTooltip> :
(c2.c2profile.container_running ? (
<MythicStyledTooltip title="C2 Internal Server Not Running, but Container Online">
<PermScanWifiIcon color="warning"/>
</MythicStyledTooltip>
) : (
<MythicStyledTooltip title="C2 Container offline">
<CancelIcon color="error"/>
</MythicStyledTooltip>
))
)
} - {c2.c2profile.name}
</Typography>
))
props.payloadc2profiles.map( (c2, i) => {
const c2Status = getC2Status(c2);
return (
<MythicStyledTooltip title={c2Status.tooltip} key={c2.c2profile.name + props.uuid + i}>
<MythicStatusChip
label={c2Status.label}
status={c2Status.status}
icon={c2Status.icon}
sx={{maxWidth: "11rem"}}
/>
</MythicStyledTooltip>
);
})
}
</React.Fragment>
</div>
)
}
@@ -1,7 +1,4 @@
import React from 'react';
import Paper from '@mui/material/Paper';
import {useTheme} from '@mui/material/styles';
import Typography from '@mui/material/Typography';
import {ReportingTable} from './ReportingTable';
import {MythicPageBody} from "../../MythicComponents/MythicPageBody";
import {MythicPageHeader} from "../../MythicComponents/MythicPageHeader";
@@ -10,8 +7,10 @@ import {MythicPageHeader} from "../../MythicComponents/MythicPageHeader";
export function Reporting() {
return (
<MythicPageBody>
<MythicPageHeader title={"Mythic Report Generation"}>
</MythicPageHeader>
<MythicPageHeader
title={"Mythic Report Generation"}
subtitle={"Generate operation reports from Mythic data and selected report sections."}
/>
<ReportingTable />
</MythicPageBody>
);
@@ -114,22 +114,22 @@ function ArtifactTableRow(props){
<MythicStyledTableCell>
{props.needs_cleanup && !props.resolved &&
<MythicStyledTooltip title={"Artifact needs to be cleaned up"}>
<IconButton onClick={MarkResolved}>
<CleanHandsTwoToneIcon color={"warning"} />
<IconButton className="mythic-table-row-icon-action mythic-table-row-icon-action-warning" onClick={MarkResolved} size="small">
<CleanHandsTwoToneIcon fontSize="small" />
</IconButton>
</MythicStyledTooltip>
}
{props.needs_cleanup && props.resolved &&
<MythicStyledTooltip title={"Successfully cleaned up artifact"}>
<IconButton onClick={MarkUnresolved} >
<CleanHandsTwoToneIcon color={"success"} />
<IconButton className="mythic-table-row-icon-action mythic-table-row-icon-action-success" onClick={MarkUnresolved} size="small">
<CleanHandsTwoToneIcon fontSize="small" />
</IconButton>
</MythicStyledTooltip>
}
{!props.needs_cleanup &&
<MythicStyledTooltip title={"Mark artifact as needs cleanup"} >
<IconButton onClick={MarkNeedsCleanup}>
<AddAlertTwoToneIcon color={"success"} />
<IconButton className="mythic-table-row-icon-action mythic-table-row-icon-action-success" onClick={MarkNeedsCleanup} size="small">
<AddAlertTwoToneIcon fontSize="small" />
</IconButton>
</MythicStyledTooltip>
}
@@ -174,4 +174,3 @@ function ArtifactTableRow(props){
</React.Fragment>
)
}
@@ -1,5 +1,4 @@
import React from 'react';
import { styled } from '@mui/material/styles';
import Button from '@mui/material/Button';
import Switch from '@mui/material/Switch';
import DialogActions from '@mui/material/DialogActions';
@@ -7,48 +6,18 @@ import DialogContent from '@mui/material/DialogContent';
import DialogTitle from '@mui/material/DialogTitle';
import MythicTextField from '../../MythicComponents/MythicTextField';
import Select from '@mui/material/Select';
import Input from '@mui/material/Input';
import InputLabel from '@mui/material/InputLabel';
import MenuItem from '@mui/material/MenuItem';
import FormControl from '@mui/material/FormControl';
import ListItemText from '@mui/material/ListItemText';
import FormControlLabel from '@mui/material/FormControlLabel';
import { gql, useQuery} from '@apollo/client';
const PREFIX = 'ArtifactTableNewArtifactDialog';
const classes = {
formControl: `${PREFIX}-formControl`,
chips: `${PREFIX}-chips`,
chip: `${PREFIX}-chip`,
noLabel: `${PREFIX}-noLabel`
};
const Root = styled('div')((
{
theme
}
) => ({
[`& .${classes.formControl}`]: {
margin: theme.spacing(1),
width: "50%",
marginRight: "5px"
},
[`& .${classes.chips}`]: {
display: 'flex',
flexWrap: 'wrap',
},
[`& .${classes.chip}`]: {
margin: 2,
},
[`& .${classes.noLabel}`]: {
marginTop: theme.spacing(2),
}
}));
import {
MythicDialogBody,
MythicDialogChoiceDivider,
MythicDialogChoiceRow,
MythicDialogSection
} from '../../MythicComponents/MythicDialogLayout';
const artifactHostSearch = gql`
query artifactHostAndTypeQuery {
@@ -126,81 +95,93 @@ export function ArtifactTableNewArtifactDialog(props) {
setCustomBaseArtifact(value);
}
return (
<Root>
<React.Fragment>
<DialogTitle id="form-dialog-title">Register New Artifact</DialogTitle>
<DialogContent dividers={true}>
<div style={{width: "100%", display: "flex", alignItems: "center"}}>
<FormControl className={classes.formControl}>
<InputLabel id="operator-chip-label">Existing Artifact Type</InputLabel>
<Select
labelId="operator-chip-label"
id="operator-chip"
value={baseArtifact}
disabled={customBaseArtifact !== ""}
onChange={handleBaseArtifactChange}
input={<Input/>}
>
{baseArtifactOptions.map((name) => (
<MenuItem key={name} value={name}>
<ListItemText primary={name}/>
</MenuItem>
))}
</Select>
</FormControl>
{"OR "}
<MythicTextField value={customBaseArtifact} onChange={handleCustomBaseArtifactChange}
name="Custom Artifact Type" display="inline-block"/>
</div>
<div style={{width: "100%", display: "flex", alignItems: "center"}}>
<FormControl className={classes.formControl}>
<InputLabel id="operator-chip-label">Existing Host</InputLabel>
<Select
labelId="operator-chip-label"
id="operator-chip"
value={host}
disabled={customHost !== ""}
onChange={onHostChange}
input={<Input/>}
>
{hostOptions.map((name) => (
<MenuItem key={name} value={name}>
<ListItemText primary={name}/>
</MenuItem>
))}
</Select>
</FormControl>
{"OR "}
<MythicTextField value={customHost} onChange={onCustomHostChange}
name="New Host" display="inline-block"/>
</div>
<MythicTextField multiline value={artifact} onChange={onArtifactChange} name="Artifact"/>
<FormControlLabel label={"Artifact needs to be cleaned up"}
control={
<Switch
checked={needsCleanup}
onChange={onNeedsCleanupChange}
color="info"
inputProps={{'aria-label': 'primary checkbox'}}
name="needs_cleanup"
/>
}
labelPlacement={"start"}
/>
<br/>
{needsCleanup &&
<FormControlLabel label={"Artifact is already cleaned up"}
labelPlacement={"start"}
control={<Switch
checked={resolved}
onChange={onNeedsResolvedChanged}
color="info"
inputProps={{'aria-label': 'primary checkbox'}}
name="resolved"
/>}
<MythicDialogBody>
<MythicDialogSection title="Artifact Type">
<MythicDialogChoiceRow>
<FormControl fullWidth size="small">
<InputLabel id="artifact-type-label">Existing Artifact Type</InputLabel>
<Select
labelId="artifact-type-label"
id="artifact-type"
value={baseArtifact}
label="Existing Artifact Type"
disabled={customBaseArtifact !== ""}
onChange={handleBaseArtifactChange}
>
{baseArtifactOptions.map((name) => (
<MenuItem key={name} value={name}>
<ListItemText primary={name}/>
</MenuItem>
))}
</Select>
</FormControl>
<MythicDialogChoiceDivider />
<MythicTextField value={customBaseArtifact} onChange={handleCustomBaseArtifactChange}
name="Custom Artifact Type"/>
</MythicDialogChoiceRow>
</MythicDialogSection>
<MythicDialogSection title="Host">
<MythicDialogChoiceRow>
<FormControl fullWidth size="small">
<InputLabel id="artifact-host-label">Existing Host</InputLabel>
<Select
labelId="artifact-host-label"
id="artifact-host"
value={host}
label="Existing Host"
disabled={customHost !== ""}
onChange={onHostChange}
>
{hostOptions.map((name) => (
<MenuItem key={name} value={name}>
<ListItemText primary={name}/>
</MenuItem>
))}
</Select>
</FormControl>
<MythicDialogChoiceDivider />
<MythicTextField value={customHost} onChange={onCustomHostChange}
name="New Host"/>
</MythicDialogChoiceRow>
</MythicDialogSection>
<MythicDialogSection title="Artifact">
<MythicTextField multiline value={artifact} onChange={onArtifactChange} name="Artifact"/>
</MythicDialogSection>
<MythicDialogSection title="Cleanup State">
<FormControlLabel
className="mythic-dialog-switch-row"
label={"Artifact needs to be cleaned up"}
control={
<Switch
checked={needsCleanup}
onChange={onNeedsCleanupChange}
color="info"
inputProps={{'aria-label': 'primary checkbox'}}
name="needs_cleanup"
/>
}
labelPlacement={"start"}
/>
}
{needsCleanup &&
<FormControlLabel
className="mythic-dialog-switch-row"
label={"Artifact is already cleaned up"}
labelPlacement={"start"}
control={<Switch
checked={resolved}
onChange={onNeedsResolvedChanged}
color="info"
inputProps={{'aria-label': 'primary checkbox'}}
name="resolved"
/>}
/>
}
</MythicDialogSection>
</MythicDialogBody>
</DialogContent>
<DialogActions>
<Button onClick={props.onClose} variant="contained">
@@ -210,7 +191,6 @@ export function ArtifactTableNewArtifactDialog(props) {
Create
</Button>
</DialogActions>
</Root>
</React.Fragment>
);
}
@@ -10,7 +10,6 @@ import TableRow from '@mui/material/TableRow';
import {MythicConfirmDialog} from '../../MythicComponents/MythicConfirmDialog';
import { useMutation } from '@apollo/client';
import {snackActions} from '../../utilities/Snackbar';
import {useTheme} from '@mui/material/styles';
import VisibilityOffIcon from '@mui/icons-material/VisibilityOff';
import VisibilityIcon from '@mui/icons-material/Visibility';
import {toggleHideCallbackMutations} from '../Callbacks/CallbackMutations';
@@ -20,6 +19,7 @@ import InfoIconOutline from '@mui/icons-material/InfoOutlined';
import MythicStyledTableCell from '../../MythicComponents/MythicTableCell';
import {MythicAgentSVGIcon} from "../../MythicComponents/MythicAgentSVGIcon";
import {CallbacksTableLastCheckinCell} from "../Callbacks/CallbacksTableRow";
import {MythicStatusChip} from "../../MythicComponents/MythicStatusChip";
@@ -46,6 +46,7 @@ export function CallbackSearchTable(props){
<TableHead>
<TableRow>
<TableCell style={{width: "3rem"}}>View</TableCell>
<TableCell style={{width: "11rem"}}>Status</TableCell>
<TableCell >User</TableCell>
<TableCell >Domain</TableCell>
<TableCell >Host</TableCell>
@@ -74,7 +75,6 @@ export function CallbackSearchTable(props){
}
function CallbackSearchTableRow(props){
const theme = useTheme();
const [openDeleteDialog, setOpenDeleteDialog] = React.useState(false);
const [openMetaDialog, setOpenMetaDialog] = React.useState(false);
const [updateDeleted] = useMutation(toggleHideCallbackMutations, {
@@ -97,13 +97,27 @@ function CallbackSearchTableRow(props){
<MythicStyledTableCell>{!props.active ? (
<MythicStyledTooltip title="Restore Callback for Tasking">
<IconButton size="small" onClick={()=>{setOpenDeleteDialog(true);}} style={{color: theme.palette.error.main}} variant="contained"><VisibilityOffIcon/></IconButton>
<IconButton className="mythic-table-row-icon-action mythic-table-row-icon-action-danger" size="small" onClick={()=>{setOpenDeleteDialog(true);}}><VisibilityOffIcon fontSize="small" /></IconButton>
</MythicStyledTooltip>
) : (
<MythicStyledTooltip title="Hide Callback so it can't be used in Tasking">
<IconButton size="small" onClick={()=>{setOpenDeleteDialog(true);}} style={{color: theme.palette.success.main}} variant="contained"><VisibilityIcon/></IconButton>
<IconButton className="mythic-table-row-icon-action mythic-table-row-icon-action-success" size="small" onClick={()=>{setOpenDeleteDialog(true);}}><VisibilityIcon fontSize="small" /></IconButton>
</MythicStyledTooltip>
)} </MythicStyledTableCell>
<MythicStyledTableCell>
<div className="mythic-status-stack">
<MythicStatusChip
label={props.active ? "Active" : "Inactive"}
status={props.active ? "active" : "inactive"}
/>
{props.dead &&
<MythicStatusChip label="Likely dead" status="warning" />
}
{props.locked &&
<MythicStatusChip label="Locked" status="locked" />
}
</div>
</MythicStyledTableCell>
<MythicStyledTableCell>
<Typography variant="body2" style={{wordBreak: "break-all"}}>{props.user}</Typography>
</MythicStyledTableCell>
@@ -134,7 +148,11 @@ function CallbackSearchTableRow(props){
</MythicStyledTooltip>
</MythicStyledTableCell>
<MythicStyledTableCell>
<InfoIconOutline onClick={() => setOpenMetaDialog(true)} style={{color: theme.palette.info.main, cursor: "pointer"}}/>
<MythicStyledTooltip title="View callback details">
<IconButton className="mythic-table-row-icon-action mythic-table-row-icon-action-info" size="small" onClick={() => setOpenMetaDialog(true)}>
<InfoIconOutline fontSize="small" />
</IconButton>
</MythicStyledTooltip>
{openMetaDialog &&
<MythicDialog fullWidth={true} maxWidth="lg" open={openMetaDialog}
onClose={()=>{setOpenMetaDialog(false);}}
@@ -146,4 +164,3 @@ function CallbackSearchTableRow(props){
</React.Fragment>
)
}

Some files were not shown because too many files have changed in this diff Show More