Skip to content

Commit 51cf1c9

Browse files
committed
Save user preferences to database
1 parent 05ed8b1 commit 51cf1c9

36 files changed

+635
-500
lines changed

CHANGELOG.MD

+8
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,14 @@ 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+
## [3.3.1-rc23] - 2024-11-14
8+
9+
### Changed
10+
11+
- Added a new field to file browser responses, `set_as_user_output` (bool) to indicate that this structured data should be turned into JSON and set as user_output data by Mythic
12+
- This allows agents to send file browser data once, but get it counted for both the file browser and user_output
13+
- Added endpoints to leverage/set user preferences
14+
715
## [3.3.1-rc22] -2024-11-6
816

917
### Changed

MythicReactUI/CHANGELOG.MD

+8
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,14 @@ 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.55] - 2024-11-14
8+
9+
### Changed
10+
11+
- Updated the string display dialog for browser script tables to match other text fields
12+
- Updated user preferences to be stored outside of local storage
13+
- Updated Tooltip from MUI to react-tooltip to speed things up a bit
14+
715
## [0.2.54] - 2024-11-8
816

917
### Changed

MythicReactUI/package-lock.json

+21
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

MythicReactUI/package.json

+1
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@
4747
"react-scrollbar-size": "^5.0.0",
4848
"react-split": "^2.0.14",
4949
"react-toastify": "^9.1.3",
50+
"react-tooltip": "^5.28.0",
5051
"react-virtualized": "^9.22.5",
5152
"react-virtualized-auto-sizer": "^1.0.24",
5253
"react-window": "^1.8.10",

MythicReactUI/src/cache.js

+20
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,25 @@ import {snackActions} from "./components/utilities/Snackbar";
44

55
export const meState = makeVar({loggedIn:false, user: null, access_token: null, refresh_token: null});
66
export const menuOpen = makeVar(false);
7+
export const operatorSettingDefaults = {
8+
fontSize: 12,
9+
fontFamily: "Verdana",
10+
topColor: "#3c4d67",
11+
showMedia: true,
12+
hideUsernames: false,
13+
showIP: false,
14+
showHostname: false,
15+
showCallbackGroups: false,
16+
useDisplayParamsForCLIHistory: true,
17+
interactType: "interact",
18+
callbacks_table_columns: ["Interact", "Host", "Domain", "User", "Description", "Last Checkin", "Agent", "IP", "PID"],
19+
callbacks_table_filters: {},
20+
autoTaskLsOnEmptyDirectories: false,
21+
["experiment-responseStreamLimit"]: 50,
22+
23+
}
24+
export const mePreferences = makeVar(operatorSettingDefaults);
25+
726

827
export const successfulLogin = (data) => {
928
localStorage.setItem("access_token", data.access_token);
@@ -41,6 +60,7 @@ export const FailedRefresh = () =>{
4160
refresh_token: null,
4261
user: null
4362
});
63+
mePreferences(operatorSettingDefaults);
4464
snackActions.clearAll();
4565
restartWebsockets();
4666
}

MythicReactUI/src/components/App.js

+42-22
Original file line numberDiff line numberDiff line change
@@ -21,10 +21,12 @@ import { SingleTaskView } from './pages/SingleTaskView/SingleTaskView';
2121
import { createTheme, ThemeProvider, StyledEngineProvider, adaptV4Theme } from '@mui/material/styles';
2222
import { GlobalStyles } from '../themes/GlobalStyles';
2323
import CssBaseline from '@mui/material/CssBaseline';
24-
import { FailedRefresh, meState } from '../cache';
24+
import {FailedRefresh, mePreferences, meState} from '../cache';
2525
import { Reporting } from './pages/Reporting/Reporting';
2626
import { MitreAttack } from './pages/MITRE_ATTACK/MitreAttack';
2727
import {Tags} from './pages/Tags/Tags';
28+
import { Tooltip } from 'react-tooltip';
29+
import {useQuery, gql } from '@apollo/client';
2830
//background-color: #282c34;
2931
import { Route, Routes } from 'react-router-dom';
3032
import { useInterval } from './utilities/Time';
@@ -35,29 +37,23 @@ import { ToastContainer } from 'react-toastify';
3537
import "react-toastify/dist/ReactToastify.css";
3638
import {Eventing} from "./pages/Eventing/Eventing";
3739
import {InviteForm} from "./pages/Login/InviteForm";
40+
import {snackActions} from "./utilities/Snackbar";
3841

42+
const userSettingsQuery = gql`
43+
query getUserSettings {
44+
getOperatorPreferences {
45+
status
46+
error
47+
preferences
48+
}
49+
}
50+
`;
3951

4052

