Skip to content

Commit 596c6f7

Browse files
committed
Updated processing for delegates and file downloads, more ui improvement
1 parent d3d5e5c commit 596c6f7

24 files changed

+460
-94
lines changed

CHANGELOG.MD

+2
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
99
### Changed
1010

1111
- Updated the delegate checks for socks/rpfwd/interactive messages to only send delegates if there's data
12+
- Updated interactive tasking to set processed and processing timestamps more consistently
13+
- Updated file download processing to allow -1 total chunks so agents with unknown chunks can start downloads
1214

1315
## [3.3.0-rc19] - 2024-08-21
1416

MythicReactUI/CHANGELOG.MD

+7
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,13 @@ All notable changes to this project will be documented in this file.
44
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
55
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
66

7+
## [0.2.32] - 2024-08-23
8+
9+
### Changed
10+
11+
- Updated the interactive tasking for search to be a bit better
12+
- Updated interactive tasking to not scroll so much
13+
714
## [0.2.31] - 2024-08-21
815

916
### Changed

MythicReactUI/src/components/pages/Callbacks/CallbackMutations.js

+1
Original file line numberDiff line numberDiff line change
@@ -122,6 +122,7 @@ export const taskingDataFragment = gql`
122122
display_params
123123
status
124124
timestamp
125+
status_timestamp_submitted
125126
command {
126127
cmd
127128
supported_ui_features

MythicReactUI/src/components/pages/Callbacks/ResponseDisplayInteractive.js

+91-53
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import { gql, useMutation, useSubscription } from '@apollo/client';
66
import {b64DecodeUnicode} from "./ResponseDisplay";
77
import {SearchBar} from './ResponseDisplay';
88
import Pagination from '@mui/material/Pagination';
9-
import {Typography, CircularProgress, Select, IconButton} from '@mui/material';
9+
import {Typography, CircularProgress, Select, IconButton, Backdrop} from '@mui/material';
1010
import CheckCircleOutlineIcon from '@mui/icons-material/CheckCircleOutline';
1111
import Input from '@mui/material/Input';
1212
import MenuItem from '@mui/material/MenuItem';
@@ -78,54 +78,62 @@ const getClassnames = (entry) => {
7878
//console.log(entry);
7979
return classnames.join(" ");
8080
}
81-
const GetOutputFormat = ({data, myTask, taskID, useASNIColor, messagesEndRef, showTaskStatus, wrapText}) => {
81+
export const GetOutputFormatAll = ({data, myTask, taskID, useASNIColor, messagesEndRef, showTaskStatus, wrapText, search}) => {
8282
const [dataElement, setDataElement] = React.useState(null);
8383
React.useEffect( () => {
84-
85-
if(data.response) {
86-
// we're looking at response output
87-
if(data.is_error){
88-
setDataElement(<pre style={{display: "inline",backgroundColor: "#311717", margin: "0 0 0 0",
89-
wordBreak: wrapText ? "break-all" : "",
90-
whiteSpace: wrapText ? "pre-wrap" : ""}} key={data.timestamp + data.id}>
91-
{data.response}
84+
const elements = data.map( d => {
85+
if(d.response) {
86+
// we're looking at response output
87+
if(d.is_error){
88+
return (<pre id={"response" + d.timestamp + d.id} style={{display: "inline",backgroundColor: "#311717", color: "white", margin: "0 0 0 0",
89+
wordBreak: wrapText ? "break-all" : "",
90+
whiteSpace: wrapText ? "pre-wrap" : ""}} key={d.timestamp + d.id}>
91+
{d.response}
9292
</pre>)
93-
} else {
94-
if(useASNIColor){
95-
let ansiJSON = Anser.ansiToJson(data.response, { use_classes: true });
96-
//console.log(ansiJSON)
97-
setDataElement(
98-
ansiJSON.map( (a, i) => (
99-
<pre style={{display: "inline", margin: "0 0 0 0",
100-
wordBreak: wrapText ? "break-all" : "",
101-
whiteSpace: wrapText ? "pre-wrap" : "",
102-
}} className={getClassnames(a)} key={data.id + data.timestamp + i}>{a.content}</pre>
103-
))
104-
)
10593
} else {
106-
setDataElement(<pre style={{display: "inline", margin: "0 0 0 0",
107-
wordBreak: wrapText ? "break-all" : "",
108-
whiteSpace: wrapText ? "pre-wrap" : "",
109-
}} key={data.timestamp + data.id}>{data.response}</pre>)
110-
}
94+
if(useASNIColor){
95+
let ansiJSON = Anser.ansiToJson(d.response, { use_classes: true });
96+
//console.log(ansiJSON)
97+
return (
98+
ansiJSON.map( (a, i) => (
99+
<pre id={"response" + d.timestamp + d.id} style={{display: "inline", margin: "0 0 0 0",
100+
wordBreak: wrapText ? "break-all" : "",
101+
whiteSpace: wrapText ? "pre-wrap" : "",
102+
}} className={getClassnames(a)} key={d.id + d.timestamp + i}>{a.content}</pre>
103+
))
104+
)
105+
} else {
106+
return (<pre id={"response" + d.timestamp + d.id} style={{display: "inline", margin: "0 0 0 0",
107+
wordBreak: wrapText ? "break-all" : "",
108+
whiteSpace: wrapText ? "pre-wrap" : "",
109+
}} key={d.timestamp + d.id}>{d.response}</pre>)
110+
}
111111

112-
}
113-
} else {
114-
// we're looking at tasking
115-
setDataElement(
116-
<pre key={data.timestamp + data.id} style={{display: "inline",margin: "0 0 0 0",
117-
wordBreak: wrapText ? "break-all" : "", whiteSpace: "pre-wrap"}}>
118-
{showTaskStatus && getTaskingStatus(data)}
119-
{data.original_params}
112+
}
113+
} else {
114+
// we're looking at tasking
115+
return(
116+
<pre id={"task" + d.timestamp + d.id} key={d.timestamp + d.id} style={{display: "inline",margin: "0 0 0 0",
117+
wordBreak: wrapText ? "break-all" : "", whiteSpace: "pre-wrap"}}>
118+
{showTaskStatus && getTaskingStatus(d)}
119+
{d.original_params}
120120
</pre>
121-
)
122-
}
123-
}, [data.timestamp, useASNIColor, showTaskStatus, wrapText]);
121+
)
122+
}
123+
})
124+
setDataElement(elements);
125+
126+
}, [data, useASNIColor, showTaskStatus, wrapText]);
124127
React.useLayoutEffect( () => {
125128
if(myTask){
126129
let el = document.getElementById(`ptytask${taskID}`);
127130
if(el && el.scrollHeight - el.scrollTop - el.clientHeight < 500){
128-
messagesEndRef.current?.scrollIntoView({ behavior: "auto" });
131+
if(!search){
132+
messagesEndRef?.current?.scrollIntoView({ behavior: "auto", block: "nearest" });
133+
//console.log("scrolling");
134+
}
135+
} else {
136+
// console.log("not scrolled down enough")
129137
}
130138
}
131139
}, [dataElement]);
@@ -135,6 +143,7 @@ const GetOutputFormat = ({data, myTask, taskID, useASNIColor, messagesEndRef, s
135143

136144
}
137145

146+
138147
const InteractiveMessageTypes = [
139148
{"name": "None", "value": -1, "text": "None"},
140149
{"name": "Tab", "value": 13, "text": "^I"},
@@ -168,6 +177,7 @@ const EnterOptions = [
168177
];
169178
export const ResponseDisplayInteractive = (props) =>{
170179
const me = useReactiveVar(meState);
180+
const [backdropOpen, setBackdropOpen] = React.useState(false);
171181
const [scrollToBottom, setScrollToBottom] = React.useState(false);
172182
const pageSize = React.useRef(100);
173183
const highestFetched = React.useRef(0);
@@ -183,9 +193,8 @@ export const ResponseDisplayInteractive = (props) =>{
183193
const [useASNIColor, setUseANSIColor] = React.useState(true);
184194
const [showTaskStatus, setShowTaskStatus] = React.useState(true);
185195
const [wrapText, setWrapText] = React.useState(true);
186-
useSubscription(getInteractiveTaskingQuery, {
196+
const {loading: loadingTasks} = useSubscription(getInteractiveTaskingQuery, {
187197
variables: {parent_task_id: props.task.id},
188-
shouldResubscribe: true,
189198
onError: data => {
190199
console.error(data)
191200
},
@@ -206,8 +215,12 @@ export const ResponseDisplayInteractive = (props) =>{
206215
}, [...taskData]);
207216
setTaskData(newTaskData);
208217
}
218+
if(backdropOpen){
219+
setBackdropOpen(false);
220+
}
221+
209222
}
210-
})
223+
})
211224
const subscriptionDataCallback = React.useCallback( ({data}) => {
212225
// we still have some room to view more, but only room for fetchLimit - totalFetched.current
213226
if(props.task.id !== taskIDRef.current){
@@ -224,20 +237,25 @@ export const ResponseDisplayInteractive = (props) =>{
224237
highestFetched.current = highestFetchedId;
225238
taskIDRef.current = props.task.id;
226239
} else {
227-
const newResponses = data.data.response_stream.filter( r => r.id > highestFetched.current);
228-
const newerResponses = newResponses.map( (r) => { return {...r, response: b64DecodeUnicode(r.response)}});
229-
newerResponses.sort( (a,b) => a.id > b.id ? 1 : -1);
240+
const newResponses = data.data.response_stream.filter(r => r.id > highestFetched.current);
241+
const newerResponses = newResponses.map((r) => {
242+
return {...r, response: b64DecodeUnicode(r.response)}
243+
});
244+
newerResponses.sort((a, b) => a.id > b.id ? 1 : -1);
230245
let rawResponseArray = [...rawResponses];
231246
let highestFetchedId = highestFetched.current;
232-
for(let i = 0; i < newerResponses.length; i++){
247+
for (let i = 0; i < newerResponses.length; i++) {
233248
rawResponseArray.push(newerResponses[i]);
234249
highestFetchedId = newerResponses[i]["id"];
235250
}
236251
setRawResponses(rawResponseArray);
237252
highestFetched.current = highestFetchedId;
238253
}
254+
if(backdropOpen){
255+
setBackdropOpen(false);
256+
}
239257

240-
}, [highestFetched.current, rawResponses, props.task.id]);
258+
}, [highestFetched.current, rawResponses, props.task.id, backdropOpen, taskIDRef.current]);
241259
useSubscription(subResponsesQuery, {
242260
variables: {task_id: props.task.id},
243261
fetchPolicy: "no-cache",
@@ -325,31 +343,51 @@ export const ResponseDisplayInteractive = (props) =>{
325343
messagesEndRef.current.scrollIntoView();
326344
}
327345
}, [scrollToBottom]);
328-
React.useLayoutEffect(() => {
329-
if(!scrollToBottom && alloutput.length > 0){setScrollToBottom(true)}
330-
}, [alloutput]);
346+
React.useEffect( () => {
347+
if(loadingTasks){
348+
setTaskData([]);
349+
setBackdropOpen(true);
350+
}else{
351+
setBackdropOpen(false);
352+
}
353+
}, [loadingTasks]);
331354
return (
332355

333356
<div style={{
334357
display: "flex", overflowY: "auto",
335358
position: "relative", height: props.expand ? "100%" : undefined, maxHeight: props.expand ? "100%" : "500px",
336359
flexDirection: "column"
337360
}}>
361+
<Backdrop open={backdropOpen} style={{zIndex: 2, position: "absolute",}} invisible={false}>
362+
<div style={{
363+
borderRadius: "4px",
364+
border: "1px solid black",
365+
padding: "5px",
366+
backgroundColor: "rgba(37,37,37,0.92)", color: "white",
367+
alignItems: "center",
368+
display: "flex", flexDirection: "column"}}>
369+
<CircularProgress color="inherit" />
370+
<Typography variant={"h5"}>
371+
Fetching Interactive Task Data....
372+
</Typography>
373+
</div>
374+
</Backdrop>
338375
{props.searchOutput &&
339376
<SearchBar onSubmitSearch={onSubmitSearch}/>
340377
}
341378
<div style={{overflowY: "auto", width: "100%", marginBottom: "5px",
342379
flexGrow: 1, paddingLeft: "10px"}} ref={props.responseRef}
343380
id={`ptytask${props.task.id}`}>
344-
{alloutput.map((e, index) => (
345-
<GetOutputFormat key={"getoutput" + index} data={e}
381+
382+
<GetOutputFormatAll data={alloutput}
346383
myTask={props.task.operator.username === (me?.user?.username || "")}
347384
taskID={props.task.id}
348385
useASNIColor={useASNIColor}
349386
messagesEndRef={messagesEndRef}
350387
showTaskStatus={showTaskStatus}
388+
search={props.searchOutput ? search : undefined}
351389
wrapText={wrapText}/>
352-
))}
390+
353391
<div ref={messagesEndRef}/>
354392
</div>
355393
{!props.task?.is_interactive_task &&

MythicReactUI/src/components/pages/Callbacks/TaskDisplay.js

+6-6
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ import {MythicStyledTooltip} from "../../MythicComponents/MythicStyledTooltip";
2323

2424
const PREFIX = 'TaskDisplay';
2525
const ACCORDION_PREFIX = 'TaskDisplayAccordion';
26-
const classes = {
26+
export const classes = {
2727
root: `${PREFIX}-root`,
2828
heading: `${PREFIX}-heading`,
2929
secondaryHeading: `${PREFIX}-secondaryHeading`,
@@ -33,7 +33,7 @@ const classes = {
3333
details: `${PREFIX}-details`,
3434
column: `${PREFIX}-column`
3535
};
36-
const accordionClasses = {
36+
export const accordionClasses = {
3737
root: `${ACCORDION_PREFIX}-root`,
3838
content: `${ACCORDION_PREFIX}-content`,
3939
expandIcon: `${ACCORDION_PREFIX}-expandIcon`,
@@ -42,7 +42,7 @@ const accordionClasses = {
4242
detailsRoot: `${ACCORDION_PREFIX}Details-root`
4343
}
4444

45-
const StyledPaper = styled(Paper)((
45+
export const StyledPaper = styled(Paper)((
4646
{
4747
theme
4848
}
@@ -51,7 +51,7 @@ const StyledPaper = styled(Paper)((
5151
marginTop: "3px",
5252
marginRight: "0px",
5353
height: "auto",
54-
width: "100%",
54+
width: "99%",
5555
boxShadow: "unset",
5656
backgroundColor: theme.palette.background.taskLabel,
5757
},
@@ -117,7 +117,7 @@ subscription getSubTasking($task_id: Int!){
117117
}
118118
`;
119119

