better rabbitmq handling and task filter ui updates

This commit is contained in:
its-a-feature
2026-05-08 20:11:48 -05:00
parent ada9667a7f
commit f3c5978ee8
42 changed files with 1198 additions and 433 deletions
@@ -1,63 +1,40 @@
import React, {useEffect} from 'react';
import { styled } from '@mui/material/styles';
import Box from '@mui/material/Box';
import Button from '@mui/material/Button';
import DialogActions from '@mui/material/DialogActions';
import Checkbox from '@mui/material/Checkbox';
import Chip from '@mui/material/Chip';
import DialogContent from '@mui/material/DialogContent';
import DialogTitle from '@mui/material/DialogTitle';
import MythicTextField from '../../MythicComponents/MythicTextField';
import Switch from '@mui/material/Switch';
import Select from '@mui/material/Select';
import Chip from '@mui/material/Chip';
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 Checkbox from '@mui/material/Checkbox';
import MenuItem from '@mui/material/MenuItem';
import OutlinedInput from '@mui/material/OutlinedInput';
import Select from '@mui/material/Select';
import Switch from '@mui/material/Switch';
import Typography from '@mui/material/Typography';
import ClearIcon from '@mui/icons-material/Clear';
import FilterAltIcon from '@mui/icons-material/FilterAlt';
import MythicTextField from '../../MythicComponents/MythicTextField';
import {useQuery, gql } from '@apollo/client';
import { meState } from '../../../cache';
import {useReactiveVar} from '@apollo/client';
import {
MythicDialogBody,
MythicDialogButton,
MythicDialogFooter,
MythicDialogGrid,
MythicDialogSection,
MythicFormField,
MythicFormSwitchRow
} from '../../MythicComponents/MythicDialogLayout';
const PREFIX = 'CallbacksTabsTaskingFilterDialog';
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: "100%",
},
[`& .${classes.chips}`]: {
display: 'flex',
flexWrap: 'wrap',
},
[`& .${classes.chip}`]: {
margin: 2,
},
[`& .${classes.noLabel}`]: {
marginTop: theme.spacing(2),
}
}));
const ITEM_HEIGHT = 48;
const ITEM_HEIGHT = 42;
const ITEM_PADDING_TOP = 8;
const MenuProps = {
PaperProps: {
style: {
maxHeight: ITEM_HEIGHT * 4.5 + ITEM_PADDING_TOP,
width: 250,
maxHeight: ITEM_HEIGHT * 6 + ITEM_PADDING_TOP,
width: 320,
},
},
variant: "menu",
@@ -72,6 +49,60 @@ query operatorQuery($operation_id: Int!) {
}
}
}`;
const selectedLabel = (values, singular, plural = singular) => {
if(values.length === 0){
return "";
}
return `${values.length} ${values.length === 1 ? singular : plural}`;
}
const FilterSummaryChip = ({icon, label, muted=false}) => (
<Chip
className={`mythic-tasking-filter-summary-chip${muted ? " mythic-tasking-filter-summary-chip-muted" : ""}`}
icon={icon}
label={label}
size="small"
/>
)
const MultiSelectField = ({label, value, options, onChange, emptyLabel}) => {
const renderValue = (selected) => {
if(selected.length === 0){
return <span className="mythic-tasking-filter-select-empty">{emptyLabel}</span>
}
return (
<Box className="mythic-tasking-filter-select-chips">
{selected.map((selectedValue) => (
<Chip key={selectedValue} label={selectedValue} size="small" className="mythic-tasking-filter-selected-chip" />
))}
</Box>
);
}
return (
<FormControl fullWidth={true} size="small" className="mythic-tasking-filter-select">
<Select
multiple
displayEmpty
value={value}
onChange={onChange}
input={<OutlinedInput />}
renderValue={renderValue}
MenuProps={MenuProps}
aria-label={label}
>
{options.map((name) => (
<MenuItem key={name} value={name}>
<Checkbox color="primary" checked={value.indexOf(name) > -1} />
<ListItemText primary={name} />
</MenuItem>
))}
</Select>
</FormControl>
)
}
export function CallbacksTabsTaskingFilterDialog(props) {
const me = useReactiveVar(meState);
const [onlyOperators, setOnlyOperators] = React.useState([]);
@@ -111,7 +142,7 @@ export function CallbacksTabsTaskingFilterDialog(props) {
const commandOptionNames = props.filterCommandOptions.map(c => c.cmd);
setCommandOptions(commandOptionNames);
}
}, [props.filterOptions]);
}, [props.filterOptions, props.filterCommandOptions]);
const onSubmit = () => {
props.onSubmit({
"operatorsList": onlyOperators,
@@ -133,17 +164,20 @@ export function CallbacksTabsTaskingFilterDialog(props) {
setHideErrors(event.target.checked);
}
const handleOperatorChange = (event) => {
setOnlyOperators(event.target.value);
const value = typeof event.target.value === "string" ? event.target.value.split(",") : event.target.value;
setOnlyOperators(value);
}
const handleOnlyCommandsChange = (event) => {
setOnlyCommands(event.target.value);
if(event.target.value.length > 0){
const value = typeof event.target.value === "string" ? event.target.value.split(",") : event.target.value;
setOnlyCommands(value);
if(value.length > 0){
setEverythingBut([]);
}
}
const handleEverythingButChange = (event) => {
setEverythingBut(event.target.value);
if(event.target.value.length > 0){
const value = typeof event.target.value === "string" ? event.target.value.split(",") : event.target.value;
setEverythingBut(value);
if(value.length > 0){
setOnlyCommands([]);
}
}
@@ -153,110 +187,178 @@ export function CallbacksTabsTaskingFilterDialog(props) {
const clearAllEverythingBut = () => {
setEverythingBut([]);
}
const clearAllFilters = () => {
setOnlyOperators([]);
setOnlyHasComments(false);
setOnlyCommands([]);
setEverythingBut([]);
setOnlyParameters("");
setHideErrors(false);
}
const activeFilters = [
selectedLabel(onlyOperators, "operator"),
onlyHasComments ? "Has comments" : "",
hideErrors ? "Errors hidden" : "",
selectedLabel(onlyCommands, "included command", "included commands"),
selectedLabel(everythingBut, "excluded command", "excluded commands"),
onlyParameters !== "" ? "Parameter regex" : "",
].filter(Boolean);
return (
<Root>
<DialogTitle id="form-dialog-title">Filter Which Tasks Are Visible in this Callback</DialogTitle>
<DialogContent dividers={true} style={{overflow: "hidden"}}>
<React.Fragment>
<FormControl className={classes.formControl}>
<InputLabel id="operator-chip-label">Only Show Tasks by the Following Operators</InputLabel>
<Select
labelId="operator-chip-label"
multiple
id="operator-chip"
<Box className="mythic-tasking-filter-dialog">
<DialogTitle id="mythic-draggable-title" className="mythic-tasking-filter-dialog-title">
<Box className="mythic-tasking-filter-title-row">
<Box className="mythic-tasking-filter-title-icon">
<FilterAltIcon fontSize="small" />
</Box>
<Box sx={{minWidth: 0}}>
<Typography component="div" className="mythic-tasking-filter-title-main">
Task visibility filters
</Typography>
<Typography component="div" className="mythic-tasking-filter-title-subtitle">
Control which tasks are shown for this callback.
</Typography>
</Box>
</Box>
</DialogTitle>
<DialogContent dividers={true} className="mythic-tasking-filter-dialog-content">
<MythicDialogBody compact={true}>
<Box className="mythic-tasking-filter-summary">
{activeFilters.length > 0 ? (
activeFilters.map((filterLabel) => (
<FilterSummaryChip key={filterLabel} icon={<FilterAltIcon />} label={filterLabel} />
))
) : (
<FilterSummaryChip muted={true} icon={<FilterAltIcon />} label="No active filters" />
)}
</Box>
<MythicDialogSection
title="Operator and task state"
description="Narrow the task list by operator, comments, or error state."
>
<MythicDialogGrid minWidth="18rem">
<MythicFormField
label="Operators"
description="Only show tasks created by selected operators."
>
<MultiSelectField
label="Operators"
value={onlyOperators}
options={operatorUsernames}
onChange={handleOperatorChange}
input={<Input />}
renderValue={(selected) => (
<div className={classes.chips}>
{selected.map((value) => (
<Chip key={value} label={value} className={classes.chip} />
))}
</div>
)}
MenuProps={MenuProps}
>
{operatorUsernames.map((name) => (
<MenuItem key={name} value={name}>
<Checkbox color="primary" checked={onlyOperators.indexOf(name) > -1} />
<ListItemText primary={name} />
</MenuItem>
))}
</Select>
</FormControl>
Only Show Tasks with Comments: <Switch checked={onlyHasComments} onChange={handleCommentsChange} color="primary" name="Only Comments" inputProps={{'aria-label': 'primary checkbox'}}/>
<br/>
Hide Error Tasks:<Switch checked={hideErrors} onChange={handleHideErrorsChange} color="primary" name="Hide Errors" inputProps={{'aria-label': 'primary checkbox'}}/>
<FormControl className={classes.formControl}>
<InputLabel id="include-chip-label">Only Show These Commands</InputLabel>
<Select
labelId="include-chip-label"
multiple
id="include-chip"
value={onlyCommands}
onChange={handleOnlyCommandsChange}
input={<Input />}
renderValue={(selected) => (
<div className={classes.chips}>
{selected.map((value) => (
<Chip key={value} label={value} className={classes.chip} />
))}
</div>
)}
MenuProps={MenuProps}
emptyLabel="Any operator"
/>
</MythicFormField>
<Box className="mythic-tasking-filter-switch-stack">
<MythicFormSwitchRow
label="Only tasks with comments"
description="Require at least one comment."
control={
<Switch
checked={onlyHasComments}
onChange={handleCommentsChange}
color="primary"
name="Only Comments"
inputProps={{'aria-label': 'Only tasks with comments'}}
/>
}
/>
<MythicFormSwitchRow
label="Hide error tasks"
description="Remove tasks that are currently marked as errors."
control={
<Switch
checked={hideErrors}
onChange={handleHideErrorsChange}
color="primary"
name="Hide Errors"
inputProps={{'aria-label': 'Hide error tasks'}}
/>
}
/>
</Box>
</MythicDialogGrid>
</MythicDialogSection>
<MythicDialogSection
title="Command scope"
description="Choose commands to include, or choose commands to hide."
>
<Box className="mythic-tasking-filter-command-grid">
<MythicFormField
label="Only show commands"
description="When set, only matching command names remain visible."
>
<MultiSelectField
label="Only show commands"
value={onlyCommands}
options={commandOptions}
onChange={handleOnlyCommandsChange}
emptyLabel="No include filter"
/>
{onlyCommands.length > 0 &&
<Button
className="mythic-tasking-filter-clear-button"
onClick={clearAllOnlyCommands}
size="small"
startIcon={<ClearIcon fontSize="small" />}
variant="outlined"
>
{commandOptions.map((name) => (
<MenuItem key={name} value={name}>
<Checkbox color="primary" checked={onlyCommands.indexOf(name) > -1} />
<ListItemText primary={name} />
</MenuItem>
))}
</Select>
</FormControl>
{onlyCommands.length > 0 &&
<Button onClick={clearAllOnlyCommands} variant={"contained"}>Clear</Button>
}
<FormControl className={classes.formControl}>
<InputLabel id="exclude-chip-label">Do Not Show These Commands</InputLabel>
<Select
labelId="exclude-chip-label"
multiple
id="exclude-chip"
value={everythingBut}
onChange={handleEverythingButChange}
input={<Input />}
renderValue={(selected) => (
<div className={classes.chips}>
{selected.map((value) => (
<Chip key={value} label={value} className={classes.chip} />
))}
</div>
)}
MenuProps={MenuProps}
Clear include
</Button>
}
</MythicFormField>
<Box className="mythic-tasking-filter-choice-divider">or</Box>
<MythicFormField
label="Hide commands"
description="When set, selected command names are removed from view."
>
<MultiSelectField
label="Hide commands"
value={everythingBut}
options={commandOptions}
onChange={handleEverythingButChange}
emptyLabel="No exclude filter"
/>
{everythingBut.length > 0 &&
<Button
className="mythic-tasking-filter-clear-button"
onClick={clearAllEverythingBut}
size="small"
startIcon={<ClearIcon fontSize="small" />}
variant="outlined"
>
{commandOptions.map((name) => (
<MenuItem key={name} value={name}>
<Checkbox color="primary" checked={everythingBut.indexOf(name) > -1} />
<ListItemText primary={name} />
</MenuItem>
))}
</Select>
</FormControl>
{everythingBut.length > 0 &&
<Button onClick={clearAllEverythingBut} variant={"contained"}>Clear</Button>
}
<MythicTextField value={onlyParameters} onChange={onChange} name="Only Show Tasks with the Following Parameter Regex"/>
</React.Fragment>
</DialogContent>
<DialogActions>
<Button onClick={props.onClose} variant="contained" >
Close
</Button>
<Button onClick={onSubmit} color="success" variant="contained" >
Filter
</Button>
</DialogActions>
</Root>
Clear exclude
</Button>
}
</MythicFormField>
</Box>
</MythicDialogSection>
<MythicDialogSection
title="Parameter matching"
description="Apply a regular expression to task parameters."
>
<MythicTextField
value={onlyParameters}
onChange={onChange}
name="Parameter regex"
placeholder="Regex to match task parameters"
marginBottom="0px"
marginTop="0px"
/>
</MythicDialogSection>
</MythicDialogBody>
</DialogContent>
<MythicDialogFooter className="mythic-tasking-filter-dialog-actions">
<MythicDialogButton onClick={clearAllFilters}>
Clear all
</MythicDialogButton>
<MythicDialogButton onClick={props.onClose}>
Close
</MythicDialogButton>
<MythicDialogButton onClick={onSubmit} intent="primary">
Apply filters
</MythicDialogButton>
</MythicDialogFooter>
</Box>
);
}
@@ -3,6 +3,7 @@ import SendIcon from '@mui/icons-material/Send';
import React from 'react';
import {TextField} from '@mui/material';
import TuneIcon from '@mui/icons-material/Tune';
import TerminalIcon from '@mui/icons-material/Terminal';
import { MythicDialog } from '../../MythicComponents/MythicDialog';
import {CallbacksTabsTaskingFilterDialog} from './CallbacksTabsTaskingFilterDialog';
import {CallbacksTabsTaskingInputTokenSelect} from './CallbacksTabsTaskingInputTokenSelect';
@@ -20,6 +21,7 @@ import {GetMythicSetting} from "../../MythicComponents/MythicSavedUserSetting";
import {getSkewedNow} from "../../utilities/Time";
import { useTheme } from '@mui/material/styles';
import {MythicStyledTooltip} from "../../MythicComponents/MythicStyledTooltip";
import {getReadableTextColor, isValidHexColor} from "../../MythicComponents/MythicColorInput";
const GetLoadedCommandsSubscription = gql`
subscription GetLoadedCommandsSubscription($callback_id: Int!){
@@ -169,6 +171,27 @@ const IsRepeatableCLIParameterType = (parameter_type) => {
return false;
}
}
const TaskingContextChip = ({title, label, value, color, callbackColor, emphasized=false}) => {
const safeColor = isValidHexColor(color) ? color : "#000000";
const borderColor = isValidHexColor(callbackColor) ? callbackColor : undefined;
const tooltipTitle = `${title}: ${value}`;
return (
<MythicStyledTooltip title={tooltipTitle}>
<span className={`mythic-tasking-context-chip${emphasized ? " mythic-tasking-context-chip-emphasized" : ""}`}
style={{
backgroundColor: safeColor,
borderColor: borderColor,
color: getReadableTextColor(safeColor),
}}
title={tooltipTitle}>
{label !== "" &&
<span className="mythic-tasking-context-chip-label">{label}</span>
}
<span className="mythic-tasking-context-chip-value">{value}</span>
</span>
</MythicStyledTooltip>
)
}
export function CallbacksTabsTaskingInputPreMemo(props){
const toastId = "tasking-toast-message";
@@ -1607,177 +1630,186 @@ export function CallbacksTabsTaskingInputPreMemo(props){
inputRef.current.focus();
}
}, [props.focus])
const showTaskingContext = !hideTaskingContext.current;
const taskingContextChips = [
{
key: "impersonation_context",
title: "Impersonation Context",
label: "User",
value: callbackContext?.impersonation_context,
color: theme.taskContextImpersonationColor,
emphasized: true,
},
{
key: "user",
title: "User Context" + (callbackContext.integrity_level > 2 ? " (high integrity)" : ""),
label: "User",
value: callbackContext?.user ? `${callbackContext.user}${callbackContext.integrity_level > 2 ? "*" : ""}` : "",
color: theme.taskContextColor,
emphasized: callbackContext.integrity_level > 2,
},
{
key: "cwd",
title: "Current Working Directory",
label: "Dir",
value: callbackContext?.cwd,
color: theme.taskContextColor,
},
{
key: "host",
title: "Hostname",
label: "Host",
value: callbackContext?.host,
color: theme.taskContextColor,
},
{
key: "ip",
title: "First IP Address",
label: "IP",
value: callbackContext?.ip,
color: theme.taskContextColor,
},
{
key: "pid",
title: "Process ID",
label: "PID",
value: callbackContext?.pid,
color: theme.taskContextColor,
},
{
key: "architecture",
title: "Process Architecture",
label: "Arch",
value: callbackContext?.architecture,
color: theme.taskContextColor,
},
{
key: "process_short_name",
title: "Process Name",
label: "Process",
value: callbackContext?.process_short_name,
color: theme.taskContextColor,
},
{
key: "extra_info",
title: "Extra Callback Context",
label: "",
value: callbackContext?.extra_info,
color: theme.taskContextExtraColor,
emphasized: true,
},
].filter((chip) => showTaskingContext && taskingContextFields.current.includes(chip.key) && chip.value !== undefined && chip.value !== "");
return (
<div style={{position: "relative"}}>
<div className="mythic-tasking-composer">
{backdropOpen && <Backdrop open={backdropOpen} style={{zIndex: 2, position: "absolute"}} invisible={false}>
<CircularProgress color="inherit" size={30}/>
</Backdrop>
}
{reverseSearching &&
<div className="mythic-tasking-reverse-search">
<Typography component="span" className="mythic-tasking-reverse-search-label">
reverse-i-search
</Typography>
<TextField
placeholder={"Search previous commands"}
onKeyDown={onReverseSearchKeyDown}
onChange={handleReverseSearchInputChange}
size="small"
color={"secondary"}
autoFocus={true}
variant="outlined"
value={reverseSearchString}
fullWidth={true}
className="mythic-tasking-reverse-search-input"
InputProps={{
type: 'search',
}}
/>
</div>
}
{taskingContextChips.length > 0 &&
<div className="mythic-tasking-context-row">
{taskingContextChips.map((chip) => (
<TaskingContextChip
key={chip.key}
title={chip.title}
label={chip.label}
value={chip.value}
color={chip.color}
callbackColor={callbackContext.color}
emphasized={chip.emphasized}
/>
))}
</div>
}
<div className="mythic-tasking-command-row">
{tokenOptions.current.length > 0 ? (
<CallbacksTabsTaskingInputTokenSelect
options={tokenOptions.current}
changeSelectedToken={props.changeSelectedToken}
width={"13rem"}
modern={true}
className="mythic-tasking-token-select"
/>
) : null}
<TextField
placeholder={"Search previous commands"}
onKeyDown={onReverseSearchKeyDown}
onChange={handleReverseSearchInputChange}
placeholder={"Task an agent..."}
onKeyDown={onKeyDown}
onChange={handleInputChange}
size="small"
color={"secondary"}
autoFocus={true}
variant="outlined"
value={reverseSearchString}
multiline={true}
maxRows={15}
disabled={reverseSearching}
value={message}
autoFocus={true}
fullWidth={true}
inputRef={inputRef}
className="mythic-tasking-command-input"
InputProps={{
type: 'search',
startAdornment: <React.Fragment><Typography
style={{width: "10%"}}>reverse-i-search:</Typography></React.Fragment>
spellCheck: false,
autoFocus: true,
startAdornment:
<span className="mythic-tasking-command-prefix">
<TerminalIcon fontSize="small" />
</span>,
endAdornment:
<div className="mythic-tasking-action-row">
{commandPayloadType !== "" &&
<MythicStyledTooltip title={commandPayloadType}>
<span className="mythic-tasking-payload-chip">
<MythicAgentSVGIcon payload_type={commandPayloadType}
style={{width: "20px", height: "20px"}}/>
</span>
</MythicStyledTooltip>
}
{props.filterTasks &&
<MythicStyledTooltip title={activeFiltering ? "Adjust active task filters" : "Filter task history"}>
<IconButton
className={`mythic-tasking-action-button ${activeFiltering ? "mythic-tasking-action-button-warning" : "mythic-tasking-action-button-neutral"}`}
onClick={onClickFilter}
disableRipple={true}
disableFocusRipple={true}
size="small"
aria-label="Filter task history"><TuneIcon fontSize="small"/></IconButton>
</MythicStyledTooltip>
}
<MythicStyledTooltip title={"Submit task"}>
<IconButton
className="mythic-tasking-action-button mythic-tasking-action-button-success"
disableRipple={true}
disableFocusRipple={true}
onClick={onSubmitCommandLine}
size="small"
aria-label="Submit task"><SendIcon fontSize="small"/>
</IconButton>
</MythicStyledTooltip>
</div>
}}
/>
}
{callbackContext?.impersonation_context !== "" && !hideTaskingContext.current && taskingContextFields.current.includes("impersonation_context") &&
<MythicStyledTooltip title={"Impersonation Context"}>
<span className={"rounded-tab"} style={{
backgroundColor: theme.taskContextImpersonationColor,
borderColor: callbackContext.color === "" ? "" : callbackContext.color
}}>
<b>{"User: "}</b>{callbackContext.impersonation_context}
</span>
</MythicStyledTooltip>
}
{callbackContext?.user !== "" && !hideTaskingContext.current && taskingContextFields.current.includes("user") &&
<MythicStyledTooltip title={"User Context" + (callbackContext.integrity_level > 2 ? " (high integrity)" : "")}>
<span className={"rounded-tab"} style={{
backgroundColor: theme.taskContextColor,
borderColor: callbackContext.color === "" ? "" : callbackContext.color
}}>
<b>{"User: "}</b>{callbackContext.user}{callbackContext.integrity_level > 2 ? "*" : ""}
</span>
</MythicStyledTooltip>
}
{callbackContext?.cwd !== "" && !hideTaskingContext.current && taskingContextFields.current.includes("cwd") &&
<MythicStyledTooltip title={"Current Working Directory"}>
<span className={"rounded-tab"} style={{
backgroundColor: theme.taskContextColor,
borderColor: callbackContext.color === "" ? "" : callbackContext.color
}}>
<b>{"Dir: "}</b>{callbackContext.cwd}
</span>
</MythicStyledTooltip>
}
{callbackContext?.host !== "" && !hideTaskingContext.current && taskingContextFields.current.includes("host") &&
<MythicStyledTooltip title={"Hostname"}>
<span className={"rounded-tab"} style={{
backgroundColor: theme.taskContextColor,
borderColor: callbackContext.color === "" ? "" : callbackContext.color
}}>
<b>{"Host: "}</b>{callbackContext.host}
</span>
</MythicStyledTooltip>
}
{callbackContext?.ip !== "" && !hideTaskingContext.current && taskingContextFields.current.includes("ip") &&
<MythicStyledTooltip title={"First IP Address"}>
<span className={"rounded-tab"} style={{
backgroundColor: theme.taskContextColor,
borderColor: callbackContext.color === "" ? "" : callbackContext.color
}}>
<b>{"IP: "}</b>{callbackContext.ip}
</span>
</MythicStyledTooltip>
}
{callbackContext?.pid !== "" && !hideTaskingContext.current && taskingContextFields.current.includes("pid") &&
<MythicStyledTooltip title={"Process ID"}>
<span className={"rounded-tab"} style={{
backgroundColor: theme.taskContextColor,
borderColor: callbackContext.color === "" ? "" : callbackContext.color
}}>
<b>{"PID: "}</b>{callbackContext.pid}
</span>
</MythicStyledTooltip>
}
{callbackContext?.architecture !== "" && !hideTaskingContext.current && taskingContextFields.current.includes("architecture") &&
<MythicStyledTooltip title={"Process Architecture"}>
<span className={"rounded-tab"} style={{
backgroundColor: theme.taskContextColor,
borderColor: callbackContext.color === "" ? "" : callbackContext.color
}}>
<b>{"Arch: "}</b>{callbackContext.architecture}
</span>
</MythicStyledTooltip>
}
{callbackContext?.process_short_name !== "" && !hideTaskingContext.current && taskingContextFields.current.includes("process_short_name") &&
<MythicStyledTooltip title={"Process Name"}>
<span className={"rounded-tab"} style={{
backgroundColor: theme.taskContextColor,
borderColor: callbackContext.color === "" ? "" : callbackContext.color
}}>
<b>{"Process: "}</b>{callbackContext.process_short_name}
</span>
</MythicStyledTooltip>
}
{callbackContext?.extra_info !== "" && !hideTaskingContext.current && taskingContextFields.current.includes("extra_info") &&
<MythicStyledTooltip title={"Extra Callback Context"}>
<span className={"rounded-tab"} style={{
backgroundColor: theme.taskContextExtraColor,
borderColor: callbackContext.color === "" ? "" : callbackContext.color
}}>
{callbackContext.extra_info}
</span>
</MythicStyledTooltip>
}
<TextField
placeholder={"Task an agent..."}
onKeyDown={onKeyDown}
onChange={handleInputChange}
size="small"
color={"secondary"}
variant="outlined"
multiline={true}
maxRows={15}
disabled={reverseSearching}
value={message}
autoFocus={true}
fullWidth={true}
inputRef={inputRef}
style={{marginBottom: "0px", marginTop: "0px", paddingTop: "0px"}}
InputProps={{
type: 'search',
spellCheck: false,
autoFocus: true,
style: {paddingTop: "0px", paddingBottom: "0px", paddingRight: "5px"},
endAdornment:
<React.Fragment>
<IconButton
color="info"
variant="contained"
disableRipple={true}
disableFocusRipple={true}
onClick={onSubmitCommandLine}
size="large"><SendIcon/>
</IconButton>
{props.filterTasks &&
<IconButton
color={activeFiltering ? "warning" : "secondary"}
variant="contained"
onClick={onClickFilter}
style={{paddingLeft: 0}}
disableRipple={true}
disableFocusRipple={true}
size="large"><TuneIcon/></IconButton>
}
{commandPayloadType !== "" &&
<MythicAgentSVGIcon payload_type={commandPayloadType}
style={{width: "35px", height: "35px"}}/>
}
</React.Fragment>
,
startAdornment: <React.Fragment>
{tokenOptions.current.length > 0 ? (
<CallbacksTabsTaskingInputTokenSelect options={tokenOptions.current}
changeSelectedToken={props.changeSelectedToken}/>
) : null}
</React.Fragment>
}}
/>
</div>
{openFilterOptionsDialog &&
<MythicDialog fullWidth={true} maxWidth="md" open={openFilterOptionsDialog}
onClose={() => {
@@ -42,15 +42,16 @@ export function CallbacksTabsTaskingInputTokenSelect(props) {
}
}
return (
<FormControl style={{width: props.width ? props.width : "20%"}}>
<FormControl className={props.className || ""} size={props.modern ? "small" : undefined} style={{width: props.width ? props.width : "20%"}}>
<Select
labelId="demo-dialog-select-label"
id="demo-dialog-select"
value={selected}
onChange={handleChange}
variant="filled"
variant={props.modern ? "outlined" : "filled"}
size={props.modern ? "small" : undefined}
renderValue={renderValue}
input={<Input style={{width: "100%"}}/>}
input={props.modern ? undefined : <Input style={{width: "100%"}}/>}
>
<MenuItem value={"Default Token"} key={0}>Default Token</MenuItem>
{options.map( (opt) => (
@@ -60,4 +61,3 @@ export function CallbacksTabsTaskingInputTokenSelect(props) {
</FormControl>
);
}
@@ -180,9 +180,16 @@ const NonInteractiveResponseDisplay = (props) => {
fetchPolicy: "no-cache",
onCompleted: (data) => {
const responseArray = data.response.map(decodeResponse);
rawResponsesRef.current = responseArray;
setRawResponses(responseArray);
setTrackedTotalCount(getAggregateCount(data));
const shouldPreserveStreamedResponses = search === "" && currentPageRef.current === 1;
const nextResponses = shouldPreserveStreamedResponses ? mergeResponsesById({
existingResponses: rawResponsesRef.current,
incomingResponses: responseArray,
limit: pageSize,
}) : responseArray;
rawResponsesRef.current = nextResponses;
setRawResponses(nextResponses);
const aggregateCount = Math.max(getAggregateCount(data), shouldPreserveStreamedResponses ? nextResponses.length : 0);
setTrackedTotalCount(aggregateCount);
highestKnownResponseID.current = Math.max(highestKnownResponseID.current, getLatestResponseID(data));
pageDataLoaded.current = true;
setOpenBackdrop(false);
@@ -195,15 +202,20 @@ const NonInteractiveResponseDisplay = (props) => {
fetchPolicy: "no-cache",
onCompleted: (data) => {
const responseArray = data.response.map(decodeResponse);
rawResponsesRef.current = responseArray;
setRawResponses(responseArray);
const aggregateCount = getAggregateCount(data);
const nextResponses = search === "" ? mergeResponsesById({
existingResponses: rawResponsesRef.current,
incomingResponses: responseArray,
limit: MAX_SELECT_ALL_RESPONSES,
}) : responseArray;
rawResponsesRef.current = nextResponses;
setRawResponses(nextResponses);
const aggregateCount = Math.max(getAggregateCount(data), search === "" ? nextResponses.length : 0);
setTrackedTotalCount(aggregateCount);
highestKnownResponseID.current = Math.max(highestKnownResponseID.current, getLatestResponseID(data));
pageDataLoaded.current = true;
currentPageRef.current = 1;
setCurrentPage(1);
warnSelectAllCapped(responseArray.length, aggregateCount);
warnSelectAllCapped(nextResponses.length, aggregateCount);
setOpenBackdrop(false);
},
onError: (data) => {
@@ -262,17 +274,32 @@ const NonInteractiveResponseDisplay = (props) => {
return () => clearTimeout(timeoutID);
}, [openBackdrop]);
React.useEffect( () => {
taskResponseCountRef.current = props.task.response_count || 0;
const nextResponseCount = props.task.response_count || 0;
const previousTaskResponseCount = taskResponseCountRef.current;
const previousKnownResponseCount = Math.max(totalCountRef.current, previousTaskResponseCount);
taskResponseCountRef.current = nextResponseCount;
if(search !== ""){
return;
}
if(props.task.response_count > totalCountRef.current){
setTrackedTotalCount(props.task.response_count);
if(nextResponseCount > totalCountRef.current){
setTrackedTotalCount(nextResponseCount);
}
}, [props.task.response_count, search, setTrackedTotalCount]);
if(nextResponseCount <= previousKnownResponseCount){
return;
}
const pageStartIndex = (currentPageRef.current - 1) * pageSize;
const pageEndIndex = currentPageRef.current * pageSize;
const currentPageCouldContainNewResponses = previousKnownResponseCount < pageEndIndex &&
nextResponseCount > pageStartIndex;
if(props.selectAllOutput){
fetchSelectAllResponses();
}else if(rawResponsesRef.current.length === 0 || currentPageCouldContainNewResponses){
fetchResponsePage(currentPageRef.current, false);
}
}, [fetchResponsePage, fetchSelectAllResponses, pageSize, props.selectAllOutput, props.task.response_count, search, setTrackedTotalCount]);
const subscriptionDataCallback = React.useCallback( ({data}) => {
const streamedResponses = data?.data?.response_stream || [];
if(streamedResponses.length === 0 || !pageDataLoaded.current){
if(streamedResponses.length === 0){
return;
}
const previousHighestID = highestKnownResponseID.current;
@@ -869,11 +869,23 @@ export const ResponseDisplayInteractive = (props) =>{
setSearch(newSearch || "");
}, []);
React.useEffect( () => {
taskResponseCountRef.current = props.task.response_count || 0;
const nextResponseCount = props.task.response_count || 0;
const previousKnownResponseCount = Math.max(totalCountRef.current, taskResponseCountRef.current);
taskResponseCountRef.current = Math.max(taskResponseCountRef.current, nextResponseCount);
if(search === ""){
setTrackedTotalCount(props.task.response_count || 0);
setTrackedTotalCount(nextResponseCount);
}
}, [props.task.response_count, search, setTrackedTotalCount]);
if(search !== "" || nextResponseCount <= previousKnownResponseCount){
return;
}
const pageStartIndex = (currentPageRef.current - 1) * pageSize.current;
const pageEndIndex = currentPageRef.current * pageSize.current;
const currentPageCouldContainNewResponses = previousKnownResponseCount < pageEndIndex &&
nextResponseCount > pageStartIndex;
if(props.selectAllOutput || rawResponsesRef.current.length === 0 || currentPageCouldContainNewResponses){
fetchResponsePage(currentPageRef.current, false);
}
}, [fetchResponsePage, props.selectAllOutput, props.task.response_count, search, setTrackedTotalCount]);
const toggleANSIColor = () => {
setUseANSIColor(!useASNIColor);
}
+377
View File
@@ -330,6 +330,383 @@ tspan {
background-color: ${(props) => alpha(props.theme.palette.error.main, props.theme.palette.mode === "dark" ? 0.2 : 0.12)};
color: ${(props) => props.theme.palette.error.main};
}
.mythic-tasking-composer {
background-color: ${(props) => alpha(props.theme.palette.background.paper, props.theme.palette.mode === "dark" ? 0.96 : 0.98)};
border-top: 1px solid ${(props) => props.theme.table?.borderSoft || props.theme.borderColor};
box-shadow: ${(props) => props.theme.palette.mode === "dark" ? "0 -10px 28px rgba(0, 0, 0, 0.24)" : "0 -10px 28px rgba(15, 23, 42, 0.06)"};
flex: 0 0 auto;
min-width: 0;
padding: 0.55rem 0.65rem 0.6rem;
position: relative;
width: 100%;
}
.mythic-tasking-context-row {
align-items: center;
display: flex;
gap: 0.28rem;
margin-bottom: 0.45rem;
min-height: 24px;
min-width: 0;
overflow-x: auto;
overflow-y: hidden;
padding-bottom: 0.05rem;
}
.mythic-tasking-context-chip {
align-items: center;
border: 1px solid ${(props) => alpha(props.theme.palette.common.black, props.theme.palette.mode === "dark" ? 0.22 : 0.1)};
border-radius: ${(props) => props.theme.shape.borderRadius}px;
display: inline-flex;
flex: 0 0 auto;
font-size: 0.72rem;
font-weight: 750;
gap: 0.28rem;
line-height: 1;
max-width: min(28rem, 72vw);
min-height: 22px;
overflow: hidden;
padding: 0.22rem 0.45rem;
text-overflow: ellipsis;
white-space: nowrap;
}
.mythic-tasking-context-chip-emphasized {
box-shadow: inset 0 0 0 1px ${(props) => alpha(props.theme.palette.common.white, props.theme.palette.mode === "dark" ? 0.12 : 0.18)};
}
.mythic-tasking-context-chip-label {
flex: 0 0 auto;
font-size: 0.64rem;
font-weight: 850;
letter-spacing: 0;
opacity: 0.78;
}
.mythic-tasking-context-chip-value {
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
}
.mythic-tasking-command-row {
align-items: flex-end;
display: flex;
gap: 0.45rem;
min-width: 0;
width: 100%;
}
.mythic-tasking-token-select {
flex: 0 0 13rem;
max-width: 40%;
min-width: 9.5rem;
}
.mythic-tasking-token-select .MuiOutlinedInput-root {
background-color: ${(props) => props.theme.palette.background.paper};
border-radius: ${(props) => props.theme.shape.borderRadius}px;
min-height: 37px;
}
.mythic-tasking-token-select .MuiSelect-select {
font-size: 0.78rem;
font-weight: 650;
min-height: 0 !important;
overflow: hidden;
padding-bottom: 0.52rem;
padding-top: 0.52rem;
text-overflow: ellipsis;
white-space: nowrap;
}
.mythic-tasking-command-input {
flex: 1 1 auto;
min-width: 10rem;
}
.mythic-tasking-command-input .MuiOutlinedInput-root {
align-items: flex-end;
background-color: ${(props) => props.theme.palette.mode === "dark" ? alpha(props.theme.palette.common.white, 0.035) : alpha(props.theme.palette.common.black, 0.018)};
border-radius: ${(props) => props.theme.shape.borderRadius}px;
padding: 0.1rem 0.24rem 0.1rem 0;
}
.mythic-tasking-command-input .MuiOutlinedInput-root:hover {
background-color: ${(props) => props.theme.palette.mode === "dark" ? alpha(props.theme.palette.common.white, 0.05) : alpha(props.theme.palette.common.black, 0.026)};
}
.mythic-tasking-command-input .MuiInputBase-input {
font-family: ${(props) => props.theme.typography.fontFamily};
font-size: 0.86rem;
line-height: 1.45;
padding-bottom: 0.48rem;
padding-top: 0.48rem;
}
.mythic-tasking-command-prefix {
align-items: center;
color: ${(props) => props.theme.palette.text.secondary};
display: inline-flex;
flex: 0 0 auto;
height: 30px;
justify-content: center;
margin-left: 0.35rem;
margin-right: 0.35rem;
margin-top: 0.12rem;
opacity: 0.86;
}
.mythic-tasking-action-row {
align-items: center;
align-self: flex-end;
display: inline-flex;
flex: 0 0 auto;
gap: 0.18rem;
margin-bottom: 0.12rem;
padding-left: 0.28rem;
}
.mythic-tasking-action-button.MuiIconButton-root {
background-color: ${(props) => props.theme.palette.mode === "dark" ? "rgba(255,255,255,0.045)" : "rgba(0,0,0,0.025)"};
border: 1px solid ${(props) => props.theme.table?.borderSoft || props.theme.borderColor};
border-radius: ${(props) => props.theme.shape.borderRadius}px;
color: ${(props) => props.theme.palette.text.secondary};
height: 28px;
padding: 0;
transition: background-color 120ms ease, border-color 120ms ease, color 120ms ease;
width: 28px;
}
.mythic-tasking-action-button-neutral.MuiIconButton-root:hover {
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-tasking-action-button-warning.MuiIconButton-root:hover {
background-color: ${(props) => alpha(props.theme.palette.warning.main, props.theme.palette.mode === "dark" ? 0.2 : 0.12)};
border-color: ${(props) => alpha(props.theme.palette.warning.main, props.theme.palette.mode === "dark" ? 0.52 : 0.36)};
color: ${(props) => props.theme.palette.warning.main};
}
.mythic-tasking-action-button-success.MuiIconButton-root:hover {
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-tasking-payload-chip {
align-items: center;
background-color: ${(props) => props.theme.palette.mode === "dark" ? "rgba(255,255,255,0.045)" : "rgba(0,0,0,0.025)"};
border: 1px solid ${(props) => props.theme.table?.borderSoft || props.theme.borderColor};
border-radius: ${(props) => props.theme.shape.borderRadius}px;
display: inline-flex;
flex: 0 0 auto;
height: 28px;
justify-content: center;
width: 28px;
}
.mythic-tasking-reverse-search {
align-items: center;
background-color: ${(props) => props.theme.palette.mode === "dark" ? alpha(props.theme.palette.common.white, 0.045) : alpha(props.theme.palette.common.black, 0.024)};
border: 1px solid ${(props) => props.theme.table?.borderSoft || props.theme.borderColor};
border-radius: ${(props) => props.theme.shape.borderRadius}px;
display: flex;
gap: 0.45rem;
margin-bottom: 0.45rem;
min-width: 0;
padding: 0.28rem;
}
.mythic-tasking-reverse-search-label {
color: ${(props) => props.theme.palette.text.secondary};
flex: 0 0 auto;
font-size: 0.72rem;
font-weight: 800;
padding-left: 0.3rem;
}
.mythic-tasking-reverse-search-input .MuiOutlinedInput-root {
background-color: ${(props) => props.theme.palette.background.paper};
}
.mythic-tasking-filter-dialog {
background-color: ${(props) => props.theme.surfaces?.raised || props.theme.palette.background.paper};
color: ${(props) => props.theme.palette.text.primary};
}
.mythic-tasking-filter-dialog-title.MuiDialogTitle-root {
background-image: ${getSectionHeaderGradient} !important;
border-bottom: 1px solid ${(props) => props.theme.table?.borderSoft || props.theme.borderColor} !important;
color: ${(props) => props.theme.pageHeaderText?.main || props.theme.palette.text.primary};
cursor: move;
padding: 1rem 1.15rem !important;
position: relative;
}
.mythic-tasking-filter-dialog-title.MuiDialogTitle-root::before {
background-color: ${getSectionHeaderAccent};
bottom: 0;
box-shadow: 0 0 0 1px ${(props) => alpha(props.theme.pageHeaderText?.main || props.theme.palette.text.primary, 0.2)};
content: "";
left: 0;
position: absolute;
top: 0;
width: 6px;
}
.mythic-tasking-filter-title-row {
align-items: center;
display: flex;
gap: 0.8rem;
min-width: 0;
min-height: 42px;
padding-left: 0.55rem;
}
.mythic-tasking-filter-title-icon {
align-items: center;
background-color: ${(props) => alpha(props.theme.pageHeaderText?.main || props.theme.palette.text.primary, 0.12)};
border: 1px solid ${(props) => alpha(props.theme.pageHeaderText?.main || props.theme.palette.text.primary, 0.24)};
border-radius: ${(props) => props.theme.shape.borderRadius}px;
display: inline-flex;
flex: 0 0 auto;
height: 32px;
justify-content: center;
width: 32px;
}
.mythic-tasking-filter-title-main {
color: ${(props) => props.theme.pageHeaderText?.main || props.theme.palette.text.primary};
font-size: 1rem;
font-weight: 850;
line-height: 1.15;
}
.mythic-tasking-filter-title-subtitle {
color: ${(props) => alpha(props.theme.pageHeaderText?.main || props.theme.palette.text.primary, 0.78)};
font-size: 0.76rem;
font-weight: 600;
line-height: 1.25;
margin-top: 0.15rem;
}
.mythic-tasking-filter-dialog-actions.MuiDialogActions-root {
background-color: ${(props) => props.theme.surfaces?.muted || props.theme.palette.background.paper} !important;
border-top: 1px solid ${(props) => props.theme.table?.borderSoft || props.theme.borderColor} !important;
gap: 0.65rem !important;
padding: 0.85rem 1rem !important;
}
.mythic-tasking-filter-dialog-actions.MuiDialogActions-root .MuiButton-root {
min-height: 36px;
min-width: 8.25rem;
padding-left: 1rem;
padding-right: 1rem;
}
.mythic-tasking-filter-dialog-actions.MuiDialogActions-root .MuiButton-root:first-of-type {
margin-right: auto;
}
.mythic-tasking-filter-dialog-content.MuiDialogContent-root {
background-color: ${(props) => props.theme.surfaces?.raised || props.theme.palette.background.paper};
max-height: min(72vh, 48rem);
min-width: min(46rem, calc(100vw - 3rem));
overflow: auto;
padding: 1rem !important;
}
.mythic-tasking-filter-summary {
align-items: center;
display: flex;
flex-wrap: wrap;
gap: 0.35rem;
min-height: 28px;
}
.mythic-tasking-filter-summary-chip.MuiChip-root {
background-color: ${(props) => alpha(props.theme.palette.primary.main, props.theme.palette.mode === "dark" ? 0.18 : 0.1)};
border: 1px solid ${(props) => alpha(props.theme.palette.primary.main, props.theme.palette.mode === "dark" ? 0.42 : 0.28)};
border-radius: ${(props) => props.theme.shape.borderRadius}px;
color: ${(props) => props.theme.palette.primary.main};
font-size: 0.72rem;
font-weight: 750;
}
.mythic-tasking-filter-summary-chip .MuiChip-icon {
color: inherit;
font-size: 1rem;
}
.mythic-tasking-filter-summary-chip-muted.MuiChip-root {
background-color: ${(props) => props.theme.palette.mode === "dark" ? "rgba(255,255,255,0.045)" : "rgba(0,0,0,0.025)"};
border-color: ${(props) => props.theme.table?.borderSoft || props.theme.borderColor};
color: ${(props) => props.theme.palette.text.secondary};
}
.mythic-tasking-filter-switch-stack {
display: flex;
flex-direction: column;
gap: 0.55rem;
min-width: 0;
}
.mythic-tasking-filter-select .MuiOutlinedInput-root {
background-color: ${(props) => props.theme.palette.background.paper};
border-radius: ${(props) => props.theme.shape.borderRadius}px;
min-height: 38px;
}
.mythic-tasking-filter-select .MuiSelect-select {
align-items: center;
display: flex;
min-height: 0 !important;
padding-bottom: 0.42rem;
padding-top: 0.42rem;
}
.mythic-tasking-filter-select-empty {
color: ${(props) => props.theme.palette.text.secondary};
font-size: 0.78rem;
font-weight: 650;
}
.mythic-tasking-filter-select-chips {
align-items: center;
display: flex;
flex-wrap: wrap;
gap: 0.25rem;
min-width: 0;
}
.mythic-tasking-filter-selected-chip.MuiChip-root {
background-color: ${(props) => props.theme.palette.mode === "dark" ? "rgba(255,255,255,0.07)" : "rgba(0,0,0,0.045)"};
border: 1px solid ${(props) => props.theme.table?.borderSoft || props.theme.borderColor};
border-radius: ${(props) => props.theme.shape.borderRadius}px;
color: ${(props) => props.theme.palette.text.primary};
font-size: 0.72rem;
font-weight: 700;
}
.mythic-tasking-filter-command-grid {
align-items: start;
display: grid;
gap: 0.6rem;
grid-template-columns: minmax(0, 1fr) auto minmax(0, 1fr);
min-width: 0;
width: 100%;
}
.mythic-tasking-filter-choice-divider {
align-self: center;
color: ${(props) => props.theme.palette.text.secondary};
font-size: 0.72rem;
font-weight: 750;
padding-top: 1.55rem;
text-align: center;
}
.mythic-tasking-filter-clear-button.MuiButton-root {
align-self: flex-start;
background-color: ${(props) => props.theme.palette.mode === "dark" ? "rgba(255,255,255,0.05)" : "rgba(0,0,0,0.025)"} !important;
border: 1px solid ${(props) => props.theme.table?.borderSoft || props.theme.borderColor} !important;
border-radius: ${(props) => props.theme.shape.borderRadius}px;
box-shadow: none;
color: ${(props) => props.theme.palette.text.secondary} !important;
font-size: 0.72rem;
font-weight: 750;
margin-top: 0.4rem;
min-height: 28px;
text-transform: none;
}
.mythic-tasking-filter-clear-button.MuiButton-root:hover {
background-color: ${(props) => alpha(props.theme.palette.warning.main, props.theme.palette.mode === "dark" ? 0.18 : 0.1)} !important;
border-color: ${(props) => alpha(props.theme.palette.warning.main, props.theme.palette.mode === "dark" ? 0.48 : 0.32)} !important;
color: ${(props) => props.theme.palette.warning.main} !important;
}
@media screen and (max-width: 760px) {
.mythic-tasking-filter-dialog-content.MuiDialogContent-root {
min-width: 0;
}
.mythic-tasking-filter-command-grid {
grid-template-columns: 1fr;
}
.mythic-tasking-filter-choice-divider {
padding-top: 0;
}
.mythic-tasking-filter-dialog-actions.MuiDialogActions-root .MuiButton-root,
.mythic-tasking-filter-dialog-actions.MuiDialogActions-root .MuiButton-root:first-of-type {
flex: 1 1 100%;
margin-right: 0;
}
}
@media screen and (max-width: 760px) {
.mythic-tasking-command-row {
align-items: stretch;
flex-direction: column;
}
.mythic-tasking-token-select {
flex: 0 0 auto;
max-width: 100%;
width: 100% !important;
}
}
.mythic-file-browser-iconButtonCompound.MuiIconButton-root {
gap: 0;
padding: 0 0.15rem;
+4 -1
View File
@@ -82,7 +82,7 @@ func GetIntendedMythicServiceNames() ([]string, error) {
if mythicEnv.GetBool("postgres_debug") {
containerList = append(containerList, service)
}
*/
}
}
@@ -321,6 +321,9 @@ Setting this to "true" means that the local Mythic/rabbitmq-docker/Dockerfile is
mythicEnv.SetDefault("rabbitmq_vhost", "mythic_vhost")
mythicEnvInfo["rabbitmq_vhost"] = `The VHost attribute on RabbitMQ allows you to logically separate queues into separate "hosts" while keeping the same names. This helps with collisions if you have multiple instances of something running concurrently.`
mythicEnv.SetDefault("custom_rpc_timeout", 0)
mythicEnvInfo["custom_rpc_timeout"] = `This is an optional timeout in seconds for Mythic RPC calls that are expected to be slow but still valid. If set to 0, Mythic uses the default RPC timeout.`
// jwt configuration ---------------------------------------------
mythicEnv.SetDefault("jwt_secret", utils.GenerateRandomPassword(30))
mythicEnvInfo["jwt_secret"] = `This is the randomly generated password used to sign JWTs to ensure they're valid for this Mythic instance`
@@ -692,6 +692,7 @@ func AddMythicService(service string, removeVolume bool) {
"DEFAULT_OPERATION_NAME=${DEFAULT_OPERATION_NAME}",
"DEFAULT_OPERATION_WEBHOOK_CHANNEL=${DEFAULT_OPERATION_WEBHOOK_CHANNEL}",
"DEFAULT_OPERATION_WEBHOOK_URL=${DEFAULT_OPERATION_WEBHOOK_URL}",
"CUSTOM_RPC_TIMEOUT=${CUSTOM_RPC_TIMEOUT}",
"GLOBAL_SERVER_NAME=${GLOBAL_SERVER_NAME}",
"JWT_SECRET=${JWT_SECRET}",
"MYTHIC_ADMIN_USER=${MYTHIC_ADMIN_USER}",
@@ -28,6 +28,7 @@ set
excluded.last_callback_display_id
);
-- +migrate StatementBegin
do $$
begin
if exists (
@@ -48,6 +49,7 @@ begin
raise exception 'Cannot add callback(operation_id, display_id) uniqueness because duplicate callback display IDs already exist';
end if;
end $$;
-- +migrate StatementEnd
create unique index if not exists task_operation_display_id_unique
on "public"."task" using btree (operation_id, display_id);
@@ -55,6 +57,7 @@ on "public"."task" using btree (operation_id, display_id);
create unique index if not exists callback_operation_display_id_unique
on "public"."callback" using btree (operation_id, display_id);
-- +migrate StatementBegin
create or replace function public.new_task_display_id() returns trigger
language plpgsql
as $$
@@ -71,7 +74,9 @@ begin
return new;
end;
$$;
-- +migrate StatementEnd
-- +migrate StatementBegin
create or replace function public.new_callback_display_id() returns trigger
language plpgsql
as $$
@@ -88,6 +93,7 @@ begin
return new;
end;
$$;
-- +migrate StatementEnd
update "public"."task"
set response_count = response_counts.total
@@ -102,6 +108,7 @@ from (
where task.id = response_counts.id
and task.response_count is distinct from response_counts.total;
-- +migrate StatementBegin
create or replace function public.update_task_response_count() returns trigger
language plpgsql
as $$
@@ -113,10 +120,12 @@ begin
return new;
end;
$$;
-- +migrate StatementEnd
-- +migrate Down
-- SQL in section 'Down' is executed when this migration is rolled back
-- +migrate StatementBegin
create or replace function public.update_task_response_count() returns trigger
language plpgsql
as $$
@@ -132,7 +141,9 @@ begin
return new;
end;
$$;
-- +migrate StatementEnd
-- +migrate StatementBegin
create or replace function public.new_callback_display_id() returns trigger
language plpgsql
as $$
@@ -148,7 +159,9 @@ begin
return new;
end;
$$;
-- +migrate StatementEnd
-- +migrate StatementBegin
create or replace function public.new_task_display_id() returns trigger
language plpgsql
as $$
@@ -164,6 +177,7 @@ begin
return new;
end;
$$;
-- +migrate StatementEnd
drop index if exists "public"."callback_operation_display_id_unique";
drop index if exists "public"."task_operation_display_id_unique";
+11
View File
@@ -2,6 +2,8 @@ package rabbitmq
import "time"
type RPCRetryPolicy int
const (
MYTHIC_EXCHANGE = "mythic_exchange"
MYTHIC_TOPIC_EXCHANGE = "mythic_topic_exchange"
@@ -13,6 +15,15 @@ const (
TASK_STATUS_CONTAINER_DOWN = "Error: Container Down"
)
const (
// Retry response timeouts with a new RPC request, preserving Mythic's historical 3-attempt behavior.
RPC_RETRY_POLICY_RETRY_ON_TIMEOUT RPCRetryPolicy = iota
// Retry publish failures only; once a request is delivered, do not send duplicate work after response timeout.
RPC_RETRY_POLICY_NO_RETRY_ON_TIMEOUT
// Use CUSTOM_RPC_TIMEOUT for slow valid calls and do not send duplicate work after response timeout.
RPC_RETRY_POLICY_CUSTOM_TIMEOUT
)
// Direct fanout rabbitmq routes where Mythic is consuming messages, but others can also listen in and consume
const (
// PT_SYNC_ROUTING_KEY payload routes
+10
View File
@@ -44,6 +44,16 @@ type rabbitMQConnection struct {
addListenerMutex sync.RWMutex
channelMutexMap map[string]*channelMutex
channelMutex sync.RWMutex
publisherMutex sync.Mutex
publisherChannel *amqp.Channel
publisherConfirm chan amqp.Confirmation
publisherReturn chan amqp.Return
rpcClientMutex sync.Mutex
rpcChannel *amqp.Channel
rpcConfirm chan amqp.Confirmation
rpcReturn chan amqp.Return
rpcPending map[string]chan rpcResponse
rpcExchanges map[string]bool
RPCQueues []RPCQueueStruct
DirectQueues []DirectQueueStruct
}
@@ -30,6 +30,7 @@ func (r *rabbitMQConnection) SendAuthGetIDPMetadata(input GetIDPMetadataMessage)
GetAuthContainerGetIDPMetadataRoutingKey(input.ContainerName),
inputBytes,
exclusiveQueue,
RPC_RETRY_POLICY_RETRY_ON_TIMEOUT,
)
if err != nil {
logging.LogError(err, "Failed to send RPC message")
@@ -36,6 +36,7 @@ func (r *rabbitMQConnection) SendAuthGetIDPRedirect(input GetIDPRedirectMessage)
GetAuthContainerGetIDPRedirectRoutingKey(input.ContainerName),
inputBytes,
exclusiveQueue,
RPC_RETRY_POLICY_RETRY_ON_TIMEOUT,
)
if err != nil {
logging.LogError(err, "Failed to send RPC message")
@@ -30,6 +30,7 @@ func (r *rabbitMQConnection) SendAuthGetNonIDPMetadata(input GetNonIDPMetadataMe
GetAuthContainerGetNonIDPMetadataRoutingKey(input.ContainerName),
inputBytes,
exclusiveQueue,
RPC_RETRY_POLICY_RETRY_ON_TIMEOUT,
)
if err != nil {
logging.LogError(err, "Failed to send RPC message")
@@ -30,6 +30,7 @@ func (r *rabbitMQConnection) SendAuthGetNonIDPRedirect(input GetNonIDPRedirectMe
GetAuthContainerGetNonIDPRedirectRoutingKey(input.ContainerName),
inputBytes,
exclusiveQueue,
RPC_RETRY_POLICY_RETRY_ON_TIMEOUT,
)
if err != nil {
logging.LogError(err, "Failed to send RPC message")
@@ -35,6 +35,7 @@ func (r *rabbitMQConnection) SendAuthProcessIDPResponse(input ProcessIDPResponse
GetAuthContainerProcessIDPResponseRoutingKey(input.ContainerName),
inputBytes,
exclusiveQueue,
RPC_RETRY_POLICY_NO_RETRY_ON_TIMEOUT,
)
if err != nil {
logging.LogError(err, "Failed to send RPC message")
@@ -31,6 +31,7 @@ func (r *rabbitMQConnection) SendAuthProcessNonIDPResponse(input ProcessNonIDPRe
GetAuthContainerProcessNonIDPResponseRoutingKey(input.ContainerName),
inputBytes,
exclusiveQueue,
RPC_RETRY_POLICY_NO_RETRY_ON_TIMEOUT,
)
if err != nil {
logging.LogError(err, "Failed to send RPC message")
@@ -32,6 +32,7 @@ func (r *rabbitMQConnection) SendC2RPCConfigCheck(configCheck C2ConfigCheckMessa
GetC2RPCConfigChecksRoutingKey(configCheck.Name),
configBytes,
exclusiveQueue,
RPC_RETRY_POLICY_CUSTOM_TIMEOUT,
)
if err != nil {
logging.LogError(err, "Failed to send RPC message")
@@ -32,6 +32,7 @@ func (r *rabbitMQConnection) SendC2RPCGetDebugOutput(getDebugOutput C2GetDebugOu
GetC2RPCGetServerDebugOutputRoutingKey(getDebugOutput.Name),
opsecBytes,
exclusiveQueue,
RPC_RETRY_POLICY_CUSTOM_TIMEOUT,
); err != nil {
logging.LogError(err, "Failed to send RPC message")
return &c2GetDebutOutputResponse, err
@@ -32,6 +32,7 @@ func (r *rabbitMQConnection) SendC2RPCGetIOC(getIOC C2GetIOCMessage) (*C2GetIOCM
GetC2RPCGetIOCRoutingKey(getIOC.Name),
configBytes,
exclusiveQueue,
RPC_RETRY_POLICY_CUSTOM_TIMEOUT,
); err != nil {
logging.LogError(err, "Failed to send RPC message")
return nil, err
@@ -32,6 +32,7 @@ func (r *rabbitMQConnection) SendC2RPCGetRedirectorRules(redirectorRules C2GetRe
GetC2RPCRedirectorRulesRoutingKey(redirectorRules.Name),
opsecBytes,
exclusiveQueue,
RPC_RETRY_POLICY_CUSTOM_TIMEOUT,
); err != nil {
logging.LogError(err, "Failed to send RPC message")
return nil, err
@@ -35,6 +35,7 @@ func (r *rabbitMQConnection) SendC2RPCHostFile(hostFile C2HostFileMessage) (*C2H
GetC2RPCHostFileRoutingKey(hostFile.Name),
opsecBytes,
exclusiveQueue,
RPC_RETRY_POLICY_CUSTOM_TIMEOUT,
)
if err != nil {
logging.LogError(err, "Failed to send RPC message")
@@ -35,6 +35,7 @@ func (r *rabbitMQConnection) SendC2RPCOpsecCheck(opsecCheck C2OPSECMessage) (*C2
GetC2RPCOpsecChecksRoutingKey(opsecCheck.Name),
opsecBytes,
exclusiveQueue,
RPC_RETRY_POLICY_CUSTOM_TIMEOUT,
)
if err != nil {
logging.LogError(err, "Failed to send RPC message")
@@ -28,6 +28,7 @@ func (r *rabbitMQConnection) SendC2RPCReSync(resync C2RPCReSyncMessage) (*C2RPCR
GetC2RPCReSyncRoutingKey(resync.Name),
configBytes,
exclusiveQueue,
RPC_RETRY_POLICY_CUSTOM_TIMEOUT,
); err != nil {
logging.LogError(err, "Failed to send RPC message")
return nil, err
@@ -29,6 +29,7 @@ func (r *rabbitMQConnection) SendC2RPCSampleMessage(getSampleMessage C2SampleMes
GetC2RPCSampleMessageRoutingKey(getSampleMessage.Name),
configBytes,
exclusiveQueue,
RPC_RETRY_POLICY_CUSTOM_TIMEOUT,
); err != nil {
logging.LogError(err, "Failed to send RPC message")
return nil, err
@@ -92,6 +92,7 @@ func (r *rabbitMQConnection) SendC2RPCStartServer(startServer C2StartServerMessa
GetC2RPCStartServerRoutingKey(startServer.Name),
opsecBytes,
exclusiveQueue,
RPC_RETRY_POLICY_NO_RETRY_ON_TIMEOUT,
)
if err != nil {
logging.LogError(err, "Failed to send RPC message")
@@ -34,6 +34,7 @@ func (r *rabbitMQConnection) SendC2RPCStopServer(stopServer C2StopServerMessage)
GetC2RPCStopServerRoutingKey(stopServer.Name),
opsecBytes,
exclusiveQueue,
RPC_RETRY_POLICY_NO_RETRY_ON_TIMEOUT,
)
if err != nil {
logging.LogError(err, "Failed to send RPC message")
@@ -28,6 +28,7 @@ func (r *rabbitMQConnection) SendContainerRPCGetFile(getFile ContainerGetFileMes
GetC2RPCGetFileRoutingKey(getFile.Name),
opsecBytes,
exclusiveQueue,
RPC_RETRY_POLICY_CUSTOM_TIMEOUT,
); err != nil {
logging.LogError(err, "Failed to send RPC message")
return nil, err
@@ -27,6 +27,7 @@ func (r *rabbitMQConnection) SendContainerRPCListFile(getFile ContainerRPCListFi
GetC2RPCListFileRoutingKey(getFile.Name),
opsecBytes,
exclusiveQueue,
RPC_RETRY_POLICY_CUSTOM_TIMEOUT,
); err != nil {
logging.LogError(err, "Failed to send RPC message")
return nil, err
@@ -27,6 +27,7 @@ func (r *rabbitMQConnection) SendContainerRPCRemoveFile(getFile ContainerRemoveF
GetC2RPCRemoveFileRoutingKey(getFile.Name),
opsecBytes,
exclusiveQueue,
RPC_RETRY_POLICY_CUSTOM_TIMEOUT,
); err != nil {
logging.LogError(err, "Failed to send RPC message")
return nil, err
@@ -29,6 +29,7 @@ func (r *rabbitMQConnection) SendContainerRPCWriteFile(writeFile ContainerWriteF
GetC2RPCWriteFileRoutingKey(writeFile.Name),
opsecBytes,
exclusiveQueue,
RPC_RETRY_POLICY_CUSTOM_TIMEOUT,
); err != nil {
logging.LogError(err, "Failed to send RPC message")
return nil, err
@@ -29,6 +29,7 @@ func (r *rabbitMQConnection) SendPtRPCCommandHelp(msg CommandHelpMessage) (*Comm
GetPtCommandHelpRoutingKey(msg.PayloadType),
configBytes,
exclusiveQueue,
RPC_RETRY_POLICY_RETRY_ON_TIMEOUT,
)
if err != nil {
logging.LogError(err, "Failed to send RPC message")
@@ -126,6 +126,7 @@ func (r *rabbitMQConnection) SendPTRPCCheckIfCallbacksAlive(sendMsg PTCheckIfCal
GetPtCheckIfCallbacksAliveRoutingKey(sendMsg.ContainerName),
opsecBytes,
exclusiveQueue,
RPC_RETRY_POLICY_RETRY_ON_TIMEOUT,
)
if err != nil {
return nil, err
@@ -40,6 +40,7 @@ func (r *rabbitMQConnection) SendPtRPCDynamicQueryBuildParameterFunction(dynamic
GetPtRPCDynamicQueryBuildParameterFunctionRoutingKey(dynamicQuery.PayloadType),
configBytes,
exclusiveQueue,
RPC_RETRY_POLICY_CUSTOM_TIMEOUT,
)
if err != nil {
logging.LogError(err, "Failed to send RPC message")
@@ -74,6 +74,7 @@ func (r *rabbitMQConnection) SendPtRPCDynamicQueryFunction(dynamicQuery PTRPCDyn
GetPtRPCDynamicQueryFunctionRoutingKey(dynamicQuery.CommandPayloadType),
configBytes,
exclusiveQueue,
RPC_RETRY_POLICY_CUSTOM_TIMEOUT,
)
if err != nil {
logging.LogError(err, "Failed to send RPC message")
@@ -30,6 +30,7 @@ func (r *rabbitMQConnection) SendPTRPCReSync(resync PTRPCReSyncMessage) (*PTRPCR
GetPTRPCReSyncRoutingKey(resync.Name),
configBytes,
exclusiveQueue,
RPC_RETRY_POLICY_CUSTOM_TIMEOUT,
)
if err != nil {
logging.LogError(err, "Failed to send RPC message")
@@ -41,6 +41,7 @@ func (r *rabbitMQConnection) SendPtRPCTypedArrayParse(dynamicQuery PTRPCTypedArr
GetPtRPCTypedArrayParseRoutingKey(dynamicQuery.CommandPayloadType),
configBytes,
exclusiveQueue,
RPC_RETRY_POLICY_CUSTOM_TIMEOUT,
); err != nil {
logging.LogError(err, "Failed to send RPC message")
return nil, err
@@ -33,6 +33,7 @@ func (r *rabbitMQConnection) SendTrRPCDecryptBytes(input TrDecryptBytesMessage)
GetTrRPCDecryptBytesRoutingKey(input.TranslationContainerName),
opsecBytes,
!exclusiveQueue,
RPC_RETRY_POLICY_RETRY_ON_TIMEOUT,
); err != nil {
logging.LogError(err, "Failed to send RPC message")
return nil, err
@@ -34,6 +34,7 @@ func (r *rabbitMQConnection) SendTrRPCEncryptBytes(input TrEncryptBytesMessage)
GetTrRPCEncryptBytesRoutingKey(input.TranslationContainerName),
opsecBytes,
!exclusiveQueue,
RPC_RETRY_POLICY_RETRY_ON_TIMEOUT,
); err != nil {
logging.LogError(err, "Failed to send RPC message")
return nil, err
@@ -28,6 +28,7 @@ func (r *rabbitMQConnection) SendTRRPCReSync(resync TRRPCReSyncMessage) (*TRRPCR
GetTrRPCReSyncRoutingKey(resync.Name),
configBytes,
exclusiveQueue,
RPC_RETRY_POLICY_CUSTOM_TIMEOUT,
); err != nil {
logging.LogError(err, "Failed to send RPC message")
return nil, err
@@ -14,6 +14,11 @@ import (
amqp "github.com/rabbitmq/amqp091-go"
)
type rpcResponse struct {
Body []byte
Err error
}
// payload routing key functions
func GetPtBuildRoutingKey(container string) string {
return fmt.Sprintf("%s_%s", container, PT_BUILD_ROUTING_KEY)
@@ -191,41 +196,62 @@ func (r *rabbitMQConnection) SendStructMessage(exchange string, queue string, co
}
return r.SendMessage(exchange, queue, correlationId, jsonBody, ignoreErrorMessage)
}
func (r *rabbitMQConnection) SendRPCStructMessage(exchange string, queue string, body interface{}) ([]byte, error) {
func (r *rabbitMQConnection) SendRPCStructMessage(exchange string, queue string, body interface{}, retryPolicy RPCRetryPolicy) ([]byte, error) {
if inputBytes, err := json.Marshal(body); err != nil {
logging.LogError(err, "Failed to convert input to JSON", "input", body)
return nil, err
} else {
return r.SendRPCMessage(exchange, queue, inputBytes, true)
return r.SendRPCMessage(exchange, queue, inputBytes, true, retryPolicy)
}
}
func (r *rabbitMQConnection) getPublisherChannel() (*amqp.Channel, chan amqp.Confirmation, chan amqp.Return, error) {
if r.publisherChannel != nil && !r.publisherChannel.IsClosed() {
return r.publisherChannel, r.publisherConfirm, r.publisherReturn, nil
}
conn, err := r.GetConnection()
if err != nil {
return nil, nil, nil, err
}
ch, err := conn.Channel()
if err != nil {
return nil, nil, nil, err
}
if err = ch.Confirm(false); err != nil {
ch.Close()
return nil, nil, nil, err
}
r.publisherChannel = ch
r.publisherConfirm = ch.NotifyPublish(make(chan amqp.Confirmation, 1))
r.publisherReturn = ch.NotifyReturn(make(chan amqp.Return, 1))
return r.publisherChannel, r.publisherConfirm, r.publisherReturn, nil
}
func (r *rabbitMQConnection) resetPublisherChannel(ch *amqp.Channel) {
if ch != nil && !ch.IsClosed() {
ch.Close()
}
if r.publisherChannel == ch {
r.publisherChannel = nil
r.publisherConfirm = nil
r.publisherReturn = nil
}
}
func (r *rabbitMQConnection) SendMessage(exchange string, queue string, correlationId string, body []byte, ignoreErrormessage bool) error {
// to send a normal message out to a direct queue set:
// exchange: MYTHIC_EXCHANGE
// queue: which routing key is listening (this is the direct name)
// correlation_id: empty string
for attempt := 0; attempt < 3; attempt++ {
conn, err := r.GetConnection()
r.publisherMutex.Lock()
ch, confirmChannel, notifyReturnChannel, err := r.getPublisherChannel()
if err != nil {
logging.LogError(err, "Failed to get rabbitmq connection", "queue", queue)
logging.LogError(err, "Failed to get rabbitmq publisher channel", "queue", queue)
r.publisherMutex.Unlock()
time.Sleep(RPC_TIMEOUT)
continue
}
ch, err := conn.Channel()
if err != nil {
logging.LogError(err, "Failed to open rabbitmq channel", "queue", queue)
time.Sleep(RPC_TIMEOUT)
continue
}
err = ch.Confirm(false)
if err != nil {
logging.LogError(err, "Channel could not be put into confirm mode", "queue", queue)
ch.Close()
time.Sleep(RPC_TIMEOUT)
continue
}
confirmChannel := ch.NotifyPublish(make(chan amqp.Confirmation, 1))
notifyReturnChannel := ch.NotifyReturn(make(chan amqp.Return, 1))
msg := amqp.Publishing{
ContentType: "application/json",
CorrelationId: correlationId,
@@ -240,7 +266,8 @@ func (r *rabbitMQConnection) SendMessage(exchange string, queue string, correlat
)
if err != nil {
logging.LogError(err, "there was an error publishing a message", "queue", queue)
ch.Close()
r.resetPublisherChannel(ch)
r.publisherMutex.Unlock()
time.Sleep(RPC_TIMEOUT)
continue
}
@@ -249,7 +276,8 @@ func (r *rabbitMQConnection) SendMessage(exchange string, queue string, correlat
if !ntf.Ack {
err = errors.New("Failed to deliver message, not ACK-ed by receiver")
logging.LogError(err, "failed to deliver message to exchange/queue, notifyPublish")
ch.Close()
r.resetPublisherChannel(ch)
r.publisherMutex.Unlock()
time.Sleep(RPC_TIMEOUT)
continue
}
@@ -258,16 +286,18 @@ func (r *rabbitMQConnection) SendMessage(exchange string, queue string, correlat
if !ignoreErrormessage {
logging.LogError(err, "failed to deliver message to exchange/queue, NotifyReturn", "errorCode", ret.ReplyCode, "errorText", ret.ReplyText)
}
ch.Close()
r.resetPublisherChannel(ch)
r.publisherMutex.Unlock()
time.Sleep(RPC_TIMEOUT)
continue
case <-time.After(RPC_TIMEOUT):
err = errors.New("Message delivery confirmation timed out")
logging.LogError(err, "message delivery confirmation to exchange/queue timed out")
ch.Close()
r.resetPublisherChannel(ch)
r.publisherMutex.Unlock()
continue
}
ch.Close()
r.publisherMutex.Unlock()
return nil
}
if !ignoreErrormessage {
@@ -275,108 +305,221 @@ func (r *rabbitMQConnection) SendMessage(exchange string, queue string, correlat
}
return errors.New(fmt.Sprintf("failed 3 times to send to queue %s", queue))
}
func (r *rabbitMQConnection) SendRPCMessage(exchange string, queue string, body []byte, exclusiveQueue bool) ([]byte, error) {
var finalError error
for attempt := 0; attempt < 3; attempt++ {
conn, err := r.GetConnection()
if err != nil {
return nil, err
func (r *rabbitMQConnection) getRPCTimeout(retryPolicy RPCRetryPolicy) time.Duration {
if retryPolicy == RPC_RETRY_POLICY_CUSTOM_TIMEOUT && utils.MythicConfig.CustomRPCTimeout > 0 {
return utils.MythicConfig.CustomRPCTimeout
}
return RPC_TIMEOUT
}
func (r *rabbitMQConnection) getRPCClientLocked(exchange string, exclusiveQueue bool) (*amqp.Channel, chan amqp.Confirmation, chan amqp.Return, error) {
if r.rpcChannel != nil && !r.rpcChannel.IsClosed() {
if err := r.declareRPCExchangeLocked(r.rpcChannel, exchange); err != nil {
r.resetRPCClientLocked(r.rpcChannel, err)
return nil, nil, nil, err
}
ch, err := conn.Channel()
if err != nil {
logging.LogError(err, "Failed to open rabbitmq channel")
return nil, err
return r.rpcChannel, r.rpcConfirm, r.rpcReturn, nil
}
conn, err := r.GetConnection()
if err != nil {
return nil, nil, nil, err
}
ch, err := conn.Channel()
if err != nil {
return nil, nil, nil, err
}
if err = ch.Confirm(false); err != nil {
ch.Close()
return nil, nil, nil, err
}
confirmChannel := ch.NotifyPublish(make(chan amqp.Confirmation, 1))
notifyReturnChannel := ch.NotifyReturn(make(chan amqp.Return, 1))
msgs, err := ch.Consume(
"amq.rabbitmq.reply-to", // queue name
"", // consumer
true, // auto-ack
exclusiveQueue, // exclusive
false, // no local
false, // no wait
nil, // args
)
if err != nil {
ch.Close()
return nil, nil, nil, err
}
r.rpcChannel = ch
r.rpcConfirm = confirmChannel
r.rpcReturn = notifyReturnChannel
if r.rpcPending == nil {
r.rpcPending = make(map[string]chan rpcResponse)
}
r.rpcExchanges = make(map[string]bool)
if err = r.declareRPCExchangeLocked(ch, exchange); err != nil {
r.resetRPCClientLocked(ch, err)
return nil, nil, nil, err
}
go r.listenForRPCReplies(ch, msgs)
return r.rpcChannel, r.rpcConfirm, r.rpcReturn, nil
}
func (r *rabbitMQConnection) declareRPCExchangeLocked(ch *amqp.Channel, exchange string) error {
if r.rpcExchanges == nil {
r.rpcExchanges = make(map[string]bool)
}
if r.rpcExchanges[exchange] {
return nil
}
err := ch.ExchangeDeclare(
exchange, // exchange name
"direct", // type of exchange, ex: topic, fanout, direct, etc
true, // durable
true, // auto-deleted
false, // internal
false, // no-wait
nil, // arguments
)
if err == nil {
r.rpcExchanges[exchange] = true
}
return err
}
func (r *rabbitMQConnection) listenForRPCReplies(ch *amqp.Channel, msgs <-chan amqp.Delivery) {
for d := range msgs {
r.rpcClientMutex.Lock()
responseChannel := r.rpcPending[d.CorrelationId]
if responseChannel != nil {
delete(r.rpcPending, d.CorrelationId)
}
err = ch.Confirm(false)
if err != nil {
logging.LogError(err, "Channel could not be put into confirm mode")
ch.Close()
return nil, err
r.rpcClientMutex.Unlock()
if responseChannel != nil {
responseChannel <- rpcResponse{Body: d.Body}
}
err = ch.ExchangeDeclare(
exchange, // exchange name
"direct", // type of exchange, ex: topic, fanout, direct, etc
true, // durable
true, // auto-deleted
false, // internal
false, // no-wait
nil, // arguments
)
if err != nil {
logging.LogError(err, "Failed to declare exchange", "exchange", exchange, "exchange_type", "direct", "retry_wait_time", RETRY_CONNECT_DELAY)
ch.Close()
return nil, err
}
msgs, err := ch.Consume(
"amq.rabbitmq.reply-to", // queue name
"", // consumer
true, // auto-ack
exclusiveQueue, // exclusive
false, // no local
false, // no wait
nil, // args
)
if err != nil {
logging.LogError(err, "Failed to start consuming for RPC replies")
ch.Close()
return nil, err
}
confirmChannel := ch.NotifyPublish(make(chan amqp.Confirmation, 1))
notifyReturnChannel := ch.NotifyReturn(make(chan amqp.Return, 1))
msg := amqp.Publishing{
ContentType: "application/json",
CorrelationId: uuid.NewString(),
Body: body,
ReplyTo: "amq.rabbitmq.reply-to",
}
err = ch.Publish(
exchange, // exchange
queue, // routing key
true, // mandatory
false, // immediate
msg, // publishing
)
finalError = err
if err != nil {
logging.LogError(err, "there was an error publishing an rpc message", "queue", queue)
ch.Close()
time.Sleep(RPC_TIMEOUT)
continue
}
r.rpcClientMutex.Lock()
if r.rpcChannel == ch {
r.resetRPCClientLocked(ch, errors.New("rpc reply consumer stopped"))
}
r.rpcClientMutex.Unlock()
}
func (r *rabbitMQConnection) resetRPCClientLocked(ch *amqp.Channel, err error) {
if ch != nil && !ch.IsClosed() {
ch.Close()
}
if r.rpcChannel == ch {
for correlationID, responseChannel := range r.rpcPending {
select {
case responseChannel <- rpcResponse{Err: err}:
default:
}
delete(r.rpcPending, correlationID)
}
r.rpcChannel = nil
r.rpcConfirm = nil
r.rpcReturn = nil
r.rpcExchanges = nil
}
}
func (r *rabbitMQConnection) removePendingRPCResponse(correlationID string) {
r.rpcClientMutex.Lock()
delete(r.rpcPending, correlationID)
r.rpcClientMutex.Unlock()
}
func (r *rabbitMQConnection) publishRPCMessage(exchange string, queue string, correlationID string, body []byte, exclusiveQueue bool, responseChannel chan rpcResponse) error {
r.rpcClientMutex.Lock()
defer r.rpcClientMutex.Unlock()
ch, confirmChannel, notifyReturnChannel, err := r.getRPCClientLocked(exchange, exclusiveQueue)
if err != nil {
logging.LogError(err, "Failed to get rabbitmq rpc channel", "queue", queue)
return err
}
r.rpcPending[correlationID] = responseChannel
msg := amqp.Publishing{
ContentType: "application/json",
CorrelationId: correlationID,
Body: body,
ReplyTo: "amq.rabbitmq.reply-to",
}
err = ch.Publish(
exchange, // exchange
queue, // routing key
true, // mandatory
false, // immediate
msg, // publishing
)
if err != nil {
delete(r.rpcPending, correlationID)
logging.LogError(err, "there was an error publishing an rpc message", "queue", queue)
r.resetRPCClientLocked(ch, err)
return err
}
timeoutChannel := time.After(RPC_TIMEOUT)
for {
select {
case ntf := <-confirmChannel:
if !ntf.Ack {
err = errors.New("failed to deliver message, not ACK-ed by receiver")
finalError = err
delete(r.rpcPending, correlationID)
logging.LogError(err, "failed to deliver message to exchange/queue, notifyPublish", "queue", queue)
ch.Close()
time.Sleep(RPC_TIMEOUT)
r.resetRPCClientLocked(ch, err)
return err
}
return nil
case ret := <-notifyReturnChannel:
if ret.CorrelationId != correlationID {
continue
}
case ret := <-notifyReturnChannel:
err = errors.New(getMeaningfulRabbitmqError(ret))
delete(r.rpcPending, correlationID)
r.resetRPCClientLocked(ch, err)
return err
case <-timeoutChannel:
err = errors.New("message delivery confirmation timed out in SendRPCMessage")
delete(r.rpcPending, correlationID)
logging.LogError(err, "message delivery confirmation to exchange/queue timed out when sending", "queue", queue)
r.resetRPCClientLocked(ch, err)
return err
}
}
}
func (r *rabbitMQConnection) SendRPCMessage(exchange string, queue string, body []byte, exclusiveQueue bool, retryPolicy RPCRetryPolicy) ([]byte, error) {
var finalError error
timeout := r.getRPCTimeout(retryPolicy)
for attempt := 0; attempt < 3; attempt++ {
correlationID := uuid.NewString()
responseChannel := make(chan rpcResponse, 1)
err := r.publishRPCMessage(exchange, queue, correlationID, body, exclusiveQueue, responseChannel)
if err != nil {
finalError = err
time.Sleep(RPC_TIMEOUT)
ch.Close()
continue
case <-time.After(RPC_TIMEOUT):
err = errors.New("message delivery confirmation timed out in SendRPCMessage")
finalError = err
logging.LogError(err, "message delivery confirmation to exchange/queue timed out when sending", "queue", queue)
ch.Close()
continue
}
//logging.LogDebug("Sent RPC message", "queue", queue)
select {
case m := <-msgs:
case response := <-responseChannel:
if response.Err != nil {
finalError = response.Err
logging.LogError(response.Err, "rpc channel failed while waiting for response", "queue", queue)
if retryPolicy != RPC_RETRY_POLICY_RETRY_ON_TIMEOUT {
return nil, response.Err
}
time.Sleep(RPC_TIMEOUT)
continue
}
//logging.LogDebug("Got RPC Reply", "queue", queue)
ch.Close()
return m.Body, nil
case <-time.After(RPC_TIMEOUT):
err = errors.New("message delivery confirmation timed out")
return response.Body, nil
case <-time.After(timeout):
r.removePendingRPCResponse(correlationID)
err = errors.New("rpc response timed out")
finalError = err
logging.LogError(err, "message delivery confirmation to exchange/queue timed out when receiving", "queue", queue)
ch.Close()
if retryPolicy != RPC_RETRY_POLICY_RETRY_ON_TIMEOUT {
return nil, err
}
continue
}
}
+4
View File
@@ -7,6 +7,7 @@ import (
"path/filepath"
"strconv"
"strings"
"time"
"github.com/spf13/viper"
)
@@ -40,6 +41,7 @@ type Config struct {
RabbitmqUser string
RabbitmqPassword string
RabbitmqVHost string
CustomRPCTimeout time.Duration
// postgres configuration
PostgresHost string
@@ -84,6 +86,7 @@ func Initialize() {
mythicEnv.SetDefault("rabbitmq_user", "mythic_user")
mythicEnv.SetDefault("rabbitmq_password", "")
mythicEnv.SetDefault("rabbitmq_vhost", "mythic_vhost")
mythicEnv.SetDefault("custom_rpc_timeout", 0)
// jwt configuration
mythicEnv.SetDefault("jwt_secret", "")
// default operation configuration
@@ -189,6 +192,7 @@ func setConfigFromEnv(mythicEnv *viper.Viper) {
if MythicConfig.RabbitmqVHost == "" {
MythicConfig.RabbitmqVHost = "mythic_vhost"
}
MythicConfig.CustomRPCTimeout = time.Duration(mythicEnv.GetUint("custom_rpc_timeout")) * time.Second
// jwt configuration
MythicConfig.JWTSecret = []byte(mythicEnv.GetString("jwt_secret"))
}