mirror of
https://github.com/its-a-feature/Mythic
synced 2026-06-08 14:55:38 +00:00
major updates of the search, eventing, and payloads pages
This commit is contained in:
@@ -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) {
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -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"
|
||||
>
|
||||
>>
|
||||
</StyledButton>
|
||||
<StyledButton
|
||||
variant="contained"
|
||||
size="small"
|
||||
className={classes.button}
|
||||
onClick={handleCheckedRight}
|
||||
aria-label="move selected right"
|
||||
>
|
||||
>
|
||||
</StyledButton>
|
||||
<StyledButton
|
||||
variant="contained"
|
||||
size="small"
|
||||
className={classes.button}
|
||||
onClick={handleCheckedLeft}
|
||||
aria-label="move selected left"
|
||||
>
|
||||
<
|
||||
</StyledButton>
|
||||
<StyledButton
|
||||
variant="contained"
|
||||
size="small"
|
||||
className={classes.button}
|
||||
onClick={handleAllLeft}
|
||||
disabled={right.length === 0}
|
||||
aria-label="move all left"
|
||||
>
|
||||
<<
|
||||
</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"
|
||||
>
|
||||
>>
|
||||
</StyledButton>
|
||||
<StyledButton
|
||||
variant="contained"
|
||||
size="small"
|
||||
className={classes.button}
|
||||
onClick={handleCheckedRight}
|
||||
aria-label="move selected right"
|
||||
>
|
||||
>
|
||||
</StyledButton>
|
||||
<StyledButton
|
||||
variant="contained"
|
||||
size="small"
|
||||
className={classes.button}
|
||||
onClick={handleCheckedLeft}
|
||||
aria-label="move selected left"
|
||||
>
|
||||
<
|
||||
</StyledButton>
|
||||
<StyledButton
|
||||
variant="contained"
|
||||
size="small"
|
||||
className={classes.button}
|
||||
onClick={handleAllLeft}
|
||||
disabled={right.length === 0}
|
||||
aria-label="move all left"
|
||||
>
|
||||
<<
|
||||
</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;
|
||||
}
|
||||
|
||||
+52
-43
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
+5
-3
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
+1
-1
@@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
+64
-67
@@ -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">
|
||||
|
||||
+32
-25
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
+28
-15
@@ -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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
+59
-17
@@ -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>
|
||||
|
||||
+11
-12
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
+24
-19
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
+11
-12
@@ -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}) {
|
||||
</>
|
||||
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
+9
-9
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
+79
-167
@@ -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>
|
||||
|
||||
+85
@@ -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>
|
||||
);
|
||||
}
|
||||
+31
-58
@@ -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}){
|
||||
</>
|
||||
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
+252
-218
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
+83
-40
@@ -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;
|
||||
|
||||
+12
-11
@@ -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"
|
||||
>
|
||||
>>
|
||||
</StyledButton>
|
||||
<StyledButton
|
||||
variant="contained"
|
||||
size="small"
|
||||
className={classes.button}
|
||||
onClick={handleCheckedRight}
|
||||
disabled={leftChecked.length === 0}
|
||||
aria-label="move selected right"
|
||||
>
|
||||
>
|
||||
</StyledButton>
|
||||
<StyledButton
|
||||
variant="contained"
|
||||
size="small"
|
||||
className={classes.button}
|
||||
onClick={handleCheckedLeft}
|
||||
disabled={rightChecked.length === 0}
|
||||
aria-label="move selected left"
|
||||
>
|
||||
<
|
||||
</StyledButton>
|
||||
<StyledButton
|
||||
variant="contained"
|
||||
size="small"
|
||||
className={classes.button}
|
||||
onClick={handleAllLeft}
|
||||
disabled={right.length === 0}
|
||||
aria-label="move all left"
|
||||
>
|
||||
<<
|
||||
</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"
|
||||
>
|
||||
>>
|
||||
</StyledButton>
|
||||
<StyledButton
|
||||
variant="contained"
|
||||
size="small"
|
||||
className={classes.button}
|
||||
onClick={handleCheckedRight}
|
||||
disabled={leftChecked.length === 0}
|
||||
aria-label="move selected right"
|
||||
>
|
||||
>
|
||||
</StyledButton>
|
||||
<StyledButton
|
||||
variant="contained"
|
||||
size="small"
|
||||
className={classes.button}
|
||||
onClick={handleCheckedLeft}
|
||||
disabled={rightChecked.length === 0}
|
||||
aria-label="move selected left"
|
||||
>
|
||||
<
|
||||
</StyledButton>
|
||||
<StyledButton
|
||||
variant="contained"
|
||||
size="small"
|
||||
className={classes.button}
|
||||
onClick={handleAllLeft}
|
||||
disabled={right.length === 0}
|
||||
aria-label="move all left"
|
||||
>
|
||||
<<
|
||||
</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
Reference in New Issue
Block a user