120-
const StyledAccordionSummary = styled(AccordionSummary)((
120+
export const StyledAccordionSummary = styled(AccordionSummary)((
121121
{
122122
theme
123123
}
@@ -227,7 +227,7 @@ const GetOperatorDisplay = ({initialHideUsernameValue, task}) => {
227227
}
228228
return "/ " + task.operator.username;
229229
}
230-
const ColoredTaskLabel = ({task, theme, me, taskDivID, onClick, displayChildren, toggleDisplayChildren, expanded }) => {
230+
export const ColoredTaskLabel = ({task, theme, me, taskDivID, onClick, displayChildren, toggleDisplayChildren, expanded }) => {
231231
const [displayComment, setDisplayComment] = React.useState(false);
232232
const [alertBadges, setAlertBadges] = React.useState(0);
233233
const initialHideUsernameValue = useMythicSetting({setting_name: "hideUsernames", default_value: "false"});

MythicReactUI/src/components/pages/Callbacks/TaskDisplayContainer.js

+2-1
Original file line numberDiff line numberDiff line change
@@ -93,7 +93,7 @@ export const TaskDisplayContainer = ({task, me}) => {
9393
fabStyle={{ }}
9494
viewAllOutput={selectAllOutput}/>
9595
<Grid item xs={12}>
96-
<ResponseDisplay
96+
<ResponseDisplay
9797
task={task}
9898
me={me}
9999
command_id={commandID}
@@ -131,6 +131,7 @@ export const TaskDisplayContainerFlat = ({task, me}) => {
131131
return (
132132
<div style={{ height: "100%", position: "relative", display: "flex", flexDirection: "column", overflowY: "auto", }}>
133133
<ResponseDisplay
134+
key={task.id}
134135
task={task}
135136
me={me}
136137
command_id={commandID}

MythicReactUI/src/components/pages/Search/Search.js

+1
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import {SearchTabArtifactsLabel, SearchTabArtifactsPanel} from './SearchTabArtif
1313
import {SearchTabSocksLabel, SearchTabSocksPanel} from './SearchTabProxies';
1414
import {SearchTabProcessesLabel, SearchTabProcessPanel} from "./SearchTabProcesses";
1515
import {SearchTabTagsLabel, SearchTabTagsPanel} from "./SearchTabTags";
16+
import {SearchTabInteractiveTasksLabel, SearchTabInteractiveTasksPanel} from "./SearchTabInteractiveTasks";
1617

1718
const PREFIX = 'Search';
1819

0 commit comments

Comments
 (0)