mirror of
https://github.com/its-a-feature/Mythic
synced 2026-06-08 14:55:38 +00:00
UI updates and back-end better file transfer tracking
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 @@
|
||||
<!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>
|
||||
+3
-3
File diff suppressed because one or more lines are too long
+1
-1
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user