Skip to content

Commit

Permalink
Updated processing for delegates and file downloads, more ui improvement
Browse files Browse the repository at this point in the history
  • Loading branch information
its-a-feature committed Aug 23, 2024
1 parent d3d5e5c commit 596c6f7
Show file tree
Hide file tree
Showing 24 changed files with 460 additions and 94 deletions.
2 changes: 2 additions & 0 deletions CHANGELOG.MD
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Changed

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

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

Expand Down
7 changes: 7 additions & 0 deletions MythicReactUI/CHANGELOG.MD
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,13 @@ 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.2.32] - 2024-08-23

### Changed

- Updated the interactive tasking for search to be a bit better
- Updated interactive tasking to not scroll so much

## [0.2.31] - 2024-08-21

### Changed
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,7 @@ export const taskingDataFragment = gql`
display_params
status
timestamp
status_timestamp_submitted
command {
cmd
supported_ui_features
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { gql, useMutation, useSubscription } from '@apollo/client';
import {b64DecodeUnicode} from "./ResponseDisplay";
import {SearchBar} from './ResponseDisplay';
import Pagination from '@mui/material/Pagination';
import {Typography, CircularProgress, Select, IconButton} from '@mui/material';
import {Typography, CircularProgress, Select, IconButton, Backdrop} from '@mui/material';
import CheckCircleOutlineIcon from '@mui/icons-material/CheckCircleOutline';
import Input from '@mui/material/Input';
import MenuItem from '@mui/material/MenuItem';
Expand Down Expand Up @@ -78,54 +78,62 @@ const getClassnames = (entry) => {
//console.log(entry);
return classnames.join(" ");
}
const GetOutputFormat = ({data, myTask, taskID, useASNIColor, messagesEndRef, showTaskStatus, wrapText}) => {
export const GetOutputFormatAll = ({data, myTask, taskID, useASNIColor, messagesEndRef, showTaskStatus, wrapText, search}) => {
const [dataElement, setDataElement] = React.useState(null);
React.useEffect( () => {

if(data.response) {
// we're looking at response output
if(data.is_error){
setDataElement(<pre style={{display: "inline",backgroundColor: "#311717", margin: "0 0 0 0",
wordBreak: wrapText ? "break-all" : "",
whiteSpace: wrapText ? "pre-wrap" : ""}} key={data.timestamp + data.id}>
{data.response}
const elements = data.map( d => {
if(d.response) {
// we're looking at response output
if(d.is_error){
return (<pre id={"response" + d.timestamp + d.id} style={{display: "inline",backgroundColor: "#311717", color: "white", margin: "0 0 0 0",
wordBreak: wrapText ? "break-all" : "",
whiteSpace: wrapText ? "pre-wrap" : ""}} key={d.timestamp + d.id}>
{d.response}
</pre>)
} else {
if(useASNIColor){
let ansiJSON = Anser.ansiToJson(data.response, { use_classes: true });
//console.log(ansiJSON)
setDataElement(
ansiJSON.map( (a, i) => (
<pre style={{display: "inline", margin: "0 0 0 0",
wordBreak: wrapText ? "break-all" : "",
whiteSpace: wrapText ? "pre-wrap" : "",
}} className={getClassnames(a)} key={data.id + data.timestamp + i}>{a.content}</pre>
))
)
} else {
setDataElement(<pre style={{display: "inline", margin: "0 0 0 0",
wordBreak: wrapText ? "break-all" : "",
whiteSpace: wrapText ? "pre-wrap" : "",
}} key={data.timestamp + data.id}>{data.response}</pre>)
}
if(useASNIColor){
let ansiJSON = Anser.ansiToJson(d.response, { use_classes: true });
//console.log(ansiJSON)
return (
ansiJSON.map( (a, i) => (
<pre id={"response" + d.timestamp + d.id} style={{display: "inline", margin: "0 0 0 0",
wordBreak: wrapText ? "break-all" : "",
whiteSpace: wrapText ? "pre-wrap" : "",
}} className={getClassnames(a)} key={d.id + d.timestamp + i}>{a.content}</pre>
))
)
} else {
return (<pre id={"response" + d.timestamp + d.id} style={{display: "inline", margin: "0 0 0 0",
wordBreak: wrapText ? "break-all" : "",
whiteSpace: wrapText ? "pre-wrap" : "",
}} key={d.timestamp + d.id}>{d.response}</pre>)
}

}
} else {
// we're looking at tasking
setDataElement(
<pre key={data.timestamp + data.id} style={{display: "inline",margin: "0 0 0 0",
wordBreak: wrapText ? "break-all" : "", whiteSpace: "pre-wrap"}}>
{showTaskStatus && getTaskingStatus(data)}
{data.original_params}
}
} else {
// we're looking at tasking
return(
<pre id={"task" + d.timestamp + d.id} key={d.timestamp + d.id} style={{display: "inline",margin: "0 0 0 0",
wordBreak: wrapText ? "break-all" : "", whiteSpace: "pre-wrap"}}>
{showTaskStatus && getTaskingStatus(d)}
{d.original_params}
</pre>
)
}
}, [data.timestamp, useASNIColor, showTaskStatus, wrapText]);
)
}
})
setDataElement(elements);

}, [data, useASNIColor, showTaskStatus, wrapText]);
React.useLayoutEffect( () => {
if(myTask){
let el = document.getElementById(`ptytask${taskID}`);
if(el && el.scrollHeight - el.scrollTop - el.clientHeight < 500){
messagesEndRef.current?.scrollIntoView({ behavior: "auto" });
if(!search){
messagesEndRef?.current?.scrollIntoView({ behavior: "auto", block: "nearest" });
//console.log("scrolling");
}
} else {
// console.log("not scrolled down enough")
}
}
}, [dataElement]);
Expand All @@ -135,6 +143,7 @@ const GetOutputFormat = ({data, myTask, taskID, useASNIColor, messagesEndRef, s

}


const InteractiveMessageTypes = [
{"name": "None", "value": -1, "text": "None"},
{"name": "Tab", "value": 13, "text": "^I"},
Expand Down Expand Up @@ -168,6 +177,7 @@ const EnterOptions = [
];
export const ResponseDisplayInteractive = (props) =>{
const me = useReactiveVar(meState);
const [backdropOpen, setBackdropOpen] = React.useState(false);
const [scrollToBottom, setScrollToBottom] = React.useState(false);
const pageSize = React.useRef(100);
const highestFetched = React.useRef(0);
Expand All @@ -183,9 +193,8 @@ export const ResponseDisplayInteractive = (props) =>{
const [useASNIColor, setUseANSIColor] = React.useState(true);
const [showTaskStatus, setShowTaskStatus] = React.useState(true);
const [wrapText, setWrapText] = React.useState(true);
useSubscription(getInteractiveTaskingQuery, {
const {loading: loadingTasks} = useSubscription(getInteractiveTaskingQuery, {
variables: {parent_task_id: props.task.id},
shouldResubscribe: true,
onError: data => {
console.error(data)
},
Expand All @@ -206,8 +215,12 @@ export const ResponseDisplayInteractive = (props) =>{
}, [...taskData]);
setTaskData(newTaskData);
}
if(backdropOpen){
setBackdropOpen(false);
}

}
})
})
const subscriptionDataCallback = React.useCallback( ({data}) => {
// we still have some room to view more, but only room for fetchLimit - totalFetched.current
if(props.task.id !== taskIDRef.current){
Expand All @@ -224,20 +237,25 @@ export const ResponseDisplayInteractive = (props) =>{
highestFetched.current = highestFetchedId;
taskIDRef.current = props.task.id;
} else {
const newResponses = data.data.response_stream.filter( r => r.id > highestFetched.current);
const newerResponses = newResponses.map( (r) => { return {...r, response: b64DecodeUnicode(r.response)}});
newerResponses.sort( (a,b) => a.id > b.id ? 1 : -1);
const newResponses = data.data.response_stream.filter(r => r.id > highestFetched.current);
const newerResponses = newResponses.map((r) => {
return {...r, response: b64DecodeUnicode(r.response)}
});
newerResponses.sort((a, b) => a.id > b.id ? 1 : -1);
let rawResponseArray = [...rawResponses];
let highestFetchedId = highestFetched.current;
for(let i = 0; i < newerResponses.length; i++){
for (let i = 0; i < newerResponses.length; i++) {
rawResponseArray.push(newerResponses[i]);
highestFetchedId = newerResponses[i]["id"];
}
setRawResponses(rawResponseArray);
highestFetched.current = highestFetchedId;
}
if(backdropOpen){
setBackdropOpen(false);
}

}, [highestFetched.current, rawResponses, props.task.id]);
}, [highestFetched.current, rawResponses, props.task.id, backdropOpen, taskIDRef.current]);
useSubscription(subResponsesQuery, {
variables: {task_id: props.task.id},
fetchPolicy: "no-cache",
Expand Down Expand Up @@ -325,31 +343,51 @@ export const ResponseDisplayInteractive = (props) =>{
messagesEndRef.current.scrollIntoView();
}
}, [scrollToBottom]);
React.useLayoutEffect(() => {
if(!scrollToBottom && alloutput.length > 0){setScrollToBottom(true)}
}, [alloutput]);
React.useEffect( () => {
if(loadingTasks){
setTaskData([]);
setBackdropOpen(true);
}else{
setBackdropOpen(false);
}
}, [loadingTasks]);
return (

<div style={{
display: "flex", overflowY: "auto",
position: "relative", height: props.expand ? "100%" : undefined, maxHeight: props.expand ? "100%" : "500px",
flexDirection: "column"
}}>
<Backdrop open={backdropOpen} style={{zIndex: 2, position: "absolute",}} invisible={false}>
<div style={{
borderRadius: "4px",
border: "1px solid black",
padding: "5px",
backgroundColor: "rgba(37,37,37,0.92)", color: "white",
alignItems: "center",
display: "flex", flexDirection: "column"}}>
<CircularProgress color="inherit" />
<Typography variant={"h5"}>
Fetching Interactive Task Data....
</Typography>
</div>
</Backdrop>
{props.searchOutput &&
<SearchBar onSubmitSearch={onSubmitSearch}/>
}
<div style={{overflowY: "auto", width: "100%", marginBottom: "5px",
flexGrow: 1, paddingLeft: "10px"}} ref={props.responseRef}
id={`ptytask${props.task.id}`}>
{alloutput.map((e, index) => (
<GetOutputFormat key={"getoutput" + index} data={e}

<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 &&
Expand Down
12 changes: 6 additions & 6 deletions MythicReactUI/src/components/pages/Callbacks/TaskDisplay.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ import {MythicStyledTooltip} from "../../MythicComponents/MythicStyledTooltip";

const PREFIX = 'TaskDisplay';
const ACCORDION_PREFIX = 'TaskDisplayAccordion';
const classes = {
export const classes = {
root: `${PREFIX}-root`,
heading: `${PREFIX}-heading`,
secondaryHeading: `${PREFIX}-secondaryHeading`,
Expand All @@ -33,7 +33,7 @@ const classes = {
details: `${PREFIX}-details`,
column: `${PREFIX}-column`
};
const accordionClasses = {
export const accordionClasses = {
root: `${ACCORDION_PREFIX}-root`,
content: `${ACCORDION_PREFIX}-content`,
expandIcon: `${ACCORDION_PREFIX}-expandIcon`,
Expand All @@ -42,7 +42,7 @@ const accordionClasses = {
detailsRoot: `${ACCORDION_PREFIX}Details-root`
}

const StyledPaper = styled(Paper)((
export const StyledPaper = styled(Paper)((
{
theme
}
Expand All @@ -51,7 +51,7 @@ const StyledPaper = styled(Paper)((
marginTop: "3px",
marginRight: "0px",
height: "auto",
width: "100%",
width: "99%",
boxShadow: "unset",
backgroundColor: theme.palette.background.taskLabel,
},
Expand Down Expand Up @@ -117,7 +117,7 @@ subscription getSubTasking($task_id: Int!){
}
`;

