UI updates and back-end better file transfer tracking

This commit is contained in:
its-a-feature
2026-03-20 17:27:25 -05:00
parent 3e611494cc
commit 09ac733d45
31 changed files with 446 additions and 279 deletions
+9
View File
@@ -4,6 +4,15 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [3.4.30] - 2026-03-20
### Changed
- Added a `received_chunk_ids` map to the database for `filemeta` to track not just chunks, but which chunks are received
- this helps prevent duplicate chunks getting counted multiple times and causing faulty downloads
- Added some default values for `DisplayPath` that were causing null constraint errors on the database
- Fixed a remote path null value with uploading eventing files
## [3.4.29] - 2026-03-18
### Changed
+8
View File
@@ -4,6 +4,14 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [0.3.105] - 2026-03-20
### Changed
- Updated some of the file upload pieces in the UI to support drag-and-drop
- Added an `autoScroll` button to interactive tasking so you can force auto scroll or not
- Fixed some of the drag and drop modal components
## [0.3.104] - 2026-03-19
### Changed
@@ -51,8 +51,9 @@ export function MythicDialog(props) {
}, [props.open]);
const dialogOnClick = (e) => {
e.stopPropagation();
e.preventDefault();
if(e.target.classList.length > 0 && e.target.classList.contains("MuiDialog-container")){
//props.onClose();
props.onClose();
}
}
const dialogOnContextMenu = (e) => {
@@ -65,43 +66,58 @@ export function MythicDialog(props) {
props.onClose();
}
const onStart = (e) => {
if(e){
e.stopPropagation();
e.preventDefault();
}
if(!draggedState.modified){
setDraggedState({
style: {
width: e.target.parentElement.offsetWidth + "px",
height: e.target.offsetParent.offsetHeight + "px",
width: e.target.offsetParent.offsetWidth + "px",
margin: "auto",
overflowY: "auto",
},
paperStyle: {
height: e.target.offsetParent.offsetHeight + "px",
width: e.target.parentElement.offsetWidth + "px",
margin: 0
//height: e.target.offsetParent.offsetHeight + "px",
width: e.target.offsetParent.offsetWidth + "px",
margin: 0,
overflowY: "auto",
},
containerStyle: {
height: "fit-content"
height: "fit-content",
overflowY: "auto"
},
hideBackdrop: true,
modified: true,
})
}
}
const onStop = (e) => {
if(e){
e.stopPropagation();
e.preventDefault();
}
}
return (
<Draggable
nodeRef={nodeRef}
handle="#mythic-draggable-title"
cancel={'[class*="MuiDialogContent-root"]'}
onStart={onStart}
onStop={onStop}
>
<Dialog
ref={nodeRef}
open={props.open}
onClick={dialogOnClick}
onClose={handleOnClose}
scroll="paper"
maxWidth={props.maxWidth}
fullWidth={true}
style={{...props.style, ...draggedState.style}}
disableEnforceFocus={true}
disablePortal={true}
disablePortal={false}
hideBackdrop={draggedState.hideBackdrop}
aria-labelledby="scroll-dialog-title"
aria-describedby="scroll-dialog-description"
@@ -71,6 +71,13 @@ export const MythicFileContext = ({agent_file_id, display_link, filename, extraS
if(fileData.agent_file_id === '' || fileData.filename === undefined){
return agent_file_id
}
const onClose = (e) => {
if(e){
e.preventDefault();
e.stopPropagation();
}
setOpenPreviewMediaDialog(false)
}
return (
<>
<MythicStyledTooltip title={"Preview Media"} tooltipStyle={extraStyles ? extraStyles : {}}>
@@ -83,11 +90,11 @@ export const MythicFileContext = ({agent_file_id, display_link, filename, extraS
</Link>
{openPreviewMediaDialog &&
<MythicDialog fullWidth={true} maxWidth="xl" open={openPreviewMediaDialog}
onClose={(e)=>{setOpenPreviewMediaDialog(false);}}
onClose={onClose}
innerDialog={<PreviewFileMediaDialog
agent_file_id={fileData.agent_file_id}
filename={fileData.filename}
onClose={(e)=>{setOpenPreviewMediaDialog(false);}} />}
onClose={onClose} />}
/>
}
</>
@@ -185,8 +185,8 @@ export function AddRemoveCallbackCommandsDialog(props) {
}
return (
<React.Fragment>
<MythicDraggableDialogTitle>Add or Remove Commands for Callback {props.display_id} </MythicDraggableDialogTitle>
<DialogContent dividers={true} style={{height: "100%", display: "flex", flexDirection: "column", position: "relative", maxHeight: "100%"}}>
<DialogTitle>Add or Remove Commands for Callback {props.display_id} </DialogTitle>
<DialogContent dividers={true} style={{height: "100%", display: "flex", flexDirection: "column", position: "relative", overflowY: "auto"}}>
This will add or remove commands associated with this callback from Mythic's perspective.
This does NOT add or remove commands within the payload itself that's beaconing out to Mythic.
<div style={{display: "flex", flexDirection: "row", overflowY: "auto", flexGrow: 1, minHeight: 0}}>
@@ -97,9 +97,9 @@ export const ResponseDisplay = (props) =>{
const interactive = props?.task?.command?.supported_ui_features.includes("task_response:interactive") || false;
return (
interactive ? (
<ResponseDisplayInteractive {...props} />
<ResponseDisplayInteractive {...props} key={props?.task?.id} />
) : (
<NonInteractiveResponseDisplay {...props} />
<NonInteractiveResponseDisplay {...props} key={props?.task?.id} />
)
)
}
@@ -21,6 +21,7 @@ import WrapTextIcon from '@mui/icons-material/WrapText';
import { useReactiveVar } from '@apollo/client';
import { meState } from '../../../cache';
import {useTheme} from '@mui/material/styles';
import HeightIcon from '@mui/icons-material/Height';
const getInteractiveTaskingQuery = gql`
${taskingDataFragment}
@@ -204,7 +205,7 @@ export const ResponseDisplayInteractive = (props) =>{
const me = useReactiveVar(meState);
const theme = useTheme();
const [backdropOpen, setBackdropOpen] = React.useState(false);
const [scrollToBottom, setScrollToBottom] = React.useState(false);
const [scrollToBottom, setScrollToBottom] = React.useState(true);
const pageSize = React.useRef(100);
const highestFetched = React.useRef(0);
const [taskData, setTaskData] = React.useState([]);
@@ -219,6 +220,7 @@ export const ResponseDisplayInteractive = (props) =>{
const [useASNIColor, setUseANSIColor] = React.useState(true);
const [showTaskStatus, setShowTaskStatus] = React.useState(true);
const [wrapText, setWrapText] = React.useState(true);
const [autoScroll, setAutoScroll] = React.useState(true);
const {loading: loadingTasks} = useSubscription(getInteractiveTaskingQuery, {
variables: {parent_task_id: props.task.id},
onError: data => {
@@ -280,8 +282,7 @@ export const ResponseDisplayInteractive = (props) =>{
if(backdropOpen){
setBackdropOpen(false);
}
}, [highestFetched.current, rawResponses, props.task.id, backdropOpen, taskIDRef.current]);
}, [highestFetched.current, rawResponses, props.task.id, backdropOpen, taskIDRef.current, autoScroll]);
useSubscription(subResponsesQuery, {
variables: {task_id: props.task.id},
fetchPolicy: "no-cache",
@@ -368,11 +369,14 @@ export const ResponseDisplayInteractive = (props) =>{
const toggleWrapText = () => {
setWrapText(!wrapText);
}
useEffect( () => {
if(scrollToBottom){
messagesEndRef.current.scrollIntoView();
const toggleAutoScroll = () => {
setAutoScroll(!autoScroll);
}
useEffect(() => {
if(autoScroll){
messagesEndRef?.current?.scrollIntoView({ behavior: "smooth" });
}
}, [scrollToBottom]);
}, [props.responseRef?.current?.scrollHeight, autoScroll]);
React.useEffect( () => {
if(loadingTasks){
setTaskData([]);
@@ -417,16 +421,14 @@ export const ResponseDisplayInteractive = (props) =>{
<div style={{overflowY: "auto", width: "100%", marginBottom: "5px", height: props.expand ? "100%": undefined,
flexGrow: 1, paddingLeft: "10px", minHeight: "50px"}} ref={props.responseRef}
id={`ptytask${props.task.id}`}>
<GetOutputFormatAll data={alloutput}
myTask={props.task.operator.username === (me?.user?.username || "")}
taskID={props.task.id}
useASNIColor={useASNIColor}
messagesEndRef={messagesEndRef}
showTaskStatus={showTaskStatus}
search={props.searchOutput ? search : undefined}
wrapText={wrapText}/>
<GetOutputFormatAll data={alloutput}
myTask={props.task.operator.username === (me?.user?.username || "")}
taskID={props.task.id}
useASNIColor={useASNIColor}
messagesEndRef={messagesEndRef}
showTaskStatus={showTaskStatus}
search={props.searchOutput ? search : undefined}
wrapText={wrapText}/>
<div ref={messagesEndRef}/>
</div>
{!props.task?.is_interactive_task &&
@@ -435,6 +437,7 @@ export const ResponseDisplayInteractive = (props) =>{
useASNIColor={useASNIColor} toggleANSIColor={toggleANSIColor}
showTaskStatus={showTaskStatus} toggleShowTaskStatus={toggleShowTaskStatus}
wrapText={wrapText} toggleWrapText={toggleWrapText}
autoScroll={autoScroll} toggleAutoScroll={toggleAutoScroll}
/>
</div>
}
@@ -448,7 +451,8 @@ export const ResponseDisplayInteractive = (props) =>{
}
const InteractiveTaskingBar = ({
task, taskData, useASNIColor, toggleANSIColor,
showTaskStatus, toggleShowTaskStatus, wrapText, toggleWrapText
showTaskStatus, toggleShowTaskStatus, wrapText, toggleWrapText,
autoScroll, toggleAutoScroll
}) => {
const [inputText, setInputText] = React.useState("");
const theme = useTheme();
@@ -465,8 +469,8 @@ const InteractiveTaskingBar = ({
});
const [taskOptionsIndex, setTaskOptionsIndex] = React.useState(-1);
const [taskOptions, setTaskOptions] = React.useState([]);
const [selectedEnterOption, setSelectedEnterOption] = React.useState(EnterOptions[1]);
const [selectedControlOption, setSelectedControlOption] = React.useState(InteractiveMessageTypes[0]);
const [selectedEnterOption, setSelectedEnterOption] = React.useState(1);
const [selectedControlOption, setSelectedControlOption] = React.useState(0);
React.useEffect( () => {
const newTaskOptions = taskData.filter( t => t.display_params.length > 1 && (t.interactive_task_type === 0 || t.interactive_task_type === 8));
newTaskOptions.sort( (a,b) => a.id > b.id ? -1 : 1);
@@ -516,23 +520,23 @@ const InteractiveTaskingBar = ({
event.stopPropagation();
event.preventDefault();
if(event.shiftKey){
setInputText(inputText + selectedEnterOption.value);
setInputText(inputText + EnterOptions[selectedEnterOption].value);
return;
}
if(event.metaKey || event.ctrlKey){
setInputText(inputText + selectedEnterOption.value);
setInputText(inputText + EnterOptions[selectedEnterOption].value);
return;
}
if(selectedControlOption.value > 0){
let ctrlSequence = selectedControlOption.text;
let enterOption = selectedEnterOption.value;
if(InteractiveMessageTypes[selectedControlOption].value > 0){
let ctrlSequence = InteractiveMessageTypes[selectedControlOption].text;
let enterOption = EnterOptions[selectedEnterOption].value;
let originalParams = inputText + ctrlSequence + enterOption;
// if we're looking at a tab, never send enter along with it
if (selectedControlOption.value === 8){
if (InteractiveMessageTypes[selectedControlOption].value === 8){
originalParams = inputText + ctrlSequence;
enterOption = "";
// if we're looking at escape, never send enter along with it
} else if(selectedControlOption.value === 4){
} else if(InteractiveMessageTypes[selectedControlOption].value === 4){
originalParams = ctrlSequence + inputText;
enterOption = "";
}
@@ -545,16 +549,16 @@ const InteractiveTaskingBar = ({
parameter_group_name: "default",
parent_task_id: task.id,
is_interactive_task: true,
interactive_task_type: selectedControlOption.value,
interactive_task_type: InteractiveMessageTypes[selectedControlOption].value,
}})
}else {
// no control option selected, just send data along as input
createTask({variables: {
callback_id: task.callback.display_id,
command: task.command.cmd,
params: inputText + selectedEnterOption.value,
params: inputText + EnterOptions[selectedEnterOption].value,
tasking_location: "command_line",
original_params: inputText + selectedEnterOption.value,
original_params: inputText + EnterOptions[selectedEnterOption].value,
parameter_group_name: "default",
parent_task_id: task.id,
is_interactive_task: true,
@@ -562,7 +566,7 @@ const InteractiveTaskingBar = ({
}})
}
setInputText("");
setSelectedControlOption(InteractiveMessageTypes[0]);
setSelectedControlOption(0);
setTaskOptionsIndex(-1);
}
const onChangeSelect = (event) => {
@@ -600,8 +604,8 @@ const InteractiveTaskingBar = ({
}}
input={<Input style={{margin: 0, color: theme.outputTextColor}} />}
>
{InteractiveMessageTypes.map( (opt) => (
<MenuItem value={opt} key={opt.name}>{opt.name}</MenuItem>
{InteractiveMessageTypes.map( (opt,index) => (
<MenuItem value={index} key={opt.name}>{opt.name}</MenuItem>
) )}
</Select>
</FormControl>
@@ -626,8 +630,8 @@ const InteractiveTaskingBar = ({
input={<Input style={{color: theme.outputTextColor}} />}
IconComponent={KeyboardReturnIcon}
>
{EnterOptions.map( (opt) => (
<MenuItem value={opt} key={opt.name}>{opt.name}</MenuItem>
{EnterOptions.map( (opt,index) => (
<MenuItem value={index} key={opt.name}>{opt.name}</MenuItem>
) )}
</Select>
</FormControl>
@@ -637,7 +641,6 @@ const InteractiveTaskingBar = ({
style={{cursor: "pointer"}}
/>
</IconButton>
</MythicStyledTooltip>
<MythicStyledTooltip title={showTaskStatus ? "Hide Task Status" : "Show Task Status"} >
<IconButton onClick={toggleShowTaskStatus} style={{paddingLeft: 0, paddingRight: 0}} disableRipple={true} disableFocusRipple={true}>
@@ -645,7 +648,6 @@ const InteractiveTaskingBar = ({
style={{cursor: "pointer",}}
/>
</IconButton>
</MythicStyledTooltip>
<MythicStyledTooltip title={wrapText ? "Unwrap Text" : "Wrap Text"} >
<IconButton onClick={toggleWrapText} style={{paddingLeft: 0, paddingRight: 0}} disableRipple={true} disableFocusRipple={true}>
@@ -653,7 +655,13 @@ const InteractiveTaskingBar = ({
style={{cursor: "pointer"}}
/>
</IconButton>
</MythicStyledTooltip>
<MythicStyledTooltip title={autoScroll ? "Stop Auto Scroll" : "Auto Scroll"} >
<IconButton onClick={toggleAutoScroll} style={{paddingLeft: 0, paddingRight: 0}} disableRipple={true} disableFocusRipple={true}>
<HeightIcon color={autoScroll ? "success" : "secondary"}
style={{cursor: "pointer"}}
/>
</IconButton>
</MythicStyledTooltip>
</div>
)
@@ -27,6 +27,7 @@ import MythicStyledTableCell from '../../MythicComponents/MythicTableCell';
import {MythicFileContext} from "../../MythicComponents/MythicFileContext";
import RefreshIcon from '@mui/icons-material/Refresh';
import {updateCredentialDeleted} from "../Search/CredentialTable";
import CloudUploadTwoToneIcon from '@mui/icons-material/CloudUploadTwoTone';
export const getDynamicQueryParamsString = `
mutation getDynamicParamsMutation($callback: Int!, $command: String!, $payload_type: String!, $parameter_name: String!, $other_parameters: jsonb){
@@ -559,13 +560,13 @@ export function TaskParametersDialogRow(props){
setValue(event.target.checked);
props.onChange(props.name, event.target.checked);
}
const onFileChange = (evt) => {
setFileValue({name: evt.target.files[0].name});
props.onChange(props.name, evt.target.files[0]);
const onFileChange = (newFile) => {
setFileValue({name: newFile.name});
props.onChange(props.name, newFile);
}
const onFileMultChange = (evt) => {
setFileMultValue([...evt.target.files]);
props.onChange(props.name, [...evt.target.files]);
const onFileMultChange = (newFiles) => {
setFileMultValue([...newFiles]);
props.onChange(props.name, [...newFiles]);
}
const onChangeAgentConnectHost = (event) => {
setAgentConnectHost(event.target.value);
@@ -923,28 +924,14 @@ export function TaskParametersDialogRow(props){
case "File":
return (
<>
<Button variant="contained" component="label">
{ fileValue.name === "" ? "Select File" : fileValue.name }
<input onChange={onFileChange} type="file" hidden />
</Button>
<DragAndDropFileUpload value={fileValue} multiple={false} onChange={onFileChange} />
</>
)
case "FileMultiple":
return (
<>
<Button variant="contained" component="label">
Select Files
<input onChange={onFileMultChange} type="file" hidden multiple />
</Button>
{ fileMultValue.length > 0 &&
fileMultValue.map((f, i) => (
<div key={i}>
{typeof f === "string" && <MythicFileContext agent_file_id={f} />}
{typeof f !== "string" && (f.name)}
</div>
))
}
<DragAndDropFileUpload values={fileMultValue} multiple={true} onChange={onFileMultChange} />
</>
)
case "LinkInfo":
@@ -1219,3 +1206,115 @@ export function TaskParametersDialogRow(props){
)
}
export const DragAndDropFileUpload = ({value, values, multiple, onChange}) => {
const theme = useTheme();
const inputRef = React.useRef(null);
const [files, setFiles] = React.useState(values ? values : []);
const [file, setFile] = React.useState(value ? value : {name: ""});
const [isDragging, setIsDragging] = React.useState(false);
React.useEffect( () => {
if(value){
setFile(value);
}
if(values){
setFiles(values);
}
}, [value, values]);
const handleDragEnter = () => setIsDragging(true);
const handleDragLeave = (event) => {
if (event.currentTarget.contains(event.relatedTarget)) {
return; // Don't treat as a real leave event
}
setIsDragging(false);
}
const handleDrop = (e) => {
e.preventDefault();
setIsDragging(false);
const droppedFiles = Array.from(e.dataTransfer.files);
if(multiple){
setFiles(droppedFiles);
onChange(droppedFiles)
} else {
setFile(droppedFiles[0]);
onChange(droppedFiles[0]);
}
};
const handleDragOver = (e) => {
e.preventDefault();
};
const onFileChange = (evt) => {
setFile({name: evt.target.files[0].name});
onChange(evt.target.files[0]);
}
const onFileMultChange = (evt) => {
setFiles([...evt.target.files]);
onChange(evt.target.files)
}
const onClick = (e) => {
inputRef.current.click();
}
return (
<div
onDrop={handleDrop}
onDragOver={handleDragOver}
onDragEnter={handleDragEnter}
onDragLeave={handleDragLeave}
onClick={onClick}
style={{
border: isDragging ? `4px dashed ${theme.palette.success.main}` : `4px dashed ${theme.palette.primary.main}`,
padding: "20px",
textAlign: "center",
borderRadius: "10px",
cursor: "pointer"
//backgroundColor: isDragging ? "#f0f8ff" : "#fff",
}}
>
<input ref={inputRef} onChange={multiple ? onFileMultChange : onFileChange} type="file" hidden multiple={multiple} />
<div style={{display: "flex", flexDirection: "column", alignItems: "center"}}>
{!multiple && file.name !== "" &&
<>
<CloudUploadTwoToneIcon fontSize={"large"} color={"success"} />
<Typography>
Selected:
</Typography>
<Typography>
{!file.legacy && file.name}
{file.legacy &&
<MythicFileContext agent_file_id={file.name}
extraStyles={{ position: "relative", marginLeft: "5px", marginRight: "5px"}} />
}
</Typography>
</>
}
{multiple && files.length > 0 &&
<>
<CloudUploadTwoToneIcon fontSize={"large"} color={"success"} />
<Typography>
Selected:
</Typography>
{files.map( (f, i) => (
<div key={i}>
{typeof f === "string" && <MythicFileContext agent_file_id={f} />}
{typeof f !== "string" && (f.name)}
</div>
))}
</>
}
{file.name === "" && files.length === 0 &&
<>
<CloudUploadTwoToneIcon fontSize={"large"} color={"success"} />
<Typography>
Drag and drop files here
</Typography>
<Typography>
Click to open dialog
</Typography>
</>
}
</div>
</div>
);
}
@@ -25,6 +25,7 @@ import { Backdrop } from '@mui/material';
import {CircularProgress} from '@mui/material';
import RefreshIcon from '@mui/icons-material/Refresh';
import InputLabel from '@mui/material/InputLabel';
import {DragAndDropFileUpload} from "../Callbacks/TaskParametersDialogRow";
export const getDynamicQueryBuildParameterParams = gql`
mutation getDynamicBuildParamsMutation($payload_type: String!, $parameter_name: String!, $selected_os: String!){
@@ -182,13 +183,13 @@ export function CreatePayloadParameter({onChange, parameter_type, default_value,
const submitDictChange = (list) => {
onChange(name, list, false);
};
const onFileChange = (evt) => {
setFileValue({name: evt.target.files[0].name});
onChange(name, evt.target.files[0]);
const onFileChange = (newFile) => {
setFileValue({name: newFile.name});
onChange(name, newFile);
}
const onFileMultChange = (evt) => {
setFileMultValue([...evt.target.files]);
onChange(name, [...evt.target.files]);
const onFileMultChange = (newFiles) => {
setFileMultValue([...newFiles]);
onChange(name, [...newFiles]);
}
useEffect( () => {
if( parameter_type === "ChooseOne" || parameter_type === "ChooseOneCustom" ){
@@ -474,32 +475,13 @@ export function CreatePayloadParameter({onChange, parameter_type, default_value,
case "FileMultiple":
return (
<>
<Button variant="contained" component="label">
Select Files
<input onChange={onFileMultChange} type="file" hidden multiple />
</Button>
{ fileMultValue.length > 0 &&
fileMultValue?.map((f, i) => (
<div key={i} style={{display: "inline-block"}}>
{typeof f === "string" && <MythicFileContext agent_file_id={f}
extraStyles={{bottom: "-10px", position: "relative", marginLeft: "5px", marginRight: "5px"}} />}
{typeof f !== "string" && (f.name)}
</div>
))
}
<DragAndDropFileUpload values={fileMultValue} multiple={true} onChange={onFileMultChange} />
</>
)
case "File":
return (
<>
<Button variant="contained" component="label">
{ fileValue.legacy ? "Select New File" : fileValue.name === "" ? "Select File" : fileValue.name }
<input onChange={onFileChange} type="file" hidden />
</Button>
{fileValue.legacy &&
<MythicFileContext agent_file_id={fileValue.name}
extraStyles={{ position: "relative", marginLeft: "5px", marginRight: "5px"}} />
}
<DragAndDropFileUpload value={fileValue} multiple={false} onChange={onFileChange} />
</>
)
@@ -66,7 +66,7 @@ mutation eventingManualTrigger($eventgroup_id: Int!){
}
}
`)
export function EventGroupTable({selectedEventGroup, me, showInstances, showGraph}) {
export function EventGroupTable({selectedEventGroup, me, showInstances, showGraph, height}) {
const [openEventStepRender, setOpenEventStepRender] = React.useState(false);
const [openEnvView, setOpenEnvView] = React.useState(false);
const [openTriggerDataView, setOpenTriggerDataView] = React.useState(false);
@@ -141,7 +141,7 @@ export function EventGroupTable({selectedEventGroup, me, showInstances, showGrap
triggerManually({variables: {eventgroup_id: selectedEventGroup.id}});
}
return (
<div style={{marginLeft: "5px", display: "flex", overflowY: "auto", flexDirection: "column", height: "100%"}}>
<div style={{marginLeft: "5px", display: "flex", overflowY: "auto", flexDirection: "column", height: height || "100%"}}>
{selectedEventGroup.id === 0 &&
<Typography variant={"h4"}><strong>All Eventing Runs</strong></Typography>
@@ -221,7 +221,7 @@ export function EventTriggerContextSelectDialog({onClose, triggerContext}) {
}
</Select>
{selectedEventGroup.id !== 0 &&
<EventGroupTable me={me} selectedEventGroup={selectedEventGroup} showInstances={false} showGraph={false}/>
<EventGroupTable me={me} selectedEventGroup={selectedEventGroup} showInstances={false} showGraph={false} height={"unset"}/>
}
<Table>
<TableHead>
@@ -59,8 +59,8 @@ export function ExpandedCallbackSideDetailsTable(props){
<Table size="small" style={{tableLayout: "fixed"}}>
<TableHead>
<TableRow>
<TableCell style={{width: "12rem"}}></TableCell>
<TableCell></TableCell>
<TableCell style={{width: "12rem"}}>Callback Info</TableCell>
<TableCell>Value</TableCell>
</TableRow>
</TableHead>
<TableBody style={{whiteSpace: "pre-wrap", wordBreak: "break-all"}}>
@@ -5,6 +5,7 @@ import DialogContent from '@mui/material/DialogContent';
import DialogTitle from '@mui/material/DialogTitle';
import {gql, useMutation} from '@apollo/client';
import { snackActions } from '../../utilities/Snackbar';
import {DragAndDropFileUpload} from "../Callbacks/TaskParametersDialogRow";
const create_payload = gql`
mutation createPayloadMutation($payload: String!) {
@@ -31,22 +32,19 @@ export function ImportPayloadConfigDialog(props) {
createPayloadMutation({variables: {payload: fileValue.contents}}).catch( (e) => {console.log(e)} );
props.onClose();
}
const onFileChange = (evt) => {
const onFileChange = (newFile) => {
const reader = new FileReader();
reader.onload = (e) => {
const contents = e.target.result;
setFileValue({name: evt.target.files[0].name, contents: contents});
setFileValue({name: newFile.name, contents: contents});
}
reader.readAsBinaryString(evt.target.files[0]);
reader.readAsBinaryString(newFile);
}
return (
<React.Fragment>
<DialogTitle id="form-dialog-title">Import Payload Config to Generate New Payload</DialogTitle>
<DialogContent dividers={true}>
<Button variant="contained" component="label">
{ fileValue.name === "" ? "Select File" : fileValue.name }
<input onChange={onFileChange} type="file" hidden />
</Button>
<DragAndDropFileUpload value={fileValue} multiple={false} onChange={onFileChange} />
</DialogContent>
<DialogActions>
<Button variant="contained" onClick={props.onClose} color="primary">
@@ -5,12 +5,7 @@ import IconButton from '@mui/material/IconButton';
import DeleteIcon from '@mui/icons-material/Delete';
import { MythicDialog } from '../../MythicComponents/MythicDialog';
import {DetailedPayloadComparisonTable, DetailedPayloadTable} from './DetailedPayloadTable';
import Grow from '@mui/material/Grow';
import Popper from '@mui/material/Popper';
import MenuItem from '@mui/material/MenuItem';
import MenuList from '@mui/material/MenuList';
import ClickAwayListener from '@mui/material/ClickAwayListener';
import Paper from '@mui/material/Paper';
import {MythicConfirmDialog} from '../../MythicComponents/MythicConfirmDialog';
import {PayloadDescriptionDialog} from './PayloadDescriptionDialog';
import {PayloadFilenameDialog} from './PayloadFilenameDialog';
@@ -14,7 +14,7 @@ import TableRow from '@mui/material/TableRow';
import Table from '@mui/material/Table';
import TableBody from '@mui/material/TableBody';
import TableHead from '@mui/material/TableHead';
import TableContainer from '@mui/material/TableContainer';
import DialogContent from '@mui/material/DialogContent';
export function PayloadsTableRowBuildProgress(props){
const [buildProgressData, setBuildProgressData] = React.useState({
@@ -109,23 +109,26 @@ export function PayloadsTableRowBuildProgress(props){
}
}
return (
<span style={props.build_phase === "success" ? {
filter: "grayscale(1)",
opacity: 0.5} : {}}>
{buildProgressData.total_steps > 0 &&
<>
<span style={props.build_phase === "success" ? {
filter: "grayscale(1)",
opacity: 0.5} : {}}>
{buildProgressData.total_steps > 0 &&
props.payload_build_steps.map( step => (
<MythicStyledTooltip title={step.step_name} key={"buildstep" + step.step_number}>
{getButton(step)}
</MythicStyledTooltip>
))
}
{openStatusDialog &&
<MythicDialog fullWidth={true} maxWidth="lg" open={openStatusDialog}
onClose={()=>{setOpenStatusDialog(false);}}
innerDialog={<PayloadBuildStepStatusDialog step={displayData} onClose={()=>{setOpenStatusDialog(false);}} />}
/>
}
</span>
</span>
{openStatusDialog &&
<MythicDialog fullWidth={true} maxWidth="lg" open={openStatusDialog}
onClose={()=>{setOpenStatusDialog(false);}}
innerDialog={<PayloadBuildStepStatusDialog step={displayData} onClose={()=>{setOpenStatusDialog(false);}} />}
/>
}
</>
);
}
@@ -167,44 +170,43 @@ export function PayloadBuildStepStatusDialog(props) {
return (
<React.Fragment>
<DialogTitle id="form-dialog-title">Step {props.step.step_number + 1} - {props.step.step_name}</DialogTitle>
<TableContainer className="mythicElement">
<Table size="small" style={{ "maxWidth": "100%", "overflow": "scroll"}}>
<TableHead>
<TableRow>
<TableCell style={{width: "30%"}}>Parameter</TableCell>
<TableCell >Value</TableCell>
</TableRow>
</TableHead>
<TableBody style={{whiteSpace: "pre"}}>
<TableRow hover>
<TableCell>Step Start Time</TableCell>
<TableCell>{props.step.start_time}</TableCell>
</TableRow>
<TableRow hover>
<TableCell>Step End Time</TableCell>
<TableCell>{props.step.end_time}</TableCell>
</TableRow>
<TableRow hover>
<TableCell>Duration</TableCell>
<TableCell>{duration}</TableCell>
</TableRow>
<TableRow hover>
<TableCell>Status</TableCell>
<TableCell> {getStatusMessage()}
</TableCell>
</TableRow>
<TableRow hover>
<TableCell>Step Output</TableCell>
<TableCell>{props.step.step_stdout}</TableCell>
</TableRow>
<TableRow hover>
<TableCell>Step Error</TableCell>
<TableCell>{props.step.step_stderr}</TableCell>
</TableRow>
</TableBody>
</Table>
</TableContainer>
<DialogContent>
<Table size="small" style={{ maxWidth: "100%", overflow: "scroll"}}>
<TableHead>
<TableRow>
<TableCell style={{width: "30%"}}>Parameter</TableCell>
<TableCell >Value</TableCell>
</TableRow>
</TableHead>
<TableBody style={{whiteSpace: "pre"}}>
<TableRow hover>
<TableCell>Step Start Time</TableCell>
<TableCell>{props.step.start_time}</TableCell>
</TableRow>
<TableRow hover>
<TableCell>Step End Time</TableCell>
<TableCell>{props.step.end_time}</TableCell>
</TableRow>
<TableRow hover>
<TableCell>Duration</TableCell>
<TableCell>{duration}</TableCell>
</TableRow>
<TableRow hover>
<TableCell>Status</TableCell>
<TableCell> {getStatusMessage()}
</TableCell>
</TableRow>
<TableRow hover>
<TableCell>Step Output</TableCell>
<TableCell>{props.step.step_stdout}</TableCell>
</TableRow>
<TableRow hover>
<TableCell>Step Error</TableCell>
<TableCell>{props.step.step_stderr}</TableCell>
</TableRow>
</TableBody>
</Table>
</DialogContent>
<DialogActions>
<Button variant="contained" onClick={props.onClose} color="primary">
Close
@@ -308,16 +310,11 @@ export function PayloadsTableRowBuildProcessPerStep(props){
}
return (
<React.Fragment>
{buildProgressData.total_steps > 0 ? (
<React.Fragment>
{props.payload_build_steps.map( step => (
step.step_number === props.step_number ? (
<React.Fragment key={"buildstep" + step.step_number}>{getButton(step)}</React.Fragment>
) : null
))}
</React.Fragment>
) : null}
{buildProgressData.total_steps > 0 &&
props.payload_build_steps.map( step => (
step.step_number === props.step_number &&
<React.Fragment key={"buildstep" + step.step_number}>{getButton(step)}</React.Fragment>
))}
{openStatusDialog &&
<MythicDialog fullWidth={true} maxWidth="lg" open={openStatusDialog}
onClose={()=>{setOpenStatusDialog(false);}}
@@ -7,14 +7,21 @@ import 'ace-builds/src-noconflict/mode-json';
import 'ace-builds/src-noconflict/theme-monokai';
import 'ace-builds/src-noconflict/theme-xcode';
import {ResponseDisplayMedia} from "../Callbacks/ResponseDisplayMedia";
import {MythicDraggableDialogTitle} from "../../MythicComponents/MythicDraggableDialogTitle";
export function PreviewFileMediaDialog({agent_file_id, filename, onClose}) {
return (
const onClick = (e) => {
if(e){
e.preventDefault();
e.stopPropagation();
}
}
return (
<React.Fragment>
<DialogTitle id="form-dialog-title">
<MythicDraggableDialogTitle>
Previewing <b>{filename}</b>
</DialogTitle>
<DialogContent style={{height: "calc(95vh)", margin: 0, padding: 0}}>
</MythicDraggableDialogTitle>
<DialogContent onClick={onClick} style={{height: "calc(80vh)", margin: 0, padding: 0}}>
<ResponseDisplayMedia media={{agent_file_id, filename}} expand={true} />
</DialogContent>
<DialogActions>
+1 -1
View File
@@ -15,7 +15,7 @@ import {jwtDecode} from 'jwt-decode';
import {meState} from './cache';
import {getSkewedNow} from "./components/utilities/Time";
export const mythicUIVersion = "0.3.104";
export const mythicUIVersion = "0.3.105";
let fetchingNewToken = false;
+1 -1
View File
@@ -1 +1 @@
3.4.29
3.4.30
+80 -75
View File
@@ -4307,34 +4307,35 @@
- role: developer
permission:
columns:
- size
- complete
- delete_after_fetch
- deleted
- is_download_from_agent
- is_payload
- is_screenshot
- filename
- full_remote_path
- agent_file_id
- apitokens_id
- chunk_size
- chunks_received
- comment
- complete
- copy_of_file_id
- delete_after_fetch
- deleted
- eventgroup_id
- eventstepinstance_id
- filename
- full_remote_path
- host
- id
- is_download_from_agent
- is_payload
- is_screenshot
- md5
- mythictree_id
- operation_id
- operator_id
- task_id
- total_chunks
- agent_file_id
- comment
- host
- md5
- path
- received_chunk_ids
- sha1
- size
- task_id
- timestamp
- total_chunks
computed_fields:
- filename_text
- filename_utf8
@@ -4347,34 +4348,35 @@
- role: mythic_admin
permission:
columns:
- size
- complete
- delete_after_fetch
- deleted
- is_download_from_agent
- is_payload
- is_screenshot
- filename
- full_remote_path
- agent_file_id
- apitokens_id
- chunk_size
- chunks_received
- comment
- complete
- copy_of_file_id
- delete_after_fetch
- deleted
- eventgroup_id
- eventstepinstance_id
- filename
- full_remote_path
- host
- id
- is_download_from_agent
- is_payload
- is_screenshot
- md5
- mythictree_id
- operation_id
- operator_id
- task_id
- total_chunks
- agent_file_id
- comment
- host
- md5
- path
- received_chunk_ids
- sha1
- size
- task_id
- timestamp
- total_chunks
computed_fields:
- filename_text
- filename_utf8
@@ -4387,34 +4389,35 @@
- role: operation_admin
permission:
columns:
- size
- complete
- delete_after_fetch
- deleted
- is_download_from_agent
- is_payload
- is_screenshot
- filename
- full_remote_path
- agent_file_id
- apitokens_id
- chunk_size
- chunks_received
- comment
- complete
- copy_of_file_id
- delete_after_fetch
- deleted
- eventgroup_id
- eventstepinstance_id
- filename
- full_remote_path
- host
- id
- is_download_from_agent
- is_payload
- is_screenshot
- md5
- mythictree_id
- operation_id
- operator_id
- task_id
- total_chunks
- agent_file_id
- comment
- host
- md5
- path
- received_chunk_ids
- sha1
- size
- task_id
- timestamp
- total_chunks
computed_fields:
- filename_text
- filename_utf8
@@ -4427,34 +4430,35 @@
- role: operator
permission:
columns:
- size
- complete
- delete_after_fetch
- deleted
- is_download_from_agent
- is_payload
- is_screenshot
- filename
- full_remote_path
- agent_file_id
- apitokens_id
- chunk_size
- chunks_received
- comment
- complete
- copy_of_file_id
- delete_after_fetch
- deleted
- eventgroup_id
- eventstepinstance_id
- filename
- full_remote_path
- host
- id
- is_download_from_agent
- is_payload
- is_screenshot
- md5
- mythictree_id
- operation_id
- operator_id
- task_id
- total_chunks
- agent_file_id
- comment
- host
- md5
- path
- received_chunk_ids
- sha1
- size
- task_id
- timestamp
- total_chunks
computed_fields:
- filename_text
- filename_utf8
@@ -4467,34 +4471,35 @@
- role: spectator
permission:
columns:
- size
- complete
- delete_after_fetch
- deleted
- is_download_from_agent
- is_payload
- is_screenshot
- filename
- full_remote_path
- agent_file_id
- apitokens_id
- chunk_size
- chunks_received
- comment
- complete
- copy_of_file_id
- delete_after_fetch
- deleted
- eventgroup_id
- eventstepinstance_id
- filename
- full_remote_path
- host
- id
- is_download_from_agent
- is_payload
- is_screenshot
- md5
- mythictree_id
- operation_id
- operator_id
- task_id
- total_chunks
- agent_file_id
- comment
- host
- md5
- path
- received_chunk_ids
- sha1
- size
- task_id
- timestamp
- total_chunks
computed_fields:
- filename_text
- filename_utf8
+1 -1
View File
@@ -15,7 +15,7 @@ import (
)
var DB *sqlx.DB
var currentMigrationVersion int64 = 3003017
var currentMigrationVersion int64 = 3003018
// initial structs made with './tables-to-go -u mythic_user -p [password here] -h [ip here] -v -d mythic_db -of output -pn database_structs'
// package pulled from https://github.com/fraenky8/tables-to-go
@@ -0,0 +1,9 @@
-- +migrate Up
-- SQL in section 'Up' is executed when this migration is applied
alter table "public"."filemeta" add column IF NOT EXISTS "received_chunk_ids" jsonb not null default jsonb_build_object();
-- +migrate Down
-- SQL in section 'Down' is executed when this migration is rolled back
@@ -38,4 +38,5 @@ type Filemeta struct {
EventStepInstanceID structs.NullInt64 `db:"eventstepinstance_id" json:"event_step_instance_id" mapstructure:"event_step_instance_id"`
APITokensID structs.NullInt64 `db:"apitokens_id" json:"api_tokens_id" mapstructure:"apitokens_id"`
CopyOfFileID structs.NullInt64 `db:"copy_of_file_id" json:"copy_of_file_id" mapstructure:"copy_of_file_id"`
ReceivedChunkIDs MythicJSONText `db:"received_chunk_ids" json:"received_chunk_ids" mapstructure:"received_chunk_id"`
}
@@ -85,6 +85,17 @@ func (j MythicJSONText) StructValue() map[string]interface{} {
}
return newMap
}
func (j MythicJSONText) StructValueMapInt() map[int]interface{} {
newMap := make(map[int]interface{})
if err := j.Unmarshal(&newMap); err != nil {
logging.LogError(err, "Failed to unmarshal types.JSONText into map[int]interface{}")
return newMap
}
if newMap == nil {
newMap = make(map[int]interface{})
}
return newMap
}
func (j MythicJSONArray) StructValue() []interface{} {
newArray := []interface{}{}
if err := j.Unmarshal(&newArray); err != nil {
@@ -411,7 +411,7 @@ func handleAgentMessagePostResponse(incoming *map[string]interface{}, uUIDInfo *
} else {
fileMeta = databaseStructs.Filemeta{AgentFileID: *agentMessage.Responses[i].Download.FileID}
err = database.DB.Get(&fileMeta, `SELECT
id, "path", total_chunks, chunks_received, host, is_screenshot, full_remote_path, complete, md5, sha1, filename, chunk_size, operation_id, mythictree_id
id, "path", total_chunks, chunks_received, host, is_screenshot, full_remote_path, complete, md5, sha1, filename, chunk_size, operation_id, mythictree_id, received_chunk_ids
FROM filemeta
WHERE agent_file_id=$1`, *agentMessage.Responses[i].Download.FileID)
if err != nil {
@@ -1144,8 +1144,9 @@ func handleAgentMessagePostResponseEvent(task databaseStructs.Task, eventingData
}
type chunkWriterData struct {
FileMetaID int
Chunks int
FileMetaID int
Chunks int
ReceivedChunkIDs map[int]interface{}
}
type writeDownloadChunkToDiskResponse struct {
Success bool
@@ -1153,7 +1154,7 @@ type writeDownloadChunkToDiskResponse struct {
}
func listenForWriteDownloadChunkToLocalDisk() {
updateChunksStatement, err := database.DB.Prepare(`UPDATE filemeta SET chunks_received=$2 WHERE id=$1`)
updateChunksStatement, err := database.DB.Prepare(`UPDATE filemeta SET chunks_received=$2, received_chunk_ids=$3 WHERE id=$1`)
openFiles := make(map[string]*os.File)
newChunks := make(map[string]*chunkWriterData)
if err != nil {
@@ -1175,17 +1176,19 @@ func listenForWriteDownloadChunkToLocalDisk() {
continue
}
openFiles[agentResponse.LocalMythicPath] = f
chunksReceived := 0
err = database.DB.Get(&chunksReceived, `SELECT chunks_received FROM filemeta WHERE id=$1`,
fileMetaChunkData := databaseStructs.Filemeta{}
err = database.DB.Get(&fileMetaChunkData, `SELECT chunks_received, received_chunk_ids FROM filemeta WHERE id=$1`,
agentResponse.FileMetaID)
if err != nil {
logging.LogError(err, "Failed to update chunks_received count")
continue
}
newChunks[agentResponse.LocalMythicPath] = &chunkWriterData{
FileMetaID: agentResponse.FileMetaID,
Chunks: chunksReceived,
FileMetaID: agentResponse.FileMetaID,
Chunks: fileMetaChunkData.ChunksReceived,
ReceivedChunkIDs: fileMetaChunkData.ReceivedChunkIDs.StructValueMapInt(),
}
newChunks[agentResponse.LocalMythicPath].Chunks = len(newChunks[agentResponse.LocalMythicPath].ReceivedChunkIDs)
}
if agentResponse.KnownChunkSize > 0 {
//logging.LogDebug("1. downloading with known chunk size", "chunk_num", agentResponse.ChunkNum, "chunk size", agentResponse.KnownChunkSize)
@@ -1223,7 +1226,11 @@ func listenForWriteDownloadChunkToLocalDisk() {
agentResponse.ChunksWritten <- writeDownloadChunkToDiskResponse{Success: false}
continue
}
newChunks[agentResponse.LocalMythicPath].Chunks += 1
if _, ok := newChunks[agentResponse.LocalMythicPath].ReceivedChunkIDs[agentResponse.ChunkNum]; !ok {
// only increment our chunks if we haven't seen this one before
newChunks[agentResponse.LocalMythicPath].ReceivedChunkIDs[agentResponse.ChunkNum] = totalWritten
newChunks[agentResponse.LocalMythicPath].Chunks += 1
}
agentResponse.ChunksWritten <- writeDownloadChunkToDiskResponse{
Success: true,
ChunksReceived: newChunks[agentResponse.LocalMythicPath].Chunks,
@@ -1233,7 +1240,7 @@ func listenForWriteDownloadChunkToLocalDisk() {
f.Sync()
f.Close()
delete(openFiles, key)
_, err = updateChunksStatement.Exec(newChunks[key].FileMetaID, newChunks[key].Chunks)
_, err = updateChunksStatement.Exec(newChunks[key].FileMetaID, newChunks[key].Chunks, GetMythicJSONTextFromStruct(newChunks[key].ReceivedChunkIDs))
if err != nil {
logging.LogError(err, "failed to update chunk count for file")
}
@@ -1241,7 +1248,7 @@ func listenForWriteDownloadChunkToLocalDisk() {
}
case <-syncChunksTimer.C:
for key, _ := range openFiles {
_, err = updateChunksStatement.Exec(newChunks[key].FileMetaID, newChunks[key].Chunks)
_, err = updateChunksStatement.Exec(newChunks[key].FileMetaID, newChunks[key].Chunks, GetMythicJSONTextFromStruct(newChunks[key].ReceivedChunkIDs))
if err != nil {
logging.LogError(err, "failed to update chunk count for file")
}
@@ -1659,6 +1666,7 @@ func associateFileMetaWithMythicTree(pathData utils.AnalyzedPath, fileMeta datab
TreeType: databaseStructs.TREE_TYPE_FILE,
CanHaveChildren: false,
Deleted: false,
DisplayPath: []byte{},
}
parentPath, _, name := getParentPathFullPathName(pathData, len(pathData.PathPieces)-1, databaseStructs.TREE_TYPE_FILE)
if parentPath == "" && name == "" {
@@ -1840,6 +1848,7 @@ func HandleAgentMessagePostResponseFileBrowser(task databaseStructs.Task, fileBr
Deleted: false,
HasChildren: !fileBrowser.IsFile && fileBrowser.Files != nil && len(*fileBrowser.Files) > 0,
Os: getOSTypeBasedOnPathSeparator(pathData.PathSeparator, databaseStructs.TREE_TYPE_FILE),
DisplayPath: []byte{},
}
if fileBrowser.Success != nil {
newTree.Success.Valid = true
@@ -1874,6 +1883,7 @@ func HandleAgentMessagePostResponseFileBrowser(task databaseStructs.Task, fileBr
CanHaveChildren: !newEntry.IsFile,
Deleted: false,
Os: newTree.Os,
DisplayPath: []byte{},
}
newTreeChild.FullPath = treeNodeGetFullPath(fullPath, []byte(newEntry.Name), []byte(pathData.PathSeparator), databaseStructs.TREE_TYPE_FILE)
fileMetaData = addChildFilePermissions(&newEntry)
@@ -2018,6 +2028,7 @@ func listenForFileBrowserUpdateDeleted() {
TreeType: databaseStructs.TREE_TYPE_FILE,
CanHaveChildren: !newEntry.IsFile,
Deleted: false,
DisplayPath: []byte{},
}
newTreeChild.FullPath = treeNodeGetFullPath(fullPath, []byte(newEntry.Name), []byte(pathData.PathSeparator), databaseStructs.TREE_TYPE_FILE)
fileMetaData := addChildFilePermissions(&newEntry)
@@ -2100,6 +2111,7 @@ func HandleAgentMessagePostResponseProcesses(task databaseStructs.Task, processe
TreeType: databaseStructs.TREE_TYPE_PROCESS,
CanHaveChildren: true,
Deleted: false,
DisplayPath: []byte{},
}
if newEntry.OS != nil {
newTree.Os = *newEntry.OS
@@ -2167,6 +2179,7 @@ func HandleAgentMessagePostResponseProcesses(task databaseStructs.Task, processe
TreeType: databaseStructs.TREE_TYPE_PROCESS,
CanHaveChildren: true,
Deleted: false,
DisplayPath: []byte{},
}
if newEntry.OS != nil {
newTree.Os = *newEntry.OS
@@ -2251,6 +2264,7 @@ func HandleAgentMessagePostResponseProcesses(task databaseStructs.Task, processe
TreeType: databaseStructs.TREE_TYPE_PROCESS,
CanHaveChildren: true,
Deleted: false,
DisplayPath: []byte{},
}
if newEntry.OS != nil {
newTree.Os = *newEntry.OS
+1 -1
View File
@@ -11,7 +11,7 @@ import (
"github.com/spf13/viper"
)
const mythicServerVersion = "3.4.29"
const mythicServerVersion = "3.4.30"
type Config struct {
// server configuration
@@ -44,6 +44,7 @@ func EventingImportWebhook(c *gin.Context) {
operatorOperation := ginOperatorOperation.(*databaseStructs.Operatoroperation)
fileData := databaseStructs.Filemeta{
AgentFileID: uuid.New().String(),
FullRemotePath: make([]byte, 0),
TotalChunks: 1,
ChunksReceived: 1,
ChunkSize: int(file.Size),
@@ -1,17 +1,17 @@
{
"files": {
"main.css": "/new/static/css/main.262ce320.css",
"main.js": "/new/static/js/main.97297187.js",
"main.js": "/new/static/js/main.1b10444d.js",
"sql-wasm.wasm": "/new/sql-wasm-7f26ab750fcdafabbe18c6525a66a0bd.wasm",
"static/media/mythic-red.png": "/new/static/media/mythic-red.203468a4e5240d239aa0.png",
"static/media/graphql.png": "/new/static/media/graphql.8f15978b39b0870a9f0e.png",
"static/media/Mythic_Logo.svg": "/new/static/media/Mythic_Logo.6842c911bebe36d6f83fc7ced4a2cd99.svg",
"index.html": "/new/index.html",
"main.262ce320.css.map": "/new/static/css/main.262ce320.css.map",
"main.97297187.js.map": "/new/static/js/main.97297187.js.map"
"main.1b10444d.js.map": "/new/static/js/main.1b10444d.js.map"
},
"entrypoints": [
"static/css/main.262ce320.css",
"static/js/main.97297187.js"
"static/js/main.1b10444d.js"
]
}
+1 -1
View File
@@ -1 +1 @@
<!doctype html><html lang="en"><head><meta charset="utf-8"/><link rel="icon" href="/new/favicon.ico"/><meta name="viewport" content="width=device-width,initial-scale=1"/><meta name="theme-color" content="#000000"/><link rel="apple-touch-icon" href="/new/logo192.png"/><link rel="manifest" href="/new/manifest.json"/><title>Mythic</title><script defer="defer" src="/new/static/js/main.97297187.js"></script><link href="/new/static/css/main.262ce320.css" rel="stylesheet"></head><body><noscript>You need to enable JavaScript to run this app.</noscript><div id="root"></div></body></html>
<!doctype html><html lang="en"><head><meta charset="utf-8"/><link rel="icon" href="/new/favicon.ico"/><meta name="viewport" content="width=device-width,initial-scale=1"/><meta name="theme-color" content="#000000"/><link rel="apple-touch-icon" href="/new/logo192.png"/><link rel="manifest" href="/new/manifest.json"/><title>Mythic</title><script defer="defer" src="/new/static/js/main.1b10444d.js"></script><link href="/new/static/css/main.262ce320.css" rel="stylesheet"></head><body><noscript>You need to enable JavaScript to run this app.</noscript><div id="root"></div></body></html>