4153
export function App(props) {
4254
const me = useReactiveVar(meState);
55+
const preferences = useReactiveVar(mePreferences);
4356
const [themeMode, themeToggler] = useDarkMode();
44-
const localStorageFontSize = localStorage.getItem(`${me?.user?.user_id || 0}-fontSize`);
45-
const initialLocalStorageFontSizeValue = localStorageFontSize === null ? 12 : parseInt(localStorageFontSize);
46-
const localStorageFontFamily = localStorage.getItem(`${me?.user?.user_id || 0}-fontFamily`);
47-
const initialLocalStorageFontFamilyValue = localStorageFontFamily === null ? [
48-
'-apple-system',
49-
'BlinkMacSystemFont',
50-
'"Segoe UI"',
51-
'Roboto',
52-
'"Helvetica Neue"',
53-
'Arial',
54-
'sans-serif',
55-
'"Apple Color Emoji"',
56-
'"Segoe UI Emoji"',
57-
'"Segoe UI Symbol"',
58-
].join(',') : localStorageFontFamily;
59-
const localStorageTopColor = localStorage.getItem(`${me?.user?.user_id || 0}-topColor`);
60-
const initialLocalStorageTopColorValue = localStorageTopColor === null ? "#3c4d67" : localStorageTopColor;
6157
const theme = React.useMemo(
6258
() =>
6359
createTheme(adaptV4Theme({
@@ -122,16 +118,35 @@ export function App(props) {
122118
pageHeaderText: {
123119
main: 'white',
124120
},
125-
topAppBarColor: initialLocalStorageTopColorValue,
121+
topAppBarColor: preferences?.topColor,
126122
typography: {
127-
fontSize: initialLocalStorageFontSizeValue,
128-
fontFamily: initialLocalStorageFontFamilyValue
123+
fontSize: preferences?.fontSize,
124+
fontFamily: preferences?.fontFamily
129125
},
130126
})),
131-
[themeMode]
127+
[themeMode, preferences.topColor, preferences.fontSize, preferences.fontFamily]
132128
);
133129
const mountedRef = React.useRef(true);
134130
const [openRefreshDialog, setOpenRefreshDialog] = React.useState(false);
131+
const [loadingPreference, setLoadingPreferences] = React.useState(true);
132+
useQuery(userSettingsQuery, {
133+
onCompleted: (data) => {
134+
//console.log("got preferences", data.getOperatorPreferences.preferences)
135+
if(data.getOperatorPreferences.status === "success"){
136+
if(data.getOperatorPreferences.preferences !== null){
137+
mePreferences(data.getOperatorPreferences.preferences);
138+
}
139+
} else {
140+
snackActions.error(`Failed to get user preferences:\n${data.getOperatorPreferences.error}`);
141+
}
142+
setLoadingPreferences(false);
143+
},
144+
onError: (error) => {
145+
console.log(error);
146+
snackActions.error(error.message);
147+
setLoadingPreferences(false);
148+
}
149+
})
135150
useInterval( () => {
136151
// interval should run every 10 minutes (600000 milliseconds) to check JWT status
137152
let millisecondsLeft = JWTTimeLeft();
@@ -145,11 +160,16 @@ export function App(props) {
145160
}
146161
}
147162
}, 600000, mountedRef, mountedRef);
163+
if(loadingPreference){
164+
// make sure we've loaded preferences before loading actual app content
165+
return null
166+
}
148167
return (
149168
<StyledEngineProvider injectFirst>
150169
<ThemeProvider theme={theme}>
151170
<GlobalStyles theme={theme} />
152171
<CssBaseline />
172+
<Tooltip id={"my-tooltip"} style={{zIndex: 100000}}/>
153173
<ToastContainer limit={2} autoClose={3000}
154174
theme={themeMode}
155175
style={{maxWidth: "100%", minWidth: "40%", width: "40%", marginTop: "20px", display: "flex", flexWrap: "wrap",
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,16 @@
11
import React from 'react';
2-
import Button from '@mui/material/Button';
3-
import DialogActions from '@mui/material/DialogActions';
4-
import DialogContent from '@mui/material/DialogContent';
5-
import DialogTitle from '@mui/material/DialogTitle';
6-
import {MythicDialog} from './MythicDialog';
2+
import {MythicDialog, MythicModifyStringDialog} from './MythicDialog';
73

84
export function MythicDisplayTextDialog(props) {
95
return (
10-
<MythicDialog fullWidth={props.fullWidth === null ? false : props.fullWidth} maxWidth={props.maxWidth === null ? "sm" : props.maxWidth} open={props.open} onClose={()=>{props.onClose()}} innerDialog={
11-
<React.Fragment>
12-
<DialogTitle id="form-dialog-title">{props.title}</DialogTitle>
13-
<DialogContent dividers={true}>
14-
<pre style={{whiteSpace: "pre-wrap"}}>
15-
{props.value}
16-
</pre>
17-
</DialogContent>
18-
<DialogActions>
19-
<Button onClick={props.onClose} variant="contained" color="primary">
20-
Close
21-
</Button>
22-
</DialogActions>
23-
</React.Fragment>
6+
<MythicDialog fullWidth={props.fullWidth === null ? false : props.fullWidth}
7+
maxWidth={props.maxWidth === null ? "sm" : props.maxWidth}
8+
open={props.open}
9+
onClose={()=>{props.onClose()}} innerDialog={
10+
<MythicModifyStringDialog title={props.title}
11+
onClose={props.onClose}
12+
value={props.value}
13+
maxRows={20} />
2414
} />
2515
);
2616
}

MythicReactUI/src/components/MythicComponents/MythicResizableGrid/Cell.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import {classes} from './styles';
33
import ClickAwayListener from '@mui/material/ClickAwayListener';
44
import {Dropdown, DropdownMenuItem, DropdownNestedMenuItem} from "../MythicNestedMenus";
55

6-
const CellPreMemo = ({ VariableSizeGridProps: { style, rowIndex, columnIndex, data } }) => {
6+
const CellPreMemo = ({ style, rowIndex, columnIndex, data }) => {
77
const [openContextMenu, setOpenContextMenu] = React.useState(false);
88
const rowClassName = data.gridUUID + "row" + rowIndex;
99
const [contextMenuOptions, setContextMenuOptions] = React.useState(data?.rowContextMenuOptions || []);

0 commit comments

Comments
 (0)