better json table displays and search table responses

This commit is contained in:
its-a-feature
2026-05-10 16:09:54 -05:00
parent 4c2f5e3756
commit d662928b3f
10 changed files with 1299 additions and 687 deletions
@@ -267,140 +267,216 @@ export function MythicModifyStringDialog(props) {
);
}
export function MythicViewJSONAsTableDialog(props) {
const [comment, setComment] = React.useState([]);
const [tableType, setTableType] = React.useState("dictionary");
const [headers, setHeaders] = React.useState([]);
useEffect( () => {
let permissions = [];
try{
let permissionDict;
if(props.value.constructor === Object){
permissionDict = props.value;
}else{
permissionDict = JSON.parse(props.value);
}
if(!Array.isArray(permissionDict) && typeof permissionDict !== 'string'){
for(let key in permissionDict){
if(permissionDict[key] && permissionDict[key].constructor === Object){
// potentially have a nested dictionary here or array to become a dictionary, mark it
permissions.push({"name": key, "value": permissionDict[key], new_table: true, is_dictionary: true, headers: ["Name", "Value"]});
} else if(permissionDict[key] && Array.isArray(permissionDict[key])) {
if (permissionDict[key].length === 1){
if(permissionDict[key][0].constructor === Object){
permissions.push({"name": key, "value": permissionDict[key][0], new_table: true, is_dictionary: true, headers: ["Name", "Value"]});
}else{
permissions.push({"name": key, "value": JSON.stringify(permissionDict[key], null, 2)});
}
} else if (permissionDict[key].length > 1) {
if (permissionDict[key][0].constructor === Object) {
let newHeaders = [];
for(let i = 0; i < permissionDict[key].length; i++){
for(let newKey in permissionDict[key][i]){
if(!newHeaders.includes(newKey)){newHeaders.push(newKey)}
}
}
newHeaders.sort()
permissions.push({"name": key, "value": permissionDict[key], new_table: true, is_array: true, headers: newHeaders});
} else {
// it's an array, but not of dictionaries, so just stringify it
permissions.push({"name": key, "value": JSON.stringify(permissionDict[key], null, 2)});
}
} else {
permissions.push({"name": key, "value": JSON.stringify(permissionDict[key], null, 2)});
}
}else if(permissionDict[key] !== undefined && permissionDict[key] !== null){
permissions.push({"name": key, "value": permissionDict[key]});
}
setHeaders([props.leftColumn, props.rightColumn]);
}
}else{
setTableType("array");
if(permissionDict.length > 0){
setHeaders(Object.keys(permissionDict[0]));
permissions = permissionDict;
}else{
setHeaders([]);
}
}
}catch(error){
console.log(error);
const isPlainObject = (value) => {
return value !== null && typeof value === "object" && !Array.isArray(value);
}
const parseJSONDialogValue = (value) => {
if(value === undefined || value === null){
return value;
}
if(typeof value !== "string"){
return value;
}
try{
return JSON.parse(value);
}catch(error){
return value;
}
}
const getJSONValueType = (value) => {
if(value === null){return "null"}
if(value === undefined){return "empty"}
if(Array.isArray(value)){return "array"}
if(isPlainObject(value)){return "object"}
return typeof value;
}
const getJSONValueCount = (value) => {
if(Array.isArray(value)){return value.length}
if(isPlainObject(value)){return Object.keys(value).length}
if(value === undefined || value === null || value === ""){return 0}
return 1;
}
const getArrayObjectHeaders = (rows) => {
const headers = [];
rows.forEach((row) => {
if(!isPlainObject(row)){return}
Object.keys(row).forEach((key) => {
if(!headers.includes(key)){
headers.push(key);
}
setComment(permissions);
}, [props.value, props.leftColumn, props.rightColumn]);
});
});
return headers;
}
const JSONTypeBadge = ({value}) => {
const type = getJSONValueType(value);
const count = getJSONValueCount(value);
const label = type === "array" ? `${count} item${count === 1 ? "" : "s"}` :
type === "object" ? `${count} field${count === 1 ? "" : "s"}` :
type;
return <span className={`mythic-json-type-badge mythic-json-type-${type}`}>{label}</span>
}
const JSONPrimitiveValue = ({name, value, me}) => {
const type = getJSONValueType(value);
if(type === "null" || type === "empty" || value === ""){
return <span className="mythic-json-value-empty">None</span>
}
if(type === "boolean"){
return <span className={`mythic-json-value-boolean ${value ? "mythic-json-value-true" : "mythic-json-value-false"}`}>{value ? "True" : "False"}</span>
}
return <span className="mythic-json-value-primitive">{convertValueToContextValue(name, value, me)}</span>
}
const JSONTableCellValue = ({name, value, me, depth}) => {
if(Array.isArray(value) || isPlainObject(value)){
return (
<JSONTableValue
label={name}
value={value}
me={me}
depth={depth + 1}
/>
);
}
return <JSONPrimitiveValue name={name} value={value} me={me} />;
}
const JSONTableValue = ({label, value, me, depth=0, leftColumn="Name", rightColumn="Value"}) => {
const type = getJSONValueType(value);
const showPanelHeader = Boolean(label) && depth === 0;
if(type !== "array" && type !== "object"){
return <JSONPrimitiveValue name={label || ""} value={value} me={me} />;
}
if(type === "object"){
const entries = Object.entries(value);
return (
<div className={`mythic-json-panel ${depth === 0 ? "mythic-json-panel-root" : ""}`}>
{showPanelHeader &&
<div className="mythic-json-panel-header">
<span className="mythic-json-panel-title">{label}</span>
<JSONTypeBadge value={value} />
</div>
}
{entries.length === 0 ? (
<div className="mythic-json-empty-state">No fields to display.</div>
) : (
<TableContainer className="mythicElement mythic-json-table-wrap">
<Table size="small" stickyHeader={depth === 0} style={{tableLayout: "fixed"}}>
<TableHead>
<TableRow>
<TableCell style={{width: depth === 0 ? "14rem" : "12rem"}}>{leftColumn}</TableCell>
<TableCell>{rightColumn}</TableCell>
</TableRow>
</TableHead>
<TableBody>
{entries.map(([key, entryValue]) => (
<TableRow key={`${depth}-${key}`} hover>
<TableCell className="mythic-json-key-cell">
<div className="mythic-json-key-stack">
<span className="mythic-json-key">{key}</span>
<JSONTypeBadge value={entryValue} />
</div>
</TableCell>
<TableCell className="mythic-json-value-cell">
<JSONTableCellValue name={key} value={entryValue} me={me} depth={depth} />
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</TableContainer>
)}
</div>
);
}
const objectHeaders = getArrayObjectHeaders(value);
const isObjectArray = value.length > 0 && objectHeaders.length > 0 && value.every((row) => isPlainObject(row));
return (
<div className={`mythic-json-panel ${depth === 0 ? "mythic-json-panel-root" : ""}`}>
{showPanelHeader &&
<div className="mythic-json-panel-header">
<span className="mythic-json-panel-title">{label}</span>
<JSONTypeBadge value={value} />
</div>
}
{value.length === 0 ? (
<div className="mythic-json-empty-state">No items to display.</div>
) : isObjectArray ? (
<TableContainer className="mythicElement mythic-json-table-wrap">
<Table size="small" stickyHeader={depth === 0} style={{tableLayout: "fixed", minWidth: `${Math.max(38, objectHeaders.length * 12)}rem`}}>
<TableHead>
<TableRow>
<TableCell style={{width: "4rem"}}>#</TableCell>
{objectHeaders.map((header) => (
<TableCell key={`array-header-${header}`} style={{width: "12rem"}}>{header}</TableCell>
))}
</TableRow>
</TableHead>
<TableBody>
{value.map((row, rowIndex) => (
<TableRow key={`array-row-${rowIndex}`} hover>
<TableCell className="mythic-json-index-cell">{rowIndex + 1}</TableCell>
{objectHeaders.map((header) => (
<TableCell key={`array-row-${rowIndex}-${header}`} className="mythic-json-value-cell">
<JSONTableCellValue name={header} value={row[header]} me={me} depth={depth} />
</TableCell>
))}
</TableRow>
))}
</TableBody>
</Table>
</TableContainer>
) : (
<TableContainer className="mythicElement mythic-json-table-wrap">
<Table size="small" stickyHeader={depth === 0} style={{tableLayout: "fixed"}}>
<TableHead>
<TableRow>
<TableCell style={{width: "4rem"}}>#</TableCell>
<TableCell>Value</TableCell>
</TableRow>
</TableHead>
<TableBody>
{value.map((entryValue, rowIndex) => (
<TableRow key={`array-value-row-${rowIndex}`} hover>
<TableCell className="mythic-json-index-cell">{rowIndex + 1}</TableCell>
<TableCell className="mythic-json-value-cell">
<JSONTableCellValue name={`${label || "value"} ${rowIndex + 1}`} value={entryValue} me={me} depth={depth} />
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</TableContainer>
)}
</div>
);
}
export function MythicViewJSONAsTableDialog(props) {
const parsedValue = React.useMemo(() => parseJSONDialogValue(props.value), [props.value]);
const rootLabel = props.title || "JSON Data";
return (
<React.Fragment>
<MythicDraggableDialogTitle style={{wordBreak: "break-all", maxWidth: "100%"}}>{props.title}</MythicDraggableDialogTitle>
<TableContainer className="mythicElement" style={{paddingLeft: "10px"}}>
<Table size="small" style={{"tableLayout": "fixed", "maxWidth": "calc(100vw)", "overflow": "scroll"}}>
<TableHead>
<TableRow>
{headers.map( (header, index) => (
<TableCell key={'header' + index} style={index === 0 ? {width: "15%", wordBreak: "break-all"} : {wordBreak: "break-all"}}>{header}</TableCell>
))}
</TableRow>
</TableHead>
<TableBody>
{tableType === "dictionary" ? (
comment.map( (element, index) => (
<TableRow key={'row' + index} hover>
<TableCell style={{wordBreak: "break-all"}}>{element.name}</TableCell>
{element.new_table ?
(
<TableContainer className="mythicElement">
<Table size="small" style={{"tableLayout": "fixed", "maxWidth": "calc(100vw)", "overflow": "scroll"}}>
<TableHead>
<TableRow>
{element.headers.map( (header, index) => (
<TableCell key={'eheader' + header + index} style={index === 0 ? {width: "15%", wordBreak: "break-all"} : {wordBreak: "break-all"}}>{header}</TableCell>
))}
</TableRow>
</TableHead>
<TableBody>
{element.is_dictionary ? (
Object.keys(element.value).map( (key, dictIndex) => (
<TableRow key={'element' + dictIndex + "dictheader"}>
<TableCell style={{width: "30%", wordBreak: "break-all"}}>{key}</TableCell>
<TableCell style={{wordBreak: "break-all", whiteSpace: "pre-wrap"}}>{convertValueToContextValue(key, element.value[key], props.me)}</TableCell>
</TableRow>
))
): (
element.value.map( (e, elementIndex) => (
<TableRow>
{element.headers.map( (header, headerIndex) => (
<TableCell key={'element' + elementIndex + "header" + headerIndex} style={headerIndex === 0 ? {width: "15%", wordBreak: "break-all"} : {wordBreak: "break-all", whiteSpace: "pre-wrap"}}>{convertValueToContextValue(header, e[header], props.me)}</TableCell>
))}
</TableRow>
))
)}
</TableBody>
</Table>
</TableContainer>
)
:
(<TableCell style={{wordBreak: "break-all", whiteSpace: "pre-wrap"}}>{convertValueToContextValue(element.name, element.value, props.me) }</TableCell>)
}
</TableRow>
))
) : (
comment.map( (row, index) => (
<TableRow key={'row' + index} hover>
{Object.keys(row).map( (key) => (
<TableCell key={"row" + index + "cell" + key} style={{wordBreak: "break-all"}}>{convertValueToContextValue(key, row[key], props.me)}</TableCell>
))}
</TableRow>
))
) }
</TableBody>
</Table>
</TableContainer>
<MythicDraggableDialogTitle style={{wordBreak: "break-all", maxWidth: "100%"}}>
<div className="mythic-json-title-row">
<span>{rootLabel}</span>
<JSONTypeBadge value={parsedValue} />
</div>
</MythicDraggableDialogTitle>
<DialogContent dividers={true} className="mythic-dialog-body mythic-json-dialog-body">
<JSONTableValue
value={parsedValue}
me={props.me}
leftColumn={props.leftColumn || "Name"}
rightColumn={props.rightColumn || "Value"}
/>
</DialogContent>
<MythicDialogFooter>
<MythicDialogButton onClick={props.onClose}>
Close
@@ -458,21 +534,25 @@ export function MythicViewObjectPropertiesAsTableDialog(props) {
);
}
const convertValueToContextValue = (key, value, me) => {
if( key.includes("time") ){
const keyText = String(key || "").toLowerCase();
if(value === undefined || value === null){
return "";
}
if( keyText.includes("time") ){
try{
return TableRowDateCell({cellData: value, view_utc_time: me?.user?.view_utc_time})
}catch(error){
console.log("failed to parse metadata as date", key, value);
return value;
}
} else if( key.includes("size") ){
} else if( keyText.includes("size") ){
try{
return TableRowSizeCell({cellData: value})
}catch(error){
console.log("failed to parse metadata as size", key, value);
return value;
}
} else if (value.constructor === Object) {
} else if (isPlainObject(value)) {
return JSON.stringify(value, null, 2);
} else if (Array.isArray(value)){
return JSON.stringify(value, null, 2);
@@ -1,5 +1,5 @@
import React, { useEffect } from 'react';
import {Typography, Link, IconButton} from '@mui/material';
import {Link, IconButton} from '@mui/material';
import { gql, useMutation} from '@apollo/client';
import Table from '@mui/material/Table';
import TableBody from '@mui/material/TableBody';
@@ -11,6 +11,14 @@ import MythicStyledTableCell from '../../MythicComponents/MythicTableCell';
import CleanHandsTwoToneIcon from '@mui/icons-material/CleanHandsTwoTone';
import AddAlertTwoToneIcon from '@mui/icons-material/AddAlertTwoTone';
import {MythicStyledTooltip} from "../../MythicComponents/MythicStyledTooltip";
import {MythicStateChip} from "../../MythicComponents/MythicStateChip";
const singleLineCellStyle = {
minWidth: 0,
overflow: "hidden",
textOverflow: "ellipsis",
whiteSpace: "nowrap",
};
const updateNeedsCleanupMutation = gql`
mutation updateNeedsCleanupStatus($taskartifact_id: Int!, $needs_cleanup: Boolean!){
@@ -70,15 +78,14 @@ export function ArtifactTable(props){
}
return (
<TableContainer className="mythicElement" style={{height: "100%", overflowY: "auto"}}>
<Table stickyHeader size="small" style={{}}>
<Table stickyHeader size="small" style={{tableLayout: "fixed"}}>
<TableHead>
<TableRow>
<TableCell style={{width: "4rem"}}>Cleanup</TableCell>
<TableCell >Type</TableCell>
<TableCell >Command</TableCell>
<TableCell style={{width: "12rem"}}>Task</TableCell>
<TableCell >Operator</TableCell>
<TableCell >Host</TableCell>
<TableCell style={{width: "7rem"}}>Cleanup</TableCell>
<TableCell style={{width: "9rem"}}>Type</TableCell>
<TableCell style={{width: "9rem"}}>Command</TableCell>
<TableCell style={{width: "11rem"}}>Task</TableCell>
<TableCell style={{width: "12rem"}}>Host</TableCell>
<TableCell >Artifact</TableCell>
</TableRow>
</TableHead>
@@ -99,6 +106,9 @@ export function ArtifactTable(props){
}
function ArtifactTableRow(props){
const cleanupState = !props.needs_cleanup ? "neutral" : props.resolved ? "active" : "warning";
const cleanupLabel = !props.needs_cleanup ? "None" : props.resolved ? "Done" : "Needed";
const cleanupTooltip = !props.needs_cleanup ? "No cleanup needed" : props.resolved ? "Cleanup resolved" : "Artifact needs cleanup";
const MarkNeedsCleanup = () => {
props.MarkNeedsCleanup({id: props.id, needs_cleanup: true});
}
@@ -112,62 +122,75 @@ function ArtifactTableRow(props){
<React.Fragment>
<TableRow hover>
<MythicStyledTableCell>
{props.needs_cleanup && !props.resolved &&
<MythicStyledTooltip title={"Artifact needs to be cleaned up"}>
<IconButton className="mythic-table-row-icon-action mythic-table-row-icon-action-warning" onClick={MarkResolved} size="small">
<CleanHandsTwoToneIcon fontSize="small" />
</IconButton>
<div className="mythic-search-result-action-row">
{props.needs_cleanup && !props.resolved &&
<MythicStyledTooltip title={"Mark artifact as cleaned up"}>
<IconButton className="mythic-table-row-icon-action mythic-table-row-icon-action-hover-success" onClick={MarkResolved} size="small">
<CleanHandsTwoToneIcon fontSize="small" />
</IconButton>
</MythicStyledTooltip>
}
{props.needs_cleanup && props.resolved &&
<MythicStyledTooltip title={"Mark artifact as unresolved"}>
<IconButton className="mythic-table-row-icon-action mythic-table-row-icon-action-hover-warning" onClick={MarkUnresolved} size="small">
<CleanHandsTwoToneIcon fontSize="small" />
</IconButton>
</MythicStyledTooltip>
}
{!props.needs_cleanup &&
<MythicStyledTooltip title={"Mark artifact as needs cleanup"} >
<IconButton className="mythic-table-row-icon-action mythic-table-row-icon-action-hover-warning" onClick={MarkNeedsCleanup} size="small">
<AddAlertTwoToneIcon fontSize="small" />
</IconButton>
</MythicStyledTooltip>
}
<MythicStyledTooltip title={cleanupTooltip}>
<span>
<MythicStateChip compact label={cleanupLabel} state={cleanupState} />
</span>
</MythicStyledTooltip>
}
{props.needs_cleanup && props.resolved &&
<MythicStyledTooltip title={"Successfully cleaned up artifact"}>
<IconButton className="mythic-table-row-icon-action mythic-table-row-icon-action-success" onClick={MarkUnresolved} size="small">
<CleanHandsTwoToneIcon fontSize="small" />
</IconButton>
</MythicStyledTooltip>
}
{!props.needs_cleanup &&
<MythicStyledTooltip title={"Mark artifact as needs cleanup"} >
<IconButton className="mythic-table-row-icon-action mythic-table-row-icon-action-success" onClick={MarkNeedsCleanup} size="small">
<AddAlertTwoToneIcon fontSize="small" />
</IconButton>
</MythicStyledTooltip>
}
</div>
</MythicStyledTableCell>
<MythicStyledTableCell>
<Typography variant="body2" >{props.base_artifact}</Typography>
<div className="mythic-search-result-value" style={singleLineCellStyle} title={props.base_artifact}>
{props.base_artifact}
</div>
</MythicStyledTableCell>
<MythicStyledTableCell >
<Typography variant="body2" >{props?.task?.command?.cmd}</Typography>
<div className="mythic-search-result-value" style={singleLineCellStyle} title={props?.task?.command?.cmd}>
{props?.task?.command?.cmd}
</div>
</MythicStyledTableCell>
<MythicStyledTableCell style={{wordBreak: "break-all"}}>
{props.task &&
<>
<Link style={{wordBreak: "break-all"}} color="textPrimary" underline="always" target="_blank"
href={"/new/callbacks/" + props.task.callback.display_id}>
C-{props.task.callback.display_id}
</Link>{" / "}
<Link style={{wordBreak: "break-all"}} color="textPrimary" underline="always" target="_blank"
href={"/new/task/" + props.task.display_id}>
T-{props.task.display_id}
</Link>
<div className="mythic-search-result-stack">
<div className="mythic-search-result-link-row">
<Link style={{wordBreak: "break-all"}} color="textPrimary" underline="always" target="_blank"
href={"/new/callbacks/" + props.task.callback.display_id}>
C-{props.task.callback.display_id}
</Link>
<span className="mythic-search-result-secondary">/</span>
<Link style={{wordBreak: "break-all"}} color="textPrimary" underline="always" target="_blank"
href={"/new/task/" + props.task.display_id}>
T-{props.task.display_id}
</Link>
</div>
{props.task?.callback?.mythictree_groups.length > 0 ? (
<Typography variant="body2" style={{whiteSpace: "pre"}}>
<b>Groups: </b>{"\n" + props?.task?.callback.mythictree_groups.join("\n")}
</Typography>
<div className="mythic-search-result-secondary">
Groups: {props?.task?.callback.mythictree_groups.join(", ")}
</div>
) : null}
</>
</div>
}
</MythicStyledTableCell>
<MythicStyledTableCell>
<Typography variant="body2" style={{ display: "inline-block"}}>{props?.task?.operator?.username || null}</Typography>
</MythicStyledTableCell>
<MythicStyledTableCell >
<Typography variant="body2" style={{ display: "inline-block"}}>{props.host}</Typography>
<div className="mythic-search-result-value" style={singleLineCellStyle} title={props.host}>
{props.host}
</div>
</MythicStyledTableCell>
<MythicStyledTableCell>
<Typography variant="body2" style={{wordBreak: "break-all", display: "inline-block"}}>{props.artifact_text}</Typography>
<div className="mythic-search-result-code">{props.artifact_text}</div>
</MythicStyledTableCell>
</TableRow>
@@ -1,5 +1,5 @@
import React, { useEffect } from 'react';
import {IconButton, Typography, Link} from '@mui/material';
import {IconButton, Link} from '@mui/material';
import Table from '@mui/material/Table';
import TableBody from '@mui/material/TableBody';
import TableCell from '@mui/material/TableCell';
@@ -15,6 +15,23 @@ import MythicStyledTableCell from '../../MythicComponents/MythicTableCell';
import {TagsDisplay, ViewEditTags} from '../../MythicComponents/MythicTag';
import { MythicStyledTooltip } from '../../MythicComponents/MythicStyledTooltip';
const singleLineCellStyle = {
minWidth: 0,
overflow: "hidden",
textOverflow: "ellipsis",
whiteSpace: "nowrap",
};
const formatMetadataValue = (value) => {
if(value === null || value === undefined){
return "";
}
if(typeof value === "object"){
return JSON.stringify(value);
}
return String(value);
};
const updateFileComment = gql`
mutation updateCommentMutation($mythictree_id: Int!, $comment: String!){
update_mythictree_by_pk(pk_columns: {id: $mythictree_id}, _set: {comment: $comment}) {
@@ -24,7 +41,7 @@ mutation updateCommentMutation($mythictree_id: Int!, $comment: String!){
}
`;
export function CustomBrowserTable({rows, columns, me}){
export function CustomBrowserTable({rows, columns=[], me}){
const [entries, setEntries] = React.useState([]);
useEffect( () => {
setEntries([...rows]);
@@ -39,17 +56,19 @@ export function CustomBrowserTable({rows, columns, me}){
});
setEntries(updates);
}
const metadataColumnWidth = columns.length > 0 ? "12rem" : undefined;
const tableMinWidth = `${48 + (columns.length * 12)}rem`;
return (
<TableContainer className="mythicElement" style={{height: "100%", overflowY: "auto"}} >
<Table stickyHeader size="small" style={{ "maxWidth": "100%", "overflow": "scroll"}}>
<TableContainer className="mythicElement" style={{height: "100%", overflow: "auto"}} >
<Table stickyHeader size="small" style={{tableLayout: "fixed", minWidth: tableMinWidth}}>
<TableHead>
<TableRow>
<TableCell >Name</TableCell>
<TableCell style={{width: "24rem"}}>Entry</TableCell>
{columns.map((column, index) => (
<TableCell key={index}>{column.name}</TableCell>
<TableCell key={index} style={{width: metadataColumnWidth}}>{column.name}</TableCell>
))}
<TableCell >Comment</TableCell>
<TableCell >Tags</TableCell>
<TableCell style={{width: "16rem"}}>Comment</TableCell>
<TableCell style={{width: "8rem"}}>Tags</TableCell>
</TableRow>
</TableHead>
@@ -95,54 +114,95 @@ function CustomBrowserTableRow(props){
/>
}
<MythicStyledTableCell>
<MythicStyledTooltip title="View metadata">
<IconButton
className="mythic-table-row-icon-action mythic-table-row-icon-action-info"
size="small"
onClick={() => setViewPermissionsDialogOpen(true)}
>
<PlaylistAddCheckIcon fontSize="small" />
</IconButton>
</MythicStyledTooltip>
<Typography variant="body2" style={{wordBreak: "break-all"}}><b>Host: </b> {props.host}</Typography>
{props.callback?.mythictree_groups.length > 0 ? (
<Typography variant="body2" style={{whiteSpace: "pre"}}>
<b>Groups: </b>{props?.callback.mythictree_groups.join(", ")}<br/>
<b>Callback: </b>{
<Link style={{wordBreak: "break-all"}} color="textPrimary" underline="always" target="_blank"
<div className="mythic-search-result-stack mythic-search-result-stack-spacious">
<div className="mythic-search-result-action-row">
<MythicStyledTooltip title="View metadata">
<IconButton
className="mythic-table-row-icon-action mythic-table-row-icon-action-hover-info"
size="small"
onClick={() => setViewPermissionsDialogOpen(true)}
>
<PlaylistAddCheckIcon fontSize="small" />
</IconButton>
</MythicStyledTooltip>
<span
className="mythic-search-result-primary"
style={{...singleLineCellStyle, textDecoration: props.deleted ? "line-through" : ""}}
title={props.name_text || props.full_path_text}
>
{props.name_text || props.full_path_text}
</span>
</div>
<div className="mythic-search-result-inline">
<span className="mythic-search-result-label">Host</span>
<span className="mythic-search-result-value" style={singleLineCellStyle} title={props.host}>{props.host}</span>
</div>
{props.full_path_text && props.full_path_text !== props.name_text ? (
<div className="mythic-search-result-code mythic-search-result-code-compact" title={props.full_path_text}>
{props.full_path_text}
</div>
) : null}
{props.callback ? (
<div className="mythic-search-result-link-row">
<span className="mythic-search-result-label">Callback</span>
<Link color="textPrimary" underline="always" target="_blank"
href={"/new/callbacks/" + props.callback.display_id}>
C-{props.callback.display_id}
</Link>
}{", "}
<b>Task: </b>{
<Link style={{wordBreak: "break-all"}} color="textPrimary" underline="always" target="_blank"
href={"/new/task/" + props.task.display_id}>
T-{props.task.display_id}
</Link>
}
</Typography>
) : null}
<Typography variant="body2" style={{wordBreak: "break-all", textDecoration: props.deleted ? "strike-through" : ""}}>{props.full_path_text}</Typography>
{props.task ? (
<React.Fragment>
<span className="mythic-search-result-secondary">/</span>
<span className="mythic-search-result-label">Task</span>
<Link color="textPrimary" underline="always" target="_blank"
href={"/new/task/" + props.task.display_id}>
T-{props.task.display_id}
</Link>
</React.Fragment>
) : null}
</div>
) : null}
{props.callback?.mythictree_groups?.length > 0 ? (
<div className="mythic-search-result-secondary">
Groups: {props.callback.mythictree_groups.join(", ")}
</div>
) : null}
</div>
</MythicStyledTableCell>
{props.columns.map((column, index) => (
<MythicStyledTableCell key={index}>
<Typography variant="body2" style={{wordBreak: "break-all", display: "inline-block"}}>{props.metadata[column.key]}</Typography>
<div
className="mythic-search-result-value"
style={singleLineCellStyle}
title={formatMetadataValue(props.metadata?.[column.key])}
>
{formatMetadataValue(props.metadata?.[column.key]) || <span className="mythic-search-result-secondary">None</span>}
</div>
</MythicStyledTableCell>
))}
<MythicStyledTableCell>
<IconButton
className="mythic-table-row-icon-action mythic-table-row-icon-action-info"
onClick={() => setEditCommentDialogOpen(true)}
size="small"
>
<EditIcon fontSize="small" />
</IconButton>
<Typography variant="body2" style={{wordBreak: "break-all", display: "inline-block"}}>{props.comment}</Typography>
</MythicStyledTableCell>
<div className="mythic-search-result-action-row">
<MythicStyledTooltip title="Edit comment">
<IconButton
className="mythic-table-row-icon-action mythic-table-row-icon-action-hover-info"
onClick={() => setEditCommentDialogOpen(true)}
size="small"
>
<EditIcon fontSize="small" />
</IconButton>
</MythicStyledTooltip>
<span className="mythic-search-result-secondary" style={singleLineCellStyle} title={props.comment}>
{props.comment || "No comment"}
</span>
</div>
</MythicStyledTableCell>
<MythicStyledTableCell>
<ViewEditTags target_object={"mythictree_id"} target_object_id={props.id} me={me} />
<TagsDisplay tags={props.tags} />
<div className="mythic-custom-browser-tags-cell">
<ViewEditTags target_object={"mythictree_id"} target_object_id={props.id} me={me} />
<div className="mythic-custom-browser-tags-list">
<TagsDisplay tags={props.tags} />
</div>
</div>
</MythicStyledTableCell>
</TableRow>
@@ -11,7 +11,7 @@ import {Link} from '@mui/material';
import {snackActions} from '../../utilities/Snackbar';
import { meState } from '../../../cache';
import {useReactiveVar} from '@apollo/client';
import {IconButton, Typography} from '@mui/material';
import {IconButton} from '@mui/material';
import { toLocalTime } from '../../utilities/Time';
import MythicStyledTableCell from '../../MythicComponents/MythicTableCell';
import {b64DecodeUnicode} from "../Callbacks/ResponseDisplay";
@@ -166,60 +166,77 @@ function KeylogTableRow(props){
<React.Fragment>
<TableRow hover>
<MythicStyledTableCell>
<Link style={{wordBreak: "break-all"}} color="textPrimary" underline="always" target="_blank" href={"/new/callbacks/" + props.task.callback.display_id}>C-{props.task.callback.display_id}</Link>
{" / "}
<Link style={{wordBreak: "break-all"}} color="textPrimary" underline="always" target="_blank" href={"/new/task/" + props.task.display_id}>T-{props.task.display_id}</Link>
<div className="mythic-search-result-stack">
<div className="mythic-search-result-link-row">
<Link style={{wordBreak: "break-all"}} color="textPrimary" underline="always" target="_blank" href={"/new/callbacks/" + props.task.callback.display_id}>C-{props.task.callback.display_id}</Link>
<span className="mythic-search-result-secondary">/</span>
<Link style={{wordBreak: "break-all"}} color="textPrimary" underline="always" target="_blank" href={"/new/task/" + props.task.display_id}>T-{props.task.display_id}</Link>
</div>
{props.task?.callback?.mythictree_groups.length > 0 ? (
<Typography variant="body2" style={{wordBreak: "break-all"}}>
<b>Groups: </b>{props?.task?.callback.mythictree_groups.join(", ")}
</Typography>
) : null}
{props.task?.callback?.mythictree_groups.length > 0 ? (
<div className="mythic-search-result-secondary">
Groups: {props?.task?.callback.mythictree_groups.join(", ")}
</div>
) : null}
</div>
</MythicStyledTableCell>
<MythicStyledTableCell style={{wordBreak: "break-all"}}>
<Typography variant="body2" ><b>User: </b>{props.user}</Typography>
<Typography variant="body2" ><b>Host: </b>{props.task.callback.host}</Typography>
<Typography variant="body2" ><b>Window:</b> {props.window} </Typography>
<Typography variant="body2" ><b>Time: </b>{toLocalTime(props.timestamp, me?.user?.view_utc_time || false)}</Typography>
<Typography variant="body2" style={{display: "inline-block"}}><b>View Window Together: </b></Typography>
<MythicStyledTooltip title={"View current page data grouped together for this program"} tooltipStyle={{
display: "inline-block"
}}>
<IconButton
className="mythic-table-row-icon-action mythic-table-row-icon-action-info"
onClick={() => props.onGroupKeylogData(props.window, props.user, props.task.callback.host)}
size="small"
>
<FullscreenIcon fontSize="small" />
</IconButton>
</MythicStyledTooltip>
</MythicStyledTableCell>
<MythicStyledTableCell >
<MythicStyledTooltip title={"Copy to clipboard"} style={{display: "inline-block"}}>
<IconButton
className="mythic-table-row-icon-action mythic-table-row-icon-action-info"
onClick={() => onCopyToClipboard(keylogData)}
size="small"
>
<ContentCopyIcon fontSize="small" />
</IconButton>
</MythicStyledTooltip>
<Typography variant="body2" style={{wordBreak: "break-all", whiteSpace: "pre-wrap", display: "inline-block"}}>
{keylogData.slice(0, 500)}
{keylogData.length > 500 ? (
<>
{"..."}<br/>
<div className="mythic-search-result-stack mythic-search-result-stack-spacious">
<div className="mythic-search-result-inline">
<span className="mythic-search-result-label">User</span>
<span className="mythic-search-result-value">{props.user}</span>
</div>
<div className="mythic-search-result-inline">
<span className="mythic-search-result-label">Host</span>
<span className="mythic-search-result-value">{props.task.callback.host}</span>
</div>
<div className="mythic-search-result-inline">
<span className="mythic-search-result-label">Window</span>
<span className="mythic-search-result-value">{props.window}</span>
</div>
<div className="mythic-search-result-inline">
<span className="mythic-search-result-label">Time</span>
<span className="mythic-search-result-value">{toLocalTime(props.timestamp, me?.user?.view_utc_time || false)}</span>
</div>
<div className="mythic-search-result-action-row">
<MythicStyledTooltip title={"View current page data grouped together for this program"}>
<IconButton
className="mythic-table-row-icon-action mythic-table-row-icon-action-info"
onClick={() => {setOpenDisplayKeylogData(true);}}
className="mythic-table-row-icon-action mythic-table-row-icon-action-hover-info"
onClick={() => props.onGroupKeylogData(props.window, props.user, props.task.callback.host)}
size="small"
>
<OpenInNewIcon fontSize="small" />
<FullscreenIcon fontSize="small" />
</IconButton>
</>
) : null}
</MythicStyledTooltip>
<span className="mythic-search-result-secondary">View window together</span>
</div>
</div>
</MythicStyledTableCell>
<MythicStyledTableCell >
<div className="mythic-search-result-stack">
<div className="mythic-search-result-action-row">
<MythicStyledTooltip title={"Copy to clipboard"}>
<IconButton
className="mythic-table-row-icon-action mythic-table-row-icon-action-hover-info"
onClick={() => onCopyToClipboard(keylogData)}
size="small"
>
<ContentCopyIcon fontSize="small" />
</IconButton>
</MythicStyledTooltip>
{keylogData.length > 500 ? (
<MythicStyledTooltip title={"Open full keylog data"}>
<IconButton
className="mythic-table-row-icon-action mythic-table-row-icon-action-hover-info"
onClick={() => {setOpenDisplayKeylogData(true);}}
size="small"
>
<OpenInNewIcon fontSize="small" />
</IconButton>
</MythicStyledTooltip>
) : null}
</div>
<pre className="mythic-search-result-code mythic-search-result-code-compact">{keylogData.slice(0, 500)}{keylogData.length > 500 ? "..." : null}</pre>
{openDisplayKeylogData &&
<MythicDialog maxWidth={"100%"} fullWidth={true} open={openDisplayKeylogData} onClose={() => {setOpenDisplayKeylogData(false);}}
innerDialog={
@@ -227,7 +244,7 @@ function KeylogTableRow(props){
title={"Full keylog data"} value={keylogData}/>
} />
}
</Typography>
</div>
</MythicStyledTableCell>
</TableRow>
</React.Fragment>
@@ -1,5 +1,5 @@
import React, { useEffect } from 'react';
import {IconButton, Typography, Link} from '@mui/material';
import {IconButton, Link} from '@mui/material';
import Table from '@mui/material/Table';
import TableBody from '@mui/material/TableBody';
import TableCell from '@mui/material/TableCell';
@@ -14,6 +14,14 @@ import EditIcon from '@mui/icons-material/Edit';
import MythicStyledTableCell from '../../MythicComponents/MythicTableCell';
import {TagsDisplay, ViewEditTags} from '../../MythicComponents/MythicTag';
import { MythicStyledTooltip } from '../../MythicComponents/MythicStyledTooltip';
import {MythicStateChip} from "../../MythicComponents/MythicStateChip";
const singleLineCellStyle = {
minWidth: 0,
overflow: "hidden",
textOverflow: "ellipsis",
whiteSpace: "nowrap",
};
const updateFileComment = gql`
mutation updateCommentMutation($mythictree_id: Int!, $comment: String!){
@@ -97,7 +105,7 @@ function ProcessTableRow(props){
<MythicStyledTableCell>
<MythicStyledTooltip title="View permissions data">
<IconButton
className="mythic-table-row-icon-action mythic-table-row-icon-action-info"
className="mythic-table-row-icon-action mythic-table-row-icon-action-hover-info"
size="small"
onClick={() => setViewPermissionsDialogOpen(true)}
>
@@ -106,44 +114,67 @@ function ProcessTableRow(props){
</MythicStyledTooltip>
</MythicStyledTableCell>
<MythicStyledTableCell>
<Typography variant="body2" style={{wordBreak: "break-all", textDecoration: props.deleted ? "strike-through" : ""}}>{props.full_path_text}</Typography>
<div
className="mythic-search-result-value"
style={{...singleLineCellStyle, textDecoration: props.deleted ? "line-through" : ""}}
title={props.full_path_text}
>
{props.full_path_text}
</div>
</MythicStyledTableCell>
<MythicStyledTableCell>
<Typography variant="body2" style={{wordBreak: "break-all"}}><b>Host: </b> {props.host}</Typography>
{props.callback?.mythictree_groups.length > 0 ? (
<Typography variant="body2" style={{whiteSpace: "pre"}}>
<b>Groups: </b>{props?.callback.mythictree_groups.join(", ")}<br/>
<b>Callback: </b>{
<div className="mythic-search-result-stack">
<div className="mythic-search-result-inline">
<span className="mythic-search-result-label">Host</span>
<span className="mythic-search-result-value">{props.host}</span>
</div>
{props.callback ? (
<div className="mythic-search-result-link-row">
<span className="mythic-search-result-label">Callback</span>
<Link style={{wordBreak: "break-all"}} color="textPrimary" underline="always" target="_blank"
href={"/new/callbacks/" + props.callback.display_id}>
C-{props.callback.display_id}
</Link>
}<br/>
<b>Task: </b>{
<Link style={{wordBreak: "break-all"}} color="textPrimary" underline="always" target="_blank"
href={"/new/task/" + props.task.display_id}>
T-{props.task.display_id}
</Link>
}
</Typography>
) : null}
{props.task ? (
<>
<span className="mythic-search-result-secondary">/</span>
<Link style={{wordBreak: "break-all"}} color="textPrimary" underline="always" target="_blank"
href={"/new/task/" + props.task.display_id}>
T-{props.task.display_id}
</Link>
</>
) : null}
</div>
) : null}
{props.callback?.mythictree_groups.length > 0 ? (
<div className="mythic-search-result-secondary">
Groups: {props?.callback.mythictree_groups.join(", ")}
</div>
) : null}
</div>
</MythicStyledTableCell>
<MythicStyledTableCell>
<Typography variant="body2" style={{wordBreak: "break-all"}}>{props.name_text}</Typography>
{props.deleted &&
<Typography variant="body2" style={{wordBreak: "break-all"}}>{" (deleted)"}</Typography>
}
<div className="mythic-search-result-stack">
<div className="mythic-search-result-value" style={singleLineCellStyle} title={props.name_text}>
{props.name_text}
</div>
{props.deleted &&
<MythicStateChip compact label="Deleted" state="disabled" />
}
</div>
</MythicStyledTableCell>
<MythicStyledTableCell>
<IconButton
className="mythic-table-row-icon-action mythic-table-row-icon-action-info"
onClick={() => setEditCommentDialogOpen(true)}
size="small"
>
<EditIcon fontSize="small" />
</IconButton>
<Typography variant="body2" style={{wordBreak: "break-all", display: "inline-block"}}>{props.comment}</Typography>
<div className="mythic-search-result-action-row">
<IconButton
className="mythic-table-row-icon-action mythic-table-row-icon-action-hover-info"
onClick={() => setEditCommentDialogOpen(true)}
size="small"
>
<EditIcon fontSize="small" />
</IconButton>
<span className="mythic-search-result-secondary">{props.comment || "No comment"}</span>
</div>
</MythicStyledTableCell>
<MythicStyledTableCell>
<ViewEditTags target_object={"mythictree_id"} target_object_id={props.id} me={me} />
@@ -1,5 +1,5 @@
import React, { useEffect } from 'react';
import {IconButton, Typography, Link} from '@mui/material';
import {IconButton, Link} from '@mui/material';
import Table from '@mui/material/Table';
import TableBody from '@mui/material/TableBody';
import TableCell from '@mui/material/TableCell';
@@ -16,6 +16,8 @@ import {getStringSize} from '../Callbacks/ResponseDisplayTable';
import {MythicStyledTooltip} from "../../MythicComponents/MythicStyledTooltip";
import {adjustOutput} from "../Eventing/EventGroupInstancesTable";
import SpeedIcon from '@mui/icons-material/Speed';
import MythicStyledTableCell from '../../MythicComponents/MythicTableCell';
import {MythicStateChip} from "../../MythicComponents/MythicStateChip";
const toggleProxy = gql`
mutation ToggleProxyMutation($callbackport_id: Int!, $action: String!){
@@ -56,7 +58,7 @@ export function ProxySearchTable(props){
<Table stickyHeader size="small" style={{tableLayout: "fixed"}}>
<TableHead>
<TableRow>
<TableCell style={{width: "3.5rem"}}></TableCell>
<TableCell style={{width: "8rem"}}>State</TableCell>
<TableCell >User@Host</TableCell>
<TableCell style={{width: "9rem"}}>Task Info</TableCell>
<TableCell style={{width: "7rem"}}>Bound Port</TableCell>
@@ -142,72 +144,98 @@ function ProxySearchTableRow(props){
dialogText={confirmDialogText}
/>
}
<TableCell>{props.deleted ? (
<MythicStyledTooltip title="Start Proxy Port on Mythic Server">
<IconButton
className="mythic-table-row-icon-action mythic-table-row-icon-action-success"
size="small"
onClick={()=>{setOpenDeleteDialog(true);}}
>
<RestoreFromTrashIcon fontSize="small" />
</IconButton>
</MythicStyledTooltip>
) :
(<MythicStyledTooltip title="Stop Proxy Port on Mythic Server">
<IconButton
className="mythic-table-row-icon-action mythic-table-row-icon-action-hover-danger"
size="small"
onClick={()=>{setOpenDeleteDialog(true);}}
>
<DeleteIcon fontSize="small" />
</IconButton>
</MythicStyledTooltip>
)} </TableCell>
<TableCell>
<Typography variant="body2" style={{wordBreak: "break-all"}}>
<b>{props.callback.user}</b>@<i>{props.callback.host}</i>
</Typography>
<Typography variant="body2" style={{wordBreak: "break-all", display: "inline-block"}}>{props.callback.description}</Typography>
</TableCell>
<TableCell>
<Link style={{wordBreak: "break-all"}} color="textPrimary" underline="always" target="_blank"
href={"/new/callbacks/" + props.callback.display_id}>
C-{props.callback.display_id}
</Link>
{" / "}
<Link style={{wordBreak: "break-all"}} color="textPrimary" underline="always" target="_blank"
href={"/new/task/" + props.task.display_id}>
T-{props.task.display_id}
</Link>
</TableCell>
<TableCell>
<Typography variant="body2" style={{wordBreak: "break-all"}}>{props.local_port}</Typography>
</TableCell>
<TableCell>
<MythicStyledTableCell>
<div className="mythic-search-result-action-row">
{props.deleted ? (
<MythicStyledTooltip title="Start Proxy Port on Mythic Server">
<IconButton
className="mythic-table-row-icon-action mythic-table-row-icon-action-hover-success"
size="small"
onClick={()=>{setOpenDeleteDialog(true);}}
>
<RestoreFromTrashIcon fontSize="small" />
</IconButton>
</MythicStyledTooltip>
) :
(<MythicStyledTooltip title="Stop Proxy Port on Mythic Server">
<IconButton
className="mythic-table-row-icon-action mythic-table-row-icon-action-hover-danger"
size="small"
onClick={()=>{setOpenDeleteDialog(true);}}
>
<DeleteIcon fontSize="small" />
</IconButton>
</MythicStyledTooltip>
)}
<MythicStateChip compact label={props.deleted ? "Stopped" : "Running"} state={props.deleted ? "disabled" : "active"} />
</div>
</MythicStyledTableCell>
<MythicStyledTableCell>
<div className="mythic-search-result-stack">
<div className="mythic-search-result-primary">{props.callback.user}@{props.callback.host}</div>
{props.callback.description &&
<div className="mythic-search-result-secondary">{props.callback.description}</div>
}
</div>
</MythicStyledTableCell>
<MythicStyledTableCell>
<div className="mythic-search-result-link-row">
<Link style={{wordBreak: "break-all"}} color="textPrimary" underline="always" target="_blank"
href={"/new/callbacks/" + props.callback.display_id}>
C-{props.callback.display_id}
</Link>
<span className="mythic-search-result-secondary">/</span>
<Link style={{wordBreak: "break-all"}} color="textPrimary" underline="always" target="_blank"
href={"/new/task/" + props.task.display_id}>
T-{props.task.display_id}
</Link>
</div>
</MythicStyledTableCell>
<MythicStyledTableCell>
<span className="mythic-search-result-code">{props.local_port}</span>
</MythicStyledTableCell>
<MythicStyledTableCell>
<div className="mythic-search-result-stack">
{props.remote_port !== 0 &&
<Typography variant="body2" style={{wordBreak: "break-all"}}>{props.remote_ip}:{props.remote_port}</Typography>
<div className="mythic-search-result-primary">{props.remote_ip}:{props.remote_port}</div>
}
{props.remote_port === 0 &&
<div className="mythic-search-result-secondary">No remote endpoint</div>
}
{props.username !== "" &&
<Typography variant="body2" style={{wordBreak: "break-all"}}>
<b>Authentication Required</b><br/>
Username: <b>{props.username}</b><br/>
Password: <b>{props.password}</b>
</Typography>
<div className="mythic-search-result-stack">
<div className="mythic-search-result-inline">
<span className="mythic-search-result-label">Auth</span>
<span className="mythic-search-result-value">{props.username}</span>
</div>
<div className="mythic-search-result-inline">
<span className="mythic-search-result-label">Password</span>
<span className="mythic-search-result-value">{props.password}</span>
</div>
</div>
}
</TableCell>
<TableCell>
<b><MythicStyledTooltip title={"Rx is bytes Mythic received from the agent"}>{"Rx: "}</MythicStyledTooltip></b>
{getStringSize({cellData: {"plaintext": String(props.bytes_received)}})}
<br/>
<b><MythicStyledTooltip
title={"Tx is bytes Mythic sent to the agent"}>{"Tx: "}</MythicStyledTooltip></b>
{getStringSize({cellData: {"plaintext": String(props.bytes_sent)}})}
</TableCell>
<TableCell>
<Typography variant="body2" style={{wordBreak: "break-all"}}>{props.port_type}</Typography>
</TableCell>
<TableCell>
</div>
</MythicStyledTableCell>
<MythicStyledTableCell>
<div className="mythic-search-result-stack">
<span className="mythic-search-result-metric">
<MythicStyledTooltip title={"Rx is bytes Mythic received from the agent"}>
<span className="mythic-search-result-metric-label">Rx</span>
</MythicStyledTooltip>
<span className="mythic-search-result-value">{getStringSize({cellData: {"plaintext": String(props.bytes_received)}})}</span>
</span>
<span className="mythic-search-result-metric">
<MythicStyledTooltip title={"Tx is bytes Mythic sent to the agent"}>
<span className="mythic-search-result-metric-label">Tx</span>
</MythicStyledTooltip>
<span className="mythic-search-result-value">{getStringSize({cellData: {"plaintext": String(props.bytes_sent)}})}</span>
</span>
</div>
</MythicStyledTableCell>
<MythicStyledTableCell>
<MythicStateChip compact label={props.port_type} state="neutral" />
</MythicStyledTableCell>
<MythicStyledTableCell>
<Moment filter={(newTime) => adjustOutput(props, newTime)} interval={1000}
parse={"YYYY-MM-DDTHH:mm:ss.SSSSSSZ"}
withTitle
@@ -216,12 +244,12 @@ function ProxySearchTableRow(props){
>
{props.updated_at + "Z"}
</Moment>
</TableCell>
<TableCell>
</MythicStyledTableCell>
<MythicStyledTableCell>
{props.remote_port !== 0 &&
<MythicStyledTooltip title={"Test Remote Connection"} >
<IconButton
className="mythic-table-row-icon-action mythic-table-row-icon-action-success"
className="mythic-table-row-icon-action mythic-table-row-icon-action-hover-success"
size="small"
onClick={onTestProxy}
>
@@ -229,7 +257,7 @@ function ProxySearchTableRow(props){
</IconButton>
</MythicStyledTooltip>
}
</TableCell>
</MythicStyledTableCell>
</TableRow>
</React.Fragment>
)
@@ -150,6 +150,7 @@ const SearchTabCustomBrowserSearchPanel = (props) => {
props.onCommentSearch({search:adjustedSearch, searchHost:adjustedSearchHost, offset: 0})
break;
case "Tag":
case "Tags":
props.onTagSearch({search:adjustedSearch, searchHost:adjustedSearchHost, offset: 0});
break;
case "Metadata":
@@ -178,10 +179,11 @@ const SearchTabCustomBrowserSearchPanel = (props) => {
setSearch(queryParams.get("search"));
adjustedSearch = queryParams.get("search");
}
if(queryParams.has("searchField") && searchFieldOptions.includes(queryParams.get("searchField"))){
setSearchField(queryParams.get("searchField"));
props.onChangeSearchField(queryParams.get("searchField"));
adjustedSearchField = queryParams.get("searchField");
if(queryParams.has("searchField") && (searchFieldOptions.includes(queryParams.get("searchField")) || queryParams.get("searchField") === "Tag")){
const nextSearchField = queryParams.get("searchField") === "Tag" ? "Tags" : queryParams.get("searchField");
setSearchField(nextSearchField);
props.onChangeSearchField(nextSearchField);
adjustedSearchField = nextSearchField;
}else{
setSearchField("Name");
props.onChangeSearchField("Name");
@@ -252,15 +254,19 @@ export const SearchTabCustomBrowserPanel = (props) =>{
onCommentSearch({search, searchHost, offset: 0});
break;
case "Tag":
case "Tags":
onTagSearch({search, searchHost, offset: 0});
break;
case "Metadata":
onOtherSearch({search, searchHost, offset: 0});
break;
default:
break;
}
}
const handleSearchResults = (data) => {
snackActions.dismiss();
if(searchField === "Tag"){
if(searchField === "Tag" || searchField === "Tags"){
setTotalCount(data.tag_aggregate.aggregate.count);
setProcessData(data?.tag?.map(t => t.mythictree) || []);
} else {
@@ -361,6 +367,7 @@ export const SearchTabCustomBrowserPanel = (props) =>{
onCommentSearch({search, searchHost:searchHost, offset: (value - 1) * fetchLimit});
break;
case "Tag":
case "Tags":
onTagSearch({search, searchHost:searchHost, offset: (value - 1) * fetchLimit});
break;
case "Metadata":
@@ -385,7 +392,7 @@ export const SearchTabCustomBrowserPanel = (props) =>{
changeSearchParam={props.changeSearchParam}/>
<div style={{overflowY: "auto", flexGrow: 1}}>
{processData.length > 0 ? (
<CustomBrowserTable rows={processData} columns={selectedBrowser.columns} />) : (
<CustomBrowserTable rows={processData} columns={selectedBrowser.columns} me={props.me} />) : (
<MythicSearchEmptyState
compact
description="Adjust the custom browser query or selected browser field and search again."
@@ -1,5 +1,5 @@
import React, { useEffect } from 'react';
import {Link, IconButton, Typography} from '@mui/material';
import {Link, IconButton} from '@mui/material';
import Table from '@mui/material/Table';
import TableBody from '@mui/material/TableBody';
import TableCell from '@mui/material/TableCell';
@@ -21,6 +21,31 @@ import {HostFileDialog} from "../Payloads/HostFileDialog";
import PublicIcon from '@mui/icons-material/Public';
import {DetailedPayloadTable} from "../Payloads/DetailedPayloadTable";
import InfoIconOutline from '@mui/icons-material/InfoOutlined';
import {getReadableTextColor, isValidHexColor} from "../../MythicComponents/MythicColorInput";
const singleLineCellStyle = {
minWidth: 0,
overflow: "hidden",
textOverflow: "ellipsis",
whiteSpace: "nowrap",
};
const formatTagData = (data) => {
if(data === null || data === undefined){
return "";
}
if(typeof data === "string"){
return data;
}
return JSON.stringify(data, null, 2);
};
const safeDecode = (value) => {
if(!value){
return "";
}
return b64DecodeUnicode(value);
};
export function TagTable(props){
const [tags, setTags] = React.useState([]);
@@ -33,12 +58,12 @@ export function TagTable(props){
}
return (
<TableContainer className="mythicElement" style={{height: "100%", overflowY: "auto"}} >
<Table stickyHeader size="small" style={{"tableLayout": "fixed", "maxWidth": "100%", "overflow": "scroll"}}>
<Table stickyHeader size="small" style={{tableLayout: "fixed"}}>
<TableHead>
<TableRow>
<TableCell style={{width: "4rem"}}>Delete</TableCell>
<TableCell style={{width: "20%"}}>TagType</TableCell>
<TableCell style={{width: "8rem"}}>Source</TableCell>
<TableCell style={{width: "14rem"}}>Tag Type</TableCell>
<TableCell style={{width: "10rem"}}>Source</TableCell>
<TableCell>Tagged Element Information</TableCell>
</TableRow>
</TableHead>
@@ -56,14 +81,15 @@ export function TagTable(props){
</TableContainer>
)
}
function TagTableRow(props){
const [openDeleteDialog, setOpenDeleteDialog] = React.useState(false);
const [deleteTag] = useMutation(deleteTagMutation, {
onCompleted: (data) => {
onCompleted: () => {
snackActions.success("Successfully deleted tag");
props.onDelete(props.id);
},
onError: (data) => {
onError: () => {
snackActions.error("Failed to delete tag");
}
})
@@ -84,21 +110,71 @@ function TagTableRow(props){
</IconButton>
</MythicStyledTooltip>
</MythicStyledTableCell>
<MythicStyledTableCell >
<div style={{float: "left"}}><TagsDisplay expand={true} tags={[props]} /></div>
<MythicStyledTableCell>
<div className="mythic-tag-search-tag-cell"><TagsDisplay expand={true} tags={[props]} /></div>
</MythicStyledTableCell>
<MythicStyledTableCell>{props.source}</MythicStyledTableCell>
<MythicStyledTableCell style={{wordBreak: "break-all"}}><TagTableRowElement {...props} /></MythicStyledTableCell>
<MythicStyledTableCell>
<div className="mythic-search-result-value" style={singleLineCellStyle} title={props.source}>
{props.source}
</div>
</MythicStyledTableCell>
<MythicStyledTableCell><TagTableRowElement {...props} /></MythicStyledTableCell>
</TableRow>
{openDeleteDialog &&
<MythicConfirmDialog onClose={() => {setOpenDeleteDialog(false);}}
onSubmit={onAcceptDelete} open={openDeleteDialog}
acceptText={ "Remove" }/>
}
</React.Fragment>
)
}
const TagElementPanel = ({type, summary, actions, children}) => (
<div className="mythic-tag-search-element-card">
<div className="mythic-tag-search-element-header">
<div className="mythic-search-result-action-row">
<span className="mythic-tag-search-element-type">{type}</span>
{summary}
</div>
{actions ? <div className="mythic-search-result-action-row">{actions}</div> : null}
</div>
<div className="mythic-tag-search-details-grid">
{children}
</div>
</div>
);
const TagDetailItem = ({label, children, wide=false, code=false, title}) => (
<div className={wide ? "mythic-tag-search-detail mythic-tag-search-detail-wide" : "mythic-tag-search-detail"}>
<div className="mythic-search-result-label">{label}</div>
{code ? (
<pre className="mythic-search-result-code mythic-tag-search-code">{children}</pre>
) : (
<div className="mythic-search-result-value" title={title}>
{children || <span className="mythic-search-result-secondary">None</span>}
</div>
)}
</div>
);
const CallbackSummary = ({callback, includeDescription=false}) => {
if(!callback){
return null;
}
const safeColor = isValidHexColor(callback.color) ? callback.color : "";
const callbackStyle = safeColor ? {
backgroundColor: safeColor,
borderColor: safeColor,
color: getReadableTextColor(safeColor),
} : {};
return (
<span className="mythic-tag-search-callback-summary" style={callbackStyle}>
{callback.user}{callback.integrity_level > 2 ? "*" : ""}@{callback.host}
{includeDescription && callback.description ? ` - ${callback.description}` : ""}
</span>
);
};
function TagTableRowElement(props){
const [viewPermissionsDialogOpen, setViewPermissionsDialogOpen] = React.useState(false);
const [openDetailedView, setOpenDetailedView] = React.useState(false);
@@ -106,136 +182,69 @@ function TagTableRowElement(props){
const getElement = () => {
if(props.task) {
return (
<TableContainer className="mythicElement" >
<Table stickyHeader size="small" style={{"tableLayout": "fixed", "maxWidth": "100%", "overflow": "scroll"}}>
<TableBody>
<TableRow hover>
<TableCell style={{width: "20%"}}>Element Type</TableCell>
<TableCell><b>Task</b></TableCell>
</TableRow>
<TableRow hover>
<TableCell >Task / Callback</TableCell>
<TableCell>
<Link href={"/new/task/" + props.task.display_id} color="textPrimary" target={"_blank"}>
T-{props.task.display_id}
</Link>
{" / "}
<Link href={"/new/callbacks/" + props.task.callback.display_id} color="textPrimary" target={"_blank"}>
C-{props.task.callback.display_id}
</Link>
<div style={{border: props.task.callback.color === "" ? "" : `1px solid ${props.task.callback.color}`}}>
{props.task.callback.user}{props.task.callback.integrity_level > 2 ? "*" : ""}@{props.task.callback.host}
{" - "}{props.task.callback.description}
</div>
</TableCell>
</TableRow>
<TableRow hover>
<TableCell>Command</TableCell>
<TableCell>
{props.task.command_name} {props.task.display_params}
</TableCell>
</TableRow>
<TableRow hover>
<TableCell>Comment</TableCell>
<TableCell>{props.task.comment}</TableCell>
</TableRow>
<TableRow hover>
<TableCell>Data</TableCell>
<TableCell>{JSON.stringify(props.data, null, 2)}</TableCell>
</TableRow>
</TableBody>
</Table>
</TableContainer>
<TagElementPanel
type="Task"
summary={
<div className="mythic-search-result-link-row">
<Link href={"/new/task/" + props.task.display_id} color="textPrimary" target={"_blank"}>
T-{props.task.display_id}
</Link>
<span className="mythic-search-result-secondary">/</span>
<Link href={"/new/callbacks/" + props.task.callback.display_id} color="textPrimary" target={"_blank"}>
C-{props.task.callback.display_id}
</Link>
</div>
}
>
<TagDetailItem label="Callback" wide>
<CallbackSummary callback={props.task.callback} includeDescription />
</TagDetailItem>
<TagDetailItem label="Command" wide>
<span className="mythic-search-result-code mythic-tag-search-inline-code">
{props.task.command_name} {props.task.display_params}
</span>
</TagDetailItem>
<TagDetailItem label="Comment" wide>{props.task.comment}</TagDetailItem>
<TagDetailItem label="Tag Data" wide code>{formatTagData(props.data)}</TagDetailItem>
</TagElementPanel>
)
} else if(props.credential) {
return (
<TableContainer className="mythicElement" >
<Table stickyHeader size="small" style={{"tableLayout": "fixed", "maxWidth": "100%", "overflow": "scroll"}}>
<TableBody>
<TableRow hover>
<TableCell style={{width: "20%"}}>Element Type</TableCell>
<TableCell><b>Credential</b></TableCell>
</TableRow>
<TableRow hover>
<TableCell>Account</TableCell>
<TableCell>{props.credential.account}</TableCell>
</TableRow>
<TableRow hover>
<TableCell>Realm</TableCell>
<TableCell>{props.credential.realm}</TableCell>
</TableRow>
<TableRow hover>
<TableCell>Comment</TableCell>
<TableCell>{props.credential.comment}</TableCell>
</TableRow>
<TableRow hover>
<TableCell>Credential Type</TableCell>
<TableCell>{props.credential.type}</TableCell>
</TableRow>
<TableRow hover style={{whiteSpace: "pre"}}>
<TableCell>Credential</TableCell>
<TableCell>{props.credential.credential_text}</TableCell>
</TableRow>
<TableRow hover>
<TableCell>Data</TableCell>
<TableCell>{JSON.stringify(props.data, null, 2)}</TableCell>
</TableRow>
</TableBody>
</Table>
</TableContainer>
<TagElementPanel type="Credential">
<TagDetailItem label="Account">{props.credential.account}</TagDetailItem>
<TagDetailItem label="Realm">{props.credential.realm}</TagDetailItem>
<TagDetailItem label="Type">{props.credential.type}</TagDetailItem>
<TagDetailItem label="Comment" wide>{props.credential.comment}</TagDetailItem>
<TagDetailItem label="Credential" wide code>{props.credential.credential_text}</TagDetailItem>
<TagDetailItem label="Tag Data" wide code>{formatTagData(props.data)}</TagDetailItem>
</TagElementPanel>
)
} else if(props.mythictree) {
const treeType = props.mythictree.tree_type === "file" ? "File Browser" : "Process Browser";
return (
<TableContainer className="mythicElement" >
<Table stickyHeader size="small" style={{"tableLayout": "fixed", "maxWidth": "100%", "overflow": "scroll"}}>
<TableBody>
<TableRow hover>
<TableCell style={{width: "15%"}}>Element Type</TableCell>
<TableCell>{props.mythictree.tree_type === "file" ? <b>File Browser</b> : <b>Process Browser</b>}</TableCell>
</TableRow>
<TableRow hover>
<TableCell >Name</TableCell>
<TableCell>{props.mythictree.name_text}</TableCell>
</TableRow>
<TableRow hover>
<TableCell>
{props.mythictree.tree_type === "file" ? (
"Path"
) : (
"PID"
)}
</TableCell>
<TableCell>{props.mythictree.full_path_text}</TableCell>
</TableRow>
<TableRow hover>
<TableCell>Host</TableCell>
<TableCell>{props.mythictree.host}</TableCell>
</TableRow>
<TableRow hover>
<TableCell>Comment</TableCell>
<TableCell>{props.mythictree.comment}</TableCell>
</TableRow>
<TableRow hover>
<TableCell>Metadata</TableCell>
<TableCell>
<MythicStyledTooltip title="View metadata">
<IconButton
className="mythic-table-row-icon-action mythic-table-row-icon-action-info"
size="small"
onClick={() => setViewPermissionsDialogOpen(true)}
>
<PlaylistAddCheckIcon fontSize="small" />
</IconButton>
</MythicStyledTooltip>
</TableCell>
</TableRow>
<TableRow hover>
<TableCell>Data</TableCell>
<TableCell>{JSON.stringify(props.data, null, 2)}</TableCell>
</TableRow>
</TableBody>
</Table>
<React.Fragment>
<TagElementPanel
type={treeType}
actions={
<MythicStyledTooltip title="View metadata">
<IconButton
className="mythic-table-row-icon-action mythic-table-row-icon-action-hover-info"
size="small"
onClick={() => setViewPermissionsDialogOpen(true)}
>
<PlaylistAddCheckIcon fontSize="small" />
</IconButton>
</MythicStyledTooltip>
}
>
<TagDetailItem label="Name">{props.mythictree.name_text}</TagDetailItem>
<TagDetailItem label={props.mythictree.tree_type === "file" ? "Path" : "PID"}>
{props.mythictree.full_path_text}
</TagDetailItem>
<TagDetailItem label="Host">{props.mythictree.host}</TagDetailItem>
<TagDetailItem label="Comment" wide>{props.mythictree.comment}</TagDetailItem>
<TagDetailItem label="Tag Data" wide code>{formatTagData(props.data)}</TagDetailItem>
</TagElementPanel>
{viewPermissionsDialogOpen &&
<MythicDialog fullWidth={true} maxWidth="md" open={viewPermissionsDialogOpen}
onClose={()=>{setViewPermissionsDialogOpen(false);}}
@@ -245,160 +254,118 @@ function TagTableRowElement(props){
onClose={()=>{setViewPermissionsDialogOpen(false);}} />}
/>
}
</TableContainer>
</React.Fragment>
)
} else if(props.filemetum) {
const fileKind = props.filemetum.is_screenshot ? "Screenshot" : props.filemetum.is_download_from_agent ? "File Download" : "File Upload";
const filename = safeDecode(props.filemetum.filename_text);
const remotePath = safeDecode(props.filemetum.full_remote_path_text);
return (
<TableContainer className="mythicElement" >
<Table stickyHeader size="small" style={{"tableLayout": "fixed", "maxWidth": "100%", "overflow": "scroll"}}>
<TableBody>
<TableRow hover>
<TableCell style={{width: "15%"}}>Element Type</TableCell>
<TableCell><b>{props.filemetum.is_screenshot ? "Screenshot" : props.filemetum.is_download_from_agent ? "File Download" : "File Upload"}</b></TableCell>
</TableRow>
<TableRow hover>
<TableCell >Filename</TableCell>
<TableCell>
<Link style={{wordBreak: "break-all"}} color="textPrimary" download underline="always" target="_blank" href={"/direct/download/" + props.filemetum.agent_file_id}>{b64DecodeUnicode(props.filemetum.filename_text)}</Link>
</TableCell>
</TableRow>
<TableRow hover>
<TableCell>Hash</TableCell>
<TableCell>
<Typography variant="body2" style={{wordBreak: "break-all"}}>MD5: {props.filemetum.md5}</Typography>
<Typography variant="body2" style={{wordBreak: "break-all"}}>SHA1: {props.filemetum.sha1}</Typography>
</TableCell>
</TableRow>
<TableRow hover>
<TableCell>Comment</TableCell>
<TableCell>{props.filemetum.comment}</TableCell>
</TableRow>
<TableRow hover>
<TableCell>Full Remote Path</TableCell>
<TableCell>
{props.filemetum.host}<br/>
{b64DecodeUnicode(props.filemetum.full_remote_path_text)}
</TableCell>
</TableRow>
<TableRow hover>
<TableCell>File Hosting</TableCell>
<TableCell>
<MythicStyledTooltip title={"Host Payload Through C2"} >
<IconButton
className="mythic-table-row-icon-action mythic-table-row-icon-action-info"
size="small"
onClick={()=>{setOpenHostDialog(true);}}
>
<PublicIcon fontSize="small" />
</IconButton>
</MythicStyledTooltip>
</TableCell>
</TableRow>
<TableRow hover>
<TableCell>Data</TableCell>
<TableCell>{JSON.stringify(props.data, null, 2)}</TableCell>
</TableRow>
</TableBody>
{openHostDialog &&
<MythicDialog fullWidth={true} maxWidth="md" open={openHostDialog}
onClose={()=>{setOpenHostDialog(false);}}
innerDialog={<HostFileDialog file_uuid={props.filemetum.agent_file_id}
file_name={props.filemetum.full_remote_path_text === "" ? b64DecodeUnicode(props.filemetum.filename_text) : b64DecodeUnicode(props.filemetum.full_remote_path_text)}
onClose={()=>{setOpenHostDialog(false);}} />}
/>
<React.Fragment>
<TagElementPanel
type={fileKind}
actions={
<MythicStyledTooltip title={"Host Payload Through C2"} >
<IconButton
className="mythic-table-row-icon-action mythic-table-row-icon-action-hover-info"
size="small"
onClick={()=>{setOpenHostDialog(true);}}
>
<PublicIcon fontSize="small" />
</IconButton>
</MythicStyledTooltip>
}
</Table>
</TableContainer>
>
<TagDetailItem label="Filename" wide>
<Link color="textPrimary" download underline="always" target="_blank" href={"/direct/download/" + props.filemetum.agent_file_id}>
{filename}
</Link>
</TagDetailItem>
<TagDetailItem label="Hash" wide>
<div className="mythic-search-result-stack">
<span>MD5: {props.filemetum.md5}</span>
<span>SHA1: {props.filemetum.sha1}</span>
</div>
</TagDetailItem>
<TagDetailItem label="Comment" wide>{props.filemetum.comment}</TagDetailItem>
<TagDetailItem label="Full Remote Path" wide code>
{`${props.filemetum.host || ""}${remotePath ? "\n" + remotePath : ""}`}
</TagDetailItem>
<TagDetailItem label="Tag Data" wide code>{formatTagData(props.data)}</TagDetailItem>
</TagElementPanel>
{openHostDialog &&
<MythicDialog fullWidth={true} maxWidth="md" open={openHostDialog}
onClose={()=>{setOpenHostDialog(false);}}
innerDialog={<HostFileDialog file_uuid={props.filemetum.agent_file_id}
file_name={props.filemetum.full_remote_path_text === "" ? filename : remotePath}
onClose={()=>{setOpenHostDialog(false);}} />}
/>
}
</React.Fragment>
)
}else if(props.keylog_id) {
return <Typography ><b>Keylog: </b> {props.keylog_id}</Typography>
return (
<TagElementPanel type="Keylog">
<TagDetailItem label="Keylog ID">{props.keylog_id}</TagDetailItem>
<TagDetailItem label="Tag Data" wide code>{formatTagData(props.data)}</TagDetailItem>
</TagElementPanel>
)
}else if(props.payload){
return (
<TableContainer className="mythicElement" >
<Table stickyHeader size="small" style={{"tableLayout": "fixed", "maxWidth": "100%", "overflow": "scroll"}}>
<TableBody>
<TableRow hover>
<TableCell style={{width: "20%"}}>Element Type</TableCell>
<TableCell><b>Payload</b></TableCell>
</TableRow>
<TableRow hover>
<TableCell >Filename</TableCell>
<TableCell>
{b64DecodeUnicode(props.payload.filemetum.filename_text)}
</TableCell>
</TableRow>
<TableRow hover>
<TableCell >UUID</TableCell>
<TableCell>
{props.payload.uuid}
<IconButton
className="mythic-table-row-icon-action mythic-table-row-icon-action-info"
onClick={()=>setOpenDetailedView(true)}
size="small"
>
<InfoIconOutline fontSize="small" />
</IconButton>
{openDetailedView &&
<MythicDialog fullWidth={true} maxWidth="lg" open={openDetailedView}
onClose={()=>{setOpenDetailedView(false);}}
innerDialog={<DetailedPayloadTable {...props.payload} payload_id={props.payload.id} onClose={()=>{setOpenDetailedView(false);}} />}
/>}
</TableCell>
</TableRow>
<TableRow hover>
<TableCell>Description</TableCell>
<TableCell>{props.payload.description}</TableCell>
</TableRow>
<TableRow hover>
<TableCell>Data</TableCell>
<TableCell>{JSON.stringify(props.data, null, 2)}</TableCell>
</TableRow>
</TableBody>
</Table>
</TableContainer>
<TagElementPanel
type="Payload"
summary={props.payload.payloadtype?.name ? <span className="mythic-search-result-secondary">{props.payload.payloadtype.name}</span> : null}
actions={
<MythicStyledTooltip title="View payload details">
<IconButton
className="mythic-table-row-icon-action mythic-table-row-icon-action-hover-info"
onClick={()=>setOpenDetailedView(true)}
size="small"
>
<InfoIconOutline fontSize="small" />
</IconButton>
</MythicStyledTooltip>
}
>
<TagDetailItem label="Filename" wide>{safeDecode(props.payload.filemetum?.filename_text)}</TagDetailItem>
<TagDetailItem label="UUID" wide code>{props.payload.uuid}</TagDetailItem>
<TagDetailItem label="Description" wide>{props.payload.description}</TagDetailItem>
<TagDetailItem label="Tag Data" wide code>{formatTagData(props.data)}</TagDetailItem>
{openDetailedView &&
<MythicDialog fullWidth={true} maxWidth="lg" open={openDetailedView}
onClose={()=>{setOpenDetailedView(false);}}
innerDialog={<DetailedPayloadTable {...props.payload} payload_id={props.payload.id} onClose={()=>{setOpenDetailedView(false);}} />}
/>}
</TagElementPanel>
)
}else if(props.taskartifact_id) {
return <Typography><b>Artifact: </b> {props.taskartifact_id}</Typography>
return (
<TagElementPanel type="Artifact">
<TagDetailItem label="Artifact ID">{props.taskartifact_id}</TagDetailItem>
<TagDetailItem label="Tag Data" wide code>{formatTagData(props.data)}</TagDetailItem>
</TagElementPanel>
)
}else if(props.callback){
return (
<TableContainer className="mythicElement" >
<Table stickyHeader size="small" style={{"tableLayout": "fixed", "maxWidth": "100%", "overflow": "scroll"}}>
<TableBody>
<TableRow hover>
<TableCell style={{width: "20%"}}>Element Type</TableCell>
<TableCell><b>Callback</b></TableCell>
</TableRow>
<TableRow hover>
<TableCell >Callback</TableCell>
<TableCell>
<Link href={"/new/callbacks/" + props.callback.display_id} color="textPrimary" target={"_blank"}>
C-{props.callback.display_id}
</Link>
<div style={{border: props.callback.color === "" ? "" : `1px solid ${props.callback.color}`}}>
{props.callback.user}{props.callback.integrity_level > 2 ? "*" : ""}@{props.callback.host}
</div>
</TableCell>
</TableRow>
<TableRow hover>
<TableCell>Description</TableCell>
<TableCell>{props.callback.description}</TableCell>
</TableRow>
<TableRow hover>
<TableCell>IP</TableCell>
<TableCell>{props.callback.ip}</TableCell>
</TableRow>
<TableRow hover>
<TableCell>Data</TableCell>
<TableCell>{JSON.stringify(props.data, null, 2)}</TableCell>
</TableRow>
</TableBody>
</Table>
</TableContainer>
<TagElementPanel
type="Callback"
summary={
<Link href={"/new/callbacks/" + props.callback.display_id} color="textPrimary" target={"_blank"}>
C-{props.callback.display_id}
</Link>
}
>
<TagDetailItem label="Callback" wide>
<CallbackSummary callback={props.callback} />
</TagDetailItem>
<TagDetailItem label="Description" wide>{props.callback.description}</TagDetailItem>
<TagDetailItem label="IP">{props.callback.ip}</TagDetailItem>
<TagDetailItem label="Tag Data" wide code>{formatTagData(props.data)}</TagDetailItem>
</TagElementPanel>
)
} else {
console.log("unknown id for tag", props)
return null;
}
}
return getElement()
@@ -1,5 +1,5 @@
import React, { useEffect } from 'react';
import {IconButton, Typography, Link} from '@mui/material';
import {IconButton, Link} from '@mui/material';
import Table from '@mui/material/Table';
import TableBody from '@mui/material/TableBody';
import TableCell from '@mui/material/TableCell';
@@ -19,6 +19,7 @@ import {TokenDescriptionDialog} from './TokenDescriptionDialog';
import {TokenUserDialog} from './TokenUserDialog';
import { MythicStyledTooltip } from '../../MythicComponents/MythicStyledTooltip';
import MythicStyledTableCell from '../../MythicComponents/MythicTableCell';
import {MythicStateChip} from "../../MythicComponents/MythicStateChip";
const updateCredentialDeleted = gql`
mutation updateCredentialDeletedMutation($token_id: Int!, $deleted: Boolean!){
@@ -78,7 +79,7 @@ export function TokenTable(props){
<Table stickyHeader size="small" style={{"maxWidth": "100%", "overflow": "scroll"}}>
<TableHead>
<TableRow>
<TableCell style={{width: "5rem"}}>Visibility</TableCell>
<TableCell style={{width: "9rem"}}>State</TableCell>
<TableCell >User</TableCell>
<TableCell >TokenId</TableCell>
<TableCell >Description</TableCell>
@@ -140,18 +141,25 @@ function TokenTableRow(props){
<MythicConfirmDialog onClose={() => {setOpenDeleteDialog(false);}} onSubmit={onAcceptDelete} open={openDeleteDialog} acceptText={props.deleted ? "Restore" : "Hide" }/>
}
<MythicStyledTableCell>{props.deleted ? (
<MythicStyledTooltip title="Restore Token for use in Tasking">
<IconButton className="mythic-table-row-icon-action mythic-table-row-icon-action-danger" size="small" onClick={()=>{setOpenDeleteDialog(true);}}><VisibilityOffIcon fontSize="small" /></IconButton>
</MythicStyledTooltip>
) : (
<MythicStyledTooltip title="Delete Token so it can't be used in Tasking">
<IconButton className="mythic-table-row-icon-action mythic-table-row-icon-action-success" size="small" onClick={()=>{setOpenDeleteDialog(true);}}><VisibilityIcon fontSize="small" /></IconButton>
</MythicStyledTooltip>
)} </MythicStyledTableCell>
<MythicStyledTableCell>
<IconButton className="mythic-table-row-icon-action mythic-table-row-icon-action-info" onClick={() => setEditUserDialog(true)} size="small"><EditIcon fontSize="small" /></IconButton>
<Typography variant="body2" style={{wordBreak: "break-all", display: "inline-block"}}>{props.user}</Typography>
<div className="mythic-search-result-action-row">
{props.deleted ? (
<MythicStyledTooltip title="Restore Token for use in Tasking">
<IconButton className="mythic-table-row-icon-action mythic-table-row-icon-action-hover-success" size="small" onClick={()=>{setOpenDeleteDialog(true);}}><VisibilityOffIcon fontSize="small" /></IconButton>
</MythicStyledTooltip>
) : (
<MythicStyledTooltip title="Hide Token so it can't be used in Tasking">
<IconButton className="mythic-table-row-icon-action mythic-table-row-icon-action-hover-warning" size="small" onClick={()=>{setOpenDeleteDialog(true);}}><VisibilityIcon fontSize="small" /></IconButton>
</MythicStyledTooltip>
)}
<MythicStateChip compact label={props.deleted ? "Hidden" : "Available"} state={props.deleted ? "disabled" : "active"} />
</div>
</MythicStyledTableCell>
<MythicStyledTableCell>
<div className="mythic-search-result-action-row">
<IconButton className="mythic-table-row-icon-action mythic-table-row-icon-action-hover-info" onClick={() => setEditUserDialog(true)} size="small"><EditIcon fontSize="small" /></IconButton>
<span className="mythic-search-result-primary">{props.user}</span>
</div>
{editUserDialog && <MythicDialog fullWidth={true} maxWidth="md" open={editUserDialog}
onClose={()=>{setEditUserDialog(false);}}
@@ -160,18 +168,22 @@ function TokenTableRow(props){
}
</MythicStyledTableCell>
<MythicStyledTableCell >
<MythicStyledTooltip title="View Token Information">
<IconButton className="mythic-table-row-icon-action mythic-table-row-icon-action-info" size="small" onClick={()=>{setViewTokenDialog(true);}}><ConfirmationNumberIcon fontSize="small" /></IconButton>
</MythicStyledTooltip>
<Typography variant="body2" style={{wordBreak: "break-all", display: "inline-block"}}>{props.token_id}</Typography>
<div className="mythic-search-result-action-row">
<MythicStyledTooltip title="View Token Information">
<IconButton className="mythic-table-row-icon-action mythic-table-row-icon-action-hover-info" size="small" onClick={()=>{setViewTokenDialog(true);}}><ConfirmationNumberIcon fontSize="small" /></IconButton>
</MythicStyledTooltip>
<span className="mythic-search-result-code">{props.token_id}</span>
</div>
{viewTokenDialog && <MythicDialog fullWidth={true} maxWidth="md" open={viewTokenDialog}
onClose={()=>{setViewTokenDialog(false);}}
innerDialog={<TaskTokenDialog token_id={props.id} onClose={()=>{setViewTokenDialog(false);}} />}
/>}
</MythicStyledTableCell>
<MythicStyledTableCell>
<IconButton className="mythic-table-row-icon-action mythic-table-row-icon-action-info" onClick={() => setEditDescriptionDialog(true)} size="small"><EditIcon fontSize="small" /></IconButton>
<Typography variant="body2" style={{wordBreak: "break-all", display: "inline-block"}}>{props.description}</Typography>
<div className="mythic-search-result-action-row">
<IconButton className="mythic-table-row-icon-action mythic-table-row-icon-action-hover-info" onClick={() => setEditDescriptionDialog(true)} size="small"><EditIcon fontSize="small" /></IconButton>
<span className="mythic-search-result-secondary">{props.description || "No description"}</span>
</div>
{editDescriptionDialog && <MythicDialog fullWidth={true} maxWidth="md" open={editDescriptionDialog}
onClose={()=>{setEditDescriptionDialog(false);}}
@@ -180,22 +192,26 @@ function TokenTableRow(props){
}
</MythicStyledTableCell>
<MythicStyledTableCell>
<Link style={{wordBreak: "break-all"}} color="textPrimary" underline="always" target="_blank"
href={"/new/task/" + props.task.display_id}>
T-{props.task.display_id}
</Link>
</MythicStyledTableCell>
<MythicStyledTableCell>{props.callbacktokens?.map( (cbt) => (
<React.Fragment key={"callbacktoken-" + props.id + "-" + cbt.id}>
<Link style={{wordBreak: "break-all"}} color="textPrimary" underline="always" target="_blank" key={"callbacklink" + cbt.callback.display_id + "row" + props.id}
href={"/new/callbacks/" + cbt.callback.display_id}>
C-{cbt.callback.display_id}
<div className="mythic-search-result-link-row">
<Link style={{wordBreak: "break-all"}} color="textPrimary" underline="always" target="_blank"
href={"/new/task/" + props.task.display_id}>
T-{props.task.display_id}
</Link>
{" "}
</React.Fragment>
))|| null
}</MythicStyledTableCell>
<MythicStyledTableCell>{props.host}</MythicStyledTableCell>
</div>
</MythicStyledTableCell>
<MythicStyledTableCell>
<div className="mythic-search-result-link-row">
{props.callbacktokens?.length > 0 ? props.callbacktokens.map( (cbt) => (
<Link style={{wordBreak: "break-all"}} color="textPrimary" underline="always" target="_blank" key={"callbacklink" + cbt.callback.display_id + "row" + props.id}
href={"/new/callbacks/" + cbt.callback.display_id}>
C-{cbt.callback.display_id}
</Link>
)) : <span className="mythic-search-result-secondary">No callbacks</span>}
</div>
</MythicStyledTableCell>
<MythicStyledTableCell>
<div className="mythic-search-result-primary">{props.host}</div>
</MythicStyledTableCell>
</TableRow>
</React.Fragment>
)
+383
View File
@@ -3280,6 +3280,152 @@ tspan {
padding: 0.55rem 0.65rem;
white-space: pre;
}
.mythic-json-title-row {
align-items: center;
display: flex;
flex-wrap: wrap;
gap: 0.55rem;
justify-content: space-between;
min-width: 0;
width: 100%;
}
.mythic-json-dialog-body.MuiDialogContent-root {
gap: 0.65rem;
max-height: min(72vh, 54rem);
overflow: auto;
padding: 0.85rem !important;
}
.mythic-json-panel {
background-color: ${(props) => props.theme.palette.mode === "dark" ? "rgba(255,255,255,0.035)" : "rgba(0,0,0,0.018)"};
border: 1px solid ${(props) => props.theme.table?.borderSoft || props.theme.borderColor};
border-radius: ${(props) => props.theme.shape.borderRadius}px;
display: flex;
flex-direction: column;
gap: 0.45rem;
min-width: 0;
padding: 0.55rem;
}
.mythic-json-panel-root {
background-color: ${(props) => props.theme.surfaces?.muted || props.theme.palette.background.paper};
}
.mythic-json-panel-header {
align-items: center;
display: flex;
flex-wrap: wrap;
gap: 0.45rem;
justify-content: space-between;
min-width: 0;
}
.mythic-json-panel-title {
color: ${(props) => props.theme.palette.text.primary};
font-size: 0.78rem;
font-weight: 800;
line-height: 1.25;
min-width: 0;
overflow-wrap: anywhere;
}
.mythic-json-type-badge {
align-items: center;
background-color: ${(props) => props.theme.palette.mode === "dark" ? "rgba(255,255,255,0.055)" : "rgba(0,0,0,0.035)"};
border: 1px solid ${(props) => props.theme.table?.borderSoft || props.theme.borderColor};
border-radius: ${(props) => props.theme.shape.borderRadius}px;
color: ${(props) => props.theme.palette.text.secondary};
display: inline-flex;
flex: 0 0 auto;
font-size: 0.68rem;
font-weight: 800;
line-height: 1;
min-height: 22px;
padding: 0.22rem 0.42rem;
white-space: nowrap;
}
.mythic-json-type-object,
.mythic-json-type-array {
background-color: ${(props) => alpha(props.theme.palette.info.main, props.theme.palette.mode === "dark" ? 0.15 : 0.08)};
border-color: ${(props) => alpha(props.theme.palette.info.main, props.theme.palette.mode === "dark" ? 0.34 : 0.22)};
color: ${(props) => props.theme.palette.info.main};
}
.mythic-json-table-wrap {
border: 1px solid ${(props) => props.theme.table?.borderSoft || props.theme.borderColor};
border-radius: ${(props) => props.theme.shape.borderRadius}px;
max-height: 48vh;
overflow: auto;
}
.mythic-json-table-wrap .MuiTableCell-root {
border-bottom-color: ${(props) => props.theme.table?.borderSoft || props.theme.borderColor};
vertical-align: top;
}
.mythic-json-table-wrap .MuiTableHead-root .MuiTableCell-root {
color: ${(props) => props.theme.palette.text.primary};
font-size: 0.72rem;
font-weight: 800;
}
.mythic-json-key-cell {
background-color: ${(props) => props.theme.palette.mode === "dark" ? "rgba(255,255,255,0.018)" : "rgba(0,0,0,0.012)"};
}
.mythic-json-key-stack {
display: flex;
flex-direction: column;
gap: 0.28rem;
min-width: 0;
}
.mythic-json-key {
color: ${(props) => props.theme.palette.text.primary};
font-size: 0.78rem;
font-weight: 800;
line-height: 1.25;
overflow-wrap: anywhere;
}
.mythic-json-value-cell {
color: ${(props) => props.theme.palette.text.primary};
font-size: 0.78rem;
line-height: 1.35;
overflow-wrap: anywhere;
white-space: pre-wrap;
}
.mythic-json-index-cell {
color: ${(props) => props.theme.palette.text.secondary};
font-size: 0.74rem;
font-weight: 800;
text-align: right;
}
.mythic-json-value-primitive {
color: ${(props) => props.theme.palette.text.primary};
font-size: 0.78rem;
line-height: 1.35;
overflow-wrap: anywhere;
white-space: pre-wrap;
}
.mythic-json-value-empty {
color: ${(props) => props.theme.palette.text.disabled};
font-size: 0.76rem;
font-style: italic;
}
.mythic-json-value-boolean {
border: 1px solid ${(props) => props.theme.table?.borderSoft || props.theme.borderColor};
border-radius: ${(props) => props.theme.shape.borderRadius}px;
display: inline-flex;
font-size: 0.72rem;
font-weight: 800;
line-height: 1;
padding: 0.22rem 0.45rem;
}
.mythic-json-value-true {
background-color: ${(props) => alpha(props.theme.palette.success.main, props.theme.palette.mode === "dark" ? 0.14 : 0.08)};
border-color: ${(props) => alpha(props.theme.palette.success.main, props.theme.palette.mode === "dark" ? 0.34 : 0.24)};
color: ${(props) => props.theme.palette.success.main};
}
.mythic-json-value-false {
background-color: ${(props) => alpha(props.theme.palette.warning.main, props.theme.palette.mode === "dark" ? 0.14 : 0.08)};
border-color: ${(props) => alpha(props.theme.palette.warning.main, props.theme.palette.mode === "dark" ? 0.34 : 0.24)};
color: ${(props) => props.theme.palette.warning.main};
}
.mythic-json-empty-state {
color: ${(props) => props.theme.palette.text.secondary};
font-size: 0.78rem;
font-style: italic;
padding: 0.45rem 0.2rem;
}
.MuiDialogActions-root,
.mythic-dialog-actions {
align-items: center;
@@ -5931,6 +6077,243 @@ tspan {
.mythic-table-row-icon-action-hover-danger:hover {
color: ${(props) => props.theme.palette.error.main} !important;
}
.mythic-search-result-stack {
display: flex;
flex-direction: column;
gap: 0.22rem;
min-width: 0;
}
.mythic-search-result-stack-spacious {
gap: 0.38rem;
}
.mythic-search-result-inline {
align-items: center;
display: flex;
flex-wrap: wrap;
gap: 0.35rem;
min-width: 0;
}
.mythic-search-result-inline-nowrap {
flex-wrap: nowrap;
}
.mythic-search-result-primary {
color: ${(props) => props.theme.palette.text.primary};
font-size: 0.82rem;
font-weight: 750;
line-height: 1.35;
min-width: 0;
overflow-wrap: anywhere;
}
.mythic-search-result-secondary {
color: ${(props) => props.theme.palette.text.secondary};
font-size: 0.74rem;
line-height: 1.35;
min-width: 0;
overflow-wrap: anywhere;
}
.mythic-search-result-label {
color: ${(props) => props.theme.palette.text.secondary};
font-size: 0.68rem;
font-weight: 750;
line-height: 1.2;
}
.mythic-search-result-value {
color: ${(props) => props.theme.palette.text.primary};
font-size: 0.78rem;
line-height: 1.35;
overflow-wrap: anywhere;
}
.mythic-search-result-code {
background-color: ${(props) => props.theme.palette.mode === "dark" ? "rgba(255,255,255,0.045)" : "rgba(0,0,0,0.035)"};
border: 1px solid ${(props) => props.theme.table?.borderSoft || props.theme.borderColor};
border-radius: ${(props) => props.theme.shape.borderRadius}px;
color: ${(props) => props.theme.palette.text.primary};
display: block;
font-family: ${(props) => props.theme.typography.fontFamilyMonospace || "monospace"};
font-size: 0.76rem;
line-height: 1.4;
margin: 0;
max-width: 100%;
min-width: 0;
overflow-wrap: anywhere;
padding: 0.42rem 0.5rem;
white-space: pre-wrap;
}
.mythic-search-result-code-compact {
max-height: 7.5rem;
overflow: hidden;
}
.mythic-search-result-metric {
align-items: center;
background-color: ${(props) => props.theme.palette.mode === "dark" ? "rgba(255,255,255,0.045)" : "rgba(0,0,0,0.028)"};
border: 1px solid ${(props) => props.theme.table?.borderSoft || props.theme.borderColor};
border-radius: ${(props) => props.theme.shape.borderRadius}px;
display: inline-flex;
gap: 0.28rem;
min-height: 24px;
padding: 0.18rem 0.45rem;
}
.mythic-search-result-metric-label {
color: ${(props) => props.theme.palette.text.secondary};
font-size: 0.68rem;
font-weight: 750;
}
.mythic-search-result-action-row {
align-items: center;
display: flex;
flex-wrap: nowrap;
gap: 0.35rem;
min-width: 0;
}
.mythic-search-result-link-row {
align-items: center;
display: inline-flex;
flex-wrap: wrap;
gap: 0.2rem;
min-width: 0;
}
.mythic-tag-search-tag-cell {
align-items: center;
display: flex;
min-width: 0;
overflow: hidden;
}
.mythic-tag-search-tag-cell .MuiChip-root {
flex: 0 1 auto;
float: none !important;
height: 20px !important;
max-width: 100%;
}
.mythic-tag-search-tag-cell .MuiChip-label {
font-size: 0.72rem;
font-weight: 800;
min-width: 0;
overflow: hidden !important;
padding-left: 0.45rem;
padding-right: 0.45rem;
text-overflow: ellipsis;
}
.mythic-tag-search-element-card {
background-color: ${(props) => props.theme.palette.mode === "dark" ? "rgba(255,255,255,0.035)" : "rgba(0,0,0,0.018)"};
border: 1px solid ${(props) => props.theme.table?.borderSoft || props.theme.borderColor};
border-radius: ${(props) => props.theme.shape.borderRadius}px;
display: flex;
flex-direction: column;
gap: 0.45rem;
min-width: 0;
padding: 0.48rem 0.55rem;
}
.mythic-tag-search-element-header {
align-items: center;
display: flex;
gap: 0.5rem;
justify-content: space-between;
min-width: 0;
}
.mythic-tag-search-element-type {
background-color: ${(props) => alpha(props.theme.palette.primary.main, props.theme.palette.mode === "dark" ? 0.16 : 0.08)};
border: 1px solid ${(props) => alpha(props.theme.palette.primary.main, props.theme.palette.mode === "dark" ? 0.34 : 0.22)};
border-radius: ${(props) => props.theme.shape.borderRadius}px;
color: ${(props) => props.theme.palette.text.primary};
display: inline-flex;
font-size: 0.72rem;
font-weight: 760;
line-height: 1.25;
padding: 0.16rem 0.45rem;
white-space: nowrap;
}
.mythic-tag-search-details-grid {
display: grid;
gap: 0.4rem 0.65rem;
grid-template-columns: repeat(auto-fit, minmax(11rem, 1fr));
min-width: 0;
}
.mythic-tag-search-detail {
display: flex;
flex-direction: column;
gap: 0.15rem;
min-width: 0;
}
.mythic-tag-search-detail-wide {
grid-column: 1 / -1;
}
.mythic-tag-search-code {
font-size: 0.72rem;
max-height: 7.5rem;
overflow: auto;
}
.mythic-tag-search-inline-code {
display: inline-block;
padding: 0.24rem 0.42rem;
}
.mythic-tag-search-callback-summary {
background-color: ${(props) => props.theme.palette.mode === "dark" ? "rgba(255,255,255,0.045)" : "rgba(0,0,0,0.035)"};
border: 1px solid ${(props) => props.theme.table?.borderSoft || props.theme.borderColor};
border-radius: ${(props) => props.theme.shape.borderRadius}px;
display: inline-block;
font-size: 0.76rem;
line-height: 1.35;
max-width: 100%;
min-width: 0;
overflow: hidden;
padding: 0.28rem 0.45rem;
text-overflow: ellipsis;
white-space: nowrap;
}
.mythic-custom-browser-tags-cell {
align-items: center;
display: flex;
gap: 0.35rem;
min-width: 0;
overflow: hidden;
}
.mythic-custom-browser-tags-cell .MuiIconButton-root {
align-items: center !important;
background-color: ${(props) => props.theme.palette.mode === "dark" ? "rgba(255,255,255,0.045)" : "rgba(0,0,0,0.025)"} !important;
border: 1px solid ${(props) => props.theme.table?.borderSoft || props.theme.borderColor};
border-radius: ${(props) => props.theme.shape.borderRadius}px;
color: ${(props) => props.theme.palette.text.secondary} !important;
display: inline-flex !important;
flex: 0 0 auto;
float: none !important;
height: 22px;
justify-content: center !important;
padding: 0 !important;
transition: background-color 120ms ease, border-color 120ms ease, color 120ms ease;
width: 22px;
}
.mythic-custom-browser-tags-cell .MuiIconButton-root:hover {
background-color: ${(props) => alpha(props.theme.palette.primary.main, props.theme.palette.mode === "dark" ? 0.16 : 0.09)} !important;
border-color: ${(props) => alpha(props.theme.palette.primary.main, props.theme.palette.mode === "dark" ? 0.44 : 0.3)};
color: ${(props) => props.theme.palette.primary.main} !important;
}
.mythic-custom-browser-tags-cell .MuiIconButton-root svg {
font-size: 0.92rem;
}
.mythic-custom-browser-tags-list {
align-items: center;
display: flex;
flex: 1 1 auto;
flex-wrap: nowrap;
gap: 0.2rem;
min-width: 0;
overflow: hidden;
}
.mythic-custom-browser-tags-list .MuiChip-root {
flex: 0 0 auto;
float: none !important;
height: 18px !important;
max-width: 5.5rem;
}
.mythic-custom-browser-tags-list .MuiChip-label {
font-size: 0.68rem;
font-weight: 800;
min-width: 0;
overflow: hidden !important;
padding-left: 0.35rem;
padding-right: 0.35rem;
text-overflow: ellipsis;
}
.mythic-eventing-workspace {
display: flex;
flex: 1 1 auto;