mirror of
https://github.com/its-a-feature/Mythic
synced 2026-06-08 14:55:38 +00:00
updated single task view page
This commit is contained in:
@@ -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&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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user