tag updates

This commit is contained in:
its-a-feature
2026-05-07 20:43:36 -05:00
parent d2fb6504ee
commit d8dedb482e
9 changed files with 769 additions and 387 deletions
@@ -1,6 +1,8 @@
import React from 'react';
import Box from '@mui/material/Box';
import {HexColorInput} from 'react-colorful';
import ClickAwayListener from '@mui/material/ClickAwayListener';
import Popper from '@mui/material/Popper';
import {HexColorInput, HexColorPicker} from 'react-colorful';
export const isValidHexColor = (color) => {
if(typeof color !== "string"){
@@ -27,54 +29,92 @@ export const getReadableTextColor = (backgroundColor) => {
}
export const MythicColorSwatchInput = ({color, label, onChange, inputWidth = "96px", sx = {}}) => {
const [pickerOpen, setPickerOpen] = React.useState(false);
const swatchRef = React.useRef(null);
const safeColor = isValidHexColor(color) ? color : "#000000";
return (
<Box sx={{display: "flex", alignItems: "center", gap: 1, minWidth: 0, ...sx}}>
<Box
component="input"
type="color"
value={safeColor}
onChange={(evt) => onChange(evt.target.value)}
aria-label={label}
sx={{
width: 34,
height: 30,
flex: "0 0 auto",
p: 0,
border: "none",
backgroundColor: "transparent",
cursor: "pointer",
"&::-webkit-color-swatch-wrapper": {p: 0},
"&::-webkit-color-swatch": {
<ClickAwayListener onClickAway={() => setPickerOpen(false)}>
<Box sx={{display: "flex", alignItems: "center", gap: 1, minWidth: 0, position: "relative", ...sx}}>
<Box
component="button"
type="button"
ref={swatchRef}
onClick={(evt) => {
evt.preventDefault();
evt.stopPropagation();
setPickerOpen((open) => !open);
}}
aria-label={label}
aria-expanded={pickerOpen}
sx={{
alignItems: "center",
backgroundColor: "transparent",
border: "1px solid",
borderColor: "divider",
borderRadius: "4px",
},
"&::-moz-color-swatch": {
border: "1px solid",
borderColor: "divider",
cursor: "pointer",
display: "inline-flex",
flex: "0 0 auto",
height: 30,
justifyContent: "center",
p: "2px",
width: 34,
}}
>
<Box
sx={{
backgroundColor: safeColor,
borderRadius: "3px",
height: "100%",
width: "100%",
}}
/>
</Box>
<Popper
open={pickerOpen}
anchorEl={swatchRef.current}
placement="bottom-start"
style={{zIndex: 100000}}
>
<Box
onMouseDown={(evt) => evt.stopPropagation()}
onClick={(evt) => evt.stopPropagation()}
sx={{
backgroundColor: "background.paper",
border: "1px solid",
borderColor: "divider",
borderRadius: "6px",
boxShadow: 8,
mt: 0.5,
p: 1,
"& .react-colorful": {
width: 210,
height: 160,
},
}}
>
<HexColorPicker color={safeColor} onChange={onChange} />
</Box>
</Popper>
<HexColorInput
color={color}
onChange={onChange}
prefixed={true}
style={{
width: inputWidth,
minWidth: 0,
height: "30px",
boxSizing: "border-box",
borderRadius: "4px",
},
}}
/>
<HexColorInput
color={color}
onChange={onChange}
prefixed={true}
style={{
width: inputWidth,
minWidth: 0,
height: "30px",
boxSizing: "border-box",
borderRadius: "4px",
border: "1px solid rgba(128,128,128,0.45)",
background: "transparent",
color: "inherit",
padding: "0 8px",
fontFamily: "inherit",
fontSize: "0.85rem",
}}
/>
</Box>
border: "1px solid rgba(128,128,128,0.45)",
background: "transparent",
color: "inherit",
padding: "0 8px",
fontFamily: "inherit",
fontSize: "0.85rem",
}}
/>
</Box>
</ClickAwayListener>
);
}
@@ -1,16 +1,9 @@
import React from 'react';
import Button from '@mui/material/Button';
import DialogActions from '@mui/material/DialogActions';
import DialogContent from '@mui/material/DialogContent';
import DialogTitle from '@mui/material/DialogTitle';
import MythicTextField from './MythicTextField';
import {useQuery, gql, useMutation} from '@apollo/client';
import { Select, Input, MenuItem, Link, IconButton } from '@mui/material';
import Table from '@mui/material/Table';
import TableBody from '@mui/material/TableBody';
import TableCell from '@mui/material/TableCell';
import TableContainer from '@mui/material/TableContainer';
import TableRow from '@mui/material/TableRow';
import { Box, Select, MenuItem, Link, IconButton } from '@mui/material';
import AceEditor from 'react-ace';
import 'ace-builds/src-noconflict/mode-json';
import 'ace-builds/src-noconflict/theme-monokai';
@@ -25,11 +18,20 @@ import Chip from '@mui/material/Chip';
import LocalOfferOutlinedIcon from '@mui/icons-material/LocalOfferOutlined';
import {MythicStyledTooltip} from "./MythicStyledTooltip";
import AddCircleOutlineIcon from '@mui/icons-material/AddCircleOutline';
import Typography from '@mui/material/Typography';
import MythicStyledTableCell from "./MythicTableCell";
import {meState} from "../../cache";
import { useReactiveVar } from '@apollo/client';
import {MythicDraggableDialogTitle} from "./MythicDraggableDialogTitle";
import {NewTagtypesDialog} from "../pages/Tags/NewTagtypesDialog";
import {getTagReadableTextColor, TagTypeChip} from "./MythicTagChip";
import {
MythicDialogBody,
MythicDialogButton,
MythicDialogFooter,
MythicDialogSection,
MythicFormField,
MythicFormGrid,
MythicFormNote
} from "./MythicDialogLayout";
const createNewTagMutationTemplate = ({target_object}) => {
// target_object should be something like "task_id"
@@ -121,8 +123,11 @@ export const TagsDisplay = ({tags, expand}) => {
)
}
const TagChipDisplay = ({tag, expand}) => {
const theme = useTheme();
const [openTagDisplay, setOpenTagDisplay] = React.useState(false);
const [label, setLabel] = React.useState(expand ? tag.tagtype.name : tag.tagtype.name[0]);
const color = tag?.tagtype?.color || "";
const textColor = getTagReadableTextColor(theme, color);
const onSelectTag = (event, tag) => {
if(event){
event.preventDefault();
@@ -152,9 +157,18 @@ const TagChipDisplay = ({tag, expand}) => {
return (
<React.Fragment>
<Chip onMouseOver={onMouseOver} onMouseOut={onMouseOut} label={label} size="small" onClick={(e) => onSelectTag(e)}
style={{float: "right", backgroundColor:tag.tagtype.color, height: "15px"}}
sx={{
"& .MuiChip-label": {overflow: "visible"}
backgroundColor: color || "transparent",
color: textColor,
float: "right",
height: "15px",
"& .MuiChip-label": {
color: "inherit",
overflow: "visible"
},
"&:hover": {
backgroundColor: color || "transparent",
},
}}
/>
{openTagDisplay &&
@@ -220,6 +234,56 @@ const StringTagDataEntry = ({name, value}) => {
}
return value;
}
const RenderedTagDataPreview = ({data}) => {
const trimmedData = typeof data === "string" ? data.trim() : data;
if(trimmedData === ""){
return <Box className="mythic-tag-data-preview-empty">No JSON data to render.</Box>;
}
try {
const parsedData = typeof data === "string" ? JSON.parse(data) : data;
if(parsedData === null || typeof parsedData !== "object"){
return (
<Box className="mythic-tag-data-preview-row">
<Box className="mythic-tag-data-preview-key">value</Box>
<Box className="mythic-tag-data-preview-value">{String(parsedData)}</Box>
</Box>
);
}
const entries = Array.isArray(parsedData) ?
parsedData.map((value, index) => [`[${index}]`, value]) :
Object.entries(parsedData);
if(entries.length === 0){
return <Box className="mythic-tag-data-preview-empty">JSON object is empty.</Box>;
}
return (
<Box className="mythic-tag-data-preview-list">
{entries.map(([key, value]) => (
<Box className="mythic-tag-data-preview-row" key={key}>
<Box className="mythic-tag-data-preview-key">{key}</Box>
<Box className="mythic-tag-data-preview-value">
{typeof value === "string" ? (
<StringTagDataEntry name={key} value={value} />
) : typeof value === "boolean" ? (
value ? "True" : "False"
) : typeof value === "object" && value !== null ? (
<pre>{JSON.stringify(value, null, 2)}</pre>
) : (
String(value)
)}
</Box>
</Box>
))}
</Box>
);
} catch (error) {
return <Box className="mythic-tag-data-preview-empty">Enter valid JSON to render a preview.</Box>;
}
}
const TagReadonlyValue = ({children}) => (
<Box className="mythic-tag-readonly-value">
{children || "None"}
</Box>
)
function ViewTagDialog(props) {
const theme = useTheme();
const [selectedTag, setSelectedTag] = React.useState({});
@@ -277,118 +341,81 @@ function ViewTagDialog(props) {
}
props.onClose(event);
}
const stopClicks = (e) => {
const stopPropagation = (e) => {
e.stopPropagation();
e.preventDefault();
}
const rawData = selectedTag?.is_json ? JSON.stringify(selectedTag?.data || {}, null, 2) : selectedTag?.data || "";
return (
<React.Fragment>
<DialogTitle id="form-dialog-title" onClick={stopClicks}>View Tag</DialogTitle>
<TableContainer className="mythicElement" onClick={stopClicks}>
<Table size="small" style={{ "maxWidth": "100%", "overflow": "scroll"}}>
<TableBody>
<TableRow hover>
<TableCell style={{width: "20%"}}>Tag Type</TableCell>
<TableCell style={{display: "inline-flex", flexDirection: "row", width: "100%"}}>
<Chip label={selectedTag?.tagtype?.name||""} size="small" style={{float: "right", backgroundColor:selectedTag?.tagtype?.color||""}} />
<ViewEditTags target_object={objectInfo.object_type} target_object_id={objectInfo.object_id} me={props.me} />
</TableCell>
</TableRow>
<TableRow hover>
<TableCell>Description</TableCell>
<TableCell>{selectedTag?.tagtype?.description || ""}</TableCell>
</TableRow>
<TableRow hover>
<MythicStyledTableCell>Source</MythicStyledTableCell>
<MythicStyledTableCell>
{selectedTag?.source ||""}
</MythicStyledTableCell>
</TableRow>
<TableRow hover>
<MythicStyledTableCell>Reference URL</MythicStyledTableCell>
<MythicStyledTableCell>
{selectedTag?.url === "" ? (
"No reference link provided"
) : (
<Link href={selectedTag?.url || "#"} color="textPrimary" target="_blank" referrerPolicy='no'>{selectedTag?.url ? "click here" : "No reference link provided"}</Link>
)}
</MythicStyledTableCell>
</TableRow>
<TableRow>
<TableCell>Data</TableCell>
<TableCell>
{selectedTag?.is_json ? (
<TableContainer className="mythicElement">
<Table size="small" style={{ "maxWidth": "100%", "overflow": "scroll"}}>
<TableBody>
{Object.keys(selectedTag.data).map( key => (
<TableRow key={key} hover>
<MythicStyledTableCell>{key}</MythicStyledTableCell>
{typeof selectedTag.data[key] === "string" ? (
<MythicStyledTableCell style={{whiteSpace: "pre-wrap"}}>
<StringTagDataEntry name={key} value={String(selectedTag.data[key])} />
</MythicStyledTableCell>
) : typeof selectedTag.data[key] === "object" ? (
Array.isArray(selectedTag.data[key]) ? (
<TableCell style={{whiteSpace: "pre-wrap"}}>{JSON.stringify(selectedTag.data[key], null, 2)}</TableCell>
) : (
<MythicStyledTableCell>
<Table size={"small"} >
<TableBody>
{Object.keys(selectedTag.data[key]).map(key2 => (
<TableRow key={key2} >
<MythicStyledTableCell>{key2}</MythicStyledTableCell>
<MythicStyledTableCell>
<StringTagDataEntry name={key2} value={String(selectedTag.data[key][key2])} />
</MythicStyledTableCell>
</TableRow>
))}
</TableBody>
</Table>
</MythicStyledTableCell>
)
) : typeof selectedTag.data[key] === "boolean" ? (
<MythicStyledTableCell>{selectedTag.data[key] ? "True" : "False"}</MythicStyledTableCell>
) :
(
<MythicStyledTableCell>{String(selectedTag.data[key])}</MythicStyledTableCell>
)
}
</TableRow>
))}
</TableBody>
</Table>
</TableContainer>
) : (
<AceEditor
mode="json"
theme={theme.palette.mode === "dark" ? "monokai" : "xcode"}
fontSize={14}
showGutter={true}
maxLines={20}
highlightActiveLine={false}
value={selectedTag?.data || ""}
width={"100%"}
setOptions={{
showLineNumbers: true,
tabSize: 4,
useWorker: false,
wrapBehavioursEnabled: true,
wrap: true
}}/>
)}
</TableCell>
</TableRow>
</TableBody>
</Table>
</TableContainer>
<DialogActions onClick={(e) => e.stopPropagation()}>
<Button onClick={onClose} variant="contained" color="primary">
<MythicDraggableDialogTitle>View Tag</MythicDraggableDialogTitle>
<DialogContent dividers={true} style={{width: "100%"}} onClick={stopPropagation}>
<MythicDialogBody>
<MythicDialogSection
title="Tag Type"
description={selectedTag?.tagtype?.description || "No description provided."}
actions={
objectInfo.object_type !== "" &&
<ViewEditTags target_object={objectInfo.object_type} target_object_id={objectInfo.object_id} me={props.me} />
}
>
{selectedTag?.tagtype &&
<TagTypeChip tagtype={selectedTag.tagtype} />
}
</MythicDialogSection>
<MythicDialogSection title="Reference Details">
<MythicFormGrid minWidth="16rem">
<MythicFormField label="Source">
<TagReadonlyValue>{selectedTag?.source || "None"}</TagReadonlyValue>
</MythicFormField>
<MythicFormField label="External URL">
<TagReadonlyValue>
{selectedTag?.url ? (
<Link href={selectedTag.url} color="textPrimary" target="_blank" referrerPolicy='no'>
{selectedTag.url}
</Link>
) : (
"No reference link provided"
)}
</TagReadonlyValue>
</MythicFormField>
</MythicFormGrid>
</MythicDialogSection>
<MythicDialogSection title="Tag Data">
{selectedTag?.is_json ? (
<Box className="mythic-tag-data-preview-frame mythic-tag-data-preview-frame-full">
<RenderedTagDataPreview data={selectedTag?.data || {}} />
</Box>
) : (
<Box className="mythic-tag-editor-frame">
<AceEditor
mode="json"
theme={theme.palette.mode === "dark" ? "monokai" : "xcode"}
fontSize={14}
showGutter={true}
showPrintMargin={false}
minLines={8}
maxLines={18}
highlightActiveLine={false}
readOnly={true}
value={rawData}
width={"100%"}
setOptions={{
showLineNumbers: true,
tabSize: 4,
useWorker: false,
wrapBehavioursEnabled: true,
wrap: true
}}/>
</Box>
)}
</MythicDialogSection>
</MythicDialogBody>
</DialogContent>
<MythicDialogFooter onClick={stopPropagation}>
<MythicDialogButton onClick={onClose}>
Close
</Button>
</DialogActions>
</MythicDialogButton>
</MythicDialogFooter>
</React.Fragment>
);
}
@@ -486,8 +513,15 @@ const handleTaskTypeChange = (evt) => {
setNewData(String(evt.target.value.data));
}
}
const handleNewTagCreate = ({tagtype_id, source, url, data, id}) => {
props.onClose();
const handleNewTagCreate = ({tagtype_id, source, url, data, id, tagtype}) => {
const newTag = {id, source, url, data, tagtype_id, tagtype};
const newTags = [...existingTags, newTag].sort((a, b) => a.tagtype.name.localeCompare(b.tagtype.name));
setExistingTags(newTags);
setSelectedTag(newTag);
setNewSource(source);
setNewURL(url);
setNewData(data);
setOpenNewDialog(false);
}
const onAcceptDelete = () => {
deleteTag({variables: {tag_id: selectedTag.id}});
@@ -506,88 +540,117 @@ return (
onClose={()=>{setOpenNewDialog(false);}}
onSubmit={handleNewTagCreate} />}
/>}
<Table size="small" style={{ width: "100%", overflow: "scroll"}}>
<TableBody>
<TableRow hover>
<MythicStyledTableCell style={{width: "30%"}}>Select Existing Tag to Edit or Add New</MythicStyledTableCell>
<MythicStyledTableCell style={{display: "inline-flex", flexDirection: "row-reverse"}}>
<MythicStyledTooltip title={"Add New Tag"}>
<IconButton variant='contained' color="success" style={{float: "right"}} onClick={() => setOpenNewDialog(true)} >
<AddCircleOutlineIcon />
<MythicDialogBody>
<MythicDialogSection
title="Tag Selection"
description={existingTags.length === 0 ? "No tags are attached yet." : "Choose the tag you want to update."}
actions={
<MythicStyledTooltip title={"Add New Tag"}>
<IconButton className="mythic-table-row-icon-action mythic-table-row-icon-action-hover-success" size="small" onClick={() => setOpenNewDialog(true)}>
<AddCircleOutlineIcon fontSize="small" />
</IconButton>
</MythicStyledTooltip>
}
>
{existingTags.length === 0 ? (
<MythicFormNote>
Add a tag to capture source, reference, and structured data for this item.
</MythicFormNote>
) : (
<Box sx={{display: "grid", gap: 1, gridTemplateColumns: {xs: "1fr", md: selectedTag.id ? "minmax(0, 1fr) auto" : "1fr"}, alignItems: "end"}}>
<MythicFormField label="Existing Tag">
<Select
fullWidth
size="small"
value={selectedTag}
onChange={handleTaskTypeChange}
renderValue={(tag) => tag?.id ? <TagTypeChip tagtype={tag.tagtype} /> : "Select a tag"}
>
{existingTags.map( (opt) => (
<MenuItem value={opt} key={opt.id}>
<TagTypeChip tagtype={opt.tagtype} />
</MenuItem>
) )}
</Select>
</MythicFormField>
{selectedTag.id &&
<MythicStyledTooltip title={"Delete Tag"}>
<Box sx={{alignItems: "center", alignSelf: "end", display: "flex", height: 38}}>
<IconButton className="mythic-table-row-icon-action mythic-table-row-icon-action-hover-danger" size="small" onClick={()=>{setOpenDeleteDialog(true);}}>
<DeleteIcon fontSize="small" />
</IconButton>
</MythicStyledTooltip>
<Select
labelId="demo-dialog-select-label"
id="demo-dialog-select"
value={selectedTag}
onChange={handleTaskTypeChange}
input={<Input />}
>
{existingTags.map( (opt) => (
<MenuItem value={opt} key={opt.id}>
<Chip label={opt.tagtype.name} size="small" style={{float: "right", backgroundColor:opt.tagtype.color}} />
</MenuItem>
) )}
</Select>
{selectedTag.id &&
<IconButton className="mythic-table-row-icon-action mythic-table-row-icon-action-hover-danger" size="small" style={{float: "right"}} onClick={()=>{setOpenDeleteDialog(true);}}><DeleteIcon fontSize="small" /></IconButton>
</Box>
</MythicStyledTooltip>
}
{selectedTag.id && selectedTag?.tagtype?.description &&
<Box sx={{gridColumn: "1 / -1"}}>
<MythicFormNote>{selectedTag.tagtype.description}</MythicFormNote>
</Box>
}
</Box>
)}
{openDelete &&
<MythicConfirmDialog onClose={() => {setOpenDeleteDialog(false);}} onSubmit={onAcceptDelete} open={openDelete}/>
}
</MythicDialogSection>
{selectedTag.id &&
<>
<MythicDialogSection title="Reference Details">
<MythicFormGrid minWidth="16rem">
<MythicFormField label="Source">
<MythicTextField value={newSource} onChange={onChangeSource} name="Source of tag data" showLabel={false} marginTop="0" marginBottom="0" />
</MythicFormField>
<MythicFormField label="External URL">
<MythicTextField value={newURL} onChange={onChangeURL} name="External URL reference" showLabel={false} marginTop="0" marginBottom="0" />
{newURL &&
<Link href={newURL} color="textPrimary" target="_blank" referrerPolicy='no' sx={{display: "inline-flex", mt: 0.5, fontSize: "0.78rem"}}>
Open reference
</Link>
}
{openDelete &&
<MythicConfirmDialog onClose={() => {setOpenDeleteDialog(false);}} onSubmit={onAcceptDelete} open={openDelete}/>
}
</MythicStyledTableCell>
</TableRow>
<TableRow hover>
<MythicStyledTableCell>Tag Description</MythicStyledTableCell>
<MythicStyledTableCell>{selectedTag?.tagtype?.description || ""}</MythicStyledTableCell>
</TableRow>
<TableRow hover>
<MythicStyledTableCell>Source</MythicStyledTableCell>
<MythicStyledTableCell>
<MythicTextField value={newSource} onChange={onChangeSource} name="Source of tag data" />
</MythicStyledTableCell>
</TableRow>
<TableRow hover>
<MythicStyledTableCell>External URL</MythicStyledTableCell>
<MythicStyledTableCell>
<MythicTextField value={newURL} onChange={onChangeURL} name="External URL reference" />
<Link href={newURL} color="textPrimary" target="_blank" referrerPolicy='no'>{newURL ? "click here" : ""}</Link>
</MythicStyledTableCell>
</TableRow>
<TableRow hover>
<MythicStyledTableCell>JSON Data</MythicStyledTableCell>
<MythicStyledTableCell>
<AceEditor
mode="json"
theme={theme.palette.mode === "dark" ? "monokai" : "xcode"}
onChange={onEditorChange}
fontSize={14}
showGutter={true}
maxLines={20}
highlightActiveLine={true}
value={newData}
width={"100%"}
setOptions={{
showLineNumbers: true,
tabSize: 4,
useWorker: false
}}/>
</MythicStyledTableCell>
</TableRow>
</TableBody>
</Table>
</MythicFormField>
</MythicFormGrid>
</MythicDialogSection>
<MythicDialogSection title="JSON Data">
<Box className="mythic-tag-data-split">
<Box className="mythic-tag-editor-frame">
<AceEditor
mode="json"
theme={theme.palette.mode === "dark" ? "monokai" : "xcode"}
onChange={onEditorChange}
fontSize={14}
showGutter={true}
showPrintMargin={false}
minLines={10}
maxLines={18}
highlightActiveLine={true}
value={newData}
width={"100%"}
setOptions={{
showLineNumbers: true,
tabSize: 4,
useWorker: false,
wrap: true
}}/>
</Box>
<Box className="mythic-tag-data-preview-frame">
<RenderedTagDataPreview data={newData} />
</Box>
</Box>
</MythicDialogSection>
</>
}
</MythicDialogBody>
</DialogContent>
<DialogActions>
<Button onClick={props.onClose} variant="contained" color="primary">
<MythicDialogFooter>
<MythicDialogButton onClick={props.onClose}>
Close
</Button>
</MythicDialogButton>
{selectedTag.id &&
<Button onClick={onSubmit} variant="contained" color="success">
<MythicDialogButton intent="primary" onClick={onSubmit}>
Submit
</Button>
</MythicDialogButton>
}
</DialogActions>
</MythicDialogFooter>
</React.Fragment>
);
}
@@ -598,7 +661,8 @@ export function NewTagDialog(props) {
const [newData, setNewData] = React.useState("");
const [selectedTagType, setSelectedTagType] = React.useState("");
const [existingTagTypes, setExistingTagTypes] = React.useState([]);
const { loading, error } = useQuery(getTagtypesQuery, {
const [openNewTagTypeDialog, setOpenNewTagTypeDialog] = React.useState(false);
useQuery(getTagtypesQuery, {
onCompleted: data => {
setExistingTagTypes(data.tagtype);
if(data.tagtype.length > 0){
@@ -611,7 +675,7 @@ export function NewTagDialog(props) {
onCompleted: data => {
if(data.createTag.status === "success"){
snackActions.success("Successfully created new tag!");
props.onSubmit({source:newSource, url:newURL, data:newData, tagtype_id:selectedTagType.id, id: data.createTag.id});
props.onSubmit({source:newSource, url:newURL, data:newData, tagtype_id:selectedTagType.id, id: data.createTag.id, tagtype: selectedTagType});
props.onClose()
} else {
snackActions.error(data.createTag.error);
@@ -625,6 +689,11 @@ export function NewTagDialog(props) {
const handleTaskTypeChange = (evt) => {
setSelectedTagType(evt.target.value);
}
const onNewTagType = ({name, description, id, color}) => {
const newTagType = {name, description, id, color};
setExistingTagTypes((prev) => [...prev, newTagType].sort((a, b) => a.name.localeCompare(b.name)));
setSelectedTagType(newTagType);
}
const onSubmit = () => {
newTag({variables:
{source:newSource, url:newURL, data:newData,
@@ -644,87 +713,103 @@ export function NewTagDialog(props) {
return (
<React.Fragment>
{openNewTagTypeDialog &&
<MythicDialog fullWidth={true} maxWidth="md" open={openNewTagTypeDialog}
onClose={()=>{setOpenNewTagTypeDialog(false);}}
innerDialog={
<NewTagtypesDialog
onClose={()=>{setOpenNewTagTypeDialog(false);}}
onSubmit={onNewTagType}
/>
}
/>
}
<MythicDraggableDialogTitle >Add New Tag</MythicDraggableDialogTitle>
<DialogContent dividers={true} style={{width: "100%"}}>
<Table size="small" style={{ overflow: "scroll", width: "100%"}}>
<TableBody>
<TableRow hover>
<MythicStyledTableCell style={{width: "20%"}}>
<Typography>
Tag
</Typography>
<Typography size="small" component="span" style={{fontSize: theme.typography.pxToRem(15)}}>
To create a new tag type click <Link style={{wordBreak: "break-all"}}
color="textPrimary"
href={"/new/tagtypes"}
underline="always" target="_blank">
here
</Link>
</Typography>
</MythicStyledTableCell>
<MythicStyledTableCell>
<Select
labelId="demo-dialog-select-label"
id="demo-dialog-select"
value={selectedTagType}
onChange={handleTaskTypeChange}
input={<Input style={{width: "100%"}}/>}
>
{existingTagTypes.map( (opt) => (
<MenuItem value={opt} key={opt.name}>
<Chip label={opt.name} size="small" style={{ backgroundColor:opt.color}} />
</MenuItem>
) )}
</Select>
</MythicStyledTableCell>
</TableRow>
<TableRow hover>
<MythicStyledTableCell>Source</MythicStyledTableCell>
<MythicStyledTableCell>
<MythicTextField value={newSource} onChange={onChangeSource} name="Source of tag data" />
</MythicStyledTableCell>
</TableRow>
<TableRow hover>
<MythicStyledTableCell>External URL</MythicStyledTableCell>
<MythicStyledTableCell>
<MythicTextField value={newURL} onChange={onChangeURL} name="External URL reference" />
<Link href={newURL} color="textPrimary" target="_blank" referrerPolicy='no'>{newURL}</Link>
</MythicStyledTableCell>
</TableRow>
<TableRow hover>
<MythicStyledTableCell>JSON Data</MythicStyledTableCell>
<MythicStyledTableCell>
<AceEditor
<MythicDialogBody>
<MythicDialogSection
title="Tag Type"
description="Select the taxonomy entry this tag should use."
actions={
<Button className="mythic-table-row-action mythic-table-row-action-hover-info" size="small" variant="outlined" onClick={() => setOpenNewTagTypeDialog(true)}>
Manage Tag Types
</Button>
}
>
<MythicFormField label="Tag">
<Select
fullWidth
size="small"
value={selectedTagType}
onChange={handleTaskTypeChange}
displayEmpty
renderValue={(tagtype) => tagtype?.id ? <TagTypeChip tagtype={tagtype} /> : "No tag types available"}
>
{existingTagTypes.map( (opt) => (
<MenuItem value={opt} key={opt.name}>
<TagTypeChip tagtype={opt} />
</MenuItem>
) )}
</Select>
</MythicFormField>
{selectedTagType?.description &&
<MythicFormNote>{selectedTagType.description}</MythicFormNote>
}
</MythicDialogSection>
<MythicDialogSection title="Reference Details">
<MythicFormGrid minWidth="16rem">
<MythicFormField label="Source">
<MythicTextField value={newSource} onChange={onChangeSource} name="Source of tag data" showLabel={false} marginTop="0" marginBottom="0" />
</MythicFormField>
<MythicFormField label="External URL">
<MythicTextField value={newURL} onChange={onChangeURL} name="External URL reference" showLabel={false} marginTop="0" marginBottom="0" />
{newURL &&
<Link href={newURL} color="textPrimary" target="_blank" referrerPolicy='no' sx={{display: "inline-flex", mt: 0.5, fontSize: "0.78rem"}}>
Open reference
</Link>
}
</MythicFormField>
</MythicFormGrid>
</MythicDialogSection>
<MythicDialogSection title="JSON Data">
<Box className="mythic-tag-data-split">
<Box className="mythic-tag-editor-frame">
<AceEditor
mode="json"
theme={theme.palette.mode === "dark" ? "monokai" : "xcode"}
onChange={onEditorChange}
fontSize={14}
showGutter={true}
maxLines={20}
showPrintMargin={false}
minLines={10}
maxLines={18}
highlightActiveLine={true}
value={newData}
width={"100%"}
setOptions={{
showLineNumbers: true,
tabSize: 4,
useWorker: false
useWorker: false,
wrap: true
}}/>
</MythicStyledTableCell>
</TableRow>
</TableBody>
</Table>
</Box>
<Box className="mythic-tag-data-preview-frame">
<RenderedTagDataPreview data={newData} />
</Box>
</Box>
</MythicDialogSection>
</MythicDialogBody>
</DialogContent>
<DialogActions>
<Button onClick={props.onClose} variant="contained" color="primary">
<MythicDialogFooter>
<MythicDialogButton onClick={props.onClose}>
Close
</Button>
</MythicDialogButton>
{selectedTagType !== "" &&
<Button onClick={onSubmit} variant="contained" color="success">
<MythicDialogButton intent="primary" onClick={onSubmit}>
Submit
</Button>
</MythicDialogButton>
}
</DialogActions>
</MythicDialogFooter>
</React.Fragment>
);
}
@@ -0,0 +1,42 @@
import React from 'react';
import Chip from '@mui/material/Chip';
import {useTheme} from '@mui/material/styles';
export const getTagReadableTextColor = (theme, color) => {
if(!color){
return theme.palette.text.primary;
}
try {
return theme.palette.getContrastText(color);
} catch (error) {
return theme.palette.text.primary;
}
}
export const TagTypeChip = ({tagtype, label, sx={}, ...props}) => {
const theme = useTheme();
const color = tagtype?.color || "";
const textColor = getTagReadableTextColor(theme, color);
return (
<Chip
label={label || tagtype?.name || "Tag"}
size="small"
sx={{
backgroundColor: color || "transparent",
border: "1px solid",
borderColor: color ? "rgba(0,0,0,0.16)" : theme.table?.borderSoft || theme.borderColor,
color: textColor,
fontWeight: 800,
maxWidth: "100%",
"& .MuiChip-label": {
color: "inherit",
minWidth: 0,
overflow: "hidden",
textOverflow: "ellipsis",
},
...sx,
}}
{...props}
/>
);
}
@@ -56,6 +56,7 @@ export const classes = {
taskCommandTextCompact: `${PREFIX}-taskCommandTextCompact`,
taskCommandName: `${PREFIX}-taskCommandName`,
taskCommandParams: `${PREFIX}-taskCommandParams`,
taskTags: `${PREFIX}-taskTags`,
taskChildToggle: `${PREFIX}-taskChildToggle`,
taskCommentBlock: `${PREFIX}-taskCommentBlock`,
consolePrompt: `${PREFIX}-consolePrompt`
@@ -231,6 +232,9 @@ export const StyledPaper = styled(Paper)((
minWidth: 0,
width: "100%",
},
[`& .${classes.taskHeaderBodyCompact} .${classes.taskCommandRow}`]: {
alignItems: "center",
},
[`& .${classes.taskCommandText}`]: {
color: theme.taskPromptCommandTextColor,
flex: "1 1 auto",
@@ -252,6 +256,33 @@ export const StyledPaper = styled(Paper)((
color: theme.palette.text.primary,
overflowWrap: "anywhere",
},
[`& .${classes.taskTags}`]: {
alignItems: "center",
display: "inline-flex",
flex: "0 1 auto",
flexWrap: "wrap",
gap: 3,
justifyContent: "flex-end",
marginLeft: "auto",
maxWidth: "34%",
minWidth: 0,
"& .MuiChip-root": {
float: "none !important",
height: "16px !important",
margin: 0,
maxWidth: "10rem",
},
"& .MuiChip-label": {
minWidth: 0,
overflow: "hidden",
textOverflow: "ellipsis",
},
},
[`& .${classes.taskHeaderBodyCompact} .${classes.taskTags}`]: {
flexWrap: "nowrap",
maxHeight: 20,
overflow: "hidden",
},
[`& .${classes.taskChildToggle}`]: {
color: theme.palette.text.secondary,
height: 24,
@@ -494,8 +525,13 @@ const TaskHeaderAction = ({title, children, ...props}) => (
</MythicStyledTooltip>
)
const TaskTagDisplay = ({task}) => {
if(!task.tags || task.tags.length === 0){
return null;
}
return (
<TagsDisplay tags={task.tags} />
<span className={classes.taskTags}>
<TagsDisplay tags={task.tags} />
</span>
)
}
const isTaskHeaderInteractiveClick = (e) => {
@@ -628,6 +664,7 @@ export const ColoredTaskLabel = ({task, theme, me, taskDivID, onClick, displayCh
<MythicStyledTooltip maxWidth={"calc(80vw)"}
enterDelay={2000}
placement={"top"}
tooltipStyle={{display: "block", flex: "1 1 auto", minWidth: 0}}
title={commandLine} >
<Typography className={compact ? `${classes.taskCommandText} ${classes.taskCommandTextCompact}` : classes.taskCommandText} component="div">
<span className={classes.taskCommandName}>{command}</span>
@@ -636,8 +673,8 @@ export const ColoredTaskLabel = ({task, theme, me, taskDivID, onClick, displayCh
}
</Typography>
</MythicStyledTooltip>
<TaskTagDisplay task={task}/>
</div>
<TaskTagDisplay task={task}/>
</div>
{openKillTaskButton.open &&
<TaskFromUIButton ui_feature={"task:job_kill"}
@@ -26,7 +26,6 @@ import {FontAwesomeIcon} from '@fortawesome/react-fontawesome';
import {faExclamationTriangle} from '@fortawesome/free-solid-svg-icons';
import { faExternalLinkAlt, faExpandArrowsAlt } from '@fortawesome/free-solid-svg-icons';
import SearchIcon from '@mui/icons-material/Search';
import Button from '@mui/material/Button';
import Menu from '@mui/material/Menu';
import MenuItem from '@mui/material/MenuItem';
import ListItemIcon from '@mui/material/ListItemIcon';
@@ -69,6 +68,13 @@ query subResponsesQuery($task_id: Int!) {
}
}`;
const responseActionClass = (tone = "info") => {
return `mythic-response-action-button mythic-response-action-hover-${tone}`;
};
const responseMenuItemClass = (tone = "info") => {
return `mythic-response-action-menu-item mythic-response-action-hover-${tone}`;
};
export const TaskDisplayContainer = ({task, me}) => {
const [viewBrowserScript, setViewBrowserScript] = React.useState(true);
const [commandID, setCommandID] = React.useState(0);
@@ -193,7 +199,6 @@ export const TaskDisplayContainerConsole = ({task, me}) => {
const SideDisplayGeneric = ({toggleViewBrowserScript, toggleSelectAllOutput,
toggleOpenSearch, taskData, viewAllOutput, me,
responseRef, viewBrowserScript}) => {
const theme = useTheme();
const [task, setTask] = React.useState(taskData || {});
const eventingDataRef = React.useRef({name: "", value: 0});
const [openEventingDialog, setOpenEventingDialog] = React.useState(false);
@@ -338,78 +343,78 @@ const SideDisplayGeneric = ({toggleViewBrowserScript, toggleSelectAllOutput,
}
/>
}
<Paper elevation={5} style={{width: "30px", display: "flex", flexDirection: "column", alignItems: "center", overflow: "hidden",
<Paper className="mythic-response-side-actions" elevation={5} style={{width: "30px", display: "flex", flexDirection: "column", alignItems: "center", overflow: "hidden",
backgroundColor: "transparent", height: "100%"}}>
<MythicStyledTooltip title={"Toggle BrowserScript"} >
<IconButton onClick={toggleViewBrowserScript} style={{paddingBottom: "3px"}}>
{viewBrowserScript ? <CodeOffIcon color={"error"} /> : <CodeIcon color={"success"}/>}
<IconButton className={responseActionClass(viewBrowserScript ? "danger" : "success")} onClick={toggleViewBrowserScript} size="small">
{viewBrowserScript ? <CodeOffIcon fontSize="small" /> : <CodeIcon fontSize="small" />}
</IconButton>
</MythicStyledTooltip>
<MythicStyledTooltip title={viewAllOutput ? "View Paginated Output" : "View All Output"}>
<IconButton onClick={toggleSelectAllOutput} style={{paddingBottom: "3px"}}>
{viewAllOutput ? <CloseFullscreenIcon color={"error"} /> : <FontAwesomeIcon style={{color: theme.palette.success.main}} icon={faExpandArrowsAlt} size="sm" />}
<IconButton className={responseActionClass(viewAllOutput ? "danger" : "success")} onClick={toggleSelectAllOutput} size="small">
{viewAllOutput ? <CloseFullscreenIcon fontSize="small" /> : <FontAwesomeIcon icon={faExpandArrowsAlt} size="sm" />}
</IconButton>
</MythicStyledTooltip>
<MythicStyledTooltip title={"Search Output"}>
<IconButton onClick={toggleOpenSearch} style={{paddingBottom: "3px"}}>
<SearchIcon color={"info"} />
<IconButton className={responseActionClass("info")} onClick={toggleOpenSearch} size="small">
<SearchIcon fontSize="small" />
</IconButton>
</MythicStyledTooltip>
<MythicStyledTooltip title={"Download output"}>
<IconButton onClick={onDownloadResponses} style={{paddingBottom: "3px"}}>
<GetAppIcon color={"success"}/>
<IconButton className={responseActionClass("success")} onClick={onDownloadResponses} size="small">
<GetAppIcon fontSize="small" />
</IconButton>
</MythicStyledTooltip>
<MythicStyledTooltip title={"Download screenshot of output"}>
<IconButton onClick={onDownloadImageClickPng} style={{paddingBottom: "3px"}}>
<InsertPhotoIcon/>
<IconButton className={responseActionClass("success")} onClick={onDownloadImageClickPng} size="small">
<InsertPhotoIcon fontSize="small" />
</IconButton>
</MythicStyledTooltip>
<MythicStyledTooltip title={"Edit Tags"}>
<IconButton onClick={()=>{setOpenTaskTagDialog(true)}} style={{paddingBottom: "3px"}}>
<LocalOfferOutlinedIcon/>
<IconButton className={responseActionClass("info")} onClick={()=>{setOpenTaskTagDialog(true)}} size="small">
<LocalOfferOutlinedIcon fontSize="small" />
</IconButton>
</MythicStyledTooltip>
<MythicStyledTooltip title={"Open Task in New Window"}>
<IconButton onClick={()=> {window.open('/new/task/' + task.display_id, "_blank")}} style={{paddingBottom: "3px"}}>
<IconButton className={responseActionClass("info")} onClick={()=> {window.open('/new/task/' + task.display_id, "_blank")}} size="small">
<FontAwesomeIcon icon={faExternalLinkAlt} size="sm" />
</IconButton>
</MythicStyledTooltip>
<MythicStyledTooltip title={"Copy original params to clipboard"}>
<IconButton onClick={copyToClipboard} style={{paddingBottom: "3px"}}>
<FileCopyOutlinedIcon/>
<IconButton className={responseActionClass("info")} onClick={copyToClipboard} size="small">
<FileCopyOutlinedIcon fontSize="small" />
</IconButton>
</MythicStyledTooltip>
<MythicStyledTooltip title={"Edit Comment"}>
<IconButton onClick={()=>{setOpenCommentDialog(true)}} style={{paddingBottom: "3px"}}>
<RateReviewOutlinedIcon/>
<IconButton className={responseActionClass("info")} onClick={()=>{setOpenCommentDialog(true)}} size="small">
<RateReviewOutlinedIcon fontSize="small" />
</IconButton>
</MythicStyledTooltip>
<MythicStyledTooltip title={"View All Parameters And Timestamps"}>
<IconButton onClick={()=>{setOpenParametersDialog(true);}} style={{paddingBottom: "3px"}}>
<KeyboardIcon/>
<IconButton className={responseActionClass("info")} onClick={()=>{setOpenParametersDialog(true);}} size="small">
<KeyboardIcon fontSize="small" />
</IconButton>
</MythicStyledTooltip>
<MythicStyledTooltip title={"View Stdout/Stderr of Task"}>
<IconButton onClick={()=>{setOpenStdoutStderrDialog(true);}} style={{paddingBottom: "3px"}}>
<FontAwesomeIcon style={{color: theme.palette.error.main}} icon={faExclamationTriangle} size="sm" />
<IconButton className={responseActionClass("danger")} onClick={()=>{setOpenStdoutStderrDialog(true);}} size="small">
<FontAwesomeIcon icon={faExclamationTriangle} size="sm" />
</IconButton>
</MythicStyledTooltip>
<MythicStyledTooltip title={"Trigger Eventing Based on Task"}>
<IconButton onClick={()=>{onTriggerEventing();}} style={{paddingBottom: "3px"}}>
<PlayCircleFilledTwoToneIcon />
<IconButton className={responseActionClass("info")} onClick={()=>{onTriggerEventing();}} size="small">
<PlayCircleFilledTwoToneIcon fontSize="small" />
</IconButton>
</MythicStyledTooltip>
{task.opsec_pre_blocked === null ? null : ( task.opsec_pre_bypassed === false ? (
<MythicStyledTooltip title={"Submit OPSEC PreCheck Bypass Request"}>
<IconButton onClick={()=>{setOpenOpsecDialog({open: true, view: "pre"})}} style={{paddingBottom: "3px"}}>
<LockIcon style={{color: theme.palette.error.main}}/>
<IconButton className={responseActionClass("danger")} onClick={()=>{setOpenOpsecDialog({open: true, view: "pre"})}} size="small">
<LockIcon fontSize="small" />
</IconButton>
</MythicStyledTooltip>
): (
<MythicStyledTooltip title={"View OPSEC PreCheck Data"}>
<IconButton onClick={()=>{setOpenOpsecDialog({open: true, view: "pre"})}} style={{paddingBottom: "3px"}}>
<LockOpenIcon style={{color: theme.palette.success.main}}/>
<IconButton className={responseActionClass("success")} onClick={()=>{setOpenOpsecDialog({open: true, view: "pre"})}} size="small">
<LockOpenIcon fontSize="small" />
</IconButton>
</MythicStyledTooltip>
)
@@ -417,14 +422,14 @@ const SideDisplayGeneric = ({toggleViewBrowserScript, toggleSelectAllOutput,
}
{task.opsec_post_blocked === null ? null : ( task.opsec_post_bypassed === false ? (
<MythicStyledTooltip title={"Submit OPSEC PostCheck Bypass Request"}>
<IconButton onClick={()=>{setOpenOpsecDialog({open: true, view: "post"})}} style={{paddingBottom: "3px"}}>
<LockIcon style={{color: theme.palette.error.main}}/>
<IconButton className={responseActionClass("danger")} onClick={()=>{setOpenOpsecDialog({open: true, view: "post"})}} size="small">
<LockIcon fontSize="small" />
</IconButton>
</MythicStyledTooltip>
): (
<MythicStyledTooltip title={"View OPSEC PostCheck Data"}>
<IconButton onClick={()=>{setOpenOpsecDialog({open: true, view: "post"})}} style={{paddingBottom: "3px"}}>
<LockOpenIcon style={{color: theme.palette.success.main}}/>
<IconButton className={responseActionClass("success")} onClick={()=>{setOpenOpsecDialog({open: true, view: "post"})}} size="small">
<LockOpenIcon fontSize="small" />
</IconButton>
</MythicStyledTooltip>
)
@@ -432,22 +437,22 @@ const SideDisplayGeneric = ({toggleViewBrowserScript, toggleSelectAllOutput,
}
{task.token === null ? null : (
<MythicStyledTooltip title={"View Token Information"}>
<IconButton onClick={()=>{setOpenTokenDialog(true)}} style={{paddingBottom: "3px"}}>
<ConfirmationNumberIcon />
<IconButton className={responseActionClass("info")} onClick={()=>{setOpenTokenDialog(true)}} size="small">
<ConfirmationNumberIcon fontSize="small" />
</IconButton>
</MythicStyledTooltip>
)}
{task.status.toLowerCase().includes("error: container") &&
<MythicStyledTooltip title={"Resubmit Tasking"}>
<IconButton onClick={onReissueTask} style={{paddingBottom: "3px"}}>
<ReplayIcon style={{color: theme.palette.warning.main}}/>
<IconButton className={responseActionClass("warning")} onClick={onReissueTask} size="small">
<ReplayIcon fontSize="small" />
</IconButton>
</MythicStyledTooltip>
}
{task.status.toLowerCase().includes("error: task") &&
<MythicStyledTooltip title={"Resubmit Task Handler"}>
<IconButton onClick={onReissueTaskHandler} style={{paddingBottom: "3px"}}>
<ReplayIcon style={{color: theme.palette.warning.main}}/>
<IconButton className={responseActionClass("warning")} onClick={onReissueTaskHandler} size="small">
<ReplayIcon fontSize="small" />
</IconButton>
</MythicStyledTooltip>
}
@@ -616,7 +621,7 @@ const TaskActionsToolbarGeneric = ({toggleViewBrowserScript, toggleSelectAllOutp
}
/>
}
<Paper elevation={0} sx={{
<Paper className="mythic-response-actions-toolbar" elevation={0} sx={{
alignItems: "center",
backgroundColor: theme.palette.mode === "dark" ? "rgba(255,255,255,0.03)" : "rgba(0,0,0,0.02)",
borderLeft: 0,
@@ -631,63 +636,66 @@ const TaskActionsToolbarGeneric = ({toggleViewBrowserScript, toggleSelectAllOutp
py: 0.5,
}}>
<MythicStyledTooltip title={"Toggle BrowserScript"}>
<IconButton size="small" onClick={toggleViewBrowserScript}>
{viewBrowserScript ? <CodeOffIcon color={"error"} fontSize="small" /> : <CodeIcon color={"success"} fontSize="small" />}
<IconButton className={responseActionClass(viewBrowserScript ? "danger" : "success")} size="small" onClick={toggleViewBrowserScript}>
{viewBrowserScript ? <CodeOffIcon fontSize="small" /> : <CodeIcon fontSize="small" />}
</IconButton>
</MythicStyledTooltip>
<MythicStyledTooltip title={viewAllOutput ? "View Paginated Output" : "View All Output"}>
<IconButton size="small" onClick={toggleSelectAllOutput}>
{viewAllOutput ? <CloseFullscreenIcon color={"error"} fontSize="small" /> : <FontAwesomeIcon style={{color: theme.palette.success.main}} icon={faExpandArrowsAlt} size="sm" />}
<IconButton className={responseActionClass(viewAllOutput ? "danger" : "success")} size="small" onClick={toggleSelectAllOutput}>
{viewAllOutput ? <CloseFullscreenIcon fontSize="small" /> : <FontAwesomeIcon icon={faExpandArrowsAlt} size="sm" />}
</IconButton>
</MythicStyledTooltip>
<MythicStyledTooltip title={"Search Output"}>
<IconButton size="small" onClick={toggleOpenSearch}>
<SearchIcon color={"info"} fontSize="small" />
<IconButton className={responseActionClass("info")} size="small" onClick={toggleOpenSearch}>
<SearchIcon fontSize="small" />
</IconButton>
</MythicStyledTooltip>
<MythicStyledTooltip title={"Download output"}>
<IconButton size="small" onClick={onDownloadResponses}>
<GetAppIcon color={"success"} fontSize="small" />
<IconButton className={responseActionClass("success")} size="small" onClick={onDownloadResponses}>
<GetAppIcon fontSize="small" />
</IconButton>
</MythicStyledTooltip>
<MythicStyledTooltip title={"Download screenshot of output"}>
<IconButton size="small" onClick={onDownloadImageClickPng}>
<IconButton className={responseActionClass("success")} size="small" onClick={onDownloadImageClickPng}>
<InsertPhotoIcon fontSize="small" />
</IconButton>
</MythicStyledTooltip>
<Button size="small" variant="text" startIcon={<MoreHorizIcon fontSize="small" />} onClick={openActionsMenu}
sx={{ml: 0.25, minWidth: "auto", textTransform: "none"}}>
</Button>
<MythicStyledTooltip title={"More response actions"}>
<IconButton className={responseActionClass("info")} size="small" onClick={openActionsMenu}>
<MoreHorizIcon fontSize="small" />
</IconButton>
</MythicStyledTooltip>
<Menu anchorEl={actionsMenuAnchor}
open={Boolean(actionsMenuAnchor)}
onClose={closeActionsMenu}
anchorOrigin={{vertical: "bottom", horizontal: "right"}}
transformOrigin={{vertical: "top", horizontal: "right"}}>
<MenuItem onClick={()=>{setOpenTaskTagDialog(true);closeActionsMenu();}}>
transformOrigin={{vertical: "top", horizontal: "right"}}
MenuListProps={{className: "mythic-response-action-menu"}}>
<MenuItem className={responseMenuItemClass("info")} onClick={()=>{setOpenTaskTagDialog(true);closeActionsMenu();}}>
<ListItemIcon><LocalOfferOutlinedIcon fontSize="small" /></ListItemIcon>
<ListItemText>Edit Tags</ListItemText>
</MenuItem>
<MenuItem onClick={()=> {window.open('/new/task/' + task.display_id, "_blank");closeActionsMenu();}}>
<MenuItem className={responseMenuItemClass("info")} onClick={()=> {window.open('/new/task/' + task.display_id, "_blank");closeActionsMenu();}}>
<ListItemIcon><FontAwesomeIcon icon={faExternalLinkAlt} size="sm" /></ListItemIcon>
<ListItemText>Open Task in New Window</ListItemText>
</MenuItem>
<MenuItem onClick={copyToClipboard}>
<MenuItem className={responseMenuItemClass("info")} onClick={copyToClipboard}>
<ListItemIcon><FileCopyOutlinedIcon fontSize="small" /></ListItemIcon>
<ListItemText>Copy original params</ListItemText>
</MenuItem>
<MenuItem onClick={()=>{setOpenCommentDialog(true);closeActionsMenu();}}>
<MenuItem className={responseMenuItemClass("info")} onClick={()=>{setOpenCommentDialog(true);closeActionsMenu();}}>
<ListItemIcon><RateReviewOutlinedIcon fontSize="small" /></ListItemIcon>
<ListItemText>Edit Comment</ListItemText>
</MenuItem>
<MenuItem onClick={()=>{setOpenParametersDialog(true);closeActionsMenu();}}>
<MenuItem className={responseMenuItemClass("info")} onClick={()=>{setOpenParametersDialog(true);closeActionsMenu();}}>
<ListItemIcon><KeyboardIcon fontSize="small" /></ListItemIcon>
<ListItemText>View Parameters And Timestamps</ListItemText>
</MenuItem>
<MenuItem onClick={()=>{setOpenStdoutStderrDialog(true);closeActionsMenu();}}>
<ListItemIcon><FontAwesomeIcon style={{color: theme.palette.error.main}} icon={faExclamationTriangle} size="sm" /></ListItemIcon>
<MenuItem className={responseMenuItemClass("danger")} onClick={()=>{setOpenStdoutStderrDialog(true);closeActionsMenu();}}>
<ListItemIcon><FontAwesomeIcon icon={faExclamationTriangle} size="sm" /></ListItemIcon>
<ListItemText>View Stdout/Stderr</ListItemText>
</MenuItem>
<MenuItem onClick={onTriggerEventing}>
<MenuItem className={responseMenuItemClass("info")} onClick={onTriggerEventing}>
<ListItemIcon><PlayCircleFilledTwoToneIcon fontSize="small" /></ListItemIcon>
<ListItemText>Trigger Eventing Based on Task</ListItemText>
</MenuItem>
@@ -696,42 +704,42 @@ const TaskActionsToolbarGeneric = ({toggleViewBrowserScript, toggleSelectAllOutp
<Divider />
}
{task.opsec_pre_blocked !== null && (
<MenuItem onClick={()=>{setOpenOpsecDialog({open: true, view: "pre"});closeActionsMenu();}}>
<MenuItem className={responseMenuItemClass(task.opsec_pre_bypassed === false ? "danger" : "success")} onClick={()=>{setOpenOpsecDialog({open: true, view: "pre"});closeActionsMenu();}}>
<ListItemIcon>
{task.opsec_pre_bypassed === false ?
<LockIcon fontSize="small" style={{color: theme.palette.error.main}}/> :
<LockOpenIcon fontSize="small" style={{color: theme.palette.success.main}}/>
<LockIcon fontSize="small" /> :
<LockOpenIcon fontSize="small" />
}
</ListItemIcon>
<ListItemText>{task.opsec_pre_bypassed === false ? "Submit OPSEC PreCheck Bypass Request" : "View OPSEC PreCheck Data"}</ListItemText>
</MenuItem>
)}
{task.opsec_post_blocked !== null && (
<MenuItem onClick={()=>{setOpenOpsecDialog({open: true, view: "post"});closeActionsMenu();}}>
<MenuItem className={responseMenuItemClass(task.opsec_post_bypassed === false ? "danger" : "success")} onClick={()=>{setOpenOpsecDialog({open: true, view: "post"});closeActionsMenu();}}>
<ListItemIcon>
{task.opsec_post_bypassed === false ?
<LockIcon fontSize="small" style={{color: theme.palette.error.main}}/> :
<LockOpenIcon fontSize="small" style={{color: theme.palette.success.main}}/>
<LockIcon fontSize="small" /> :
<LockOpenIcon fontSize="small" />
}
</ListItemIcon>
<ListItemText>{task.opsec_post_bypassed === false ? "Submit OPSEC PostCheck Bypass Request" : "View OPSEC PostCheck Data"}</ListItemText>
</MenuItem>
)}
{task.token !== null && (
<MenuItem onClick={()=>{setOpenTokenDialog(true);closeActionsMenu();}}>
<MenuItem className={responseMenuItemClass("info")} onClick={()=>{setOpenTokenDialog(true);closeActionsMenu();}}>
<ListItemIcon><ConfirmationNumberIcon fontSize="small" /></ListItemIcon>
<ListItemText>View Token Information</ListItemText>
</MenuItem>
)}
{task.status.toLowerCase().includes("error: container") && (
<MenuItem onClick={onReissueTask}>
<ListItemIcon><ReplayIcon fontSize="small" style={{color: theme.palette.warning.main}} /></ListItemIcon>
<MenuItem className={responseMenuItemClass("warning")} onClick={onReissueTask}>
<ListItemIcon><ReplayIcon fontSize="small" /></ListItemIcon>
<ListItemText>Resubmit Tasking</ListItemText>
</MenuItem>
)}
{task.status.toLowerCase().includes("error: task") && (
<MenuItem onClick={onReissueTaskHandler}>
<ListItemIcon><ReplayIcon fontSize="small" style={{color: theme.palette.warning.main}} /></ListItemIcon>
<MenuItem className={responseMenuItemClass("warning")} onClick={onReissueTaskHandler}>
<ListItemIcon><ReplayIcon fontSize="small" /></ListItemIcon>
<ListItemText>Resubmit Task Handler</ListItemText>
</MenuItem>
)}
@@ -6,6 +6,7 @@ import {gql, useMutation} from '@apollo/client';
import { snackActions } from '../../utilities/Snackbar';
import MythicTextField from '../../MythicComponents/MythicTextField';
import {Typography, Box, Chip} from '@mui/material';
import {useTheme} from '@mui/material/styles';
import {
MythicDialogBody,
MythicDialogButton,
@@ -15,6 +16,7 @@ import {
MythicFormGrid
} from "../../MythicComponents/MythicDialogLayout";
import {isValidHexColor, MythicColorSwatchInput} from "../../MythicComponents/MythicColorInput";
import {getTagReadableTextColor} from "../../MythicComponents/MythicTagChip";
const newTagtypeMutation = gql`
mutation newTagType($name: String!, $description: String!, $color: String!) {
@@ -33,9 +35,11 @@ mutation updateTagType($id: Int!, $name: String!, $description: String!, $color:
`;
const TagColorPreview = ({mode, color, label}) => {
const theme = useTheme();
const darkMode = mode === "dark";
const hasValidColor = isValidHexColor(color);
const textColor = darkMode ? "#ffffff" : "#111827";
const chipTextColor = hasValidColor ? getTagReadableTextColor(theme, color) : textColor;
const surfaceColor = darkMode ? "#1f2937" : "#f8fafc";
const surfaceBorderColor = darkMode ? "rgba(255,255,255,0.16)" : "rgba(17,24,39,0.12)";
const chipBorderColor = hasValidColor ? (darkMode ? "rgba(255,255,255,0.2)" : "rgba(17,24,39,0.16)") : surfaceBorderColor;
@@ -69,9 +73,10 @@ const TagColorPreview = ({mode, color, label}) => {
border: "1px solid",
borderColor: chipBorderColor,
borderStyle: hasValidColor ? "solid" : "dashed",
color: textColor,
color: chipTextColor,
fontWeight: 800,
"& .MuiChip-label": {
color: "inherit",
px: 1,
minWidth: 0,
overflow: "hidden",
@@ -7,8 +7,8 @@ import {MythicConfirmDialog} from '../../MythicComponents/MythicConfirmDialog';
import { MythicStyledTooltip } from '../../MythicComponents/MythicStyledTooltip';
import MythicStyledTableCell from '../../MythicComponents/MythicTableCell';
import {NewTagtypesDialog} from './NewTagtypesDialog';
import Chip from '@mui/material/Chip';
import EditIcon from '@mui/icons-material/Edit';
import {TagTypeChip} from "../../MythicComponents/MythicTagChip";
export function TagtypesTableRow(props){
@@ -49,7 +49,7 @@ export function TagtypesTableRow(props){
{props.tags_aggregate.aggregate.count}
</MythicStyledTableCell>
<MythicStyledTableCell>
<Chip label={props.name} size="small" style={{backgroundColor:props.color}} />
<TagTypeChip tagtype={props} />
</MythicStyledTableCell>
<MythicStyledTableCell>
{props.description}
+161
View File
@@ -2410,6 +2410,103 @@ tspan {
line-height: 1.4;
padding: 0.65rem 0.75rem;
}
.mythic-tag-editor-frame {
background-color: ${(props) => props.theme.palette.background.paper};
border: 1px solid ${(props) => props.theme.table?.borderSoft || props.theme.borderColor};
border-radius: ${(props) => props.theme.shape.borderRadius}px;
min-width: 0;
overflow: hidden;
width: 100%;
}
.mythic-tag-editor-frame .ace_editor {
background-color: ${(props) => props.theme.palette.background.paper} !important;
}
.mythic-tag-data-split {
display: grid;
gap: 0.75rem;
grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
min-width: 0;
width: 100%;
}
.mythic-tag-data-preview-frame {
background-color: ${(props) => props.theme.palette.background.paper};
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};
max-height: 30rem;
min-height: 10rem;
min-width: 0;
overflow: auto;
padding: 0.55rem;
}
.mythic-tag-data-preview-frame-full {
max-height: min(48vh, 34rem);
}
.mythic-tag-readonly-value {
align-items: center;
background-color: ${(props) => props.theme.palette.background.paper};
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: flex;
font-size: 0.84rem;
line-height: 1.35;
min-height: 34px;
min-width: 0;
overflow-wrap: anywhere;
padding: 0.45rem 0.6rem;
width: 100%;
}
.mythic-tag-data-preview-list {
display: flex;
flex-direction: column;
gap: 0.4rem;
min-width: 0;
}
.mythic-tag-data-preview-row {
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: grid;
gap: 0.5rem;
grid-template-columns: minmax(7rem, 0.35fr) minmax(0, 1fr);
min-width: 0;
padding: 0.45rem 0.55rem;
}
.mythic-tag-data-preview-key {
color: ${(props) => props.theme.palette.text.secondary};
font-size: 0.76rem;
font-weight: 800;
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.mythic-tag-data-preview-value {
color: ${(props) => props.theme.palette.text.primary};
font-size: 0.82rem;
line-height: 1.35;
min-width: 0;
overflow-wrap: anywhere;
}
.mythic-tag-data-preview-value pre {
margin: 0;
white-space: pre-wrap;
}
.mythic-tag-data-preview-empty {
align-items: center;
color: ${(props) => props.theme.palette.text.secondary};
display: flex;
font-size: 0.82rem;
justify-content: center;
min-height: 9rem;
text-align: center;
}
@media (max-width: 900px) {
.mythic-tag-data-split {
grid-template-columns: 1fr;
}
}
.mythic-form-switch-row {
align-items: center;
background-color: ${(props) => props.theme.palette.background.paper};
@@ -6894,6 +6991,70 @@ tspan {
border-color: ${(props) => props.theme.table?.borderSoft || props.theme.borderColor} !important;
color: ${(props) => props.theme.palette.text.disabled} !important;
}
.MuiIconButton-root.mythic-response-action-button {
background-color: ${(props) => props.theme.palette.mode === "dark" ? alpha(props.theme.palette.common.white, 0.045) : alpha(props.theme.palette.common.black, 0.028)} !important;
border: 1px solid ${(props) => props.theme.table?.borderSoft || props.theme.borderColor} !important;
border-radius: ${(props) => props.theme.shape.borderRadius}px !important;
box-shadow: none !important;
color: ${(props) => props.theme.palette.text.secondary} !important;
height: 30px;
padding: 0;
transition: background-color 140ms ease, border-color 140ms ease, color 140ms ease, box-shadow 140ms ease;
width: 30px;
}
.MuiIconButton-root.mythic-response-action-button .MuiSvgIcon-root,
.MuiIconButton-root.mythic-response-action-button .svg-inline--fa {
color: inherit !important;
}
.MuiIconButton-root.mythic-response-action-button:hover {
background-color: ${(props) => props.theme.palette.mode === "dark" ? alpha(props.theme.palette.common.white, 0.085) : alpha(props.theme.palette.common.black, 0.052)} !important;
border-color: ${(props) => props.theme.table?.border || props.theme.borderColor} !important;
color: ${(props) => props.theme.palette.text.primary} !important;
}
.mythic-response-action-menu {
padding: 0.25rem !important;
}
.mythic-response-action-menu .MuiMenuItem-root.mythic-response-action-menu-item {
border-radius: ${(props) => props.theme.shape.borderRadius}px;
color: ${(props) => props.theme.palette.text.secondary};
min-height: 34px;
}
.mythic-response-action-menu .MuiMenuItem-root.mythic-response-action-menu-item .MuiListItemIcon-root {
color: inherit;
min-width: 2rem;
}
.mythic-response-action-menu .MuiMenuItem-root.mythic-response-action-menu-item .MuiSvgIcon-root,
.mythic-response-action-menu .MuiMenuItem-root.mythic-response-action-menu-item .svg-inline--fa {
color: inherit !important;
}
.MuiIconButton-root.mythic-response-action-button.mythic-response-action-hover-info:hover,
.mythic-response-action-menu .MuiMenuItem-root.mythic-response-action-hover-info:hover,
.mythic-response-action-menu .MuiMenuItem-root.mythic-response-action-hover-info.Mui-focusVisible {
background-color: ${(props) => alpha(props.theme.palette.info.main, props.theme.palette.mode === "dark" ? 0.18 : 0.1)} !important;
border-color: ${(props) => alpha(props.theme.palette.info.main, props.theme.palette.mode === "dark" ? 0.54 : 0.36)} !important;
color: ${(props) => props.theme.palette.info.main} !important;
}
.MuiIconButton-root.mythic-response-action-button.mythic-response-action-hover-success:hover,
.mythic-response-action-menu .MuiMenuItem-root.mythic-response-action-hover-success:hover,
.mythic-response-action-menu .MuiMenuItem-root.mythic-response-action-hover-success.Mui-focusVisible {
background-color: ${(props) => alpha(props.theme.palette.success.main, props.theme.palette.mode === "dark" ? 0.18 : 0.1)} !important;
border-color: ${(props) => alpha(props.theme.palette.success.main, props.theme.palette.mode === "dark" ? 0.54 : 0.36)} !important;
color: ${(props) => props.theme.palette.success.main} !important;
}
.MuiIconButton-root.mythic-response-action-button.mythic-response-action-hover-warning:hover,
.mythic-response-action-menu .MuiMenuItem-root.mythic-response-action-hover-warning:hover,
.mythic-response-action-menu .MuiMenuItem-root.mythic-response-action-hover-warning.Mui-focusVisible {
background-color: ${(props) => alpha(props.theme.palette.warning.main, props.theme.palette.mode === "dark" ? 0.2 : 0.12)} !important;
border-color: ${(props) => alpha(props.theme.palette.warning.main, props.theme.palette.mode === "dark" ? 0.58 : 0.4)} !important;
color: ${(props) => props.theme.palette.warning.main} !important;
}
.MuiIconButton-root.mythic-response-action-button.mythic-response-action-hover-danger:hover,
.mythic-response-action-menu .MuiMenuItem-root.mythic-response-action-hover-danger:hover,
.mythic-response-action-menu .MuiMenuItem-root.mythic-response-action-hover-danger.Mui-focusVisible {
background-color: ${(props) => alpha(props.theme.palette.error.main, props.theme.palette.mode === "dark" ? 0.18 : 0.1)} !important;
border-color: ${(props) => alpha(props.theme.palette.error.main, props.theme.palette.mode === "dark" ? 0.56 : 0.38)} !important;
color: ${(props) => props.theme.palette.error.main} !important;
}
.mythic-menu-item-hover-danger .MuiSvgIcon-root {
color: inherit !important;
}
@@ -1,9 +1,11 @@
package webcontroller
import (
"encoding/json"
"net/http"
"github.com/its-a-feature/Mythic/database"
"github.com/its-a-feature/Mythic/eventing"
"net/http"
"github.com/gin-gonic/gin"
databaseStructs "github.com/its-a-feature/Mythic/database/structs"
@@ -72,14 +74,16 @@ func TagCreateWebhook(c *gin.Context) {
Source: input.Input.Source,
TagTypeID: input.Input.TagTypeID,
}
switch input.Input.Data.(type) {
case string:
var jsonData json.RawMessage
err = json.Unmarshal([]byte(input.Input.Data.(string)), &jsonData)
if err != nil {
databaseObj.Data = rabbitmq.GetMythicJSONTextFromStruct(map[string]interface{}{
"input": input.Input.Data,
})
default:
databaseObj.Data = rabbitmq.GetMythicJSONTextFromStruct(input.Input.Data)
} else {
databaseObj.Data = rabbitmq.GetMythicJSONTextFromStruct(jsonData)
}
APITokenID, ok := c.Get("apitokens-id")
associatedWithValidObject := false
if ok {