const StyledAccordionSummary = styled(AccordionSummary)((
export const StyledAccordionSummary = styled(AccordionSummary)((
{
theme
}
Expand Down Expand Up @@ -227,7 +227,7 @@ const GetOperatorDisplay = ({initialHideUsernameValue, task}) => {
}
return "/ " + task.operator.username;
}
const ColoredTaskLabel = ({task, theme, me, taskDivID, onClick, displayChildren, toggleDisplayChildren, expanded }) => {
export const ColoredTaskLabel = ({task, theme, me, taskDivID, onClick, displayChildren, toggleDisplayChildren, expanded }) => {
const [displayComment, setDisplayComment] = React.useState(false);
const [alertBadges, setAlertBadges] = React.useState(0);
const initialHideUsernameValue = useMythicSetting({setting_name: "hideUsernames", default_value: "false"});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,7 @@ export const TaskDisplayContainer = ({task, me}) => {
fabStyle={{ }}
viewAllOutput={selectAllOutput}/>
<Grid item xs={12}>
<ResponseDisplay
<ResponseDisplay
task={task}
me={me}
command_id={commandID}
Expand Down Expand Up @@ -131,6 +131,7 @@ export const TaskDisplayContainerFlat = ({task, me}) => {
return (
<div style={{ height: "100%", position: "relative", display: "flex", flexDirection: "column", overflowY: "auto", }}>
<ResponseDisplay
key={task.id}
task={task}
me={me}
command_id={commandID}
Expand Down
1 change: 1 addition & 0 deletions MythicReactUI/src/components/pages/Search/Search.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import {SearchTabArtifactsLabel, SearchTabArtifactsPanel} from './SearchTabArtif
import {SearchTabSocksLabel, SearchTabSocksPanel} from './SearchTabProxies';
import {SearchTabProcessesLabel, SearchTabProcessPanel} from "./SearchTabProcesses";
import {SearchTabTagsLabel, SearchTabTagsPanel} from "./SearchTabTags";
import {SearchTabInteractiveTasksLabel, SearchTabInteractiveTasksPanel} from "./SearchTabInteractiveTasks";

const PREFIX = 'Search';

Expand Down
Loading

0 comments on commit 596c6f7

Please sign in to comment.