Skip to content

Commit

Permalink
few bug fixes in server and UI
Browse files Browse the repository at this point in the history
  • Loading branch information
its-a-feature committed Jan 30, 2025
1 parent 1e17d90 commit 554128d
Show file tree
Hide file tree
Showing 28 changed files with 262 additions and 178 deletions.
9 changes: 9 additions & 0 deletions CHANGELOG.MD
Original file line number Diff line number Diff line change
Expand Up @@ -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.3.1-rc38] - 2025-01-30

### Changed

- Updated callback creation for command augmentation payload types to not include deleted commands
- Updated callback command remove RPC to be more performant across large numbers of callbacks
- Fixed an issue with parameter type ChooseOneCustom that wouldn't save default values
- Updated file preview to give more meaningful error message if the file has been deleted from disk

## [3.3.1-rc37] - 2025-01-24

### Changed
Expand Down
13 changes: 13 additions & 0 deletions MythicReactUI/CHANGELOG.MD
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,19 @@ 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.4] - 2025-01-27

### Changed

- Updated the tab completion for command names and parameters to be a case-sensitive "includes" rather than a "startsWith"
- Fixed bug in file browser streaming that would sometimes return file history data out of order
- Fixed a bug in the file search page that would cause horizontal scrolling for uploads
- Fixed a bug where default values for ChooseOneCustom fields wasn't populating
- Fixed a bug where pre-supplied values for ChooseOneCustom fields in modals weren't populating
- Updated some of the default dark colors
- Fixed a bug on the new eventing wizard that would cause scrolling
-

## [0.3.3] - 2025-01-24

