mirror of
https://github.com/its-a-feature/Mythic
synced 2026-06-08 14:55:38 +00:00
updated browser script table page
This commit is contained in:
@@ -211,16 +211,21 @@ export const MythicPageHeader = ({
|
||||
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 normalizedStatus = status === "active" || status === "enabled" ? "success" :
|
||||
status === "inactive" || status === "disabled" ? "warning" :
|
||||
status === "neutral" ? "neutral" : status;
|
||||
const statusColor = normalizedStatus && normalizedStatus !== "neutral" ? theme.palette[normalizedStatus]?.main : null;
|
||||
const chipColor = statusColor || alpha(headerTextColor, 0.88);
|
||||
const neutralBackground = theme.palette.mode === "dark" ? alpha(theme.palette.common.white, 0.06) : alpha(theme.palette.common.black, 0.035);
|
||||
const neutralBorder = theme.table?.borderSoft || alpha(headerTextColor, 0.2);
|
||||
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),
|
||||
backgroundColor: statusColor ? alpha(statusColor, theme.palette.mode === "dark" ? 0.22 : 0.13) : (normalizedStatus === "neutral" ? neutralBackground : alpha(headerTextColor, 0.08)),
|
||||
borderColor: statusColor ? alpha(statusColor, theme.palette.mode === "dark" ? 0.55 : 0.38) : (normalizedStatus === "neutral" ? neutralBorder : alpha(headerTextColor, 0.2)),
|
||||
color: chipColor,
|
||||
fontSize: "0.72rem",
|
||||
fontWeight: 750,
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
import React from 'react';
|
||||
import Box from '@mui/material/Box';
|
||||
|
||||
export function MythicStateChip({children, className = "", compact = false, label, state = "neutral", ...props}) {
|
||||
const chipLabel = label ?? children;
|
||||
return (
|
||||
<Box
|
||||
component="span"
|
||||
className={`mythic-state-chip mythic-state-chip-${state}${compact ? " mythic-state-chip-compact" : ""}${className ? ` ${className}` : ""}`}
|
||||
{...props}
|
||||
>
|
||||
{chipLabel}
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
@@ -137,7 +137,7 @@ export function BrowserScripts({me}){
|
||||
}, [])
|
||||
return (
|
||||
<MythicPageBody>
|
||||
<Backdrop open={backdropOpen} style={{zIndex: 2, position: "absolute"}} invisible={false}>
|
||||
<Backdrop open={backdropOpen} style={{zIndex: 1300, position: "absolute"}} invisible={false}>
|
||||
<MythicLoadingState compact title="Loading browser scripts" description="Fetching scripts for this operator." sx={{color: "inherit"}} />
|
||||
</Backdrop>
|
||||
<BrowserScriptsTable
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
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';
|
||||
@@ -10,16 +9,52 @@ import AddCircleIcon from '@mui/icons-material/AddCircle';
|
||||
import { MythicDialog } from '../../MythicComponents/MythicDialog';
|
||||
import {EditScriptDialog} from './EditScriptDialog';
|
||||
import {MythicPageHeader, MythicPageHeaderChip} from "../../MythicComponents/MythicPageHeader";
|
||||
import {MythicToolbarButton} from "../../MythicComponents/MythicTableToolbar";
|
||||
import {
|
||||
MythicSearchField,
|
||||
MythicTableToolbar,
|
||||
MythicTableToolbarGroup,
|
||||
MythicToolbarButton,
|
||||
MythicToolbarMenuItem,
|
||||
MythicToolbarSelect
|
||||
} from "../../MythicComponents/MythicTableToolbar";
|
||||
import {MythicTableEmptyState} from "../../MythicComponents/MythicStateDisplay";
|
||||
import MythicStyledTableCell from "../../MythicComponents/MythicTableCell";
|
||||
|
||||
|
||||
export function BrowserScriptsTable(props){
|
||||
const [openNewScriptDialog, setOpenNewScriptDialog] = React.useState(false);
|
||||
const [search, setSearch] = React.useState("");
|
||||
const [statusFilter, setStatusFilter] = React.useState("all");
|
||||
const normalizedSearch = search.trim().toLowerCase();
|
||||
const visibleScripts = React.useMemo(() => {
|
||||
return props.browserscripts.filter((script) => {
|
||||
const matchesStatus = statusFilter === "all" ||
|
||||
(statusFilter === "active" && script.active) ||
|
||||
(statusFilter === "disabled" && !script.active) ||
|
||||
(statusFilter === "modified" && script.user_modified) ||
|
||||
(statusFilter === "default" && !script.user_modified);
|
||||
if(!matchesStatus){
|
||||
return false;
|
||||
}
|
||||
if(normalizedSearch === ""){
|
||||
return true;
|
||||
}
|
||||
return [
|
||||
script.payloadtype?.name,
|
||||
script.command?.cmd,
|
||||
script.author,
|
||||
].some((value) => (value || "").toLowerCase().includes(normalizedSearch));
|
||||
});
|
||||
}, [normalizedSearch, props.browserscripts, statusFilter]);
|
||||
const scriptCountLabel = props.browserscripts.length === 1 ? "1 script" : `${props.browserscripts.length} scripts`;
|
||||
const visibleCountLabel = visibleScripts.length === 1 ? "1 shown" : `${visibleScripts.length} shown`;
|
||||
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;
|
||||
const hasFilter = normalizedSearch !== "" || statusFilter !== "all";
|
||||
const onChangeSearch = (name, value) => {
|
||||
setSearch(value);
|
||||
}
|
||||
return (
|
||||
<>
|
||||
<MythicPageHeader
|
||||
@@ -28,6 +63,7 @@ export function BrowserScriptsTable(props){
|
||||
meta={
|
||||
<>
|
||||
<MythicPageHeaderChip label={scriptCountLabel} />
|
||||
{hasFilter && <MythicPageHeaderChip label={visibleCountLabel} />}
|
||||
<MythicPageHeaderChip label={activeCountLabel} />
|
||||
{modifiedCount > 0 && <MythicPageHeaderChip label={`${modifiedCount} modified`} />}
|
||||
</>
|
||||
@@ -45,28 +81,50 @@ export function BrowserScriptsTable(props){
|
||||
<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"}}>
|
||||
<MythicTableToolbar variant="search">
|
||||
<MythicTableToolbarGroup grow>
|
||||
<MythicSearchField
|
||||
name="Search browser scripts"
|
||||
placeholder="Search payload, command, or author..."
|
||||
value={search}
|
||||
onChange={onChangeSearch}
|
||||
/>
|
||||
</MythicTableToolbarGroup>
|
||||
<MythicTableToolbarGroup>
|
||||
<MythicToolbarSelect
|
||||
value={statusFilter}
|
||||
onChange={(event) => setStatusFilter(event.target.value)}
|
||||
>
|
||||
<MythicToolbarMenuItem value="all">All scripts</MythicToolbarMenuItem>
|
||||
<MythicToolbarMenuItem value="active">Active</MythicToolbarMenuItem>
|
||||
<MythicToolbarMenuItem value="disabled">Disabled</MythicToolbarMenuItem>
|
||||
<MythicToolbarMenuItem value="modified">User modified</MythicToolbarMenuItem>
|
||||
<MythicToolbarMenuItem value="default">Container default</MythicToolbarMenuItem>
|
||||
</MythicToolbarSelect>
|
||||
</MythicTableToolbarGroup>
|
||||
</MythicTableToolbar>
|
||||
<TableContainer className="mythicElement" style={{flexGrow: 1, minHeight: 0, overflow: "auto"}}>
|
||||
<Table className="mythic-browser-scripts-table" stickyHeader={true} size="small">
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
<TableCell style={{width: "3rem"}}>Edit</TableCell>
|
||||
<TableCell style={{width: "5rem"}}>Active</TableCell>
|
||||
<TableCell style={{width: "5rem"}}>Payload</TableCell>
|
||||
<TableCell style={{width: "20rem"}}>Command</TableCell>
|
||||
<TableCell style={{width: "12rem"}}> Author</TableCell>
|
||||
<TableCell style={{textAlign: "left"}}>User Modified?</TableCell>
|
||||
<MythicStyledTableCell style={{width: "24rem"}}>Script</MythicStyledTableCell>
|
||||
<MythicStyledTableCell style={{width: "10rem"}}>Author</MythicStyledTableCell>
|
||||
<MythicStyledTableCell style={{width: "9.5rem"}}>Active</MythicStyledTableCell>
|
||||
<MythicStyledTableCell style={{width: "12rem"}}>Source</MythicStyledTableCell>
|
||||
<MythicStyledTableCell style={{width: "4rem", textAlign: "center"}}>Actions</MythicStyledTableCell>
|
||||
<MythicStyledTableCell className="mythic-browser-script-spacer-cell" />
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
{props.browserscripts.length === 0 &&
|
||||
{visibleScripts.length === 0 &&
|
||||
<MythicTableEmptyState
|
||||
colSpan={6}
|
||||
compact
|
||||
title="No browser scripts"
|
||||
description="Create or import browser scripts to customize task output rendering."
|
||||
title={props.browserscripts.length === 0 ? "No browser scripts" : "No browser scripts match"}
|
||||
description={props.browserscripts.length === 0 ? "Create or import browser scripts to customize task output rendering." : "Try changing the search text or status filter."}
|
||||
/>
|
||||
}
|
||||
{props.browserscripts.map( (op) => (
|
||||
{visibleScripts.map( (op) => (
|
||||
<BrowserScriptsTableRow
|
||||
me={props.me}
|
||||
operation_id={props.operation_id} onToggleActive={props.onToggleActive}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import React from 'react';
|
||||
import { Switch} from '@mui/material';
|
||||
import TableCell from '@mui/material/TableCell';
|
||||
import Box from '@mui/material/Box';
|
||||
import { Switch } from '@mui/material';
|
||||
import TableRow from '@mui/material/TableRow';
|
||||
import { MythicDialog } from '../../MythicComponents/MythicDialog';
|
||||
import {EditScriptDialog} from './EditScriptDialog';
|
||||
@@ -8,6 +8,8 @@ import EditIcon from '@mui/icons-material/Edit';
|
||||
import IconButton from '@mui/material/IconButton';
|
||||
import {MythicStyledTooltip} from "../../MythicComponents/MythicStyledTooltip";
|
||||
import {MythicAgentSVGIcon} from "../../MythicComponents/MythicAgentSVGIcon";
|
||||
import MythicStyledTableCell from "../../MythicComponents/MythicTableCell";
|
||||
import {MythicStateChip} from "../../MythicComponents/MythicStateChip";
|
||||
|
||||
export function BrowserScriptsTableRow(props){
|
||||
const [openEdit, setOpenEdit] = React.useState(false);
|
||||
@@ -22,29 +24,45 @@ export function BrowserScriptsTableRow(props){
|
||||
}
|
||||
return (
|
||||
<React.Fragment>
|
||||
<TableRow key={"payload" + props.id} hover>
|
||||
<TableCell >
|
||||
<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
|
||||
checked={props.active}
|
||||
onChange={onToggleActive}
|
||||
color="info"
|
||||
inputProps={{ 'aria-label': 'checkbox', "track": "white" }}
|
||||
name="Active"
|
||||
/>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<MythicStyledTooltip title={props.payloadtype.name}>
|
||||
<MythicAgentSVGIcon payload_type={props.payloadtype.name} style={{width: "35px", height: "35px"}} />
|
||||
</MythicStyledTooltip>
|
||||
</TableCell>
|
||||
<TableCell>{props.command.cmd}</TableCell>
|
||||
<TableCell>{props.author}</TableCell>
|
||||
<TableCell>{props.user_modified ? "User Modified" : "" } </TableCell>
|
||||
<TableRow className={props.active ? "" : "mythic-browser-script-row-disabled"} key={"payload" + props.id} hover>
|
||||
<MythicStyledTableCell>
|
||||
<Box className="mythic-browser-script-script-cell">
|
||||
<MythicStyledTooltip title={props.payloadtype.name}>
|
||||
<Box className="mythic-browser-script-payload-icon">
|
||||
<MythicAgentSVGIcon payload_type={props.payloadtype.name} style={{width: "32px", height: "32px"}} />
|
||||
</Box>
|
||||
</MythicStyledTooltip>
|
||||
<Box className="mythic-browser-script-script-copy">
|
||||
<Box className="mythic-browser-script-command-name">{props.command.cmd}</Box>
|
||||
<Box className="mythic-browser-script-payload-name">{props.payloadtype.name}</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
</MythicStyledTableCell>
|
||||
<MythicStyledTableCell>{props.author}</MythicStyledTableCell>
|
||||
<MythicStyledTableCell>
|
||||
<Box className="mythic-browser-script-active-cell">
|
||||
<Switch
|
||||
checked={props.active}
|
||||
onChange={onToggleActive}
|
||||
color="success"
|
||||
inputProps={{ 'aria-label': 'Toggle browser script active state', "track": "white" }}
|
||||
name="Active"
|
||||
size="small"
|
||||
/>
|
||||
<MythicStateChip label={props.active ? "Active" : "Disabled"} state={props.active ? "active" : "disabled"} />
|
||||
</Box>
|
||||
</MythicStyledTableCell>
|
||||
<MythicStyledTableCell>
|
||||
<MythicStateChip label={props.user_modified ? "User modified" : "Container default"} state={props.user_modified ? "warning" : "neutral"} />
|
||||
</MythicStyledTableCell>
|
||||
<MythicStyledTableCell style={{textAlign: "center"}}>
|
||||
<Box className="mythic-table-row-actions mythic-table-row-actions-nowrap mythic-browser-script-actions">
|
||||
<IconButton className="mythic-table-row-icon-action mythic-table-row-icon-action-hover-info" size="small" onClick={()=>{setOpenEdit(true);}}>
|
||||
<EditIcon fontSize="small" />
|
||||
</IconButton>
|
||||
</Box>
|
||||
</MythicStyledTableCell>
|
||||
<MythicStyledTableCell className="mythic-browser-script-spacer-cell" />
|
||||
|
||||
{openEdit &&
|
||||
<MythicDialog fullWidth={true} maxWidth="xl" open={openEdit}
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
import React, {useEffect, useRef} from 'react';
|
||||
import Button from '@mui/material/Button';
|
||||
import DialogActions from '@mui/material/DialogActions';
|
||||
import Box from '@mui/material/Box';
|
||||
import Chip from '@mui/material/Chip';
|
||||
import Collapse from '@mui/material/Collapse';
|
||||
import DialogContent from '@mui/material/DialogContent';
|
||||
import DialogTitle from '@mui/material/DialogTitle';
|
||||
import IconButton from '@mui/material/IconButton';
|
||||
import AceEditor from 'react-ace';
|
||||
import "ace-builds/src-noconflict/mode-javascript";
|
||||
import "ace-builds/src-noconflict/theme-github";
|
||||
@@ -14,11 +16,21 @@ import MenuItem from '@mui/material/MenuItem';
|
||||
import FormControl from '@mui/material/FormControl';
|
||||
import Select from '@mui/material/Select';
|
||||
import InputLabel from '@mui/material/InputLabel';
|
||||
import Input from '@mui/material/Input';
|
||||
import Tooltip from '@mui/material/Tooltip';
|
||||
import KeyboardArrowDownIcon from '@mui/icons-material/KeyboardArrowDown';
|
||||
import KeyboardArrowUpIcon from '@mui/icons-material/KeyboardArrowUp';
|
||||
import Split from 'react-split';
|
||||
import {TaskDisplay} from "../Callbacks/TaskDisplay";
|
||||
import {taskingDataFragment} from '../Callbacks/CallbackMutations'
|
||||
|
||||
import {
|
||||
MythicDialogBody,
|
||||
MythicDialogButton,
|
||||
MythicDialogFooter,
|
||||
MythicFormField,
|
||||
MythicFormGrid,
|
||||
MythicFormNote
|
||||
} from "../../MythicComponents/MythicDialogLayout";
|
||||
import {MythicEmptyState, MythicLoadingState} from "../../MythicComponents/MythicStateDisplay";
|
||||
|
||||
|
||||
const getCommandsAndPayloadTypesQuery = gql`
|
||||
@@ -42,6 +54,38 @@ query getAvailableTasks($command_id: Int!){
|
||||
}
|
||||
`;
|
||||
|
||||
const BROWSER_SCRIPT_WORKBENCH_SPLIT_KEY = "browserScriptWorkbenchSplitSizes";
|
||||
const BROWSER_SCRIPT_TOP_SPLIT_KEY = "browserScriptTopSplitSizes";
|
||||
const defaultWorkbenchSplitSizes = [48, 52];
|
||||
const defaultTopSplitSizes = [68, 32];
|
||||
|
||||
const getStoredSplitSizes = (storageKey, fallback) => {
|
||||
try {
|
||||
if(typeof window === "undefined"){
|
||||
return fallback;
|
||||
}
|
||||
const storedSizes = window.localStorage.getItem(storageKey);
|
||||
if(!storedSizes){
|
||||
return fallback;
|
||||
}
|
||||
const parsedSizes = JSON.parse(storedSizes);
|
||||
if(!Array.isArray(parsedSizes) || parsedSizes.length !== fallback.length || !parsedSizes.every(Number.isFinite)){
|
||||
return fallback;
|
||||
}
|
||||
return parsedSizes;
|
||||
} catch (error) {
|
||||
return fallback;
|
||||
}
|
||||
}
|
||||
|
||||
const storeSplitSizes = (storageKey, sizes) => {
|
||||
try {
|
||||
window.localStorage.setItem(storageKey, JSON.stringify(sizes));
|
||||
} catch (error) {
|
||||
// Best effort only; layout still works without persisted sizes.
|
||||
}
|
||||
}
|
||||
|
||||
export function EditScriptDialog(props) {
|
||||
const theme = useTheme();
|
||||
const [script, setScript] = React.useState("");
|
||||
@@ -49,12 +93,19 @@ export function EditScriptDialog(props) {
|
||||
const [selectedCommand, setSelectedCommand] = React.useState('');
|
||||
const [payloadTypeCmdOptions, setPayloadTypeCmdOptions] = React.useState([]);
|
||||
const [commandOptions, setCommandOptions] = React.useState([]);
|
||||
const inputPTRef = useRef(null);
|
||||
const inputCMDRef = useRef(null);
|
||||
const inputTaskRef = useRef(null);
|
||||
const [availableTasks, setAvailableTasks] = React.useState([]);
|
||||
const [selectedTask, setSelectedTask] = React.useState('');
|
||||
useQuery(getCommandsAndPayloadTypesQuery, {
|
||||
const [workbenchSplitSizes, setWorkbenchSplitSizes] = React.useState(() => getStoredSplitSizes(BROWSER_SCRIPT_WORKBENCH_SPLIT_KEY, defaultWorkbenchSplitSizes));
|
||||
const [topSplitSizes, setTopSplitSizes] = React.useState(() => getStoredSplitSizes(BROWSER_SCRIPT_TOP_SPLIT_KEY, defaultTopSplitSizes));
|
||||
const [targetOpen, setTargetOpen] = React.useState(() => props.new === true);
|
||||
const selectedPayloadTypeOption = React.useMemo(() => {
|
||||
return payloadTypeCmdOptions.find((payloadType) => payloadType.id === selectedPayloadType);
|
||||
}, [payloadTypeCmdOptions, selectedPayloadType]);
|
||||
const selectedCommandOption = React.useMemo(() => {
|
||||
return commandOptions.find((command) => command.id === selectedCommand);
|
||||
}, [commandOptions, selectedCommand]);
|
||||
const taskSelectorValue = selectedTask || "";
|
||||
const {loading: targetLoading} = useQuery(getCommandsAndPayloadTypesQuery, {
|
||||
onCompleted: data => {
|
||||
setPayloadTypeCmdOptions(data.payloadtype);
|
||||
if(props.payload_type_id !== undefined){
|
||||
@@ -66,11 +117,8 @@ export function EditScriptDialog(props) {
|
||||
}
|
||||
if(props.command_id !== undefined){
|
||||
setSelectedCommand(props.command_id);
|
||||
for(let i = 0; i < data.payloadtype.length; i++){
|
||||
if(props.payload_type_id === data.payloadtype[i].id){
|
||||
setCommandOptions(data.payloadtype[i].commands)
|
||||
}
|
||||
}
|
||||
const payloadType = data.payloadtype.find((payloadType) => payloadType.id === props.payload_type_id);
|
||||
setCommandOptions(payloadType?.commands || []);
|
||||
}else{
|
||||
if(data.payloadtype.length > 0){
|
||||
if(data.payloadtype[0].commands.length > 0){
|
||||
@@ -84,11 +132,13 @@ export function EditScriptDialog(props) {
|
||||
|
||||
}
|
||||
});
|
||||
const [getAvailableTasks] = useLazyQuery(getExistingTasksForCommand, {
|
||||
const [getAvailableTasks, {loading: tasksLoading}] = useLazyQuery(getExistingTasksForCommand, {
|
||||
onCompleted: data => {
|
||||
setAvailableTasks(data.task);
|
||||
if(data.task.length > 0){
|
||||
setSelectedTask(data.task[0]);
|
||||
}else{
|
||||
setSelectedTask('');
|
||||
}
|
||||
},
|
||||
onError: data => {
|
||||
@@ -102,9 +152,12 @@ export function EditScriptDialog(props) {
|
||||
useEffect( () => {
|
||||
if(selectedCommand !== ""){
|
||||
getAvailableTasks({variables: {command_id: selectedCommand}})
|
||||
}else{
|
||||
setAvailableTasks([]);
|
||||
setSelectedTask('');
|
||||
}
|
||||
|
||||
}, [selectedCommand]);
|
||||
}, [selectedCommand, getAvailableTasks]);
|
||||
useEffect( () => {
|
||||
if(props.script !== undefined){
|
||||
try{
|
||||
@@ -137,9 +190,12 @@ export function EditScriptDialog(props) {
|
||||
}
|
||||
const onChangeSelectedPayloadType = (event) => {
|
||||
setSelectedPayloadType(event.target.value);
|
||||
const cmds = payloadTypeCmdOptions.filter( (p) => p.id === event.target.value);
|
||||
setCommandOptions(cmds[0].commands);
|
||||
setSelectedCommand(cmds[0].commands[0].id);
|
||||
const selectedPayload = payloadTypeCmdOptions.find( (p) => p.id === event.target.value);
|
||||
const cmds = selectedPayload?.commands || [];
|
||||
setCommandOptions(cmds);
|
||||
setSelectedCommand(cmds[0]?.id || '');
|
||||
setAvailableTasks([]);
|
||||
setSelectedTask('');
|
||||
}
|
||||
const onChangeTask = (event) => {
|
||||
setSelectedTask(event.target.value);
|
||||
@@ -148,34 +204,48 @@ export function EditScriptDialog(props) {
|
||||
setSelectedCommand(event.target.value);
|
||||
}
|
||||
const onLoad = (editor) => {
|
||||
// Your editor options comes here
|
||||
editor.on('change', (arg, activeEditor) => {
|
||||
editorRef.current = activeEditor;
|
||||
editor.removeEventListener('change');
|
||||
editorRef.current.resize();
|
||||
});
|
||||
editorRef.current = editor;
|
||||
requestAnimationFrame(() => editor.resize());
|
||||
}
|
||||
const onOutputLoad = (editor) => {
|
||||
// Your editor options comes here
|
||||
editor.on('change', (arg, activeEditor) => {
|
||||
outputRef.current = activeEditor;
|
||||
editor.removeEventListener('change');
|
||||
outputRef.current.resize();
|
||||
});
|
||||
outputRef.current = editor;
|
||||
requestAnimationFrame(() => editor.resize());
|
||||
}
|
||||
React.useEffect( () => {
|
||||
|
||||
var logBackup = console.log;
|
||||
console.log = function(msg) {
|
||||
logStreamRef.current += "\n" + msg;
|
||||
logBackup.apply(msg);
|
||||
setLogOutput(logStreamRef.current)
|
||||
const logBackup = console.log;
|
||||
const formatConsoleArg = (arg) => {
|
||||
if(typeof arg === "string"){
|
||||
return arg;
|
||||
}
|
||||
try {
|
||||
return JSON.stringify(arg, null, 2);
|
||||
} catch (error) {
|
||||
return String(arg);
|
||||
}
|
||||
};
|
||||
console.log = function(...args) {
|
||||
const message = args.map(formatConsoleArg).join(" ");
|
||||
logStreamRef.current += "\n" + message;
|
||||
setLogOutput(logStreamRef.current);
|
||||
logBackup.apply(console, args);
|
||||
};
|
||||
return () => {
|
||||
console.log = logBackup;
|
||||
};
|
||||
|
||||
|
||||
}, []);
|
||||
|
||||
const onDragging = () => {
|
||||
React.useEffect(() => {
|
||||
requestAnimationFrame(() => {
|
||||
if(editorRef.current){
|
||||
editorRef.current.resize();
|
||||
}
|
||||
if(outputRef.current){
|
||||
outputRef.current.resize();
|
||||
}
|
||||
});
|
||||
}, [selectedTask, tasksLoading]);
|
||||
|
||||
const resizeEditors = () => {
|
||||
if(editorRef.current){
|
||||
editorRef.current.resize();
|
||||
}
|
||||
@@ -183,128 +253,227 @@ export function EditScriptDialog(props) {
|
||||
outputRef.current.resize();
|
||||
}
|
||||
}
|
||||
const onWorkbenchDragEnd = (sizes) => {
|
||||
setWorkbenchSplitSizes(sizes);
|
||||
storeSplitSizes(BROWSER_SCRIPT_WORKBENCH_SPLIT_KEY, sizes);
|
||||
resizeEditors();
|
||||
}
|
||||
const onTopDragEnd = (sizes) => {
|
||||
setTopSplitSizes(sizes);
|
||||
storeSplitSizes(BROWSER_SCRIPT_TOP_SPLIT_KEY, sizes);
|
||||
resizeEditors();
|
||||
}
|
||||
const onToggleTargetOpen = () => {
|
||||
setTargetOpen((current) => !current);
|
||||
}
|
||||
const selectedTaskLabel = (task) => {
|
||||
return `${task.command_name} / ${task.display_id} / ${task.display_params}`;
|
||||
}
|
||||
return (
|
||||
<React.Fragment>
|
||||
<DialogTitle >
|
||||
{props.title ? props.title : "Edit " + props.author + "'s BrowserScript Code"}
|
||||
</DialogTitle>
|
||||
<DialogContent dividers={true} style={{height: `calc(100vh)`, display: "flex", flexDirection: "column", width: "100%"}}>
|
||||
<div style={{display: "flex"}}>
|
||||
<FormControl style={{width: "50%"}}>
|
||||
<InputLabel ref={inputPTRef}>Payload Type</InputLabel>
|
||||
<Select
|
||||
labelId="demo-dialog-select-label"
|
||||
id="demo-dialog-select"
|
||||
value={selectedPayloadType}
|
||||
onChange={onChangeSelectedPayloadType}
|
||||
input={<Input style={{width: "100%"}}/>}
|
||||
<DialogTitle>
|
||||
<Box className="mythic-dialog-title-row">
|
||||
<Box component="span">{props.title ? props.title : "Edit " + props.author + "'s BrowserScript Code"}</Box>
|
||||
</Box>
|
||||
</DialogTitle>
|
||||
<DialogContent className="mythic-browser-script-dialog-content" dividers={true}>
|
||||
<MythicDialogBody className="mythic-browser-script-dialog-body">
|
||||
<Box className={`mythic-browser-script-target-panel ${targetOpen ? "mythic-browser-script-target-panel-open" : ""}`}>
|
||||
<Box className="mythic-browser-script-target-summary">
|
||||
<Box className="mythic-browser-script-target-copy">
|
||||
<Box component="span" className="mythic-browser-script-target-label">Script Target</Box>
|
||||
<Box className="mythic-browser-script-target-chips">
|
||||
<Chip size="small" label={selectedPayloadTypeOption?.name || (targetLoading ? "Loading payload types" : "No payload type")} />
|
||||
<Chip size="small" label={selectedCommandOption?.cmd || "No command"} />
|
||||
</Box>
|
||||
</Box>
|
||||
<Tooltip title={targetOpen ? "Collapse target settings" : "Edit target settings"}>
|
||||
<IconButton
|
||||
aria-expanded={targetOpen}
|
||||
aria-label={targetOpen ? "Collapse target settings" : "Edit target settings"}
|
||||
className="mythic-browser-script-target-toggle"
|
||||
onClick={onToggleTargetOpen}
|
||||
size="small"
|
||||
>
|
||||
{targetOpen ? <KeyboardArrowUpIcon fontSize="small" /> : <KeyboardArrowDownIcon fontSize="small" />}
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
</Box>
|
||||
<Collapse
|
||||
in={targetOpen}
|
||||
onEntered={resizeEditors}
|
||||
onExited={resizeEditors}
|
||||
timeout="auto"
|
||||
unmountOnExit
|
||||
>
|
||||
{payloadTypeCmdOptions.map( (opt) => (
|
||||
<MenuItem value={opt.id} key={"payloadtype" + opt.id}>{opt.name}</MenuItem>
|
||||
) )}
|
||||
</Select>
|
||||
</FormControl>
|
||||
<FormControl style={{width: "50%", paddingBottom: "10px"}}>
|
||||
<InputLabel ref={inputCMDRef}>Command</InputLabel>
|
||||
<Select
|
||||
labelId="demo-dialog-select-label"
|
||||
id="demo-dialog-select"
|
||||
value={selectedCommand}
|
||||
onChange={onChangeSelectedCommand}
|
||||
input={<Input style={{width: "100%"}}/>}
|
||||
<Box className="mythic-browser-script-target-details">
|
||||
<MythicFormGrid minWidth="16rem">
|
||||
<MythicFormField label="Payload Type">
|
||||
<FormControl fullWidth size="small">
|
||||
<InputLabel id="browser-script-payload-type-label">Payload Type</InputLabel>
|
||||
<Select
|
||||
label="Payload Type"
|
||||
labelId="browser-script-payload-type-label"
|
||||
value={selectedPayloadType}
|
||||
onChange={onChangeSelectedPayloadType}
|
||||
disabled={targetLoading}
|
||||
>
|
||||
{payloadTypeCmdOptions.map( (opt) => (
|
||||
<MenuItem value={opt.id} key={"payloadtype" + opt.id}>{opt.name}</MenuItem>
|
||||
) )}
|
||||
</Select>
|
||||
</FormControl>
|
||||
</MythicFormField>
|
||||
<MythicFormField label="Command">
|
||||
<FormControl fullWidth size="small">
|
||||
<InputLabel id="browser-script-command-label">Command</InputLabel>
|
||||
<Select
|
||||
label="Command"
|
||||
labelId="browser-script-command-label"
|
||||
value={selectedCommand}
|
||||
onChange={onChangeSelectedCommand}
|
||||
disabled={targetLoading || commandOptions.length === 0}
|
||||
>
|
||||
{commandOptions.map( (opt) => (
|
||||
<MenuItem value={opt.id} key={"command" + opt.id}>{opt.cmd}</MenuItem>
|
||||
) )}
|
||||
</Select>
|
||||
</FormControl>
|
||||
</MythicFormField>
|
||||
</MythicFormGrid>
|
||||
<MythicFormNote>
|
||||
Use Save for Testing to update the script without closing the editor. Select an already executed matching task below, then collapse and re-expand that task if you need it to reload the updated renderer.
|
||||
</MythicFormNote>
|
||||
</Box>
|
||||
</Collapse>
|
||||
</Box>
|
||||
<Split
|
||||
className="mythic-browser-script-workbench"
|
||||
direction="vertical"
|
||||
gutterSize={8}
|
||||
minSize={[210, 300]}
|
||||
onDrag={resizeEditors}
|
||||
onDragEnd={onWorkbenchDragEnd}
|
||||
sizes={workbenchSplitSizes}
|
||||
>
|
||||
<Split
|
||||
className="mythic-browser-script-top-split"
|
||||
gutterSize={8}
|
||||
minSize={[320, 220]}
|
||||
onDrag={resizeEditors}
|
||||
onDragEnd={onTopDragEnd}
|
||||
sizes={topSplitSizes}
|
||||
>
|
||||
{commandOptions.map( (opt) => (
|
||||
<MenuItem value={opt.id} key={"command" + opt.id}>{opt.cmd}</MenuItem>
|
||||
) )}
|
||||
</Select>
|
||||
</FormControl>
|
||||
</div>
|
||||
<p>
|
||||
<b>To test locally</b>: Make your changes in the top left code box. Any <b>console.log</b> entries will appear in the box to the right when executed.
|
||||
Click <b>Save for Testing</b> to finalize your changes. Make sure to select an already executed task that matches this command from the bottom.
|
||||
You will need to collapse and re-expand the task to pull in your updated changes.
|
||||
</p>
|
||||
<div style={{display: "flex", width: "100%", flexGrow: 1}}>
|
||||
<Split direction="vertical" style={{height: "100%", width: "100%"}} sizes={[30, 70]} onDrag={onDragging} >
|
||||
<Split style={{display: "flex"}} sizes={[70, 30]} onDrag={onDragging} >
|
||||
<div className="bg-gray-light">
|
||||
<AceEditor
|
||||
mode="javascript"
|
||||
theme={theme.palette.mode === 'dark' ? 'monokai' : 'github'}
|
||||
width="100%"
|
||||
onLoad={onLoad}
|
||||
height="100%"
|
||||
value={script}
|
||||
focus={true}
|
||||
onChange={onChange}
|
||||
setOptions={{
|
||||
useWorker: false
|
||||
}}
|
||||
/>
|
||||
<div className="mythic-browser-script-pane mythic-browser-script-editor-pane">
|
||||
<div className="mythic-browser-script-pane-header">
|
||||
<span>Script Code</span>
|
||||
<span>JavaScript</span>
|
||||
</div>
|
||||
<div className="mythic-browser-script-editor-frame">
|
||||
<AceEditor
|
||||
mode="javascript"
|
||||
theme={theme.palette.mode === 'dark' ? 'monokai' : 'github'}
|
||||
width="100%"
|
||||
onLoad={onLoad}
|
||||
height="100%"
|
||||
value={script}
|
||||
focus={true}
|
||||
onChange={onChange}
|
||||
setOptions={{
|
||||
displayIndentGuides: true,
|
||||
fontSize: 13,
|
||||
showPrintMargin: false,
|
||||
tabSize: 4,
|
||||
useWorker: false,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="bg-gray-light" >
|
||||
<AceEditor
|
||||
mode="javascript"
|
||||
theme={theme.palette.mode === 'dark' ? 'monokai' : 'github'}
|
||||
width="100%"
|
||||
onLoad={onOutputLoad}
|
||||
height="100%"
|
||||
value={logOutput}
|
||||
focus={false}
|
||||
onChange={onChange}
|
||||
setOptions={{
|
||||
useWorker: false
|
||||
}}
|
||||
/>
|
||||
<div className="mythic-browser-script-pane mythic-browser-script-console-pane">
|
||||
<div className="mythic-browser-script-pane-header">
|
||||
<span>Console Output</span>
|
||||
<span>console.log</span>
|
||||
</div>
|
||||
<div className="mythic-browser-script-editor-frame">
|
||||
<AceEditor
|
||||
mode="javascript"
|
||||
theme={theme.palette.mode === 'dark' ? 'monokai' : 'github'}
|
||||
width="100%"
|
||||
onLoad={onOutputLoad}
|
||||
height="100%"
|
||||
value={logOutput}
|
||||
focus={false}
|
||||
readOnly
|
||||
setOptions={{
|
||||
fontSize: 12,
|
||||
highlightActiveLine: false,
|
||||
showGutter: false,
|
||||
showPrintMargin: false,
|
||||
tabSize: 4,
|
||||
useWorker: false,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</Split>
|
||||
<div className="bg-gray-light" >
|
||||
<FormControl style={{width: "100%"}}>
|
||||
<InputLabel ref={inputTaskRef} style={{paddingTop: "10px"}}>Test Script With Task</InputLabel>
|
||||
<Select
|
||||
labelId="demo-dialog-select-label"
|
||||
id="demo-dialog-select"
|
||||
value={selectedTask}
|
||||
onChange={onChangeTask}
|
||||
input={<Input style={{width: "100%"}}/>}
|
||||
>
|
||||
{availableTasks.map( (opt) => (
|
||||
<MenuItem value={opt} key={"task" + opt.id}>{opt.command_name + " / " + opt.display_id + " / " + opt.display_params}</MenuItem>
|
||||
) )}
|
||||
</Select>
|
||||
</FormControl>
|
||||
{selectedTask !== "" &&
|
||||
<TaskDisplay me={props.me} task={selectedTask} command_id={selectedTask.command == null ? 0 : selectedTask.command.id} />
|
||||
}
|
||||
<div className="mythic-browser-script-pane mythic-browser-script-preview-pane">
|
||||
<div className="mythic-browser-script-pane-header">
|
||||
<span>Test Preview</span>
|
||||
<span>{availableTasks.length === 1 ? "1 task" : `${availableTasks.length} tasks`}</span>
|
||||
</div>
|
||||
<div className="mythic-browser-script-preview-controls">
|
||||
<FormControl fullWidth size="small">
|
||||
<InputLabel id="browser-script-test-task-label">Test Script With Task</InputLabel>
|
||||
<Select
|
||||
label="Test Script With Task"
|
||||
labelId="browser-script-test-task-label"
|
||||
value={taskSelectorValue}
|
||||
onChange={onChangeTask}
|
||||
disabled={tasksLoading || availableTasks.length === 0}
|
||||
onOpen={resizeEditors}
|
||||
renderValue={(task) => task ? selectedTaskLabel(task) : ""}
|
||||
>
|
||||
{availableTasks.map( (opt) => (
|
||||
<MenuItem value={opt} key={"task" + opt.id}>{selectedTaskLabel(opt)}</MenuItem>
|
||||
) )}
|
||||
</Select>
|
||||
</FormControl>
|
||||
</div>
|
||||
<div className="mythic-browser-script-preview-frame">
|
||||
{tasksLoading ? (
|
||||
<MythicLoadingState compact title="Loading test tasks" description="Fetching previous tasks for this command." />
|
||||
) : selectedTask !== "" ? (
|
||||
<TaskDisplay me={props.me} task={selectedTask} command_id={selectedTask.command == null ? 0 : selectedTask.command.id} />
|
||||
) : (
|
||||
<MythicEmptyState compact title="No matching tasks" description="Run this command at least once to preview this browser script against task output." />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</Split>
|
||||
</div>
|
||||
</MythicDialogBody>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button onClick={props.onClose} variant="contained" color="primary">
|
||||
<MythicDialogFooter>
|
||||
<MythicDialogButton onClick={props.onClose}>
|
||||
Close
|
||||
</Button>
|
||||
</MythicDialogButton>
|
||||
{props.new ? (
|
||||
<Button onClick={onSubmit} variant="contained" color="success">
|
||||
<MythicDialogButton disabled={selectedCommand === "" || selectedPayloadType === ""} intent="primary" onClick={onSubmit}>
|
||||
Create
|
||||
</Button>
|
||||
</MythicDialogButton>
|
||||
) : (
|
||||
<React.Fragment>
|
||||
<Button onClick={onRevert} variant="contained" color="warning">
|
||||
<MythicDialogButton intent="warning" onClick={onRevert}>
|
||||
Revert
|
||||
</Button>
|
||||
<Button onClick={onTest} variant="contained" color="info">
|
||||
</MythicDialogButton>
|
||||
<MythicDialogButton className="mythic-table-row-action-hover-info" intent="info" disabled={selectedCommand === "" || selectedPayloadType === ""} onClick={onTest}>
|
||||
Save For Testing
|
||||
</Button>
|
||||
<Button onClick={onSubmit} variant="contained" color="success">
|
||||
</MythicDialogButton>
|
||||
<MythicDialogButton disabled={selectedCommand === "" || selectedPayloadType === ""} intent="primary" onClick={onSubmit}>
|
||||
Save and Exit
|
||||
</Button>
|
||||
</MythicDialogButton>
|
||||
</React.Fragment>
|
||||
)}
|
||||
|
||||
</DialogActions>
|
||||
</MythicDialogFooter>
|
||||
</React.Fragment>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -160,8 +160,7 @@ export function EventGroupTable({selectedEventGroup, me, showInstances, showGrap
|
||||
<MythicPageHeaderChip
|
||||
icon={selectedEventGroup.active ? <NotificationsActiveTwoToneIcon /> : <NotificationsOffTwoToneIcon />}
|
||||
label={selectedEventGroup.active ? "Enabled" : "Disabled"}
|
||||
status={selectedEventGroup.active ? "success" : undefined}
|
||||
className={selectedEventGroup.active ? "" : "mythic-eventing-header-chip-disabled"}
|
||||
status={selectedEventGroup.active ? "enabled" : "disabled"}
|
||||
/>
|
||||
<MythicPageHeaderChip label={selectedEventGroup.trigger} />
|
||||
<MythicPageHeaderChip label={selectedEventGroup.run_as || "unknown"} />
|
||||
|
||||
@@ -21,6 +21,7 @@ import CategoryIcon from '@mui/icons-material/Category';
|
||||
import {MythicPageBody} from "../../MythicComponents/MythicPageBody";
|
||||
import {MythicPageHeader, MythicPageHeaderChip} from "../../MythicComponents/MythicPageHeader";
|
||||
import {MythicToolbarButton, MythicToolbarToggle} from "../../MythicComponents/MythicTableToolbar";
|
||||
import {MythicStateChip} from "../../MythicComponents/MythicStateChip";
|
||||
|
||||
const get_eventgroups = gql`
|
||||
query GetEventGroups {
|
||||
@@ -180,6 +181,20 @@ const getEventGroupStatus = (eventGroup) => {
|
||||
}
|
||||
return {key: "runnable", label: "Runnable", rank: 0};
|
||||
};
|
||||
const getEventGroupStateChipState = (statusKey) => {
|
||||
switch(statusKey){
|
||||
case "runnable":
|
||||
return "enabled";
|
||||
case "needs_approval":
|
||||
return "warning";
|
||||
case "disabled":
|
||||
return "disabled";
|
||||
case "deleted":
|
||||
return "error";
|
||||
default:
|
||||
return "neutral";
|
||||
}
|
||||
}
|
||||
export function Eventing({me}){
|
||||
const [openTestModal, setOpenTestModal] = React.useState(false);
|
||||
const [openCreateEventingStepper, setOpenCreateEventingStepper] = React.useState(false);
|
||||
@@ -428,7 +443,7 @@ export function Eventing({me}){
|
||||
<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>
|
||||
<MythicStateChip compact label={visibleEventGroups.length} state="info" />
|
||||
</div>
|
||||
<div className="mythic-eventing-list-item-meta">Review instances across all event groups</div>
|
||||
</div>
|
||||
@@ -446,7 +461,7 @@ export function Eventing({me}){
|
||||
<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>
|
||||
<MythicStateChip compact label={status.label} state={getEventGroupStateChipState(status.key)} />
|
||||
</div>
|
||||
<div className="mythic-eventing-list-item-meta">
|
||||
<span>{eventGroup.trigger || "No trigger"}</span>
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import React from 'react';
|
||||
import {IconButton, Typography, Link} from '@mui/material';
|
||||
import {Box, IconButton, Typography, Link} from '@mui/material';
|
||||
import TableRow from '@mui/material/TableRow';
|
||||
import DeleteIcon from '@mui/icons-material/Delete';
|
||||
import Switch from '@mui/material/Switch';
|
||||
import MythicStyledTableCell from "../../MythicComponents/MythicTableCell";
|
||||
import {toLocalTime} from "../../utilities/Time";
|
||||
import {MythicStateChip} from "../../MythicComponents/MythicStateChip";
|
||||
|
||||
export function APITokenRow(props){
|
||||
return (
|
||||
@@ -18,14 +19,21 @@ export function APITokenRow(props){
|
||||
)}
|
||||
</MythicStyledTableCell>
|
||||
<MythicStyledTableCell>
|
||||
<Switch
|
||||
color={ "info"}
|
||||
disabled={props.deleted}
|
||||
checked={props.active}
|
||||
onChange={() => {props.onToggleActive(props.id, !props.active)}}
|
||||
inputProps={{ 'aria-label': 'primary checkbox' }}
|
||||
name="active"
|
||||
/>
|
||||
<Box className="mythic-state-toggle-cell">
|
||||
<Switch
|
||||
color="success"
|
||||
disabled={props.deleted}
|
||||
checked={props.active}
|
||||
onChange={() => {props.onToggleActive(props.id, !props.active)}}
|
||||
inputProps={{ 'aria-label': 'Toggle API token active state' }}
|
||||
name="active"
|
||||
size="small"
|
||||
/>
|
||||
<MythicStateChip
|
||||
label={props.deleted ? "Deleted" : props.active ? "Active" : "Disabled"}
|
||||
state={props.deleted ? "error" : props.active ? "active" : "disabled"}
|
||||
/>
|
||||
</Box>
|
||||
</MythicStyledTableCell>
|
||||
<MythicStyledTableCell>
|
||||
<Typography>
|
||||
|
||||
@@ -19,7 +19,7 @@ export function SettingsOperatorDeleteDialog(props) {
|
||||
{props.deleted ?
|
||||
"This restores an operator and allows them to log in again."
|
||||
:
|
||||
"This deletes an operator and hides them from this view by default. If you want a temporary solution, mark the operator as inactive."}
|
||||
"This deletes an operator and hides them from this view by default. If you want a temporary solution, mark the operator as disabled."}
|
||||
</DialogContentText>
|
||||
<DialogContentText>
|
||||
Are you sure you want to {props.deleted ? "restore" : "delete" } operator "{props.username}"?
|
||||
@@ -36,4 +36,3 @@ export function SettingsOperatorDeleteDialog(props) {
|
||||
</React.Fragment>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -149,7 +149,7 @@ export function SettingsOperatorTable(props){
|
||||
<TableCell style={{width: "4rem"}}>Login</TableCell>
|
||||
<TableCell style={{width: "6rem"}}>Use UTC</TableCell>
|
||||
<TableCell style={{width: "10rem"}}>Preferences</TableCell>
|
||||
<TableCell style={{width: "6rem"}}>Active</TableCell>
|
||||
<TableCell style={{width: "9rem"}}>Active</TableCell>
|
||||
<TableCell >Login Info</TableCell>
|
||||
<TableCell >Email</TableCell>
|
||||
<TableCell style={{width: "6rem"}}>Admin</TableCell>
|
||||
|
||||
@@ -48,6 +48,7 @@ import {
|
||||
MythicDialogSection,
|
||||
MythicFormNote
|
||||
} from "../../MythicComponents/MythicDialogLayout";
|
||||
import {MythicStateChip} from "../../MythicComponents/MythicStateChip";
|
||||
|
||||
const createAPITokenMutation = gql`
|
||||
mutation createAPITokenMutation($operator_id: Int, $name: String, $scopes: [String!]){
|
||||
@@ -351,14 +352,18 @@ export function SettingsOperatorTableRow(props){
|
||||
}
|
||||
</MythicStyledTableCell>
|
||||
<MythicStyledTableCell>
|
||||
<Switch
|
||||
color={ isMe || !props.userIsAdmin ? "secondary" : "info"}
|
||||
checked={props.active}
|
||||
disabled={isMe || !props.userIsAdmin}
|
||||
onChange={onActiveChanged}
|
||||
inputProps={{ 'aria-label': 'primary checkbox' }}
|
||||
name="active"
|
||||
/>
|
||||
<Box className="mythic-state-toggle-cell">
|
||||
<Switch
|
||||
color="success"
|
||||
checked={props.active}
|
||||
disabled={isMe || !props.userIsAdmin}
|
||||
onChange={onActiveChanged}
|
||||
inputProps={{ 'aria-label': 'Toggle operator active state' }}
|
||||
name="active"
|
||||
size="small"
|
||||
/>
|
||||
<MythicStateChip label={props.active ? "Active" : "Disabled"} state={props.active ? "active" : "disabled"} />
|
||||
</Box>
|
||||
</MythicStyledTableCell>
|
||||
<MythicStyledTableCell>
|
||||
<b>Last Login: </b>{toLocalTime(props.last_login, me?.user?.view_utc_time )}<br/>
|
||||
@@ -515,7 +520,7 @@ const APITokens = ({apiTokens, error, loading, onDeleteAPIToken, onToggleActive,
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
<MythicStyledTableCell style={{width: "3rem"}} />
|
||||
<MythicStyledTableCell style={{width: "5rem"}}>Active</MythicStyledTableCell>
|
||||
<MythicStyledTableCell style={{width: "9rem"}}>Active</MythicStyledTableCell>
|
||||
<MythicStyledTableCell>Created By</MythicStyledTableCell>
|
||||
<MythicStyledTableCell>Scopes</MythicStyledTableCell>
|
||||
<MythicStyledTableCell style={{width: "9rem"}}>Type</MythicStyledTableCell>
|
||||
|
||||
@@ -1346,6 +1346,249 @@ tspan {
|
||||
font-size: 0.78rem;
|
||||
line-height: 1.45;
|
||||
}
|
||||
.mythic-browser-script-dialog-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: min(74vh, calc(100vh - 9rem));
|
||||
min-height: min(38rem, calc(100vh - 9rem));
|
||||
overflow: hidden;
|
||||
}
|
||||
.mythic-browser-script-dialog-body {
|
||||
flex: 1 1 auto;
|
||||
min-height: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
.mythic-browser-script-dialog-body > .mythic-browser-script-target-panel {
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
.mythic-browser-script-target-panel {
|
||||
background-color: ${(props) => props.theme.surfaces?.raised || props.theme.palette.background.paper};
|
||||
border: 1px solid ${(props) => props.theme.table?.borderSoft || props.theme.borderColor};
|
||||
border-radius: ${(props) => props.theme.shape.borderRadius}px;
|
||||
box-shadow: ${(props) => props.theme.palette.mode === "dark" ? "none" : "0 8px 18px rgba(15, 23, 42, 0.04)"};
|
||||
overflow: hidden;
|
||||
}
|
||||
.mythic-browser-script-target-summary {
|
||||
align-items: center;
|
||||
display: flex;
|
||||
gap: 0.65rem;
|
||||
justify-content: space-between;
|
||||
min-height: 42px;
|
||||
padding: 0.42rem 0.5rem 0.42rem 0.7rem;
|
||||
}
|
||||
.mythic-browser-script-target-panel-open .mythic-browser-script-target-summary {
|
||||
border-bottom: 1px solid ${(props) => props.theme.table?.borderSoft || props.theme.borderColor};
|
||||
}
|
||||
.mythic-browser-script-target-copy {
|
||||
align-items: center;
|
||||
display: flex;
|
||||
gap: 0.65rem;
|
||||
min-width: 0;
|
||||
}
|
||||
.mythic-browser-script-target-label {
|
||||
color: ${(props) => props.theme.palette.text.secondary};
|
||||
flex: 0 0 auto;
|
||||
font-size: 0.74rem;
|
||||
font-weight: 800;
|
||||
line-height: 1;
|
||||
}
|
||||
.mythic-browser-script-target-chips {
|
||||
align-items: center;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.35rem;
|
||||
min-width: 0;
|
||||
}
|
||||
.mythic-browser-script-target-chips .MuiChip-root {
|
||||
background-color: ${(props) => props.theme.palette.mode === "dark" ? alpha(props.theme.palette.common.white, 0.06) : alpha(props.theme.palette.common.black, 0.035)};
|
||||
border: 1px solid ${(props) => props.theme.table?.borderSoft || props.theme.borderColor};
|
||||
color: ${(props) => props.theme.palette.text.primary};
|
||||
font-size: 0.72rem;
|
||||
font-weight: 750;
|
||||
max-width: 18rem;
|
||||
}
|
||||
.mythic-browser-script-target-chips .MuiChip-label {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
.mythic-browser-script-target-toggle {
|
||||
border: 1px solid ${(props) => props.theme.table?.borderSoft || props.theme.borderColor} !important;
|
||||
border-radius: ${(props) => props.theme.shape.borderRadius}px !important;
|
||||
color: ${(props) => props.theme.palette.text.secondary} !important;
|
||||
}
|
||||
.mythic-browser-script-target-toggle:hover {
|
||||
background-color: ${(props) => alpha(props.theme.palette.primary.main, props.theme.palette.mode === "dark" ? 0.22 : 0.1)} !important;
|
||||
border-color: ${(props) => alpha(props.theme.palette.primary.main, props.theme.palette.mode === "dark" ? 0.45 : 0.32)} !important;
|
||||
color: ${(props) => props.theme.palette.primary.main} !important;
|
||||
}
|
||||
.mythic-browser-script-target-details {
|
||||
background-color: ${(props) => props.theme.surfaces?.muted || props.theme.palette.background.paper};
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.55rem;
|
||||
padding: 0.65rem;
|
||||
}
|
||||
.mythic-browser-script-workbench {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex: 1 1 auto;
|
||||
min-height: 0;
|
||||
min-width: 0;
|
||||
width: 100%;
|
||||
}
|
||||
.mythic-browser-script-top-split {
|
||||
display: flex;
|
||||
min-height: 0;
|
||||
min-width: 0;
|
||||
width: 100%;
|
||||
}
|
||||
.mythic-browser-script-workbench > .gutter.gutter-vertical,
|
||||
.mythic-browser-script-top-split > .gutter.gutter-horizontal {
|
||||
background-color: ${(props) => props.theme.palette.mode === "dark" ? alpha(props.theme.palette.common.white, 0.1) : alpha(props.theme.palette.common.black, 0.08)};
|
||||
border-radius: 999px;
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
.mythic-browser-script-workbench > .gutter.gutter-vertical:hover,
|
||||
.mythic-browser-script-top-split > .gutter.gutter-horizontal:hover {
|
||||
background-color: ${(props) => alpha(props.theme.palette.primary.main, props.theme.palette.mode === "dark" ? 0.5 : 0.32)};
|
||||
}
|
||||
.mythic-browser-script-pane {
|
||||
background-color: ${(props) => props.theme.surfaces?.raised || props.theme.palette.background.paper};
|
||||
border: 1px solid ${(props) => props.theme.table?.borderSoft || props.theme.borderColor};
|
||||
border-radius: ${(props) => props.theme.shape.borderRadius}px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-height: 0;
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
.mythic-browser-script-editor-pane {
|
||||
height: 100%;
|
||||
}
|
||||
.mythic-browser-script-console-pane {
|
||||
height: 100%;
|
||||
}
|
||||
.mythic-browser-script-preview-pane {
|
||||
height: 100%;
|
||||
}
|
||||
.mythic-browser-script-pane-header {
|
||||
align-items: center;
|
||||
background-color: ${(props) => props.theme.surfaces?.muted || props.theme.palette.background.paper};
|
||||
border-bottom: 1px solid ${(props) => props.theme.table?.borderSoft || props.theme.borderColor};
|
||||
color: ${(props) => props.theme.palette.text.primary};
|
||||
display: flex;
|
||||
flex: 0 0 auto;
|
||||
font-size: 0.78rem;
|
||||
font-weight: 800;
|
||||
justify-content: space-between;
|
||||
letter-spacing: 0;
|
||||
min-height: 34px;
|
||||
padding: 0.4rem 0.65rem;
|
||||
}
|
||||
.mythic-browser-script-pane-header span:last-child {
|
||||
color: ${(props) => props.theme.palette.text.secondary};
|
||||
font-size: 0.68rem;
|
||||
font-weight: 750;
|
||||
}
|
||||
.mythic-browser-script-editor-frame {
|
||||
flex: 1 1 auto;
|
||||
min-height: 0;
|
||||
min-width: 0;
|
||||
}
|
||||
.mythic-browser-script-editor-frame .ace_editor {
|
||||
background-color: ${(props) => props.theme.palette.mode === "dark" ? alpha(props.theme.palette.common.black, 0.18) : alpha(props.theme.palette.common.white, 0.62)};
|
||||
font-family: ${(props) => props.theme.typography.fontFamilyMono || "monospace"} !important;
|
||||
}
|
||||
.mythic-browser-script-preview-controls {
|
||||
background-color: ${(props) => props.theme.palette.mode === "dark" ? "rgba(255,255,255,0.025)" : "rgba(0,0,0,0.012)"};
|
||||
border-bottom: 1px solid ${(props) => props.theme.table?.borderSoft || props.theme.borderColor};
|
||||
flex: 0 0 auto;
|
||||
padding: 0.65rem;
|
||||
}
|
||||
.mythic-browser-script-preview-frame {
|
||||
flex: 1 1 auto;
|
||||
min-height: 0;
|
||||
min-width: 0;
|
||||
overflow: auto;
|
||||
padding: 0.55rem;
|
||||
}
|
||||
.mythic-browser-scripts-table {
|
||||
max-width: 100%;
|
||||
min-width: 60rem;
|
||||
table-layout: fixed;
|
||||
width: 100%;
|
||||
}
|
||||
.mythic-browser-script-spacer-cell {
|
||||
min-width: 0;
|
||||
padding-left: 0 !important;
|
||||
padding-right: 0 !important;
|
||||
width: auto;
|
||||
}
|
||||
.mythic-browser-script-script-cell {
|
||||
align-items: center;
|
||||
display: flex;
|
||||
gap: 0.6rem;
|
||||
min-width: 0;
|
||||
}
|
||||
.mythic-browser-script-payload-icon {
|
||||
align-items: center;
|
||||
background-color: ${(props) => props.theme.palette.mode === "dark" ? alpha(props.theme.palette.common.white, 0.055) : alpha(props.theme.palette.common.black, 0.028)};
|
||||
border: 1px solid ${(props) => props.theme.table?.borderSoft || props.theme.borderColor};
|
||||
border-radius: ${(props) => props.theme.shape.borderRadius}px;
|
||||
display: inline-flex;
|
||||
flex: 0 0 auto;
|
||||
height: 40px;
|
||||
justify-content: center;
|
||||
width: 40px;
|
||||
}
|
||||
.mythic-browser-script-script-copy {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.12rem;
|
||||
min-width: 0;
|
||||
}
|
||||
.mythic-browser-script-command-name {
|
||||
color: ${(props) => props.theme.palette.text.primary};
|
||||
font-size: 0.86rem;
|
||||
font-weight: 750;
|
||||
line-height: 1.25;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.mythic-browser-script-payload-name {
|
||||
color: ${(props) => props.theme.palette.text.secondary};
|
||||
font-size: 0.74rem;
|
||||
font-weight: 650;
|
||||
line-height: 1.25;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.mythic-browser-script-active-cell {
|
||||
align-items: center;
|
||||
display: flex;
|
||||
gap: 0.4rem;
|
||||
min-width: 0;
|
||||
}
|
||||
.mythic-browser-script-actions {
|
||||
justify-content: center;
|
||||
}
|
||||
@media screen and (max-width: 1050px) {
|
||||
.mythic-browser-script-dialog-content {
|
||||
height: calc(100vh - 8rem);
|
||||
}
|
||||
.mythic-browser-script-dialog-body {
|
||||
overflow: auto;
|
||||
}
|
||||
.mythic-browser-script-workbench {
|
||||
flex: 0 0 auto;
|
||||
min-height: 42rem;
|
||||
}
|
||||
.mythic-browser-script-top-split {
|
||||
min-width: 44rem;
|
||||
}
|
||||
}
|
||||
.mythic-single-task-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
@@ -2196,6 +2439,58 @@ tspan {
|
||||
.mythic-table-row-actions-nowrap {
|
||||
flex-wrap: nowrap;
|
||||
}
|
||||
.mythic-state-toggle-cell {
|
||||
align-items: center;
|
||||
display: flex;
|
||||
gap: 0.4rem;
|
||||
min-width: 0;
|
||||
}
|
||||
.mythic-state-chip {
|
||||
align-items: center;
|
||||
border: 1px solid ${(props) => props.theme.table?.borderSoft || props.theme.borderColor};
|
||||
border-radius: 999px;
|
||||
display: inline-flex;
|
||||
flex: 0 0 auto;
|
||||
font-size: 0.72rem;
|
||||
font-weight: 800;
|
||||
line-height: 1;
|
||||
min-height: 24px;
|
||||
padding: 0.25rem 0.55rem;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.mythic-state-chip-compact {
|
||||
font-size: 0.66rem;
|
||||
min-height: 1.25rem;
|
||||
padding: 0 0.35rem;
|
||||
}
|
||||
.mythic-state-chip-active,
|
||||
.mythic-state-chip-enabled {
|
||||
background-color: ${(props) => alpha(props.theme.palette.success.main, props.theme.palette.mode === "dark" ? 0.18 : 0.1)};
|
||||
border-color: ${(props) => alpha(props.theme.palette.success.main, props.theme.palette.mode === "dark" ? 0.48 : 0.32)};
|
||||
color: ${(props) => props.theme.palette.success.main};
|
||||
}
|
||||
.mythic-state-chip-inactive,
|
||||
.mythic-state-chip-disabled,
|
||||
.mythic-state-chip-warning {
|
||||
background-color: ${(props) => alpha(props.theme.palette.warning.main, props.theme.palette.mode === "dark" ? 0.18 : 0.1)};
|
||||
border-color: ${(props) => alpha(props.theme.palette.warning.main, props.theme.palette.mode === "dark" ? 0.48 : 0.32)};
|
||||
color: ${(props) => props.theme.palette.warning.main};
|
||||
}
|
||||
.mythic-state-chip-neutral {
|
||||
background-color: ${(props) => props.theme.palette.mode === "dark" ? alpha(props.theme.palette.common.white, 0.045) : alpha(props.theme.palette.common.black, 0.028)};
|
||||
border-color: ${(props) => props.theme.table?.borderSoft || props.theme.borderColor};
|
||||
color: ${(props) => props.theme.palette.text.secondary};
|
||||
}
|
||||
.mythic-state-chip-error {
|
||||
background-color: ${(props) => alpha(props.theme.palette.error.main, props.theme.palette.mode === "dark" ? 0.18 : 0.1)};
|
||||
border-color: ${(props) => alpha(props.theme.palette.error.main, props.theme.palette.mode === "dark" ? 0.48 : 0.32)};
|
||||
color: ${(props) => props.theme.palette.error.main};
|
||||
}
|
||||
.mythic-state-chip-info {
|
||||
background-color: ${(props) => alpha(props.theme.palette.info.main, props.theme.palette.mode === "dark" ? 0.18 : 0.1)};
|
||||
border-color: ${(props) => alpha(props.theme.palette.info.main, props.theme.palette.mode === "dark" ? 0.48 : 0.32)};
|
||||
color: ${(props) => props.theme.palette.info.main};
|
||||
}
|
||||
.mythic-payload-progress-cell {
|
||||
align-items: center;
|
||||
display: flex;
|
||||
@@ -2556,46 +2851,6 @@ tspan {
|
||||
margin-top: 0.22rem;
|
||||
min-width: 0;
|
||||
}
|
||||
.mythic-eventing-row-chip {
|
||||
align-items: center;
|
||||
border: 1px solid transparent;
|
||||
border-radius: ${(props) => props.theme.shape.borderRadius}px;
|
||||
display: inline-flex;
|
||||
flex: 0 0 auto;
|
||||
font-size: 0.66rem;
|
||||
font-weight: 850;
|
||||
line-height: 1;
|
||||
min-height: 1.25rem;
|
||||
padding: 0 0.35rem;
|
||||
}
|
||||
.mythic-eventing-row-chip-all {
|
||||
background-color: ${(props) => props.theme.palette.info.main + "16"};
|
||||
border-color: ${(props) => props.theme.palette.info.main + "40"};
|
||||
color: ${(props) => props.theme.palette.info.main};
|
||||
}
|
||||
.mythic-eventing-row-chip-runnable {
|
||||
background-color: ${(props) => props.theme.palette.success.main + "18"};
|
||||
border-color: ${(props) => props.theme.palette.success.main + "42"};
|
||||
color: ${(props) => props.theme.palette.success.main};
|
||||
}
|
||||
.mythic-eventing-row-chip-needs_approval {
|
||||
background-color: ${(props) => props.theme.palette.warning.main + "22"};
|
||||
border-color: ${(props) => props.theme.palette.warning.main + "66"};
|
||||
color: ${(props) => props.theme.palette.warning.main};
|
||||
}
|
||||
.mythic-eventing-row-chip-disabled {
|
||||
background-color: ${(props) => props.theme.palette.action.disabledBackground};
|
||||
border-color: ${(props) => props.theme.palette.action.disabled};
|
||||
color: ${(props) => props.theme.palette.text.secondary};
|
||||
}
|
||||
.mythic-eventing-header-chip-disabled {
|
||||
background-color: ${(props) => props.theme.palette.action.disabledBackground} !important;
|
||||
border-color: ${(props) => props.theme.palette.action.disabled} !important;
|
||||
color: ${(props) => props.theme.palette.text.secondary} !important;
|
||||
}
|
||||
.mythic-eventing-header-chip-disabled .MuiChip-icon {
|
||||
color: ${(props) => props.theme.palette.text.secondary} !important;
|
||||
}
|
||||
.mythic-eventing-runas-chip {
|
||||
align-items: center;
|
||||
background-color: ${(props) => props.theme.palette.mode === "dark" ? "rgba(255,255,255,0.06)" : "rgba(0,0,0,0.04)"};
|
||||
@@ -2609,11 +2864,6 @@ tspan {
|
||||
min-height: 1.2rem;
|
||||
padding: 0 0.35rem;
|
||||
}
|
||||
.mythic-eventing-row-chip-deleted {
|
||||
background-color: ${(props) => props.theme.palette.error.main + "16"};
|
||||
border-color: ${(props) => props.theme.palette.error.main + "40"};
|
||||
color: ${(props) => props.theme.palette.error.main};
|
||||
}
|
||||
.mythic-eventing-list-empty {
|
||||
color: ${(props) => props.theme.palette.text.secondary};
|
||||
font-size: 0.78rem;
|
||||
|
||||
Reference in New Issue
Block a user