mirror of
https://github.com/its-a-feature/Mythic
synced 2026-06-08 14:55:38 +00:00
adjusted reporting page, payloads table,
This commit is contained in:
@@ -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()
|
||||
|
||||
Reference in New Issue
Block a user