### Changed
Expand Down
4 changes: 2 additions & 2 deletions MythicReactUI/src/cache.js
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ export const operatorSettingDefaults = {
light: '#a6a5a5'
},
info: {
dark: '#2574b4',
dark: '#2184d3',
light: '#4990b2'
},
warning: {
Expand All @@ -48,7 +48,7 @@ export const operatorSettingDefaults = {
light: '#f6f6f6'
},
paper: {
dark: '#616161',
dark: '#424242',
light: '#ececec'
},
tableHeader: {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -127,7 +127,7 @@ export function MythicViewJSONAsTableDialog(props) {
permissionDict = JSON.parse(props.value);
}

if(permissionDict.constructor === Object){
if(!Array.isArray(permissionDict) && typeof permissionDict !== 'string'){
for(let key in permissionDict){
if(permissionDict[key] && permissionDict[key].constructor === Object){
// potentially have a nested dictionary here or array to become a dictionary, mark it
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,32 +45,56 @@ query GetCallbackDetails($callback_id: Int!) {
}
}
`;

const CustomListElement = React.memo(({valueObj, handleToggle}) => {
return (
<ListItem style={{padding: 0}} key={valueObj.display_value} role="listitem" button
onClick={() => handleToggle(valueObj)}>
<ListItemIcon>
<Checkbox
checked={valueObj.checked}
disableRipple
inputProps={{'aria-labelledby': valueObj.label}}
/>
</ListItemIcon>
<ListItemText id={valueObj.label} primary={valueObj.display_value}/>
</ListItem>
)
});
const CustomList = ({title, items, handleToggle}) => {
const renderedList = React.useMemo( () => {
return items.map((valueObj) => (
<CustomListElement key={valueObj.display_value} valueObj={valueObj} handleToggle={handleToggle} />
))
}, [items, handleToggle]);
return (
<>
<CardHeader className={classes.cardHeader} title={title}/>
<StyledDivider classes={{root: classes.divider}}/>
<CardContent style={{flexGrow: 1, overflowY: "auto", padding: 0}}>
<List dense component="div" role="list" style={{padding: 0, width: "100%"}}>
{renderedList}
</List>
</CardContent>
</>
)
};
export function AddRemoveCallbackCommandsDialog(props) {

const [checked, setChecked] = React.useState([]);
const [left, setLeft] = React.useState([]);
const [originalLeft, setOriginalLeft] = React.useState([]);
const [originalRight, setOriginalRight] = React.useState([]);
const [right, setRight] = React.useState([]);
const [leftTitle, setLeftTitle] = React.useState("Commands Not Included");
const [rightTitle, setRightTitle] = React.useState("Commands Included");
const leftChecked = intersection(checked, left);
const rightChecked = intersection(checked, right);
const compareElements = (a, b) => {
return a.cmd === b.cmd && a.payloadtype.name === b.payloadtype.name;
}
const leftTitle = "Commands Not Included";
const rightTitle = "Commands Included";

useQuery(getCommandsQuery, {variables: {callback_id: props.callback_id},
fetchPolicy: "no-cache",
onCompleted: (data) => {
let originalLeftFromQuery = data.callback_by_pk.payload.payloadtype.commands.map(c => {
return {...c, payloadtype: {name: data.callback_by_pk.payload.payloadtype.name}}
})
originalLeftFromQuery = [...originalLeftFromQuery, ...data.command];
setOriginalLeft(originalLeftFromQuery);
setOriginalRight(data.callback_by_pk.loadedcommands);

const leftData = originalLeftFromQuery.reduce( (prev, cur) => {
let leftData = originalLeftFromQuery.reduce( (prev, cur) => {
if( data.callback_by_pk.loadedcommands.filter(c => c.command.cmd === cur.cmd && c.command.payloadtype.name === cur.payloadtype.name).length === 0){
return [...prev, cur];
} else {
Expand All @@ -79,35 +103,46 @@ export function AddRemoveCallbackCommandsDialog(props) {
}, []);

leftData.sort( (a,b) => a.cmd < b.cmd ? -1 : 1);
leftData = leftData.map( c => {return {...c,
checked: false,
display_value: c.cmd + " (" + c?.payloadtype?.name + ")",
label: `transfer-list-item-${c.cmd + " (" + c?.payloadtype?.name + ")"}-label`
}});
setLeft(leftData);
const rightData = data.callback_by_pk.loadedcommands.map( c => c.command);
let rightData = data.callback_by_pk.loadedcommands.map( c => c.command);
rightData.sort( (a,b) => a.cmd < b.cmd ? -1 : 1);
rightData = rightData.map( c => {return {...c,
checked: false,
display_value: c.cmd + " (" + c?.payloadtype?.name + ")",
label: `transfer-list-item-${c.cmd + " (" + c?.payloadtype?.name + ")"}-label`
}});
setRight(rightData);
},
onError: (data) => {

}
})
function not(a, b) {
return a.filter((value) => b.indexOf(value) === -1);
}

function intersection(a, b) {
return a.filter((value) => b.indexOf(value) !== -1);
}
const handleToggle = (value) => () => {
let currentIndex = -1;
currentIndex = checked.indexOf(value);

const newChecked = [...checked];

if (currentIndex === -1) {
newChecked.push(value);
} else {
newChecked.splice(currentIndex, 1);
const handleToggle = (value) => {
let found = false;
const newLeft = left.map( c => {
if(c.display_value === value.display_value){
c.checked = !c.checked;
found = true;
}
return c;
});
if(found){
setLeft(newLeft);
return
}

setChecked(newChecked);
const newRight = right.map( c => {
if(c.display_value === value.display_value){
c.checked = !c.checked;
}
return c;
});
setRight(newRight);
};

const handleAllRight = () => {
Expand All @@ -116,49 +151,26 @@ export function AddRemoveCallbackCommandsDialog(props) {
};

const handleCheckedRight = () => {
setRight(right.concat(leftChecked));
setLeft(not(left, leftChecked));
setChecked(not(checked, leftChecked));
let leftChecked = left.filter(c => c.checked);
leftChecked = leftChecked.map(c => {return {...c, checked: false}})
let leftNotChecked = left.filter(c => !c.checked);
setRight(right.concat(leftChecked));
setLeft(leftNotChecked);
};

const handleCheckedLeft = () => {
setLeft(left.concat(rightChecked));
setRight(not(right, rightChecked));
setChecked(not(checked, rightChecked));
let rightChecked = right.filter(c => c.checked);
rightChecked = rightChecked.map(c => {return {...c, checked: false}})
let rightNotChecked = right.filter(c => !c.checked);
setLeft(left.concat(rightChecked));
setRight(rightNotChecked);
};

const handleAllLeft = () => {
setLeft(left.concat(right));
setRight([]);
};
const customList = (title, items) => (
<>
<CardHeader className={classes.cardHeader} title={title} />
<StyledDivider classes={{root: classes.divider}}/>
<CardContent style={{flexGrow: 1, overflowY: "auto", padding: 0}}>
<List dense component="div" role="list" style={{padding:0, width: "100%"}}>
{items.map((valueObj) => {
const value = valueObj.cmd + " (" + valueObj?.payloadtype?.name + ")";
const labelId = `transfer-list-item-${value}-label`;
return (
<ListItem style={{padding:0}} key={value} role="listitem" button onClick={handleToggle(valueObj)}>
<ListItemIcon>
<Checkbox
checked={checked.findIndex( (element) => element.cmd + " (" + element?.payloadtype?.name + ")" === value) !== -1}
tabIndex={-1}
disableRipple
inputProps={{ 'aria-labelledby': labelId }}
/>
</ListItemIcon>
<ListItemText id={labelId} primary={value} />
</ListItem>
);
})}
<ListItem />
</List>
</CardContent>
</>
);

const setFinalTags = () => {
// things to add are in the `right` now but weren't for `originalRight`
const commandsToAdd = right.filter( (command) => {
Expand All @@ -177,9 +189,9 @@ export function AddRemoveCallbackCommandsDialog(props) {
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}}>
<div style={{paddingLeft: 0, flexGrow: 1, marginLeft: 0, marginRight: "10px", position: "relative", overflowY: "auto", display: "flex", flexDirection: "column" }}>
{customList(leftTitle, left)}
</div>
<div style={{paddingLeft: 0, flexGrow: 1, marginLeft: 0, marginRight: "10px", position: "relative", overflowY: "auto", display: "flex", flexDirection: "column" }}>
<CustomList title={leftTitle} items={left} handleToggle={handleToggle}/>
</div>
<div style={{display: "flex", flexDirection: "column", justifyContent: "center"}}>
<StyledButton
variant="contained"
Expand All @@ -196,7 +208,6 @@ export function AddRemoveCallbackCommandsDialog(props) {
size="small"
className={classes.button}
onClick={handleCheckedRight}
disabled={leftChecked.length === 0}
aria-label="move selected right"
>
&gt;
Expand All @@ -206,7 +217,6 @@ export function AddRemoveCallbackCommandsDialog(props) {
size="small"
className={classes.button}
onClick={handleCheckedLeft}
disabled={rightChecked.length === 0}
aria-label="move selected left"
>
&lt;
Expand All @@ -223,8 +233,8 @@ export function AddRemoveCallbackCommandsDialog(props) {
</StyledButton>

</div>
<div style={{marginLeft: "10px", position: "relative", flexGrow: 1, display: "flex", flexDirection: "column" }}>
{customList(rightTitle, right)}
<div style={{marginLeft: "10px", position: "relative", flexGrow: 1, display: "flex", flexDirection: "column" }}>
<CustomList title={rightTitle} items={right} handleToggle={handleToggle} />
</div>
</div>
</DialogContent>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -329,7 +329,7 @@ export const CallbacksTabsFileBrowserPanel = ({ index, value, tabInfo, me }) =>
return {...f, filename_text: b64DecodeUnicode(f.filename_text)};
})
existingData.filemeta = [...existingData.filemeta, ...newfileData]

existingData.filemeta.sort((a,b) => a.id > b.id ? 1 : -1);
treeRootDataRef.current[currentGroups[j]][data.data.mythictree_stream[i]["host"]][data.data.mythictree_stream[i]["full_path_text"]] = {...existingData};
if(selectedFolderData.group === currentGroups[j] && selectedFolderData.host === data.data.mythictree_stream[i]["host"] &&
selectedFolderData.full_path_text === data.data.mythictree_stream[i]["full_path_text"]){
Expand Down Expand Up @@ -455,6 +455,7 @@ export const CallbacksTabsFileBrowserPanel = ({ index, value, tabInfo, me }) =>
return {...f, filename_text: b64DecodeUnicode(f.filename_text)};
})
existingData.filemeta = [...existingData.filemeta, ...newfileData]
existingData.filemeta.sort((a,b) => a.id > b.id ? 1 : -1);
treeRootDataRef.current[currentGroups[j]][mythictree[i]["host"]][mythictree[i]["full_path_text"]] = {...existingData};
if(selectedFolderData.group === currentGroups[j] && selectedFolderData.host === mythictree[i]["host"] &&
selectedFolderData.full_path_text === mythictree[i]["full_path_text"]){
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ const getFileDownloadHistory = gql`
timestamp
filename_text
host
deleted
full_remote_path_text
task {
id
Expand Down Expand Up @@ -874,7 +875,7 @@ const FileBrowserTableRowActionCell = ({ rowData, cellData, onTaskRowAction, tre
onClose={(e)=>{setOpenPreviewMediaDialog(false);}}
innerDialog={<PreviewFileMediaDialog
agent_file_id={treeRootData[selectedFolderData.host][cellData]?.filemeta[0]?.agent_file_id}
filename={treeRootData[selectedFolderData.host][cellData]?.filemeta[0]?.filename_text}
filename={b64DecodeUnicode(treeRootData[selectedFolderData.host][cellData]?.filemeta[0]?.filename_text)}
onClose={(e)=>{setOpenPreviewMediaDialog(false);}} />}
/>
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -554,7 +554,7 @@ export function CallbacksTabsTaskingInputPreMemo(props){
// what the user typed isn't an exact match, so find things that start with what they're trying to type
paramOptions = cmd.commandparameters.reduce( (prev, cur) => {
if(cmdGroupNames.includes(cur.parameter_group_name) &&
cur.cli_name.toLowerCase().startsWith(lastFlag.slice(1).toLocaleLowerCase()) &&
cur.cli_name.toLowerCase().includes(lastFlag.slice(1).toLocaleLowerCase()) &&
IsCLIPossibleParameterType(cur.parameter_type) &&
(!(cur.cli_name in parsed) || (IsRepeatableCLIParameterType(cur.parameter_type)) ) ){
return [...prev, cur.cli_name];
Expand Down Expand Up @@ -637,10 +637,20 @@ export function CallbacksTabsTaskingInputPreMemo(props){
}else{
// somebody hit tab with either a blank message or a partial word
if(tabOptions.current.length === 0){
let opts = loadedOptions.current.filter( l => l.cmd.toLowerCase().startsWith(message.toLocaleLowerCase()) && (l.attributes.supported_os.length === 0 || l.attributes.supported_os.includes(props.callback_os)));
tabOptions.current = opts;
let opts = loadedOptions.current.filter( l => l.cmd.toLowerCase().includes(message.toLocaleLowerCase()) && (l.attributes.supported_os.length === 0 || l.attributes.supported_os.includes(props.callback_os)));
tabOptionsType.current = "param_name";
tabOptionsIndex.current = 0;
let startsWithOpts = opts.filter(s => s.cmd.startsWith(message.toLocaleLowerCase()));
let includesOpts = opts.filter(s => {
for(let i = 0; i < startsWithOpts.length; i++){
if(startsWithOpts[i].cmd === s.cmd){
return false;
}
}
return true;
})
opts = [...startsWithOpts, ...includesOpts];
tabOptions.current = opts;
if(opts.length > 0){
setMessage(opts[0].cmd);
setCommandPayloadType(opts[0]?.payloadtype?.name || "");
Expand Down
Loading

0 comments on commit 554128d

Please sign in to comment.