updated single task view page

This commit is contained in:
its-a-feature
2026-05-05 17:20:23 -05:00
parent 57bd0adabb
commit c839abcbc1
8 changed files with 444 additions and 193 deletions
@@ -1,4 +1,4 @@
import React, {useState, useRef, useEffect} from 'react';
import React, {useState, useEffect} from 'react';
import Button from '@mui/material/Button';
import DialogActions from '@mui/material/DialogActions';
import DialogContent from '@mui/material/DialogContent';
@@ -8,7 +8,7 @@ import MenuItem from '@mui/material/MenuItem';
import FormControl from '@mui/material/FormControl';
import Select from '@mui/material/Select';
import InputLabel from '@mui/material/InputLabel';
import Input from '@mui/material/Input';
import Typography from '@mui/material/Typography';
export function IncludeMoreTasksDialog(props) {
const [taskSelected, setTaskSelected] = useState(0);
@@ -22,7 +22,6 @@ export function IncludeMoreTasksDialog(props) {
{"type": "callback", "text": "This Callback"},
{"type": "operator", "text": "All callbacks but limited by operator"}
];
const inputRef = useRef(null);
const onRequestSubmit = () => {
props.submitFetchTasks({
taskSelected,
@@ -43,59 +42,69 @@ export function IncludeMoreTasksDialog(props) {
if(props.taskOptions.length > 0){
setTaskSelected(props.taskOptions[0]);
}
}, [props.tasks, props.taskOptions])
}, [props.taskOptions])
return (
<React.Fragment>
<DialogTitle id="form-dialog-title">Add More Tasks to View</DialogTitle>
<DialogContent dividers={true}>
<React.Fragment>
<FormControl style={{width:"100%"}}>
<InputLabel ref={inputRef}>View More Tasks Around Task:</InputLabel>
<DialogContent className="mythic-dialog-body" dividers={true}>
<div className="mythic-dialog-section">
<div className="mythic-dialog-section-header">
<div>
<Typography className="mythic-dialog-section-title">Task window</Typography>
<Typography className="mythic-dialog-section-description">Choose the anchor task and how many nearby tasks to pull into this view.</Typography>
</div>
</div>
<FormControl className="mythic-single-task-dialog-control" fullWidth size="small">
<InputLabel id="single-task-around-task-label">View More Tasks Around Task</InputLabel>
<Select
labelId="demo-dialog-select-label"
id="demo-dialog-select"
label="View More Tasks Around Task"
labelId="single-task-around-task-label"
id="single-task-around-task-select"
value={taskSelected}
onChange={handleTaskChange}
input={<Input />}
>
{taskOptions.map( (opt) => (
<MenuItem value={opt} key={"selectiontask:" + opt}>{opt}</MenuItem>
) )}
</Select>
</FormControl>
<br/><br/>
</React.Fragment>
<MythicTextField type="number" value={beforeCount} onChange={(name, value, error)=>setBeforeCount(value)} name={"Number of tasks before"} />
<MythicTextField type="number" value={afterCount} onChange={(name, value, error)=>setAfterCount(value)} name={"Number of tasks after"} />
<React.Fragment>
<FormControl style={{width:"100%"}}>
<InputLabel ref={inputRef}>Search Type</InputLabel>
<div className="mythic-single-task-dialog-grid">
<MythicTextField type="number" value={beforeCount} onChange={(name, value, error)=>setBeforeCount(value)} name={"Number of tasks before"} marginBottom="0px" />
<MythicTextField type="number" value={afterCount} onChange={(name, value, error)=>setAfterCount(value)} name={"Number of tasks after"} marginBottom="0px" />
</div>
</div>
<div className="mythic-dialog-section">
<div className="mythic-dialog-section-header">
<div>
<Typography className="mythic-dialog-section-title">Search scope</Typography>
<Typography className="mythic-dialog-section-description">Limit the neighboring task search to this callback, all callbacks, or an operator.</Typography>
</div>
</div>
<FormControl className="mythic-single-task-dialog-control" fullWidth size="small">
<InputLabel id="single-task-search-type-label">Search Type</InputLabel>
<Select
labelId="demo-dialog-select-label"
id="demo-dialog-select"
label="Search Type"
labelId="single-task-search-type-label"
id="single-task-search-type-select"
value={searchTerm}
onChange={handleChange}
input={<Input />}
>
{searchOptions.map( (opt) => (
<MenuItem value={opt.type} key={"selectiontype:" + opt.type}>{opt.text}</MenuItem>
) )}
</Select>
</FormControl>
<br/><br/>
</React.Fragment>
{searchTerm === 'operator' ? (
<MythicTextField multiline={false} onChange={(name, value, error)=>{setOperator(value)}} value={operator} name={"Operator Username"}/>
) : null}
{searchTerm === 'operator' ? (
<MythicTextField multiline={false} onChange={(name, value, error)=>{setOperator(value)}} value={operator} name={"Operator Username"} marginBottom="0px" />
) : null}
</div>
</DialogContent>
<DialogActions>
<Button onClick={props.onClose} variant="contained" color="primary">
<Button className="mythic-table-row-action" onClick={props.onClose} variant="contained">
Close
</Button>
<Button onClick={onRequestSubmit} variant="contained" color="success">Fetch Tasks</Button>
<Button className="mythic-table-row-action mythic-table-row-action-hover-success" disabled={taskOptions.length === 0} onClick={onRequestSubmit} variant="contained" color="success">Fetch Tasks</Button>
</DialogActions>
</React.Fragment>
);
}
@@ -1,10 +1,11 @@
import React, {useEffect} from 'react';
import {useLazyQuery, gql } from '@apollo/client';
import {gql } from '@apollo/client';
import {TaskArtifactsTable} from './TaskArtifactsTable';
import {TaskMITREATTACKTable} from './TaskMITREATTACKTable';
import {TaskFilesTable} from './TaskFilesTable';
import {TaskCredentialsTable} from './TaskCredentialsTable';
import {useMythicLazyQuery} from "../../utilities/useMythicLazyQuery";
import {MythicEmptyState} from "../../MythicComponents/MythicStateDisplay";
const MetadataQuery = gql`
@@ -68,10 +69,22 @@ query taskMetadataQuery($task_range: [Int!]) {
}`;
export function TaskMetadataTable(props){
const [tasks, setTasks] = React.useState([]);
const metadataQueryOptions = React.useMemo(() => ({fetchPolicy: "no-cache"}), []);
const hasMetadata = React.useMemo(() => {
return tasks.some((task) => (
task.credentials.length > 0 ||
task.filemeta.length > 0 ||
task.taskartifacts.length > 0 ||
task.attacktasks.length > 0
));
}, [tasks]);
const getMetadata = useMythicLazyQuery(MetadataQuery, {fetchPolicy: "no-cache"
});
const getMetadata = useMythicLazyQuery(MetadataQuery, metadataQueryOptions);
useEffect( () => {
if(props.taskIDs.length === 0){
setTasks([]);
return;
}
getMetadata({variables: {task_range: props.taskIDs } }).then(({data}) => {
setTasks(data.task);
}).catch(({data}) => {
@@ -79,11 +92,20 @@ export function TaskMetadataTable(props){
});
}, [props.taskIDs, getMetadata]);
return (
<div style={{marginTop: "10px", marginRight: "5px"}}>
<div className="mythic-single-task-metadata">
<TaskArtifactsTable tasks={tasks}/>
<TaskMITREATTACKTable tasks={tasks}/>
<TaskFilesTable tasks={tasks}/>
<TaskCredentialsTable tasks={tasks}/>
{tasks.length > 0 && !hasMetadata &&
<div className="mythic-single-task-empty-card">
<MythicEmptyState
compact
title="No task metadata"
description="These tasks do not currently have artifacts, MITRE mappings, files, screenshots, or credentials to show."
/>
</div>
}
</div>
);
}
@@ -3,18 +3,23 @@ import {TaskDisplay} from '../Callbacks/TaskDisplay';
import {gql, useLazyQuery, useQuery } from '@apollo/client';
import {useParams} from "react-router-dom";
import {TaskMetadataTable} from './MetadataTable';
import Typography from '@mui/material/Typography';
import Paper from '@mui/material/Paper';
import {Button, Link} from '@mui/material';
import Checkbox from '@mui/material/Checkbox';
import {Link} from '@mui/material';
import {IncludeMoreTasksDialog} from './IncludeMoreTasksDialog';
import { MythicDialog } from '../../MythicComponents/MythicDialog';
import {snackActions} from '../../utilities/Snackbar';
import {copyStringToClipboard} from '../../utilities/Clipboard';
import Switch from '@mui/material/Switch';
import {useTheme} from '@mui/material/styles';
import {taskingDataFragment} from '../Callbacks/CallbackMutations'
import {meState} from "../../../cache";
import { useReactiveVar } from '@apollo/client';
import ContentCopyIcon from '@mui/icons-material/ContentCopy';
import PlaylistAddIcon from '@mui/icons-material/PlaylistAdd';
import PlaylistRemoveIcon from '@mui/icons-material/PlaylistRemove';
import DeleteIcon from '@mui/icons-material/Delete';
import CloseIcon from '@mui/icons-material/Close';
import {MythicPageBody} from "../../MythicComponents/MythicPageBody";
import {MythicPageHeader, MythicPageHeaderChip, MythicSectionHeader} from "../../MythicComponents/MythicPageHeader";
import {MythicToolbarButton} from "../../MythicComponents/MythicTableToolbar";
const tasksQuery = gql`
${taskingDataFragment}
@@ -82,7 +87,11 @@ export function SingleTaskView(props){
const [tasks, setTasks] = React.useState([]);
const [removing, setRemoving] = React.useState(false);
const [openIncludeMoreTasksDialog, setOpenIncludeMoreTasksDialog] = React.useState(false);
const theme = useTheme();
const taskCount = tasks.filter((task) => task.type === "task").length;
const callbackCount = tasks.filter((task) => task.type === "callback").length;
const selectedRemoveCount = tasks.filter((task) => task.type === "task" && task.checked).length;
const taskCountLabel = taskCount === 1 ? "1 task" : `${taskCount} tasks`;
const callbackCountLabel = callbackCount === 1 ? "1 callback" : `${callbackCount} callbacks`;
const mergeData = (taskData) => {
let allNewParents = taskData.filter( (task) => task.parent_task_id === null);
let allData = [];
@@ -163,18 +172,36 @@ export function SingleTaskView(props){
});
setTasks(updated);
}
const removeTasksButton = () => {
removing ? setRemoving(false) : setRemoving(true);
const remainingTasks = tasks.filter( (task) => !task.checked);
const enterRemoveMode = () => {
setRemoving(true);
}
const cancelRemoveMode = () => {
setRemoving(false);
setTasks(tasks.map((task) => task.type === "task" ? {...task, checked: false} : task));
}
const removeSelectedTasks = () => {
const remainingTaskRows = tasks.filter( (task) => task.type === "task" && !task.checked);
const remainingCallbackDisplayIds = new Set(remainingTaskRows.map( (task) => task.callback.display_id));
const remainingTasks = tasks.filter( (task) => {
if(task.type === "task"){
return !task.checked;
}
return remainingCallbackDisplayIds.has(task.display_id);
}).map((task) => task.type === "task" ? {...task, checked: false} : task);
setTasks(remainingTasks);
const remainingTaskIDs = remainingTasks.reduce( (prev, cur) => {
if(cur.type === "task"){
return [...prev, cur.display_id];
const subIds = cur.tasks.filter( (subTask) => !prev.includes(subTask.display_id)).map( (subTask) => subTask.display_id);
if(prev.includes(cur.display_id)){
return [...prev, ...subIds];
}
return [...prev, cur.display_id, ...subIds];
}else{
return [...prev];
}
}, []);
setTaskIDs(remainingTaskIDs);
setRemoving(false);
}
const collapse_range = (all_nums) =>{
// takes in an array of the expanded numbers and collapses it down
@@ -242,44 +269,84 @@ export function SingleTaskView(props){
getTasks({variables: {task_range: [parseInt(taskId)]}});
}
}, [getTasks, taskId]);
const getCallbackTitle = (callback) => {
const domain = callback.domain === "" ? "" : `${callback.domain}\\`;
const integrity = callback.integrity_level > 2 ? "*" : "";
return `${domain}${callback.user}${integrity}@${callback.host}`;
}
return (
<div style={{height: "100%", display: "flex", flexDirection: "column", width:"100%", overflowY: 'auto',}}>
<Paper elevation={5} style={{backgroundColor: theme.pageHeader.main, color: theme.pageHeaderText.main, marginBottom: "5px"}} variant={"elevation"}>
<Typography variant="h4" style={{textAlign: "left", display: "inline-block", marginLeft: "20px"}}>
Task View
</Typography>
<Button variant="contained" size="small" style={{display: "inline-block", float: "right", marginTop:"5px", marginRight:"10px", backgroundColor: theme.palette.success.main}}
onClick={getShareableLink}>Get Shareable link</Button>
<Button variant="contained" size="small" style={{display: "inline-block", float: "right", marginTop:"5px", marginRight:"10px", backgroundColor: theme.palette.warning.main}}
onClick={removeTasksButton}>Remove Tasks From View</Button>
</Paper>
<div style={{ display: "flex", flexDirection: "column"}}>
<MythicPageBody>
<MythicPageHeader
title="Task View"
subtitle="Review task output, pull nearby task context into view, and inspect metadata collected by those tasks."
meta={
<>
<MythicPageHeaderChip label={taskCountLabel} />
<MythicPageHeaderChip label={callbackCountLabel} />
{removing && <MythicPageHeaderChip label={`${selectedRemoveCount} selected`} status={selectedRemoveCount > 0 ? "warning" : undefined} />}
</>
}
actions={
<>
<MythicToolbarButton className="mythic-table-row-action-hover-info" variant="outlined" onClick={getShareableLink} startIcon={<ContentCopyIcon fontSize="small" />}>
Share Link
</MythicToolbarButton>
{removing ? (
<>
<MythicToolbarButton className="mythic-table-row-action-hover-warning" variant="outlined" onClick={cancelRemoveMode} startIcon={<CloseIcon fontSize="small" />}>
Cancel
</MythicToolbarButton>
<MythicToolbarButton className="mythic-table-row-action-hover-danger" disabled={selectedRemoveCount === 0} variant="outlined" onClick={removeSelectedTasks} startIcon={<DeleteIcon fontSize="small" />}>
Remove Selected
</MythicToolbarButton>
</>
) : (
<MythicToolbarButton className="mythic-table-row-action-hover-warning" variant="outlined" onClick={enterRemoveMode} startIcon={<PlaylistRemoveIcon fontSize="small" />}>
Remove Tasks
</MythicToolbarButton>
)}
</>
}
/>
<div className="mythic-single-task-list">
{tasks.map( (task) => (
task.type === "task" ? (
<div key={"taskdisplay:" + task.display_id} style={{marginRight: "5px"}}>
<div style={{width: removing ? "95%" : "100%", display: "inline-block"}}>
<div className={`mythic-single-task-card-row${removing ? " mythic-single-task-card-row-removing" : ""}`} key={"taskdisplay:" + task.display_id}>
<div className="mythic-single-task-display">
<TaskDisplay me={me} task={task} command_id={task.command === null ? 0 : task.command.id} />
</div>
{removing ? (
<Switch
checked={task.checked}
onChange={toggleTaskToRemove}
name={"task" + task.display_id}
inputProps={{ 'aria-label': 'checkbox', 'color': theme.palette.error.main }}
/>
<label className={`mythic-single-task-remove-control${task.checked ? " mythic-single-task-remove-control-selected" : ""}`}>
<Checkbox
checked={task.checked}
color="error"
name={"task" + task.display_id}
onChange={toggleTaskToRemove}
size="small"
inputProps={{ 'aria-label': `remove task ${task.display_id}` }}
/>
<span>Remove</span>
</label>
) : null}
</div>
) : (
<Paper key={"taskdisplayforcallback:" + task.id} elevation={5} style={{ marginBottom: "5px", marginTop: "10px"}} variant={"elevation"}>
<Typography variant="h4" style={{textAlign: "left", display: "inline-block", marginLeft: "20px"}}>
{task.domain === "" ? null : (task.domain + "\\")}{task.user}{task.integrity_level > 2 ? ("*") : null}@{task.host} (
<Link style={{wordBreak: "break-all"}} color={"textPrimary"} underline="always" target="_blank" href={"/new/callbacks/" + task.display_id}>{task.display_id}</Link>
)
</Typography>
<Button variant="contained" size="small" style={{display: "inline-block", float: "right", marginTop:"5px", marginRight:"10px", backgroundColor: theme.palette.info.main}}
onClick={() => {setTaskSearchInfo(task.display_id)}}>Include More Tasks</Button>
</Paper>
<MythicSectionHeader
dense
key={"taskdisplayforcallback:" + task.id}
title={getCallbackTitle(task)}
subtitle={
<>
Callback <Link className="mythic-single-task-callback-link" color="inherit" underline="always" target="_blank" rel="noreferrer" href={"/new/callbacks/" + task.display_id}>#{task.display_id}</Link>
</>
}
actions={
<MythicToolbarButton className="mythic-table-row-action-hover-info" variant="outlined" onClick={() => {setTaskSearchInfo(task.display_id)}} startIcon={<PlaylistAddIcon fontSize="small" />}>
Include Tasks
</MythicToolbarButton>
}
sx={{mt: 0.75}}
/>
))
)
@@ -292,7 +359,7 @@ export function SingleTaskView(props){
/>
}
<TaskMetadataTable taskIDs={taskIDs} />
</div>
</MythicPageBody>
);
}
//
@@ -1,17 +1,14 @@
import React, { useEffect } from 'react';
import Typography from '@mui/material/Typography';
import Paper from '@mui/material/Paper';
import {useTheme} from '@mui/material/styles';
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 MythicStyledTableCell from '../../MythicComponents/MythicTableCell';
import {MythicPageHeaderChip, MythicSectionHeader} from "../../MythicComponents/MythicPageHeader";
export function TaskArtifactsTable(props){
const [artifacts, setArtifacts] = React.useState([]);
const theme = useTheme();
useEffect( () => {
const condensed = props.tasks.reduce( (prev, tsk) => {
@@ -24,38 +21,37 @@ export function TaskArtifactsTable(props){
if(artifacts.length === 0){
return null
}
const artifactCountLabel = artifacts.length === 1 ? "1 artifact" : `${artifacts.length} artifacts`;
return (
<React.Fragment>
<Paper elevation={5} style={{backgroundColor: theme.pageHeader.main, color: theme.pageHeaderText.main,marginBottom: "5px", marginTop: "10px"}} variant={"elevation"}>
<Typography variant="h4" style={{textAlign: "left", display: "inline-block", marginLeft: "20px"}}>
Artifact Tasks
</Typography>
</Paper>
<Paper elevation={5} style={{position: "relative", backgroundColor: theme.body}}>
<TableContainer className="mythicElement">
<Table size="small" style={{"tableLayout": "fixed", "maxWidth": "calc(100vw)", "overflow": "scroll"}}>
<div className="mythic-single-task-metadata-section">
<MythicSectionHeader
dense
title="Artifact Tasks"
subtitle="Artifacts created while these tasks executed."
actions={<MythicPageHeaderChip label={artifactCountLabel} />}
/>
<TableContainer className="mythicElement mythic-single-task-table-wrap">
<Table className="mythic-single-task-table" size="small">
<TableHead>
<TableRow>
<TableCell>Task ID</TableCell>
<TableCell>Artifact Type</TableCell>
<TableCell>Host</TableCell>
<TableCell>Artifact</TableCell>
<MythicStyledTableCell style={{width: "6rem"}}>Task ID</MythicStyledTableCell>
<MythicStyledTableCell style={{width: "12rem"}}>Artifact Type</MythicStyledTableCell>
<MythicStyledTableCell style={{width: "12rem"}}>Host</MythicStyledTableCell>
<MythicStyledTableCell>Artifact</MythicStyledTableCell>
</TableRow>
</TableHead>
<TableBody>
{artifacts.map( (artifact) => (
<TableRow key={"artifact" + artifact.id} hover>
<TableCell>{artifact.display_id}</TableCell>
<TableCell>{artifact.base_artifact}</TableCell>
<TableCell>{artifact.host}</TableCell>
<TableCell>{artifact.artifact_text}</TableCell>
<MythicStyledTableCell>{artifact.display_id}</MythicStyledTableCell>
<MythicStyledTableCell>{artifact.base_artifact}</MythicStyledTableCell>
<MythicStyledTableCell className="mythic-single-task-cell-break">{artifact.host}</MythicStyledTableCell>
<MythicStyledTableCell className="mythic-single-task-cell-break">{artifact.artifact_text}</MythicStyledTableCell>
</TableRow>
))}
</TableBody>
</Table>
</TableContainer>
</Paper>
</React.Fragment>
</div>
);
}
@@ -1,23 +1,21 @@
import React, { useEffect } from 'react';
import Typography from '@mui/material/Typography';
import Paper from '@mui/material/Paper';
import {useTheme} from '@mui/material/styles';
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 { MythicStyledTooltip } from '../../MythicComponents/MythicStyledTooltip';
import { copyStringToClipboard } from '../../utilities/Clipboard';
import {FontAwesomeIcon} from '@fortawesome/react-fontawesome';
import {faCopy} from '@fortawesome/free-solid-svg-icons';
import {IconButton} from '@mui/material';
import {snackActions} from '../../utilities/Snackbar';
import MythicStyledTableCell from '../../MythicComponents/MythicTableCell';
import {MythicPageHeaderChip, MythicSectionHeader} from "../../MythicComponents/MythicPageHeader";
import ContentCopyIcon from '@mui/icons-material/ContentCopy';
import {MythicStatusChip} from "../../MythicComponents/MythicStatusChip";
export function TaskCredentialsTable(props){
const [credentials, setCredentials] = React.useState([]);
const theme = useTheme();
useEffect( () => {
const condensed = props.tasks.reduce( (prev, tsk) => {
@@ -29,25 +27,25 @@ export function TaskCredentialsTable(props){
if(credentials.length === 0){
return null
}
const credentialCountLabel = credentials.length === 1 ? "1 credential" : `${credentials.length} credentials`;
return (
<React.Fragment>
<Paper elevation={5} style={{backgroundColor: theme.pageHeader.main, color: theme.pageHeaderText.main,marginBottom: "5px", marginTop: "10px"}} variant={"elevation"}>
<Typography variant="h4" style={{textAlign: "left", display: "inline-block", marginLeft: "20px"}}>
Credentials
</Typography>
</Paper>
<Paper elevation={5} style={{position: "relative", backgroundColor: theme.body}} >
<TableContainer className="mythicElement">
<Table size="small" style={{"tableLayout": "fixed", "maxWidth": "calc(100vw)", "overflow": "scroll"}}>
<div className="mythic-single-task-metadata-section">
<MythicSectionHeader
dense
title="Credentials"
subtitle="Credentials captured by the selected task set."
actions={<MythicPageHeaderChip label={credentialCountLabel} />}
/>
<TableContainer className="mythicElement mythic-single-task-table-wrap">
<Table className="mythic-single-task-table" size="small">
<TableHead>
<TableRow>
<TableCell style={{width: "4rem"}}>Task</TableCell>
<TableCell style={{width: "8rem"}}>Type</TableCell>
<TableCell>Realm</TableCell>
<TableCell>Account</TableCell>
<TableCell>Credentials</TableCell>
<TableCell>Comment</TableCell>
<MythicStyledTableCell style={{width: "4.5rem"}}>Task</MythicStyledTableCell>
<MythicStyledTableCell style={{width: "9rem"}}>Type</MythicStyledTableCell>
<MythicStyledTableCell>Realm</MythicStyledTableCell>
<MythicStyledTableCell>Account</MythicStyledTableCell>
<MythicStyledTableCell>Credentials</MythicStyledTableCell>
<MythicStyledTableCell>Comment</MythicStyledTableCell>
</TableRow>
</TableHead>
<TableBody>
@@ -57,8 +55,7 @@ export function TaskCredentialsTable(props){
</TableBody>
</Table>
</TableContainer>
</Paper>
</React.Fragment>
</div>
);
}
@@ -75,30 +72,32 @@ const CredentialTableRow = ({cred}) => {
}
return (
<TableRow key={"cred" + cred.id} hover>
<TableCell >{cred.display_id}</TableCell>
<TableCell>{cred.type}</TableCell>
<TableCell style={{whiteSpace: "pre-wrap", wordBreak: "break-all"}}>{cred.realm}</TableCell>
<TableCell style={{whiteSpace: "pre-wrap", wordBreak: "break-all"}}>{cred.account}</TableCell>
<TableCell>
<MythicStyledTableCell>{cred.display_id}</MythicStyledTableCell>
<MythicStyledTableCell>
<MythicStatusChip label={cred.type} status="neutral" showIcon={false} />
</MythicStyledTableCell>
<MythicStyledTableCell className="mythic-single-task-cell-break">{cred.realm}</MythicStyledTableCell>
<MythicStyledTableCell className="mythic-single-task-cell-break">{cred.account}</MythicStyledTableCell>
<MythicStyledTableCell>
{cred.credential_text.length > 64 ?
(
<React.Fragment>
<div className="mythic-single-task-credential-cell">
<MythicStyledTooltip title={"Copy to clipboard"}>
<IconButton onClick={() => onCopyToClipboard(cred.credential_text)} size="small">
<FontAwesomeIcon icon={faCopy} />
<IconButton className="mythic-table-row-icon-action mythic-table-row-icon-action-hover-info" onClick={() => onCopyToClipboard(cred.credential_text)} size="small">
<ContentCopyIcon fontSize="small" />
</IconButton>
</MythicStyledTooltip>
<Typography variant="body2" style={{wordBreak: "break-all", maxWidth: "40rem"}}>{displayCred}</Typography>
</React.Fragment>
<Typography className="mythic-single-task-credential-text" variant="body2">{displayCred}</Typography>
</div>
)
:
(
<React.Fragment>
<Typography variant="body2" style={{wordBreak: "break-all", maxWidth: "40rem"}}>{displayCred}</Typography>
<Typography className="mythic-single-task-credential-text" variant="body2">{displayCred}</Typography>
</React.Fragment>
)}
</TableCell>
<TableCell style={{whiteSpace: "pre-wrap", wordBreak: "break-all"}}>{cred.comment}</TableCell>
</MythicStyledTableCell>
<MythicStyledTableCell className="mythic-single-task-cell-break">{cred.comment}</MythicStyledTableCell>
</TableRow>
)
}
@@ -1,20 +1,18 @@
import React, { useEffect } from 'react';
import Typography from '@mui/material/Typography';
import Paper from '@mui/material/Paper';
import {useTheme} from '@mui/material/styles';
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 Link from '@mui/material/Link';
import {b64DecodeUnicode} from '../Callbacks/ResponseDisplay';
import MythicStyledTableCell from '../../MythicComponents/MythicTableCell';
import {MythicPageHeaderChip, MythicSectionHeader} from "../../MythicComponents/MythicPageHeader";
import {MythicStatusChip} from "../../MythicComponents/MythicStatusChip";
export function TaskFilesTable(props){
const [files, setFiles] = React.useState([]);
const theme = useTheme();
useEffect( () => {
const condensed = props.tasks.reduce( (prev, tsk) => {
@@ -27,56 +25,73 @@ export function TaskFilesTable(props){
if(files.length === 0){
return null
}
const fileCountLabel = files.length === 1 ? "1 file" : `${files.length} files`;
return (
<React.Fragment>
<Paper elevation={5} style={{backgroundColor: theme.pageHeader.main, color: theme.pageHeaderText.main,marginBottom: "5px", marginTop: "10px"}} variant={"elevation"}>
<Typography variant="h4" style={{textAlign: "left", display: "inline-block", marginLeft: "20px"}}>
Files / Screenshots
</Typography>
</Paper>
<Paper elevation={5} style={{position: "relative", backgroundColor: theme.body}}>
<TableContainer className="mythicElement">
<Table size="small" style={{ "overflow": "scroll"}}>
<div className="mythic-single-task-metadata-section">
<MythicSectionHeader
dense
title="Files / Screenshots"
subtitle="Files, payloads, downloads, uploads, and screenshots associated with these tasks."
actions={<MythicPageHeaderChip label={fileCountLabel} />}
/>
<TableContainer className="mythicElement mythic-single-task-table-wrap">
<Table className="mythic-single-task-table mythic-single-task-files-table" size="small">
<TableHead>
<TableRow>
<TableCell>Filename</TableCell>
<TableCell style={{width: "5rem"}}>Type</TableCell>
<TableCell >Remote Path</TableCell>
<TableCell >Comment</TableCell>
<TableCell >Hashes</TableCell>
<MythicStyledTableCell>Filename</MythicStyledTableCell>
<MythicStyledTableCell style={{width: "8rem"}}>Type</MythicStyledTableCell>
<MythicStyledTableCell>Remote Path</MythicStyledTableCell>
<MythicStyledTableCell>Comment</MythicStyledTableCell>
<MythicStyledTableCell>Hashes</MythicStyledTableCell>
</TableRow>
</TableHead>
<TableBody>
{files.map( (file) => (
<TableRow key={"file" + file.id} hover>
<TableCell>
<MythicStyledTableCell className="mythic-single-task-cell-break">
{!file.deleted && file.complete ? (
<Link href={"/direct/download/" + file.agent_file_id} style={{textDecoration: "underline", color: "inherit"}}>{b64DecodeUnicode(file.filename_text)}</Link>
<Link className="mythic-single-task-table-link" href={"/direct/download/" + file.agent_file_id}>{b64DecodeUnicode(file.filename_text)}</Link>
) : (
!file.complete ? (
b64DecodeUnicode(file.filename_text) + " (" + file.chunks_received + "/" + file.total_chunks + ")"
) : (b64DecodeUnicode(file.filename_text))
)}
</TableCell>
<TableCell>
{file.is_screenshot ? ("Screenshot") : (
file.is_payload ? ("Payload") : (
file.is_download_from_agent ? ("Download") : (
"Upload"
)
)
)}
</TableCell>
<TableCell style={{whiteSpace: "pre-wrap", wordBreak: "break-all"}}>{b64DecodeUnicode(file.full_remote_path_text) === "" ? ("") : (file.host + "\n" + b64DecodeUnicode(file.full_remote_path_text)) }</TableCell>
<TableCell style={{whiteSpace: "pre-wrap", wordBreak: "break-all"}}>{file.comment}</TableCell>
<TableCell>{"MD5: "}{file.md5}<br/>{"SHA1: "}{file.sha1}</TableCell>
</MythicStyledTableCell>
<MythicStyledTableCell>
<TaskFileTypeChip file={file} />
</MythicStyledTableCell>
<MythicStyledTableCell className="mythic-single-task-cell-break">{b64DecodeUnicode(file.full_remote_path_text) === "" ? ("") : (file.host + "\n" + b64DecodeUnicode(file.full_remote_path_text)) }</MythicStyledTableCell>
<MythicStyledTableCell className="mythic-single-task-cell-break">{file.comment}</MythicStyledTableCell>
<MythicStyledTableCell>
<div className="mythic-single-task-hash-list">
<span>MD5: {file.md5}</span>
<span>SHA1: {file.sha1}</span>
</div>
</MythicStyledTableCell>
</TableRow>
))}
</TableBody>
</Table>
</TableContainer>
</Paper>
</React.Fragment>
</div>
);
}
const TaskFileTypeChip = ({file}) => {
if(file.deleted){
return <MythicStatusChip label="Deleted" status="deleted" />;
}
if(!file.complete){
return <MythicStatusChip label="In progress" status="building" />;
}
if(file.is_screenshot){
return <MythicStatusChip label="Screenshot" status="info" showIcon={false} />;
}
if(file.is_payload){
return <MythicStatusChip label="Payload" status="warning" showIcon={false} />;
}
if(file.is_download_from_agent){
return <MythicStatusChip label="Download" status="success" showIcon={false} />;
}
return <MythicStatusChip label="Upload" status="neutral" showIcon={false} />;
}
@@ -1,17 +1,14 @@
import React, { useEffect } from 'react';
import Typography from '@mui/material/Typography';
import Paper from '@mui/material/Paper';
import {useTheme} from '@mui/material/styles';
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 MythicStyledTableCell from '../../MythicComponents/MythicTableCell';
import {MythicPageHeaderChip, MythicSectionHeader} from "../../MythicComponents/MythicPageHeader";
export function TaskMITREATTACKTable(props){
const [attacks, setAttacks] = React.useState([]);
const theme = useTheme();
useEffect( () => {
const condensed = props.tasks.reduce( (prev, tsk) => {
@@ -32,34 +29,33 @@ export function TaskMITREATTACKTable(props){
if(attacks.length === 0){
return null
}
const attackCountLabel = attacks.length === 1 ? "1 technique" : `${attacks.length} techniques`;
return (
<React.Fragment>
<Paper elevation={5} style={{backgroundColor: theme.pageHeader.main, color: theme.pageHeaderText.main,marginBottom: "5px", marginTop: "10px"}} variant={"elevation"}>
<Typography variant="h4" style={{textAlign: "left", display: "inline-block", marginLeft: "20px"}}>
MITRE ATT&amp;CK Mappings
</Typography>
</Paper>
<Paper elevation={5} style={{position: "relative", backgroundColor: theme.body}} >
<TableContainer className="mythicElement">
<Table size="small" style={{"tableLayout": "fixed", "maxWidth": "calc(100vw)", "overflow": "scroll"}}>
<div className="mythic-single-task-metadata-section">
<MythicSectionHeader
dense
title="MITRE ATT&CK Mappings"
subtitle="Unique techniques mapped from the selected tasks."
actions={<MythicPageHeaderChip label={attackCountLabel} />}
/>
<TableContainer className="mythicElement mythic-single-task-table-wrap">
<Table className="mythic-single-task-table mythic-single-task-mitre-table" size="small">
<TableHead>
<TableRow>
<TableCell>Technique ID</TableCell>
<TableCell>Technique</TableCell>
<MythicStyledTableCell style={{width: "10rem"}}>Technique ID</MythicStyledTableCell>
<MythicStyledTableCell>Technique</MythicStyledTableCell>
</TableRow>
</TableHead>
<TableBody>
{attacks.map( (attack) => (
<TableRow key={'attack' + attack.attack.id} hover>
<TableCell>{attack.attack.t_num}</TableCell>
<TableCell>{attack.attack.name}</TableCell>
<MythicStyledTableCell>{attack.attack.t_num}</MythicStyledTableCell>
<MythicStyledTableCell className="mythic-single-task-cell-break">{attack.attack.name}</MythicStyledTableCell>
</TableRow>
))}
</TableBody>
</Table>
</TableContainer>
</Paper>
</React.Fragment>
</div>
);
}
+147
View File
@@ -1124,6 +1124,153 @@ tspan {
.mythic-form-switch-control {
flex: 0 0 auto;
}
.mythic-single-task-list {
display: flex;
flex-direction: column;
gap: 0.5rem;
min-height: 0;
min-width: 0;
overflow: visible;
width: 100%;
}
.mythic-single-task-card-row {
align-items: flex-start;
display: grid;
gap: 0.5rem;
grid-template-columns: minmax(0, 1fr);
min-width: 0;
width: 100%;
}
.mythic-single-task-card-row-removing {
grid-template-columns: minmax(0, 1fr) auto;
}
.mythic-single-task-display {
min-width: 0;
width: 100%;
}
.mythic-single-task-remove-control {
align-items: center;
background-color: ${(props) => props.theme.palette.mode === "dark" ? alpha(props.theme.palette.error.main, 0.14) : alpha(props.theme.palette.error.main, 0.08)};
border: 1px solid ${(props) => alpha(props.theme.palette.error.main, props.theme.palette.mode === "dark" ? 0.45 : 0.28)};
border-radius: ${(props) => props.theme.shape.borderRadius}px;
color: ${(props) => props.theme.palette.error.main};
cursor: pointer;
display: flex;
flex-direction: column;
flex: 0 0 auto;
font-size: 0.72rem;
font-weight: 750;
gap: 0.1rem;
justify-content: center;
margin-top: 4px;
min-height: 52px;
min-width: 4.25rem;
padding: 0.35rem 0.45rem;
transition: background-color 120ms ease, border-color 120ms ease, color 120ms ease;
}
.mythic-single-task-remove-control:hover,
.mythic-single-task-remove-control-selected {
background-color: ${(props) => alpha(props.theme.palette.error.main, props.theme.palette.mode === "dark" ? 0.24 : 0.14)};
border-color: ${(props) => alpha(props.theme.palette.error.main, props.theme.palette.mode === "dark" ? 0.72 : 0.48)};
}
.mythic-single-task-remove-control .MuiCheckbox-root {
padding: 0;
}
.mythic-single-task-callback-link {
color: inherit !important;
font-weight: 800;
overflow-wrap: anywhere;
}
.mythic-single-task-metadata {
display: flex;
flex-direction: column;
gap: 0.6rem;
margin-top: 0.3rem;
min-width: 0;
width: 100%;
}
.mythic-single-task-metadata-section {
display: flex;
flex-direction: column;
gap: 0.4rem;
min-width: 0;
width: 100%;
}
.mythic-single-task-table-wrap {
background-color: ${(props) => props.theme.surfaces?.raised || props.theme.palette.background.paper};
overflow: auto;
}
.mythic-single-task-table {
min-width: 48rem;
table-layout: fixed;
}
.mythic-single-task-files-table {
min-width: 68rem;
}
.mythic-single-task-mitre-table {
min-width: 32rem;
}
.mythic-single-task-cell-break {
overflow-wrap: anywhere;
white-space: pre-wrap;
word-break: break-word;
}
.mythic-single-task-table-link {
color: inherit !important;
font-weight: 700;
text-decoration: underline !important;
}
.mythic-single-task-hash-list {
display: flex;
flex-direction: column;
font-family: ${(props) => props.theme.typography.fontFamilyMono || "monospace"};
font-size: 0.74rem;
gap: 0.2rem;
overflow-wrap: anywhere;
}
.mythic-single-task-credential-cell {
align-items: flex-start;
display: flex;
gap: 0.45rem;
min-width: 0;
}
.mythic-single-task-credential-text {
max-width: 42rem;
min-width: 0;
overflow-wrap: anywhere;
word-break: break-word;
}
.mythic-single-task-empty-card {
background-color: ${(props) => props.theme.surfaces?.raised || props.theme.palette.background.paper};
border: 1px solid ${(props) => props.theme.table?.borderSoft || props.theme.borderColor};
border-radius: ${(props) => props.theme.shape.borderRadius}px;
min-width: 0;
width: 100%;
}
.mythic-single-task-dialog-control {
margin-top: 0.45rem !important;
}
.mythic-single-task-dialog-grid {
align-items: flex-start;
display: grid;
gap: 0.65rem;
grid-template-columns: repeat(auto-fit, minmax(min(100%, 12rem), 1fr));
margin-top: 0.65rem;
min-width: 0;
}
@media screen and (max-width: 760px) {
.mythic-single-task-card-row-removing {
grid-template-columns: minmax(0, 1fr);
}
.mythic-single-task-remove-control {
align-items: center;
flex-direction: row;
justify-content: flex-start;
margin-top: 0;
min-height: 38px;
width: 100%;
}
}
.mythic-create-flow-shell {
display: flex;
flex-direction: column;