adjusted reporting page, payloads table,

This commit is contained in:
its-a-feature
2026-05-18 08:18:20 -07:00
parent 5eb3d492fb
commit 70e28b9de5
8 changed files with 353 additions and 240 deletions
@@ -18,6 +18,8 @@ export const Dropdown = React.forwardRef(
style,
transformOrigin,
anchorOrigin,
MenuListProps,
PaperProps,
},
ref
) => {
@@ -70,7 +72,11 @@ export const Dropdown = React.forwardRef(
<>
<Menu
elevation={5}
PaperProps={{ sx: { minWidth: minWidth ?? 0 } }}
PaperProps={{
...PaperProps,
sx: { minWidth: minWidth ?? 0, ...(PaperProps?.sx || {}) }
}}
MenuListProps={MenuListProps}
style={{zIndex: 100000, position: "absolute"}}
anchorEl={anchorReference === "anchorEl" ? anchorEl : undefined}
transition={"true"}
@@ -254,4 +260,4 @@ export const DropdownMenuItem = styled(MenuItem)`
export const DropdownNestedMenuItem = styled(NestedMenuItem)`
`;
`;
@@ -36,21 +36,11 @@ export function PayloadsTable({payload, onDeletePayload, onUpdateCallbackAlert,
setShowAutogenerated(!showAutogenerated);
onChangeShowAutogenerated(!showAutogenerated);
}
const payloadCountLabel = payload.length === 1 ? "1 shown" : `${payload.length} shown`;
const totalCountLabel = pageData.totalCount === 1 ? "1 total" : `${pageData.totalCount} total`;
return (
<>
<MythicPageHeader
title={"Payloads"}
subtitle={"Build, import, and manage payload artifacts for the current operation."}
meta={
<>
<MythicPageHeaderChip label={payloadCountLabel} />
<MythicPageHeaderChip label={totalCountLabel} />
{showDeleted && <MythicPageHeaderChip label="Deleted visible" />}
{showAutogenerated && <MythicPageHeaderChip label="Autogenerated" />}
</>
}
actions={
<>
<MythicToolbarButton variant="contained" color="primary" startIcon={<AddCircleIcon />} onClick={() => navigate("/new/createpayload")}>
@@ -1,5 +1,5 @@
import React, {useRef} from 'react';
import {Button} from '@mui/material';
import {Button, Divider, ListItemIcon, ListItemText} from '@mui/material';
import TableRow from '@mui/material/TableRow';
import IconButton from '@mui/material/IconButton';
import DeleteIcon from '@mui/icons-material/Delete';
@@ -39,7 +39,7 @@ import {PayloadGetIOCDialog} from "./PayloadGetIOCDialog";
import {PayloadGetSampleMessageDialog} from "./PayloadGetSampleMessageDialog";
import IosShareIcon from '@mui/icons-material/IosShare';
import {TagsDisplay, ViewEditTags} from "../../MythicComponents/MythicTag";
import {MythicAgentSVGIcon, MythicAgentSVGIconNoTooltip} from "../../MythicComponents/MythicAgentSVGIcon";
import {MythicAgentSVGIconNoTooltip} from "../../MythicComponents/MythicAgentSVGIcon";
import SmartToyTwoToneIcon from '@mui/icons-material/SmartToyTwoTone';
import ArrowDropDownIcon from '@mui/icons-material/ArrowDropDown';
import BlockIcon from '@mui/icons-material/Block';
@@ -49,6 +49,10 @@ import DifferenceIcon from '@mui/icons-material/Difference';
import {Dropdown, DropdownMenuItem, DropdownNestedMenuItem} from "../../MythicComponents/MythicNestedMenus";
import {MythicStatusChip} from "../../MythicComponents/MythicStatusChip";
const payloadMenuItemClass = (tone = "info") => {
return `mythic-response-action-menu-item mythic-response-action-hover-${tone}`;
};
const rebuildPayloadMutation = gql`
mutation triggerRebuildMutation($uuid: String!) {
rebuildPayload(uuid: $uuid) {
@@ -149,8 +153,8 @@ export function PayloadsTableRow(props){
};
const options = [
{
name: "Rename File", type: "item",
icon: <DriveFileRenameOutlineIcon style={{marginRight: "10px"}}/>,
name: "Rename File", type: "item", tone: "info",
icon: <DriveFileRenameOutlineIcon fontSize="small" />,
click: ({event}) => {
event.preventDefault();
event.stopPropagation();
@@ -158,8 +162,8 @@ export function PayloadsTableRow(props){
}
},
{
name: "Edit Description", type: "item",
icon: <DescriptionIcon color={"info"} style={{marginRight: "10px"}} />,
name: "Edit Description", type: "item", tone: "info",
icon: <DescriptionIcon fontSize="small" />,
click: ({event}) => {
event.preventDefault();
event.stopPropagation();
@@ -167,8 +171,8 @@ export function PayloadsTableRow(props){
}
},
{
name: "View Payload Configuration", type: "item",
icon: <InfoIconOutline color={"info"} style={{marginRight: "10px"}} />,
name: "View Payload Configuration", type: "item", tone: "info",
icon: <InfoIconOutline fontSize="small" />,
click: ({event}) => {
event.preventDefault();
event.stopPropagation();
@@ -176,20 +180,22 @@ export function PayloadsTableRow(props){
}
},
{
name: "Compare Payload Configuration", type: "item",
icon: <DifferenceIcon color={"info"} style={{marginRight: "10px"}} />,
name: "Compare Payload Configuration", type: "item", tone: "info",
icon: <DifferenceIcon fontSize="small" />,
click: ({event}) => {
event.preventDefault();
event.stopPropagation();
setOpenComparePayloadsDialog(true);
}
},
{type: "divider", name: "payload-status-divider"},
{
name: props.callback_alert ? "Alerting to New Callbacks" : "Not Alerting to New Callbacks",
type: "item",
tone: props.callback_alert ? "success" : "danger",
icon: props.callback_alert ?
<VisibilityIcon color={"success"} style={{marginRight: "10px"}} />:
<VisibilityOffIcon color={"error"} style={{marginRight: "10px"}} />,
<VisibilityIcon fontSize="small" />:
<VisibilityOffIcon fontSize="small" />,
click: ({event}) => {
event.preventDefault();
event.stopPropagation();
@@ -199,19 +205,22 @@ export function PayloadsTableRow(props){
{
name: props.callback_allowed ? "Allowing New Callbacks from this Payload" : "Preventing New Callbacks from this Payload",
type: "item",
tone: props.callback_allowed ? "success" : "danger",
icon: props.callback_allowed ?
<VisibilityIcon color={"success"} style={{marginRight: "10px"}} /> :
<VisibilityOffIcon color={"error"} style={{marginRight: "10px"}} />,
<VisibilityIcon fontSize="small" /> :
<VisibilityOffIcon fontSize="small" />,
click: ({event}) => {
event.preventDefault();
event.stopPropagation();
onCallbacksAllowedChanged();
}
},
{type: "divider", name: "payload-build-divider"},
{
name: "View Build Message/Stdout",
type: "item",
icon: <MessageIcon style={{marginRight: "10px"}} />,
tone: "info",
icon: <MessageIcon fontSize="small" />,
click: ({event}) => {
event.preventDefault();
event.stopPropagation();
@@ -222,7 +231,8 @@ export function PayloadsTableRow(props){
{
name: "View Build Errors",
type: "item",
icon: <ErrorIcon color={"error"} style={{marginRight: "10px"}} />,
tone: "danger",
icon: <ErrorIcon fontSize="small" />,
click: ({event}) => {
event.preventDefault();
event.stopPropagation();
@@ -233,7 +243,8 @@ export function PayloadsTableRow(props){
{
name: "Trigger New Build",
type: "item",
icon: <CachedIcon color={"success"} style={{marginRight: "10px"}} />,
tone: "success",
icon: <CachedIcon fontSize="small" />,
click: ({event}) => {
event.preventDefault();
event.stopPropagation();
@@ -243,7 +254,8 @@ export function PayloadsTableRow(props){
{
name: "Trigger New Build With Edits",
type: "item",
icon: <CachedIcon color={"success"} style={{marginRight: "10px"}} />,
tone: "success",
icon: <CachedIcon fontSize="small" />,
click: ({event}) => {
event.preventDefault();
event.stopPropagation();
@@ -253,17 +265,20 @@ export function PayloadsTableRow(props){
{
name: "Export Payload Config",
type: "item",
icon: <IosShareIcon color={"info"} style={{marginRight: "10px"}} />,
tone: "info",
icon: <IosShareIcon fontSize="small" />,
click: ({event}) => {
event.preventDefault();
event.stopPropagation();
exportConfig({variables: {uuid: props.uuid}});
}
},
{type: "divider", name: "payload-tools-divider"},
{
name: "Generate Redirect Rules",
type: "item",
icon: <PhoneMissedIcon style={{marginRight: "10px"}} />,
tone: "warning",
icon: <PhoneMissedIcon fontSize="small" />,
click: ({event}) => {
event.preventDefault();
event.stopPropagation();
@@ -273,7 +288,8 @@ export function PayloadsTableRow(props){
{
name: "Check Agent C2 Configuration",
type: "item",
icon: <VerifiedIcon style={{marginRight: "10px"}} />,
tone: "info",
icon: <VerifiedIcon fontSize="small" />,
click: ({event}) => {
event.preventDefault();
event.stopPropagation();
@@ -283,7 +299,8 @@ export function PayloadsTableRow(props){
{
name: "Generate IOCs",
type: "item",
icon: <FingerprintIcon style={{marginRight: "10px"}} />,
tone: "info",
icon: <FingerprintIcon fontSize="small" />,
click: ({event}) => {
event.preventDefault();
event.stopPropagation();
@@ -293,7 +310,8 @@ export function PayloadsTableRow(props){
{
name: "Generate Sample Message",
type: "item",
icon: <BiotechIcon style={{marginRight: "10px"}} />,
tone: "info",
icon: <BiotechIcon fontSize="small" />,
click: ({event}) => {
event.preventDefault();
event.stopPropagation();
@@ -303,19 +321,22 @@ export function PayloadsTableRow(props){
{
name: "Generate Fake Callback",
type: "item",
icon: <AddIcCallIcon color={"success"} style={{marginRight: "10px"}} />,
tone: "success",
icon: <AddIcCallIcon fontSize="small" />,
click: ({event}) => {
event.preventDefault();
event.stopPropagation();
setOpenCreateNewCallbackDialog(true);
}
},
{type: "divider", name: "payload-danger-divider"},
{
name: props.deleted ? "Restore Payload" : "Delete the Payload from Disk",
type: "item",
tone: props.deleted ? "success" : "danger",
danger: !props.deleted,
icon: props.deleted ? <RestoreFromTrashIcon color={"success"} style={{marginRight: "10px"}} /> :
<DeleteIcon style={{marginRight: "10px"}}/>,
icon: props.deleted ? <RestoreFromTrashIcon fontSize="small" /> :
<DeleteIcon fontSize="small" />,
click: ({event}) => {
event.preventDefault();
event.stopPropagation();
@@ -349,11 +370,15 @@ export function PayloadsTableRow(props){
<TableRow key={"payload" + props.uuid} hover>
<MythicStyledTableCell>
<Button
className="mythic-table-row-action"
aria-expanded={openUpdate ? "true" : undefined}
aria-haspopup="menu"
className="mythic-table-row-action mythic-table-row-action-hover-info"
endIcon={<ArrowDropDownIcon fontSize="small" />}
size="small"
onClick={openMenu}
variant="outlined"
>
Actions <ArrowDropDownIcon fontSize="small" />
Actions
</Button>
{openUpdate &&
<ClickAwayListener onClickAway={handleClose} mouseEvent={"onMouseDown"}>
@@ -361,17 +386,24 @@ export function PayloadsTableRow(props){
isOpen={dropdownAnchorRef.current}
onOpen={setOpenUpdateDialog}
externallyOpen={openUpdate}
minWidth="17rem"
anchorOrigin={{vertical: "bottom", horizontal: "left"}}
transformOrigin={{vertical: "top", horizontal: "left"}}
MenuListProps={{className: "mythic-response-action-menu"}}
menu={
options.map((option, index) => (
option.type === 'item' ? (
<DropdownMenuItem
key={option.name}
disabled={option.disabled}
className={option.danger ? "mythic-menu-item-hover-danger" : undefined}
className={payloadMenuItemClass(option.tone || (option.danger ? "danger" : "info"))}
onClick={(event) => handleMenuItemClick(event, option.click)}
>
{option.icon}{option.name}
<ListItemIcon>{option.icon}</ListItemIcon>
<ListItemText>{option.name}</ListItemText>
</DropdownMenuItem>
) : option.type === 'divider' ? (
<Divider key={option.name || `divider-${index}`} />
) : option.type === 'menu' ? (
<DropdownNestedMenuItem
label={option.name}
@@ -381,10 +413,11 @@ export function PayloadsTableRow(props){
<DropdownMenuItem
key={menuOption.name}
disabled={menuOption.disabled}
className={menuOption.danger ? "mythic-menu-item-hover-danger" : undefined}
className={payloadMenuItemClass(menuOption.tone || (menuOption.danger ? "danger" : "info"))}
onClick={(event) => handleMenuItemClick(event, menuOption.click)}
>
{menuOption.icon}{menuOption.name}
<ListItemIcon>{menuOption.icon}</ListItemIcon>
<ListItemText>{menuOption.name}</ListItemText>
</DropdownMenuItem>
))
}
@@ -1,15 +1,23 @@
import React from 'react';
import {ReportingTable} from './ReportingTable';
import {MythicPageBody} from "../../MythicComponents/MythicPageBody";
import {MythicPageHeader} from "../../MythicComponents/MythicPageHeader";
import {MythicPageHeader, MythicPageHeaderChip} from "../../MythicComponents/MythicPageHeader";
import SummarizeIcon from '@mui/icons-material/Summarize';
export function Reporting() {
return (
<MythicPageBody>
<MythicPageHeader
icon={<SummarizeIcon fontSize="small" />}
title={"Mythic Report Generation"}
subtitle={"Generate operation reports from Mythic data and selected report sections."}
meta={
<>
<MythicPageHeaderChip label="HTML" />
<MythicPageHeaderChip label="JSON" />
</>
}
/>
<ReportingTable />
</MythicPageBody>
@@ -1,16 +1,26 @@
import React, { useEffect } from 'react';
import {Button} from '@mui/material';
import React from 'react';
import Box from '@mui/material/Box';
import Paper from '@mui/material/Paper';
import Switch from '@mui/material/Switch';
import Select from '@mui/material/Select';
import Typography from '@mui/material/Typography';
import {alpha, useTheme} from '@mui/material/styles';
import ArticleOutlinedIcon from '@mui/icons-material/ArticleOutlined';
import CodeIcon from '@mui/icons-material/Code';
import FactCheckOutlinedIcon from '@mui/icons-material/FactCheckOutlined';
import ManageSearchIcon from '@mui/icons-material/ManageSearch';
import PersonSearchIcon from '@mui/icons-material/PersonSearch';
import PlayCircleOutlineIcon from '@mui/icons-material/PlayCircleOutline';
import ShieldOutlinedIcon from '@mui/icons-material/ShieldOutlined';
import TerminalIcon from '@mui/icons-material/Terminal';
import MythicTextField from '../../MythicComponents/MythicTextField';
import MenuItem from '@mui/material/MenuItem';
import Table from '@mui/material/Table';
import TableBody from '@mui/material/TableBody';
import TableCell from '@mui/material/TableCell';
import TableContainer from '@mui/material/TableContainer';
import TableHead from '@mui/material/TableHead';
import TableRow from '@mui/material/TableRow';
import {useTheme} from '@mui/material/styles';
import {MythicPageHeaderChip} from "../../MythicComponents/MythicPageHeader";
import {
MythicTableToolbar,
MythicTableToolbarGroup,
MythicToolbarButton,
MythicToolbarMenuItem,
MythicToolbarSelect
} from "../../MythicComponents/MythicTableToolbar";
import {snackActions} from '../../utilities/Snackbar';
import {useMutation, gql, useSubscription} from '@apollo/client';
import {MythicSnackDownload} from '../../MythicComponents/MythicSnackDownload';
@@ -31,18 +41,119 @@ subscription generatedReportEventSubscription($fromNow: timestamp!){
}
}
`;
export function ReportingTable(props){
const outputOptions = ["html", "json"];
const reportPanelSx = (theme) => ({
backgroundColor: theme.surfaces?.raised || theme.palette.background.paper,
border: `1px solid ${theme.table?.borderSoft || theme.borderColor}`,
borderRadius: `${theme.shape.borderRadius}px`,
boxShadow: theme.palette.mode === "dark" ? "inset 0 1px 0 rgba(255,255,255,0.04)" : "0 1px 2px rgba(15,23,42,0.05)",
display: "flex",
flexDirection: "column",
gap: 0.75,
minHeight: 0,
minWidth: 0,
overflow: "hidden",
p: 0.75,
});
const panelHeaderSx = (theme) => ({
alignItems: {xs: "stretch", md: "center"},
backgroundImage: theme.gradients?.sectionHeader,
border: `1px solid ${theme.table?.borderSoft || theme.borderColor}`,
borderRadius: `${theme.shape.borderRadius}px`,
display: "flex",
flexWrap: "wrap",
gap: 0.75,
justifyContent: "space-between",
minWidth: 0,
p: "0.65rem 0.75rem",
});
const optionGridSx = {
display: "grid",
gap: 0.75,
gridTemplateColumns: {xs: "1fr", lg: "minmax(0, 1.05fr) minmax(20rem, 0.95fr)"},
minWidth: 0,
};
const sectionSx = (theme) => ({
backgroundColor: theme.palette.mode === "dark" ? alpha(theme.palette.common.white, 0.025) : alpha(theme.palette.common.black, 0.012),
border: `1px solid ${theme.table?.borderSoft || theme.borderColor}`,
borderRadius: `${theme.shape.borderRadius}px`,
display: "flex",
flexDirection: "column",
gap: 0.55,
minWidth: 0,
p: 0.75,
});
const optionRowSx = (theme, disabled) => ({
alignItems: "center",
backgroundColor: disabled ? alpha(theme.palette.text.disabled, 0.04) : (theme.palette.mode === "dark" ? alpha(theme.palette.common.white, 0.035) : alpha(theme.palette.common.black, 0.018)),
border: `1px solid ${disabled ? alpha(theme.palette.text.disabled, 0.12) : (theme.table?.borderSoft || theme.borderColor)}`,
borderRadius: `${theme.shape.borderRadius}px`,
display: "grid",
gap: 0.75,
gridTemplateColumns: "2rem minmax(0, 1fr) auto",
minWidth: 0,
opacity: disabled ? 0.72 : 1,
p: "0.55rem 0.65rem",
});
const iconSx = (theme, tone = "info") => {
const color = theme.palette[tone]?.main || theme.palette.info.main;
return {
alignItems: "center",
backgroundColor: alpha(color, theme.palette.mode === "dark" ? 0.18 : 0.1),
border: `1px solid ${alpha(color, theme.palette.mode === "dark" ? 0.42 : 0.28)}`,
borderRadius: `${theme.shape.borderRadius}px`,
color,
display: "inline-flex",
height: 30,
justifyContent: "center",
width: 30,
};
};
const ReportOptionRow = ({checked, description, disabled = false, icon, onChange, title, tone}) => {
const theme = useTheme();
const fromNow = React.useRef( (getSkewedNow()).toISOString() );
return (
<Box sx={optionRowSx(theme, disabled)}>
<Box sx={iconSx(theme, tone)}>
{icon}
</Box>
<Box sx={{minWidth: 0}}>
<Typography component="div" sx={{color: disabled ? "text.secondary" : "text.primary", fontSize: "0.82rem", fontWeight: 800, lineHeight: 1.25}}>
{title}
</Typography>
<Typography component="div" sx={{color: "text.secondary", fontSize: "0.72rem", fontWeight: 600, lineHeight: 1.35, mt: 0.2}}>
{description}
</Typography>
</Box>
<Switch
checked={checked}
disabled={disabled}
inputProps={{'aria-label': title}}
onChange={onChange}
size="small"
/>
</Box>
);
};
export function ReportingTable(){
const theme = useTheme();
const fromNow = React.useRef((getSkewedNow()).toISOString());
const [selectedOutputFormat, setSelectedOutputFormat] = React.useState("html");
const outputOptions = ["html", "json"];
const [includeMITREPerTask, setIncludeMITREPerTask] = React.useState(false);
const [includeMITREOverview, setIncludeMITREOverview] = React.useState(false);
const [includeOutput, setIncludeOutput] = React.useState(false);
const [excludedCallbackHost, setExcludedCallbackHost] = React.useState("");
const [excludedCallbackUser, setExcludedCallbackUser] = React.useState("");
const [excludedCallbackID, setExcludedCallbackID] = React.useState("");
const [generateReport] = useMutation(generateReportMutation, {
const [generateReport, {loading: generating}] = useMutation(generateReportMutation, {
onCompleted: (data) => {
if(data.generateReport.status === "success"){
snackActions.info("Generating report...");
@@ -52,14 +163,14 @@ export function ReportingTable(props){
}
},
onError: (data) => {
snackActions.error(data);
snackActions.error(data.message || data);
}
});
useSubscription(generatedReportSubscription, {
variables: {fromNow: fromNow.current},
fetchPolicy: "no-cache",
onError: (errorData) => {
snackActions.warning("Failed to get notifications for generated payloads");
onError: () => {
snackActions.warning("Failed to get notifications for generated reports");
},
onSubscriptionData: ({subscriptionData}) => {
if(subscriptionData?.data?.operationeventlog_stream?.length > 0){
@@ -73,16 +184,7 @@ export function ReportingTable(props){
if(evt.target.value !== "json"){
setIncludeOutput(false);
}
}
const changeExcludedCallbackUser = (name, value, error) => {
setExcludedCallbackUser(value);
}
const changeExcludedCallbackHost = (name, value, error) => {
setExcludedCallbackHost(value);
}
const changeExcludedCallbackID = (name, value, error) => {
setExcludedCallbackID(value);
}
};
const onGenerateReport = () => {
generateReport({variables: {
outputFormat: selectedOutputFormat,
@@ -92,105 +194,131 @@ export function ReportingTable(props){
excludedUsers: excludedCallbackUser,
excludedHosts: excludedCallbackHost,
excludedIDs: excludedCallbackID
}})
}
}});
};
const enabledSectionCount = [includeMITREPerTask, includeMITREOverview, includeOutput].filter(Boolean).length;
const hasExclusions = Boolean(excludedCallbackHost || excludedCallbackUser || excludedCallbackID);
return (
<React.Fragment>
<TableContainer className="mythicElement">
<Table size="small" style={{"tableLayout": "fixed", width: "100%", "overflow": "scroll"}}>
<TableHead>
<TableRow>
<TableCell style={{width: "25rem"}}>Report Option</TableCell>
<TableCell >Selected Values</TableCell>
</TableRow>
</TableHead>
<TableBody>
<TableRow hover>
<TableCell>Output Format</TableCell>
<TableCell>
<Select
autoFocus
value={selectedOutputFormat}
onChange={setOutputFormat}
>
{
outputOptions.map((opt, i) => (
<MenuItem key={"outputformat" + i} value={opt}>{opt}</MenuItem>
))
}
</Select>
</TableCell>
</TableRow>
<TableRow hover>
<TableCell>Include MITRE ATT&CK Coverage Per Task</TableCell>
<TableCell>
<Switch
checked={includeMITREPerTask}
onChange={evt => setIncludeMITREPerTask(!includeMITREPerTask)}
inputProps={{ 'aria-label': 'primary checkbox' }}
name="active"
<Paper elevation={0} sx={reportPanelSx(theme)}>
<Box sx={panelHeaderSx(theme)}>
<Box sx={{minWidth: 0}}>
<Typography component="h2" sx={{color: "text.primary", fontSize: "0.92rem", fontWeight: 850, lineHeight: 1.2}}>
Generation Settings
</Typography>
<Typography component="div" sx={{color: "text.secondary", fontSize: "0.74rem", fontWeight: 650, lineHeight: 1.35, mt: 0.2}}>
Report sections, output format, and callback exclusions.
</Typography>
</Box>
<Box sx={{alignItems: "center", display: "flex", flexWrap: "wrap", gap: 0.5}}>
<MythicPageHeaderChip label={selectedOutputFormat.toUpperCase()} status="info" />
<MythicPageHeaderChip label={`${enabledSectionCount} optional ${enabledSectionCount === 1 ? "section" : "sections"}`} status={enabledSectionCount > 0 ? "success" : "neutral"} />
{hasExclusions && <MythicPageHeaderChip label="Exclusions active" status="warning" />}
</Box>
</Box>
<MythicTableToolbar>
<MythicTableToolbarGroup label="Output format" style={{minWidth: "11rem"}}>
<MythicToolbarSelect
value={selectedOutputFormat}
onChange={setOutputFormat}
>
{outputOptions.map((opt) => (
<MythicToolbarMenuItem key={opt} value={opt}>{opt.toUpperCase()}</MythicToolbarMenuItem>
))}
</MythicToolbarSelect>
</MythicTableToolbarGroup>
<MythicTableToolbarGroup grow label="Current selection">
<Box sx={{alignItems: "center", display: "flex", flexWrap: "wrap", gap: 0.45}}>
<MythicPageHeaderChip icon={<ArticleOutlinedIcon />} label={`${selectedOutputFormat.toUpperCase()} report`} />
{includeMITREPerTask && <MythicPageHeaderChip label="MITRE per task" status="success" />}
{includeMITREOverview && <MythicPageHeaderChip label="MITRE overview" status="success" />}
{includeOutput && <MythicPageHeaderChip label="Command output" status="success" />}
</Box>
</MythicTableToolbarGroup>
<MythicTableToolbarGroup label="Actions">
<MythicToolbarButton
className="mythic-toolbar-button-hover-success"
disabled={generating}
onClick={onGenerateReport}
startIcon={<PlayCircleOutlineIcon fontSize="small" />}
variant="outlined"
>
{generating ? "Generating..." : "Start Generating"}
</MythicToolbarButton>
</MythicTableToolbarGroup>
</MythicTableToolbar>
<Box sx={optionGridSx}>
<Box sx={sectionSx(theme)}>
<Typography component="h3" sx={{color: "text.primary", fontSize: "0.8rem", fontWeight: 850, lineHeight: 1.2}}>
Report Sections
</Typography>
<ReportOptionRow
checked={includeMITREPerTask}
description="Include ATT&CK coverage alongside each task."
icon={<FactCheckOutlinedIcon fontSize="small" />}
onChange={() => setIncludeMITREPerTask(!includeMITREPerTask)}
title="MITRE ATT&CK Per Task"
tone="info"
/>
<ReportOptionRow
checked={includeMITREOverview}
description="Include an operation-level ATT&CK coverage summary."
icon={<ShieldOutlinedIcon fontSize="small" />}
onChange={() => setIncludeMITREOverview(!includeMITREOverview)}
title="MITRE ATT&CK Overview"
tone="info"
/>
<ReportOptionRow
checked={includeOutput}
description="Include command output in JSON reports."
disabled={selectedOutputFormat !== "json"}
icon={<TerminalIcon fontSize="small" />}
onChange={() => setIncludeOutput(!includeOutput)}
title="Command Output"
tone="warning"
/>
</Box>
<Box sx={sectionSx(theme)}>
<Typography component="h3" sx={{color: "text.primary", fontSize: "0.8rem", fontWeight: 850, lineHeight: 1.2}}>
Callback Exclusions
</Typography>
<Box sx={{display: "grid", gap: 0.55, gridTemplateColumns: "1fr", minWidth: 0}}>
<Box sx={{alignItems: "center", display: "grid", gap: 0.65, gridTemplateColumns: {xs: "1fr", sm: "2rem minmax(0, 1fr)"}, minWidth: 0}}>
<Box sx={iconSx(theme, "info")}><PersonSearchIcon fontSize="small" /></Box>
<MythicTextField
marginBottom="0px"
marginTop="0px"
name={"Excluded Usernames"}
onChange={(name, value) => setExcludedCallbackUser(value)}
value={excludedCallbackUser}
/>
</TableCell>
</TableRow>
<TableRow hover>
<TableCell>Include MITRE ATT&CK Coverage Overview</TableCell>
<TableCell>
<Switch
checked={includeMITREOverview}
onChange={evt => setIncludeMITREOverview(!includeMITREOverview)}
inputProps={{ 'aria-label': 'primary checkbox' }}
name="active"
</Box>
<Box sx={{alignItems: "center", display: "grid", gap: 0.65, gridTemplateColumns: {xs: "1fr", sm: "2rem minmax(0, 1fr)"}, minWidth: 0}}>
<Box sx={iconSx(theme, "info")}><ManageSearchIcon fontSize="small" /></Box>
<MythicTextField
marginBottom="0px"
marginTop="0px"
name={"Excluded Hostnames"}
onChange={(name, value) => setExcludedCallbackHost(value)}
value={excludedCallbackHost}
/>
</TableCell>
</TableRow>
{
selectedOutputFormat === "json" &&
<TableRow hover>
<TableCell>Include Command Output</TableCell>
<TableCell>
<Switch
checked={includeOutput}
onChange={evt => setIncludeOutput(!includeOutput)}
inputProps={{ 'aria-label': 'primary checkbox' }}
name="active"
/>
</TableCell>
</TableRow>
}
<TableRow hover>
<TableCell>Exclude Callbacks With Matching Values (comma separated)</TableCell>
<TableCell>
<Table>
<TableBody>
<TableRow>
<TableCell style={{width: "10rem"}}>Exclude Users</TableCell>
<TableCell>
<MythicTextField onChange={changeExcludedCallbackUser} value={excludedCallbackUser} name={"Excluded Usernames"}/>
</TableCell>
</TableRow>
<TableRow>
<TableCell>Exclude Hosts</TableCell>
<TableCell>
<MythicTextField onChange={changeExcludedCallbackHost} value={excludedCallbackHost} name={"Excluded Hostnames"}/>
</TableCell>
</TableRow>
<TableRow>
<TableCell>Exclude IDs</TableCell>
<TableCell>
<MythicTextField onChange={changeExcludedCallbackID} value={excludedCallbackID} name={"Excluded Callback IDs"}/>
</TableCell>
</TableRow>
</TableBody>
</Table>
</TableCell>
</TableRow>
</TableBody>
</Table>
</TableContainer>
<div style={{display: "flex", justifyContent: "space-around"}}>
<Button color={"success"} style={{marginTop: "10px"}} onClick={onGenerateReport}>Start Generating</Button>
</div>
</React.Fragment>
)
</Box>
<Box sx={{alignItems: "center", display: "grid", gap: 0.65, gridTemplateColumns: {xs: "1fr", sm: "2rem minmax(0, 1fr)"}, minWidth: 0}}>
<Box sx={iconSx(theme, "info")}><CodeIcon fontSize="small" /></Box>
<MythicTextField
marginBottom="0px"
marginTop="0px"
name={"Excluded Callback IDs"}
onChange={(name, value) => setExcludedCallbackID(value)}
value={excludedCallbackID}
/>
</Box>
</Box>
</Box>
</Box>
</Paper>
);
}
@@ -87,12 +87,6 @@ export function Search(props){
<MythicPageHeader
title={"Search"}
subtitle={"Pivot across callbacks, tasks, files, payloads, credentials, and operation artifacts."}
meta={
<>
<MythicPageHeaderChip label={currentTabLabel} />
<MythicPageHeaderChip label={`${tabTypes.length} search views`} />
</>
}
/>
<AppBar
position="static"
@@ -37,6 +37,7 @@ type Filemeta struct {
EventGroupID structs.NullInt64 `db:"eventgroup_id" json:"eventgroup_id" mapstructure:"eventgroup_id"`
EventStepInstanceID structs.NullInt64 `db:"eventstepinstance_id" json:"event_step_instance_id" mapstructure:"event_step_instance_id"`
APITokensID structs.NullInt64 `db:"apitokens_id" json:"api_tokens_id" mapstructure:"apitokens_id"`
APIToken Apitokens `db:"apitoken" json:"apitoken"`
CopyOfFileID structs.NullInt64 `db:"copy_of_file_id" json:"copy_of_file_id" mapstructure:"copy_of_file_id"`
ReceivedChunkIDs MythicJSONText `db:"received_chunk_ids" json:"received_chunk_ids" mapstructure:"received_chunk_id"`
}
@@ -3,7 +3,6 @@ package rabbitmq
import (
"database/sql"
"encoding/json"
"fmt"
"time"
"github.com/its-a-feature/Mythic/authentication/mythicjwt"
@@ -16,13 +15,8 @@ import (
const directFileScopedTokenTTL = 30 * time.Second
type MythicRPCDirectFileTokenCreateMessage struct {
AgentTaskID *string `json:"agent_task_id"`
AgentCallbackID *string `json:"agent_callback_id"`
PayloadUUID *string `json:"payload_uuid"`
FileUUID string `json:"agent_file_id"`
APITokenID *int `json:"apitoken_id"`
EventstepInstanceID *int `json:"eventstep_instance_id"`
Action string `json:"action"` // upload, download, both
FileUUID string `json:"agent_file_id"`
Action string `json:"action"` // upload, download, both
}
type MythicRPCDirectFileTokenCreateMessageResponse struct {
@@ -40,82 +34,41 @@ func init() {
})
}
func resolveScopedTokenOperatorAndOperation(input MythicRPCDirectFileTokenCreateMessage) (int, int, error) {
if input.AgentTaskID != nil {
task := databaseStructs.Task{}
if err := database.DB.Get(&task, `SELECT operator_id, operation_id FROM task WHERE agent_task_id=$1`,
*input.AgentTaskID); err != nil {
return 0, 0, err
}
return task.OperatorID, task.OperationID, nil
}
if input.PayloadUUID != nil {
payload := databaseStructs.Payload{}
if err := database.DB.Get(&payload, `SELECT operator_id, operation_id FROM payload WHERE uuid=$1`,
*input.PayloadUUID); err != nil {
return 0, 0, err
}
return payload.OperatorID, payload.OperationID, nil
}
if input.AgentCallbackID != nil {
callback := databaseStructs.Callback{}
if err := database.DB.Get(&callback, `SELECT operation_id FROM callback WHERE agent_callback_id=$1`,
*input.AgentCallbackID); err != nil {
return 0, 0, err
}
operatorOperationData := []databaseStructs.Operatoroperation{}
if err := database.DB.Select(&operatorOperationData, `SELECT
operator.account_type "operator.account_type",
operator.id "operator.id",
operator.deleted "operator.deleted",
operator.active "operator.active"
FROM operatoroperation
JOIN operator ON operatoroperation.operator_id = operator.id
WHERE operatoroperation.operation_id=$1`, callback.OperationID); err != nil {
return 0, 0, err
}
for i := range operatorOperationData {
if operatorOperationData[i].CurrentOperator.AccountType == databaseStructs.AccountTypeBot &&
!operatorOperationData[i].CurrentOperator.Deleted &&
operatorOperationData[i].CurrentOperator.Active {
return operatorOperationData[i].CurrentOperator.ID, callback.OperationID, nil
}
}
return 0, 0, fmt.Errorf("need an active non-deleted bot account assigned to operation %d", callback.OperationID)
}
return 0, 0, fmt.Errorf("no operator/task/callback/payload information provided")
}
func MythicRPCDirectFileTokenCreate(input MythicRPCDirectFileTokenCreateMessage) MythicRPCDirectFileTokenCreateMessageResponse {
response := MythicRPCDirectFileTokenCreateMessageResponse{Success: false}
if input.FileUUID == "" {
response.Error = "file_uuid is required"
return response
}
operatorID, operationID, err := resolveScopedTokenOperatorAndOperation(input)
file := databaseStructs.Filemeta{}
err := database.DB.Get(&file, `SELECT
operator_id, operation_id, eventstepinstance_id, apitokens_id
FROM filemeta
WHERE agent_file_id=$1`,
input.FileUUID)
if err != nil {
response.Error = err.Error()
return response
}
user := databaseStructs.Operator{
ID: operatorID,
ID: file.OperatorID,
CurrentOperationID: sql.NullInt64{
Valid: true,
Int64: int64(operationID),
Int64: int64(file.OperationID),
},
}
eventstepInstanceID := 0
if input.EventstepInstanceID != nil {
eventstepInstanceID = *input.EventstepInstanceID
}
apitokenID := 0
if input.APITokenID != nil {
apitokenID = *input.APITokenID
if file.EventStepInstanceID.Valid {
eventstepInstanceID = int(file.EventStepInstanceID.Int64)
}
scope := mythicjwt.SCOPE_FILE_READ
if input.Action == "upload" {
scope = mythicjwt.SCOPE_FILE_WRITE
}
apitokenID := 0
if file.APITokensID.Valid {
apitokenID = int(file.APITokensID.Int64)
}
token, _, err := mythicjwt.GenerateScopedJWT(user, []string{scope}, input.FileUUID, directFileScopedTokenTTL, eventstepInstanceID, apitokenID)
if err != nil {
response.Error = err.Error()