From d662928b3fbd621dc37ae86b14ff8f70fd49c7bf Mon Sep 17 00:00:00 2001 From: its-a-feature Date: Sun, 10 May 2026 16:09:54 -0500 Subject: [PATCH] better json table displays and search table responses --- .../MythicComponents/MythicDialog.js | 348 +++++++----- .../components/pages/Search/ArtifactTable.js | 119 ++-- .../pages/Search/CustomBrowserTable.js | 150 +++-- .../components/pages/Search/KeylogsTable.js | 115 ++-- .../components/pages/Search/ProcessTable.js | 89 ++- .../pages/Search/ProxySearchTable.js | 164 +++--- .../pages/Search/SearchTabCustomBrowsers.js | 19 +- .../src/components/pages/Search/TagTable.js | 515 ++++++++---------- .../src/components/pages/Search/TokenTable.js | 84 +-- MythicReactUI/src/themes/GlobalStyles.js | 383 +++++++++++++ 10 files changed, 1299 insertions(+), 687 deletions(-) diff --git a/MythicReactUI/src/components/MythicComponents/MythicDialog.js b/MythicReactUI/src/components/MythicComponents/MythicDialog.js index 320169c9..b12b1ba3 100644 --- a/MythicReactUI/src/components/MythicComponents/MythicDialog.js +++ b/MythicReactUI/src/components/MythicComponents/MythicDialog.js @@ -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 {label} +} + +const JSONPrimitiveValue = ({name, value, me}) => { + const type = getJSONValueType(value); + if(type === "null" || type === "empty" || value === ""){ + return None + } + if(type === "boolean"){ + return {value ? "True" : "False"} + } + return {convertValueToContextValue(name, value, me)} +} + +const JSONTableCellValue = ({name, value, me, depth}) => { + if(Array.isArray(value) || isPlainObject(value)){ + return ( + + ); + } + return ; +} + +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 ; + } + if(type === "object"){ + const entries = Object.entries(value); + return ( +
+ {showPanelHeader && +
+ {label} + +
+ } + {entries.length === 0 ? ( +
No fields to display.
+ ) : ( + + + + + {leftColumn} + {rightColumn} + + + + {entries.map(([key, entryValue]) => ( + + +
+ {key} + +
+
+ + + +
+ ))} +
+
+
+ )} +
+ ); + } + const objectHeaders = getArrayObjectHeaders(value); + const isObjectArray = value.length > 0 && objectHeaders.length > 0 && value.every((row) => isPlainObject(row)); + return ( +
+ {showPanelHeader && +
+ {label} + +
+ } + {value.length === 0 ? ( +
No items to display.
+ ) : isObjectArray ? ( + + + + + # + {objectHeaders.map((header) => ( + {header} + ))} + + + + {value.map((row, rowIndex) => ( + + {rowIndex + 1} + {objectHeaders.map((header) => ( + + + + ))} + + ))} + +
+
+ ) : ( + + + + + # + Value + + + + {value.map((entryValue, rowIndex) => ( + + {rowIndex + 1} + + + + + ))} + +
+
+ )} +
+ ); +} + +export function MythicViewJSONAsTableDialog(props) { + const parsedValue = React.useMemo(() => parseJSONDialogValue(props.value), [props.value]); + const rootLabel = props.title || "JSON Data"; return ( - {props.title} - - - - - - {headers.map( (header, index) => ( - {header} - ))} - - - - {tableType === "dictionary" ? ( - comment.map( (element, index) => ( - - {element.name} - {element.new_table ? - ( - -
- - - {element.headers.map( (header, index) => ( - {header} - ))} - - - - {element.is_dictionary ? ( - Object.keys(element.value).map( (key, dictIndex) => ( - - {key} - {convertValueToContextValue(key, element.value[key], props.me)} - - )) - ): ( - element.value.map( (e, elementIndex) => ( - - {element.headers.map( (header, headerIndex) => ( - {convertValueToContextValue(header, e[header], props.me)} - ))} - - )) - )} - -
-
- ) - : - ({convertValueToContextValue(element.name, element.value, props.me) }) - } - - - )) - ) : ( - comment.map( (row, index) => ( - - {Object.keys(row).map( (key) => ( - {convertValueToContextValue(key, row[key], props.me)} - ))} - - )) - ) } - - - - + +
+ {rootLabel} + +
+
+ + + 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); diff --git a/MythicReactUI/src/components/pages/Search/ArtifactTable.js b/MythicReactUI/src/components/pages/Search/ArtifactTable.js index e3a4095e..78f848bb 100644 --- a/MythicReactUI/src/components/pages/Search/ArtifactTable.js +++ b/MythicReactUI/src/components/pages/Search/ArtifactTable.js @@ -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 ( - +
- Cleanup - Type - Command - Task - Operator - Host + Cleanup + Type + Command + Task + Host Artifact @@ -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){ - {props.needs_cleanup && !props.resolved && - - - - +
+ {props.needs_cleanup && !props.resolved && + + + + + + } + {props.needs_cleanup && props.resolved && + + + + + + } + {!props.needs_cleanup && + + + + + + } + + + + - } - {props.needs_cleanup && props.resolved && - - - - - - } - {!props.needs_cleanup && - - - - - - } +
- {props.base_artifact} +
+ {props.base_artifact} +
- {props?.task?.command?.cmd} +
+ {props?.task?.command?.cmd} +
{props.task && - <> - - C-{props.task.callback.display_id} - {" / "} - - T-{props.task.display_id} - +
+
+ + C-{props.task.callback.display_id} + + / + + T-{props.task.display_id} + +
{props.task?.callback?.mythictree_groups.length > 0 ? ( - - Groups: {"\n" + props?.task?.callback.mythictree_groups.join("\n")} - +
+ Groups: {props?.task?.callback.mythictree_groups.join(", ")} +
) : null} - +
} -
- - {props?.task?.operator?.username || null} - {props.host} +
+ {props.host} +
- {props.artifact_text} +
{props.artifact_text}
diff --git a/MythicReactUI/src/components/pages/Search/CustomBrowserTable.js b/MythicReactUI/src/components/pages/Search/CustomBrowserTable.js index 70ae6e6b..1883b833 100644 --- a/MythicReactUI/src/components/pages/Search/CustomBrowserTable.js +++ b/MythicReactUI/src/components/pages/Search/CustomBrowserTable.js @@ -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 ( - -
+ +
- Name + Entry {columns.map((column, index) => ( - {column.name} + {column.name} ))} - Comment - Tags + Comment + Tags @@ -95,54 +114,95 @@ function CustomBrowserTableRow(props){ /> } - - setViewPermissionsDialogOpen(true)} - > - - - - Host: {props.host} - {props.callback?.mythictree_groups.length > 0 ? ( - - Groups: {props?.callback.mythictree_groups.join(", ")}
- Callback: { - +
+ + setViewPermissionsDialogOpen(true)} + > + + + + + {props.name_text || props.full_path_text} + +
+
+ Host + {props.host} +
+ {props.full_path_text && props.full_path_text !== props.name_text ? ( +
+ {props.full_path_text} +
+ ) : null} + {props.callback ? ( +
+ Callback + C-{props.callback.display_id} - }{", "} - Task: { - - T-{props.task.display_id} - - } - - ) : null} - {props.full_path_text} + {props.task ? ( + + / + Task + + T-{props.task.display_id} + + + ) : null} +
+ ) : null} + {props.callback?.mythictree_groups?.length > 0 ? ( +
+ Groups: {props.callback.mythictree_groups.join(", ")} +
+ ) : null} +
{props.columns.map((column, index) => ( - {props.metadata[column.key]} +
+ {formatMetadataValue(props.metadata?.[column.key]) || None} +
))} - setEditCommentDialogOpen(true)} - size="small" - > - - - {props.comment} - +
+ + setEditCommentDialogOpen(true)} + size="small" + > + + + + + {props.comment || "No comment"} + +
+ - - +
+ +
+ +
+
diff --git a/MythicReactUI/src/components/pages/Search/KeylogsTable.js b/MythicReactUI/src/components/pages/Search/KeylogsTable.js index e3b719b7..61a300c6 100644 --- a/MythicReactUI/src/components/pages/Search/KeylogsTable.js +++ b/MythicReactUI/src/components/pages/Search/KeylogsTable.js @@ -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){ - C-{props.task.callback.display_id} - {" / "} - T-{props.task.display_id} +
+
+ C-{props.task.callback.display_id} + / + T-{props.task.display_id} +
- {props.task?.callback?.mythictree_groups.length > 0 ? ( - - Groups: {props?.task?.callback.mythictree_groups.join(", ")} - - ) : null} + {props.task?.callback?.mythictree_groups.length > 0 ? ( +
+ Groups: {props?.task?.callback.mythictree_groups.join(", ")} +
+ ) : null} +
- User: {props.user} - Host: {props.task.callback.host} - Window: {props.window} - Time: {toLocalTime(props.timestamp, me?.user?.view_utc_time || false)} - View Window Together: - - props.onGroupKeylogData(props.window, props.user, props.task.callback.host)} - size="small" - > - - - - - - - - - onCopyToClipboard(keylogData)} - size="small" - > - - - - - {keylogData.slice(0, 500)} - {keylogData.length > 500 ? ( - <> - {"..."}
+
+
+ User + {props.user} +
+
+ Host + {props.task.callback.host} +
+
+ Window + {props.window} +
+
+ Time + {toLocalTime(props.timestamp, me?.user?.view_utc_time || false)} +
+
+ {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" > - + - - ) : null} + + View window together +
+
+
+ +
+
+ + onCopyToClipboard(keylogData)} + size="small" + > + + + + {keylogData.length > 500 ? ( + + {setOpenDisplayKeylogData(true);}} + size="small" + > + + + + ) : null} +
+
{keylogData.slice(0, 500)}{keylogData.length > 500 ? "..." : null}
{openDisplayKeylogData && {setOpenDisplayKeylogData(false);}} innerDialog={ @@ -227,7 +244,7 @@ function KeylogTableRow(props){ title={"Full keylog data"} value={keylogData}/> } /> } - +
diff --git a/MythicReactUI/src/components/pages/Search/ProcessTable.js b/MythicReactUI/src/components/pages/Search/ProcessTable.js index 73b4ae74..5225ea3d 100644 --- a/MythicReactUI/src/components/pages/Search/ProcessTable.js +++ b/MythicReactUI/src/components/pages/Search/ProcessTable.js @@ -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){ setViewPermissionsDialogOpen(true)} > @@ -106,44 +114,67 @@ function ProcessTableRow(props){ - {props.full_path_text} +
+ {props.full_path_text} +
- Host: {props.host} - {props.callback?.mythictree_groups.length > 0 ? ( - - Groups: {props?.callback.mythictree_groups.join(", ")}
- Callback: { +
+
+ Host + {props.host} +
+ {props.callback ? ( +
+ Callback C-{props.callback.display_id} - }
- Task: { - - T-{props.task.display_id} - - } - - ) : null} + {props.task ? ( + <> + / + + T-{props.task.display_id} + + + ) : null} +
+ ) : null} + {props.callback?.mythictree_groups.length > 0 ? ( +
+ Groups: {props?.callback.mythictree_groups.join(", ")} +
+ ) : null} +
- {props.name_text} - {props.deleted && - {" (deleted)"} - } +
+
+ {props.name_text} +
+ {props.deleted && + + } +
- setEditCommentDialogOpen(true)} - size="small" - > - - - {props.comment} +
+ setEditCommentDialogOpen(true)} + size="small" + > + + + {props.comment || "No comment"} +
diff --git a/MythicReactUI/src/components/pages/Search/ProxySearchTable.js b/MythicReactUI/src/components/pages/Search/ProxySearchTable.js index b616529e..f4558230 100644 --- a/MythicReactUI/src/components/pages/Search/ProxySearchTable.js +++ b/MythicReactUI/src/components/pages/Search/ProxySearchTable.js @@ -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){
- + State User@Host Task Info Bound Port @@ -142,72 +144,98 @@ function ProxySearchTableRow(props){ dialogText={confirmDialogText} /> } - {props.deleted ? ( - - {setOpenDeleteDialog(true);}} - > - - - - ) : - ( - {setOpenDeleteDialog(true);}} - > - - - - )} - - - {props.callback.user}@{props.callback.host} - - {props.callback.description} - - - - C-{props.callback.display_id} - - {" / "} - - T-{props.task.display_id} - - - - {props.local_port} - - + +
+ {props.deleted ? ( + + {setOpenDeleteDialog(true);}} + > + + + + ) : + ( + {setOpenDeleteDialog(true);}} + > + + + + )} + +
+
+ +
+
{props.callback.user}@{props.callback.host}
+ {props.callback.description && +
{props.callback.description}
+ } +
+
+ +
+ + C-{props.callback.display_id} + + / + + T-{props.task.display_id} + +
+
+ + {props.local_port} + + +
{props.remote_port !== 0 && - {props.remote_ip}:{props.remote_port} +
{props.remote_ip}:{props.remote_port}
+ } + {props.remote_port === 0 && +
No remote endpoint
} {props.username !== "" && - - Authentication Required
- Username: {props.username}
- Password: {props.password} -
+
+
+ Auth + {props.username} +
+
+ Password + {props.password} +
+
} - - - - {"Rx: "} - {getStringSize({cellData: {"plaintext": String(props.bytes_received)}})} -
- {"Tx: "} - {getStringSize({cellData: {"plaintext": String(props.bytes_sent)}})} -
- - {props.port_type} - - +
+
+ +
+ + + Rx + + {getStringSize({cellData: {"plaintext": String(props.bytes_received)}})} + + + + Tx + + {getStringSize({cellData: {"plaintext": String(props.bytes_sent)}})} + +
+
+ + + + adjustOutput(props, newTime)} interval={1000} parse={"YYYY-MM-DDTHH:mm:ss.SSSSSSZ"} withTitle @@ -216,12 +244,12 @@ function ProxySearchTableRow(props){ > {props.updated_at + "Z"} -
- + + {props.remote_port !== 0 && @@ -229,7 +257,7 @@ function ProxySearchTableRow(props){ } - +
) diff --git a/MythicReactUI/src/components/pages/Search/SearchTabCustomBrowsers.js b/MythicReactUI/src/components/pages/Search/SearchTabCustomBrowsers.js index 9ea71be9..cfd85044 100644 --- a/MythicReactUI/src/components/pages/Search/SearchTabCustomBrowsers.js +++ b/MythicReactUI/src/components/pages/Search/SearchTabCustomBrowsers.js @@ -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}/>
{processData.length > 0 ? ( - ) : ( + ) : ( { + 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 ( -
+
Delete - TagType - Source + Tag Type + Source Tagged Element Information @@ -56,14 +81,15 @@ export function TagTable(props){ ) } + 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){ - -
+ +
- {props.source} - + +
+ {props.source} +
+
+ {openDeleteDialog && {setOpenDeleteDialog(false);}} onSubmit={onAcceptDelete} open={openDeleteDialog} acceptText={ "Remove" }/> } - ) } + +const TagElementPanel = ({type, summary, actions, children}) => ( +
+
+
+ {type} + {summary} +
+ {actions ?
{actions}
: null} +
+
+ {children} +
+
+); + +const TagDetailItem = ({label, children, wide=false, code=false, title}) => ( +
+
{label}
+ {code ? ( +
{children}
+ ) : ( +
+ {children || None} +
+ )} +
+); + +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 ( + + {callback.user}{callback.integrity_level > 2 ? "*" : ""}@{callback.host} + {includeDescription && callback.description ? ` - ${callback.description}` : ""} + + ); +}; + 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 ( - -
- - - Element Type - Task - - - Task / Callback - - - T-{props.task.display_id} - - {" / "} - - C-{props.task.callback.display_id} - -
- {props.task.callback.user}{props.task.callback.integrity_level > 2 ? "*" : ""}@{props.task.callback.host} - {" - "}{props.task.callback.description} -
- -
-
- - Command - - {props.task.command_name} {props.task.display_params} - - - - Comment - {props.task.comment} - - - Data - {JSON.stringify(props.data, null, 2)} - -
-
-
+ + + T-{props.task.display_id} + + / + + C-{props.task.callback.display_id} + + + } + > + + + + + + {props.task.command_name} {props.task.display_params} + + + {props.task.comment} + {formatTagData(props.data)} + ) } else if(props.credential) { return ( - - - - - Element Type - Credential - - - Account - {props.credential.account} - - - Realm - {props.credential.realm} - - - Comment - {props.credential.comment} - - - Credential Type - {props.credential.type} - - - Credential - {props.credential.credential_text} - - - Data - {JSON.stringify(props.data, null, 2)} - - -
-
+ + {props.credential.account} + {props.credential.realm} + {props.credential.type} + {props.credential.comment} + {props.credential.credential_text} + {formatTagData(props.data)} + ) } else if(props.mythictree) { + const treeType = props.mythictree.tree_type === "file" ? "File Browser" : "Process Browser"; return ( - - - - - Element Type - {props.mythictree.tree_type === "file" ? File Browser : Process Browser} - - - Name - {props.mythictree.name_text} - - - - {props.mythictree.tree_type === "file" ? ( - "Path" - ) : ( - "PID" - )} - - {props.mythictree.full_path_text} - - - Host - {props.mythictree.host} - - - Comment - {props.mythictree.comment} - - - Metadata - - - setViewPermissionsDialogOpen(true)} - > - - - - - - - Data - {JSON.stringify(props.data, null, 2)} - - -
+ + + setViewPermissionsDialogOpen(true)} + > + + + + } + > + {props.mythictree.name_text} + + {props.mythictree.full_path_text} + + {props.mythictree.host} + {props.mythictree.comment} + {formatTagData(props.data)} + {viewPermissionsDialogOpen && {setViewPermissionsDialogOpen(false);}} @@ -245,160 +254,118 @@ function TagTableRowElement(props){ onClose={()=>{setViewPermissionsDialogOpen(false);}} />} /> } -
+
) } 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 ( - - - - - Element Type - {props.filemetum.is_screenshot ? "Screenshot" : props.filemetum.is_download_from_agent ? "File Download" : "File Upload"} - - - Filename - - {b64DecodeUnicode(props.filemetum.filename_text)} - - - - Hash - - MD5: {props.filemetum.md5} - SHA1: {props.filemetum.sha1} - - - - Comment - {props.filemetum.comment} - - - Full Remote Path - - {props.filemetum.host}
- {b64DecodeUnicode(props.filemetum.full_remote_path_text)} -
-
- - File Hosting - - - {setOpenHostDialog(true);}} - > - - - - - - - Data - {JSON.stringify(props.data, null, 2)} - -
- - {openHostDialog && - {setOpenHostDialog(false);}} - innerDialog={{setOpenHostDialog(false);}} />} - /> + + + {setOpenHostDialog(true);}} + > + + + } -
-
+ > + + + {filename} + + + +
+ MD5: {props.filemetum.md5} + SHA1: {props.filemetum.sha1} +
+
+ {props.filemetum.comment} + + {`${props.filemetum.host || ""}${remotePath ? "\n" + remotePath : ""}`} + + {formatTagData(props.data)} + + {openHostDialog && + {setOpenHostDialog(false);}} + innerDialog={{setOpenHostDialog(false);}} />} + /> + } + ) }else if(props.keylog_id) { - return Keylog: {props.keylog_id} + return ( + + {props.keylog_id} + {formatTagData(props.data)} + + ) }else if(props.payload){ return ( - - - - - Element Type - Payload - - - Filename - - {b64DecodeUnicode(props.payload.filemetum.filename_text)} - - - - UUID - - {props.payload.uuid} - setOpenDetailedView(true)} - size="small" - > - - - {openDetailedView && - {setOpenDetailedView(false);}} - innerDialog={{setOpenDetailedView(false);}} />} - />} - - - - Description - {props.payload.description} - - - Data - {JSON.stringify(props.data, null, 2)} - - -
-
+ {props.payload.payloadtype.name} : null} + actions={ + + setOpenDetailedView(true)} + size="small" + > + + + + } + > + {safeDecode(props.payload.filemetum?.filename_text)} + {props.payload.uuid} + {props.payload.description} + {formatTagData(props.data)} + {openDetailedView && + {setOpenDetailedView(false);}} + innerDialog={{setOpenDetailedView(false);}} />} + />} + ) }else if(props.taskartifact_id) { - return Artifact: {props.taskartifact_id} + return ( + + {props.taskartifact_id} + {formatTagData(props.data)} + + ) }else if(props.callback){ return ( - - - - - Element Type - Callback - - - Callback - - - C-{props.callback.display_id} - -
- {props.callback.user}{props.callback.integrity_level > 2 ? "*" : ""}@{props.callback.host} -
-
-
- - Description - {props.callback.description} - - - IP - {props.callback.ip} - - - Data - {JSON.stringify(props.data, null, 2)} - -
-
-
+ + C-{props.callback.display_id} + + } + > + + + + {props.callback.description} + {props.callback.ip} + {formatTagData(props.data)} + ) - } else { console.log("unknown id for tag", props) + return null; } } return getElement() diff --git a/MythicReactUI/src/components/pages/Search/TokenTable.js b/MythicReactUI/src/components/pages/Search/TokenTable.js index fbc5d3f8..947c25b1 100644 --- a/MythicReactUI/src/components/pages/Search/TokenTable.js +++ b/MythicReactUI/src/components/pages/Search/TokenTable.js @@ -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){ - Visibility + State User TokenId Description @@ -140,18 +141,25 @@ function TokenTableRow(props){ {setOpenDeleteDialog(false);}} onSubmit={onAcceptDelete} open={openDeleteDialog} acceptText={props.deleted ? "Restore" : "Hide" }/> } - {props.deleted ? ( - - {setOpenDeleteDialog(true);}}> - - ) : ( - - {setOpenDeleteDialog(true);}}> - - )} - setEditUserDialog(true)} size="small"> - {props.user} +
+ {props.deleted ? ( + + {setOpenDeleteDialog(true);}}> + + ) : ( + + {setOpenDeleteDialog(true);}}> + + )} + +
+
+ +
+ setEditUserDialog(true)} size="small"> + {props.user} +
{editUserDialog && {setEditUserDialog(false);}} @@ -160,18 +168,22 @@ function TokenTableRow(props){ }
- - {setViewTokenDialog(true);}}> - - {props.token_id} +
+ + {setViewTokenDialog(true);}}> + + {props.token_id} +
{viewTokenDialog && {setViewTokenDialog(false);}} innerDialog={{setViewTokenDialog(false);}} />} />}
- setEditDescriptionDialog(true)} size="small"> - {props.description} +
+ setEditDescriptionDialog(true)} size="small"> + {props.description || "No description"} +
{editDescriptionDialog && {setEditDescriptionDialog(false);}} @@ -180,22 +192,26 @@ function TokenTableRow(props){ }
- - T-{props.task.display_id} - - - {props.callbacktokens?.map( (cbt) => ( - - - C-{cbt.callback.display_id} +
+ + T-{props.task.display_id} - {" "} - - ))|| null - } - {props.host} +
+
+ +
+ {props.callbacktokens?.length > 0 ? props.callbacktokens.map( (cbt) => ( + + C-{cbt.callback.display_id} + + )) : No callbacks} +
+
+ +
{props.host}
+
) diff --git a/MythicReactUI/src/themes/GlobalStyles.js b/MythicReactUI/src/themes/GlobalStyles.js index e2526285..2253e41c 100644 --- a/MythicReactUI/src/themes/GlobalStyles.js +++ b/MythicReactUI/src/themes/GlobalStyles.js @@ -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;